Rails: how to disable before_destroy callback when it's being destroyed because of the parent is being destroyed (:dependent => :destroy)

前端 未结 5 1537
伪装坚强ぢ
伪装坚强ぢ 2021-02-05 06:06

I have two classes: Parent and Child with

Child:

belongs_to :parent

and

Parent

has_many :children, :dependent          


        
相关标签:
5条回答
  • 2021-02-05 06:28

    In Rails 4 you can do the following:

    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 false
      end
    end
    

    This way, when calling destroy directly on a child, the check_destroy_allowed callback will be run, but when you call destroy on the parent, it won't.

    0 讨论(0)
  • 2021-02-05 06:33

    carp's answer above will work if you set prepend to true on the before_destroy method. Try this:

    Child:

    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
    

    Parent:

    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
    

    We had to do this because we're using Paranoia, and dependent: delete_all would hard-delete rather than soft-delete them. My gut tells me there's a better way to do this, but it's not obvious, and this gets the job done.

    0 讨论(0)
  • 2021-02-05 06:44
    has_many :childs, :dependent => :delete_all
    

    This will delete all the children without running any hooks.

    You can find the documentation at: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many

    0 讨论(0)
  • 2021-02-05 06:46

    The accepted answer does not solve the original problem. Jose wanted 2 things:

    1) To ensure that the Parent always has at least one child

    and

    2) To be able to delete all children when the Parent is deleted

    You do not need any before_destroy callbacks to prevent the deletion of a child.

    I wrote a detailed blog post describing the solution, but I'll cover the basics here as well.

    The solution includes various ingredients: the use of presence validation and nested attributes in the Parent model, and making sure that the method that deletes the child doesn't call .destroy on the child, but that the child is deleted from the Parent model via nested attributes.

    In the Parent model:

    attr_accessible :children_attributes
    
    has_many :children, dependent: :destroy
    accepts_nested_attributes_for :children, allow_destroy: true
    validates :children, presence: true
    

    In the child model:

    belongs_to :parent
    

    Next, the easiest way to allow children to be deleted, except for the last one, is to use nested forms, as covered in Railscasts #196. Basically, you would have one form with fields for both the Parent and the Children. Any updates to the Location, as well as the Children, including the deletion of children, would be processed by the update action in the Parent Controller.

    The way you delete a child via nested forms is by passing in a key called _destroy with a value that evaluates to true. The allow_destroy: true option we set in the Parent model is what allows this. The documentation for Active Record Nested Attributes covers this, but here's a quick example that shows how you would delete a Child whose id equals 2 from its Parent:

    parent.children_attributes = { id: '2', _destroy: '1' }
    parent.save
    

    Note that you don't need to do this yourself in the Parent Controller if you're using nested forms as in Railscasts #196. Rails takes care of it for you.

    With the presence validation in the Parent model, Rails will automatically prevent the last child from being deleted.

    I think that at the time Jose posted his question, the presence validation was not working the way it was supposed to. It wasn't fixed until July of 2012 with this pull request, but that was almost 2 years ago. Seeing dbortz post his outdated solution 12 days ago made me realize that there is still confusion about this issue, so I wanted to make sure to post the correct solution.

    For an alternate solution that doesn't use nested forms, see my blog post: http://www.moncefbelyamani.com/rails-prevent-the-destruction-of-child-object-when-parent-requires-its-presence/

    0 讨论(0)
  • 2021-02-05 06:49

    There's probably a way to accomplish this in a less hacky fashion, but here's an (untested!) idea: add an attr_accessor :destroyed_by_parent to Child and edit Child's before_destroy filter to allow the destroy when it's true.

    Add a before_destroy filter to Parent that iterates over all its children:

    private
    
    # custom before_destroy
    def set_destroyed_by_parent
      self.children.each {|child| child.destroyed_by_parent = true }
    end
    

    Provided that the destroy triggered by :dependent => :destroy is executed on the instanced children of the Parent object, it could work. If it instantiates the children separately, it won't work.

    0 讨论(0)
提交回复
热议问题