91

On destruction of a restful resource, I want to guarantee a few things before I allow a destroy operation to continue? Basically, I want the ability to stop the destroy operation if I note that doing so would place the database in a invalid state? There are no validation callbacks on a destroy operation, so how does one "validate" whether a destroy operation should be accepted?

Stephen Cagle
  • 14,124
  • 16
  • 55
  • 86

11 Answers11

76

You can raise an exception which you then catch. Rails wraps deletes in a transaction, which helps matters.

For example:

class Booking < ActiveRecord::Base
  has_many   :booking_payments
  ....
  def destroy
    raise "Cannot delete booking with payments" unless booking_payments.count == 0
    # ... ok, go ahead and destroy
    super
  end
end

Alternatively you can use the before_destroy callback. This callback is normally used to destroy dependent records, but you can throw an exception or add an error instead.

def before_destroy
  return true if booking_payments.count == 0
  errors.add :base, "Cannot delete booking with payments"
  # or errors.add_to_base in Rails 2
  false
  # Rails 5
  throw(:abort)
end

myBooking.destroy will now return false, and myBooking.errors will be populated on return.

Tobias
  • 4,523
  • 2
  • 20
  • 40
Airsource Ltd
  • 32,379
  • 13
  • 71
  • 75
  • 3
    Note that where it now says "... ok, go ahead and destroy", you need to put "super", so the original destroy method is actually called. – Alexander Malfait Aug 19 '09 at 15:13
  • 3
    errors.add_to_base is deprecated in Rails 3. Instead you should do errors.add(:base, "message"). – Ryan Jan 06 '12 at 00:50
  • 9
    Rails doesn't validate before destroying, so before_destroy would need to return false for it to cancel the destroy. Just adding errors is useless. – graywh Apr 10 '12 at 17:50
  • 26
    With Rails 5, the `false` at the end of the `before_destroy` is useless. From now on you should use `throw(:abort)` (@see: http://weblog.rubyonrails.org/2015/1/10/This-week-in-Rails/#halting-callback-chains-by-throwing-aborthttpsgithubcomrailsrailspull17227). – romainsalles Mar 06 '16 at 21:57
  • 3
    Your example of defending against orphaned records can be solved much more easily via `has_many :booking_payments, dependent: :restrict_with_error` – thisismydesign Nov 26 '19 at 10:24
49

just a note:

For rails 3

class Booking < ActiveRecord::Base

before_destroy :booking_with_payments?

private

def booking_with_payments?
        errors.add(:base, "Cannot delete booking with payments") unless booking_payments.count == 0

        errors.blank? #return false, to not destroy the element, otherwise, it will delete.
end
workdreamer
  • 2,836
  • 1
  • 35
  • 37
  • 2
    A problem with this approach is that the before_destroy callback seems to be called *after* all of the booking_payments have been destroyed. – sunkencity Mar 18 '12 at 15:01
  • 4
    Related ticket: https://github.com/rails/rails/issues/3458 @sunkencity you can declare before_destroy before association declaration to temporarily avoid this. – lulalala Apr 23 '13 at 06:11
  • 2
    Your example of defending against orphaned records can be solved much more easily via `has_many :booking_payments, dependent: :restrict_with_error` – thisismydesign Nov 26 '19 at 10:25
  • Per the rails guide before_destroy callbacks can and should be placed before associations with dependent_destroy; this triggers the callback before the associated destroys are called: https://guides.rubyonrails.org/active_record_callbacks.html#destroying-an-object – grouchomc Aug 26 '20 at 11:52
22

It is what I did with Rails 5:

before_destroy do
  cannot_delete_with_qrcodes
  throw(:abort) if errors.present?
end

def cannot_delete_with_qrcodes
  errors.add(:base, 'Cannot delete shop with qrcodes') if qrcodes.any?
end
  • 3
    This is a nice article that explains this behavior in Rails 5: http://blog.bigbinary.com/2016/02/13/rails-5-does-not-halt-callback-chain-when-false-is-returned.html – Yaro Holodiuk Sep 07 '16 at 11:15
  • 1
    Your example of defending against orphaned records can be solved much more easily via `has_many :qrcodes, dependent: :restrict_with_error` – thisismydesign Nov 26 '19 at 10:25
21

State of affairs as of Rails 6:

This works:

before_destroy :ensure_something, prepend: true do
  throw(:abort) if errors.present?
end

private

def ensure_something
  errors.add(:field, "This isn't a good idea..") if something_bad
end

validate :validate_test, on: :destroy doesn't work: https://github.com/rails/rails/issues/32376

Since Rails 5 throw(:abort) is required to cancel execution: https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain

prepend: true is required so that dependent: :destroy doesn't run before the validations are executed: https://github.com/rails/rails/issues/3458

You can fish this together from other answers and comments, but I found none of them to be complete.

As a sidenote, many used a has_many relation as an example where they want to make sure not to delete any records if it would create orphaned records. This can be solved much more easily:

has_many :entities, dependent: :restrict_with_error

thisismydesign
  • 21,553
  • 9
  • 123
  • 126
  • A small improvement: `before_destroy :handle_destroy, prepend: true; before_destroy { throw(:abort) if errors.present? }` will allow errors from other before_destroy validations to pass through instead of ending the destroy process immediately – Paul Odeon Nov 17 '20 at 15:13
6

The ActiveRecord associations has_many and has_one allows for a dependent option that will make sure related table rows are deleted on delete, but this is usually to keep your database clean rather than preventing it from being invalid.

Darth Egregious
  • 18,184
  • 3
  • 32
  • 54
go minimal
  • 1,693
  • 5
  • 25
  • 42
  • 1
    Another way to take care of underscores, if they're part of a function name or similar, is to wrap them in backticks. That will display then as code, `like_so`. – Richard Jones Feb 01 '13 at 01:37
  • Thank you. Your answer led me to another search about **types of dependent** option that was answered here: https://stackoverflow.com/a/25962390/3681793 – bonafernando Jun 11 '19 at 21:53
  • There're also `dependent` options which do not allow the removal of an entity if it would create orphaned records (this is more relevant to the question). E.g. `dependent: :restrict_with_error` – thisismydesign Nov 26 '19 at 10:27
5

You can wrap the destroy action in an "if" statement in the controller:

def destroy # in controller context
  if (model.valid_destroy?)
    model.destroy # if in model context, use `super`
  end
end

Where valid_destroy? is a method on your model class that returns true if the conditions for destroying a record are met.

Having a method like this will also let you prevent the display of the delete option to the user - which will improve the user experience as the user won't be able to perform an illegal operation.

MrYoshiji
  • 54,334
  • 13
  • 124
  • 117
Toby Hede
  • 36,755
  • 28
  • 133
  • 162
  • 1
    good catch, but I was assuming this method is in the controller, deferring to the model. If it was in the model would definitely cause issues – Toby Hede Mar 16 '11 at 23:30
  • hehe, sorry bout that... I see what you mean, I just saw "method on your model class" and quickly thought "uh oh", but you're right - destroy on the controller, that would work fine. :) – jenjenut233 Mar 17 '11 at 21:42
  • all good, in fact better to be very clear rather than make some poor beginner's life difficult with poor clarity – Toby Hede Mar 17 '11 at 23:57
  • 1
    I thought about doing it in the Controller as well, but it really does belong on the Model so that objects cannot be destroyed from the console or any other Controller that might need to destroy those objects. Keep it DRY. :) – Joshua Pinter Jul 15 '18 at 02:58
  • That being said, you can still use your `if` statement in the `destroy` action of your Controller, except instead of calling `if model.valid_destroy?`, just call `if model.destroy` and let the model handle whether the destroy was successful, etc. – Joshua Pinter Jul 15 '18 at 02:59
