При уничтожении восстанавливаемого ресурса я хочу гарантировать несколько вещей, прежде чем я позволю продолжить операцию уничтожения? В принципе, мне нужна возможность остановить операцию уничтожения, если я замечу, что это приведет к тому, что база данных перейдет в недопустимое состояние? Для операции уничтожения нет обратных вызовов проверки, так как же «проверить», следует ли принять операцию уничтожения?





Вы можете вызвать исключение, которое затем поймаете. Rails оборачивает операции удаления в транзакции, что помогает в этом.
Например:
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
В качестве альтернативы вы можете использовать обратный вызов before_destroy. Этот обратный вызов обычно используется для уничтожения зависимых записей, но вместо этого вы можете создать исключение или добавить ошибку.
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 теперь вернет false, а myBooking.errors будет заполнен при возврате.
Обратите внимание, что там, где теперь написано «... хорошо, продолжайте и уничтожайте», вам нужно поставить «super», чтобы фактически вызвался исходный метод уничтожения.
errors.add_to_base устарела в Rails 3. Вместо этого вы должны сделать errors.add (: base, "message").
Rails не проверяет перед уничтожением, поэтому before_destroy должен вернуть false, чтобы отменить уничтожение. Просто добавлять ошибки бесполезно.
С Rails 5 false в конце before_destroy бесполезен. С этого момента вы должны использовать throw(:abort) (@see: weblog.rubyonrails.org/2015/1/10/This-week-in-Rails/…).
Ваш пример защиты от потерянных записей может быть решен намного проще с помощью has_many :booking_payments, dependent: :restrict_with_error.
Вы также можете использовать обратный вызов before_destroy, чтобы вызвать исключение.
Связи ActiveRecord has_many и has_one позволяют использовать зависимую опцию, которая будет обеспечивать удаление связанных строк таблицы при удалении, но обычно это делается для того, чтобы ваша база данных была чистой, а не для предотвращения ее недействительности.
Другой способ позаботиться о подчеркивании, если они являются частью имени функции или подобного, - заключить их в обратные кавычки. Тогда это будет отображаться как код like_so.
Спасибо. Ваш ответ привел меня к еще одному поиску варианта типы зависимых, на который ответили здесь: stackoverflow.com/a/25962390/3681793
Существуют также параметры dependent, которые не позволяют удалить объект, если он создаст потерянные записи (это более актуально для вопроса). Например. dependent: :restrict_with_error
Вы можете заключить действие уничтожения в оператор «if» в контроллере:
def destroy # in controller context
if (model.valid_destroy?)
model.destroy # if in model context, use `super`
end
end
Где valid_destroy? - это метод класса модели, который возвращает истину, если выполняются условия для уничтожения записи.
Наличие такого метода также позволит вам предотвратить отображение опции удаления для пользователя, что улучшит взаимодействие с пользователем, поскольку пользователь не сможет выполнить незаконную операцию.
хороший улов, но я предполагал, что этот метод находится в контроллере, полагаясь на модель. Если бы это было в модели, определенно вызвали бы проблемы
хе-хе, извините за это ... Я понимаю, что вы имеете в виду, я только что увидел "метод в вашем классе модели" и быстро подумал "ах, ах", но вы правы - уничтожьте на контроллере, это сработает. :)
все хорошо, на самом деле лучше быть предельно ясным, чем усложнять жизнь какого-то бедного новичка плохой ясностью
Я тоже думал о том, чтобы сделать это в Контроллере, но он действительно принадлежит Модели, поэтому объекты не могут быть уничтожены с консоли или любого другого Контроллера, которому может потребоваться уничтожить эти объекты. Держите это СУХОЕ. :)
При этом вы все равно можете использовать свой оператор if в действии destroy вашего контроллера, за исключением того, что вместо вызова if model.valid_destroy? просто вызовите if model.destroy и позвольте модели определить, было ли уничтожение успешным и т. д.
просто примечание:
Для рельсов 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
Проблема с этим подходом заключается в том, что обратный вызов before_destroy, кажется, называется после, все booking_payments были уничтожены.
Связанный билет: github.com/rails/rails/issues/3458 @sunkencity, вы можете объявить before_destroy перед объявлением ассоциации, чтобы временно избежать этого.
Ваш пример защиты от потерянных записей может быть решен намного проще с помощью has_many :booking_payments, dependent: :restrict_with_error.
Согласно руководству rails, обратные вызовы before_destroy могут и должны быть размещены перед ассоциациями с зависимым_destroy; это вызывает обратный вызов до того, как будут вызваны связанные разрушения: guides.rubyonrails.org/…
В итоге я использовал код отсюда, чтобы создать переопределение can_destroy на 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
Это имеет дополнительное преимущество, позволяющее упростить скрытие / отображение кнопки удаления в пользовательском интерфейсе.
У меня есть эти классы или модели
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
Теперь, когда вы удаляете предприятие, этот процесс проверяет, есть ли продукты, связанные с предприятиями. Примечание: вы должны написать это в верхней части класса, чтобы сначала проверить его.
Это то, что я сделал с 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
Это хорошая статья, объясняющая такое поведение в Rails 5: blog.bigbinary.com/2016/02/13/…
Ваш пример защиты от потерянных записей может быть решен намного проще с помощью has_many :qrcodes, dependent: :restrict_with_error.
Используйте проверку контекста ActiveRecord в 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
Вы не можете проверить on: :destroy, см. Эта проблема
Я надеялся, что это будет поддерживаться, поэтому я открыл проблему с рельсами, чтобы добавить его:
Положение дел по состоянию на Rails 6:
Это работает:
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 не работает: https://github.com/rails/rails/issues/32376
Поскольку для отмены выполнения требуется Rails 5, throw(:abort): https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain
prepend: true требуется, чтобы dependent: :destroy не запускался до выполнения проверки: https://github.com/rails/rails/issues/3458
Вы можете выудить это вместе из других ответов и комментариев, но я не нашел ни одного из них полным.
В качестве примечания многие использовали отношение has_many в качестве примера, когда они хотят убедиться, что не удаляют какие-либо записи, если это приведет к созданию потерянных записей. Это можно решить гораздо проще:
has_many :entities, dependent: :restrict_with_error
Небольшое улучшение: before_destroy :handle_destroy, prepend: true; before_destroy { throw(:abort) if errors.present? } позволит пропускать ошибки от других проверок before_destroy вместо немедленного завершения процесса уничтожения.
Связанный: stackoverflow.com/questions/5520320/validate-before-destroy