How do I 'validate' on destroy in rails
Ruby on-RailsRubyCallbackRuby 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: