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

5

23

I have two classes: Parent and Child with

Child:

belongs_to :parent

and

Parent

has_many :children, :dependent => :destroy

The problem is that I want to check that there is always at least one child present, so I have a before_destroy method in Child that abort the destroy if it is the only child belonging to its parent.

And, if I want to destroy the parent, it will call the before_destroy callback on every child, but when there is one child, it will abort the destroy, so the parent will never get destroyed.

How can I tell the child to call the before_destroy callback only if it's not being destroyed because of its parent?

Thanks!

Frankpledge answered 25/1, 2012 at 9:45 Comment(2)
Shouldn't the Parent -> Child relation be named :children? I'm quite sure rails will understand that you're pointing to the Child model anyway.Kurtkurth
haha yeah... well, those are not the actual names, so it doesn't matter :) my fist language is not english, and i forgot the plural of child hahaFrankpledge
V
10
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

Voe answered 25/1, 2012 at 14:38 Comment(5)
Thanks! that worked, but I would like to know if there is any way to tell all the children that they are being destroyed from a :dependent => :destroy callback. Because there are other before_destroy callbacks on my Child class that I'd like to callFrankpledge
Jose, I suggest you pull your before_destroy callbacks out into methods with meaningful names. Add a new before_destroy callback which does something like: children.all { |child| child.do_something; child.do_something_else }. In other words, the parent can manually call those methods on its children before the parent is destroyed (and the children are deleted by :dependent => :delete_all)Voe
You can run into issues with this answer if child has has_many :grandchilds, :dependent => :destroy since the delete_all will not trigger the nested dependent destroys, and therefore appending the before_destroy callback with unless: :destroyed_by_association is a better solution.Niigata
@Niigata OK. Feel free to edit this answer to add additional information.Voe
@alexander answered belowNiigata
S
14

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.

Sparerib answered 10/8, 2016 at 12:56 Comment(1)
This also works in Rails 6.1Drislane
V
10
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

Voe answered 25/1, 2012 at 14:38 Comment(5)
Thanks! that worked, but I would like to know if there is any way to tell all the children that they are being destroyed from a :dependent => :destroy callback. Because there are other before_destroy callbacks on my Child class that I'd like to callFrankpledge
Jose, I suggest you pull your before_destroy callbacks out into methods with meaningful names. Add a new before_destroy callback which does something like: children.all { |child| child.do_something; child.do_something_else }. In other words, the parent can manually call those methods on its children before the parent is destroyed (and the children are deleted by :dependent => :delete_all)Voe
You can run into issues with this answer if child has has_many :grandchilds, :dependent => :destroy since the delete_all will not trigger the nested dependent destroys, and therefore appending the before_destroy callback with unless: :destroyed_by_association is a better solution.Niigata
@Niigata OK. Feel free to edit this answer to add additional information.Voe
@alexander answered belowNiigata
F
5

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.

Ferren answered 16/4, 2014 at 14:55 Comment(0)
S
1

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.

Swatter answered 25/1, 2012 at 13:54 Comment(1)
Unfortunately the children's before_destroy callbacks will be called before the parent's, so the parent won't have a chance to set the instance variable before the kids are massacred.Devon
H
1

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/

Hazlitt answered 28/4, 2014 at 4:22 Comment(2)
Hi Moncef, thank you for your answer. So bassically, I'd be able to solve this problem by using nested attributes and a method like remove_child(id) in which I update children_attributes with a _destroy set to true (or 1), and use it as the original child.destroy method, right? I think this is the right solution, but the best scenario would be where that's the default behavior of child.destroyFrankpledge
That's correct, but if you have a UI in your app with nested forms like in Railscasts #196, you don't have to create a separate method to remove the child. Rails does it automatically via the update action in the Parent. Watch Railscasts #196 to see how it works.Hazlitt

© 2022 - 2024 — McMap. All rights reserved.