How do I 'validate' on destroy in rails

Ruby on-RailsRubyCallback

Ruby on-Rails Problem Overview


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?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

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.

Solution 2 - Ruby on-Rails

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

Solution 3 - Ruby on-Rails

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

Solution 4 - Ruby on-Rails

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

Solution 5 - Ruby on-Rails

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.

Solution 6 - Ruby on-Rails

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.

Solution 7 - Ruby on-Rails

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

Solution 8 - Ruby on-Rails

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

Solution 9 - Ruby on-Rails

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.

Solution 10 - Ruby on-Rails

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

Solution 11 - Ruby on-Rails

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

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionStephen CagleView Question on Stackoverflow
Solution 1 - Ruby on-RailsAirsource LtdView Answer on Stackoverflow
Solution 2 - Ruby on-RailsworkdreamerView Answer on Stackoverflow
Solution 3 - Ruby on-RailsRaphael MonteiroView Answer on Stackoverflow
Solution 4 - Ruby on-RailsthisismydesignView Answer on Stackoverflow
Solution 5 - Ruby on-Railsgo minimalView Answer on Stackoverflow
Solution 6 - Ruby on-RailsToby HedeView Answer on Stackoverflow
Solution 7 - Ruby on-RailsHugo ForteView Answer on Stackoverflow
Solution 8 - Ruby on-RailsMatthias WinkelmannView Answer on Stackoverflow
Solution 9 - Ruby on-RailsMateo VidalView Answer on Stackoverflow
Solution 10 - Ruby on-RailsswordrayView Answer on Stackoverflow
Solution 11 - Ruby on-RailsragurneyView Answer on Stackoverflow