我如何“验证”在轨道上的破坏

关于破坏静止资源,在我允许继续破坏行动之前,我想保证一些事情?基本上,如果我注意到这样做会使数据库处于无效状态,我希望能够停止销毁操作?销毁操作没有验证回调,那么如何“验证”是否应该接受销毁操作呢?

60623 次浏览

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.

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

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.

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.

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

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

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.

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

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

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

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

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