4

I ended up using code from here to create a can_destroy override on activerecord: https://gist.github.com/andhapp/1761098

class ActiveRecord::Base
  def can_destroy?
    self.class.reflect_on_all_associations.all? do |assoc|
      assoc.options[:dependent] != :restrict || (assoc.macro == :has_one && self.send(assoc.name).nil?) || (assoc.macro == :has_many && self.send(assoc.name).empty?)
    end
  end
end

This has the added benefit of making it trivial to hide/show a delete button on the ui

Hugo Forte
  • 5,718
  • 5
  • 35
  • 44
3

You can also use the before_destroy callback to raise an exception.

Matthias Winkelmann
  • 15,870
  • 7
  • 64
  • 76
2

I have these classes or models

class Enterprise < AR::Base
   has_many :products
   before_destroy :enterprise_with_products?

   private

   def empresas_with_portafolios?
      self.portafolios.empty?  
   end
end

class Product < AR::Base
   belongs_to :enterprises
end

Now when you delete an enterprise this process validates if there are products associated with enterprises Note: You have to write this in the top of the class in order to validate it first.

k_g
  • 4,333
  • 2
  • 25
  • 40
Mateo Vidal
  • 337
  • 2
  • 4
2

Use ActiveRecord context validation in Rails 5.

class ApplicationRecord < ActiveRecord::Base
  before_destroy do
    throw :abort if invalid?(:destroy)
  end
end
class Ticket < ApplicationRecord
  validate :validate_expires_on, on: :destroy

  def validate_expires_on
    errors.add :expires_on if expires_on > Time.now
  end
end
swordray
  • 833
  • 9
  • 21
1

I was hoping this would be supported so I opened a rails issue to get it added:

https://github.com/rails/rails/issues/32376

ragurney
  • 424
  • 5
  • 16