I have two classes: Parent and Child with
Child:
belongs_to :parent
and
Parent
has_many :children, :dependent
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.
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.
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
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/
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.