Rails: как отключить обратный вызов before_destroy, когда он уничтожается из-за родителя, уничтожается (: dependent =>: destroy)
У меня есть два класса: родительский и дочерний с
Ребенок:
belongs_to :parent
и
Родитель
has_many :children, :dependent => :destroy
Проблема в том, что я хочу проверить, что всегда присутствует хотя бы один дочерний элемент, поэтому у меня есть метод before_destroy в Child, который прерывает уничтожение, если он является единственным дочерним элементом, принадлежащим его родительскому объекту.
И, если я хочу уничтожить родителя, он вызовет обратный вызов before_destroy для каждого дочернего элемента, но когда есть один ребенок, он прервет уничтожение, поэтому родитель никогда не будет уничтожен.
Как я могу сказать ребенку вызвать обратный вызов before_destroy, только если он не уничтожается из-за его родителя?
Спасибо!
Ответы
Ответ 1
has_many :childs, :dependent => :delete_all
Это приведет к удалению всех дочерних элементов без каких-либо перехватов.
Вы можете найти документацию по адресу: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
Ответ 2
Ответ carp выше будет работать, если вы установите preend true на метод before_destroy. Попробуйте следующее:
Ребенок:
belongs_to :parent
before_destroy :prevent_destroy
attr_accessor :destroyed_by_parent
...
private
def prevent_destroy
if !destroyed_by_parent
self.errors[:base] << "You may not delete this child."
return false
end
end
Родитель:
has_many :children, :dependent => :destroy
before_destroy :set_destroyed_by_parent, prepend: true
...
private
def set_destroyed_by_parent
children.each{ |child| child.destroyed_by_parent = true }
end
Мы должны были сделать это, потому что мы используем Paranoia, и dependent: delete_all
будет удалять жесткие, а не soft-delete. Моя кишка говорит мне, что есть лучший способ сделать это, но это не очевидно, и это выполняет свою работу.
Ответ 3
В Rails 4 вы можете сделать следующее:
class Parent < AR::Base
has_many :children, dependent: :destroy
end
class Child < AR::Base
belongs_to :parent
before_destroy :check_destroy_allowed, unless: :destroyed_by_association
private
def check_destroy_allowed
# some condition that returns true or falls
end
end
Таким образом, при вызове destroy
непосредственно на дочернем сервере будет выполняться обратный вызов check_destroy_allowed
, но когда вы вызываете destroy
в родительском объекте, это не будет.
Ответ 4
Вероятно, есть способ сделать это менее взломанным способом, но здесь (непроверенная!) идея: добавьте attr_accessor :destroyed_by_parent
в Child
и отредактируйте фильтр Child before_destroy, чтобы разрешить уничтожить, когда он true
.
Добавьте фильтр before_destroy к Parent
, который выполняет итерацию по всем своим дочерним элементам:
private
# custom before_destroy
def set_destroyed_by_parent
self.children.each {|child| child.destroyed_by_parent = true }
end
При условии, что уничтожение, вызванное :dependent => :destroy
, выполняется на дочерних объектах родительского объекта, оно может работать. Если он создает экземпляры отдельно для детей, это не сработает.
Ответ 5
Принятый ответ не решает исходную проблему. Хосе хотел 2 вещи:
1) Чтобы гарантировать, что у родителя всегда есть хотя бы один дочерний
и
2) Чтобы иметь возможность удалять всех дочерних элементов при удалении родителя
Вам не нужны обратные вызовы before_destroy
, чтобы предотвратить удаление дочернего элемента.
Я написал подробное сообщение в блоге, описывающее решение, но я также расскажу об основах.
Решение включает в себя различные ингредиенты: использование проверки присутствия и вложенных атрибутов в родительской модели и обеспечение того, чтобы метод, который удаляет дочерний элемент, не вызывает .destroy
для дочернего элемента, но его дочерние элементы удаляются из родительскую модель через вложенные атрибуты.
В родительской модели:
attr_accessible :children_attributes
has_many :children, dependent: :destroy
accepts_nested_attributes_for :children, allow_destroy: true
validates :children, presence: true
В дочерней модели:
belongs_to :parent
Далее, самый простой способ разрешить удаление детей, за исключением последнего, - использовать вложенные формы, как описано в Railscasts # 196. В принципе, у вас будет одна форма с полями для родителей и детей. Любые обновления для местоположения, а также детей, включая удаление дочерних элементов, будут обрабатываться действием update
в родительском контроллере.
То, как вы удаляете ребенка через вложенные формы, - это передать ключ с именем _destroy
со значением, которое равно true. Опция allow_destroy: true
, которую мы устанавливаем в родительской модели, позволяет это сделать. Документация для Active Record Nested Attributes описывает это, но здесь приведен краткий пример, показывающий, как удалить ребенка, чей id
равно 2
от родителя:
parent.children_attributes = { id: '2', _destroy: '1' }
parent.save
Обратите внимание, что вам не нужно делать это самостоятельно в родительском контроллере, если вы используете вложенные формы, как в Railscasts # 196. Rails заботится об этом для вас.
При проверке наличия в родительской модели Rails автоматически предотвратит удаление последнего дочернего элемента.
Я думаю, что в то время, когда Хосе опубликовал свой вопрос, валидация присутствия не работала так, как предполагалось. Это не было зафиксировано до июля 2012 года с помощью этого запроса на тягу, но это было почти 2 года назад. Видя, что dbortz опубликовал свое устаревшее решение за 12 дней назад, я понял, что до сих пор существует путаница в этой проблеме, поэтому я хотел убедиться, что вы опубликуете правильное решение.
Для альтернативного решения, которое не использует вложенные формы, см. мой пост в блоге: http://www.moncefbelyamani.com/rails-prevent-the-destruction-of-child-object-when-parent-requires-its-presence/