I have a Category model that uses acts_as_tree. So the subcategories
of a category are accessible via Category#children. The categories
table has the requisite parent_id column. It all works quite nicely.
I'm trying to prevent the deletion of a category if it has
subcategories. So I thought I'd specify a before_destroy callback and
return false if the Category instance had "children.length > 1". Well
it wasn't working -- the parent isn't getting destroyed, but the sub
is.
When debugging the code when attempting to delete a parent category, I
noticed that my before_destroy callback was being called TWICE --
first for the subcategory of the parent and THEN for the parent. I'm
hypothesizing that acts_as_tree sets up the dependency so that
children are automatically destroyed when their parents are
destroyed. That probably explains why before_destroy is being called
twice and in that order (sub then parent).
Regardless, I'm returning true from before_destroy when invoked on the
sub (since it has no subcategories) and then returning false from
before_destroy when invoked on its parent. Unfortunately, although
I'm halting the destruction of the parent, I'm not halting that of the
sub.
I thought that, perhaps, I should raise and Exception rather than
return false from before_destroy, but that didn't change things. Does
anyone know how to properly handle this scenario?
class Category < ActiveRecord::Base
before_destroy :ensure_no_children
acts_as_tree
def ensure_no_children
unless self.children.empty?
self.errors.add_to_base "Can't destroy a category having subcategories"
raise ActiveRecord::Rollback
end
end
end
If you raise any other kind of exception, you have to rescue it yourself
begin
@category.destory
rescue MyException => e
flash[:error] = e.message
redirect_to "_somewhere_safe_"
end
An alternative that I have been experimenting with and seems to work:
class Person < ActiveRecord::Base
def before_destroy
# add whatever logic you want here...
errors.add_to_base("Woops! Can't destroy that person.")
false
end
end
class PeopleController < ApplicationController
...
...
if @person.destroy
respond_to do |format|
format.html { redirect_to(people_url) }
format.xml { head :ok }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.xml { head :bad_request }
end
end
end
end
This is similar to Rob's solution, but does not involve catching
exceptions. Also the responsibility of error display is back with the
views rather than the controller.
Note that returning false from before_destroy cancels the call backs
before the record is destroyed preventing the deletion of the person.
As far as I can tell this seems to work nicely, but I would love some
feedback in case I've overlooked something.
I have a Category model that uses acts_as_tree. So the subcategories
of a category are accessible via Category#children. The categories
table has the requisite parent_id column. It all works quite nicely.
I'm trying to prevent the deletion of a category if it has
subcategories. So I thought I'd specify a before_destroy callback and
return false if the Category instance had "children.length > 1". Well
it wasn't working -- the parent isn't getting destroyed, but the sub
is.
Why not just use a foreign key constraint (and then you don't have to
worry about category.children having changed behind your back)
Thanks to all of you. I think I took a hybrid approach. I am
throwing an ActiveRecord::Rollback exception -- which does prevent
both records from being destroyed. This exception must be swallowed
up by the infrastructure; it is not rethrown to my controller (just as
Rob implied). So the next task was to detect if the destroy
succeeded. At first I examined the errors collection on the
ActiveRecord object (that worked), but then Roberts snippet clued me
in to the fact that destroy returns a boolean status value (is that in
the docs anywhere)?
Incidentally, I agree that the database should also have the foreign
key constraints. I'd like to put them in a db migration, but I can't
seem to find the syntax...
Just when I thought I had it licked... The technique described above
works fine in development. But I have one unit test failing (it's
deleting the sub but not the parent). I wonder if the handling of the
ActiveRecord::Rollback exception is different between the development
environment and the environment that is established when running unit
tests?
I think a more straight forward approach would be to override the
destroy method with something like this:
def destroy
super if children.blank?
end
As for the rollback code that works in development and breaks in
test. The rails test framework uses transactions and rollbacks to
reset the database after every test. This is likely changing the
behavior of your code while running in the test environment.
there are three simple solutions that while simple are not necessarily
pleasant.
the best i have come up with is find the acts_as_tree plugin in vendor
(or rails if your still in v1.x) and alter the method so you can specify
:dependent => :destroy on invocation rather than it hard coding.
the other two require either not using acts_as_tree but specifying
children and parent relations should you not be using full functionality
the third option is my favorite but was refuse from the core a few
months ago which is tho add a before/after_destroy_dependents hook into
active recored.
In this case I would suggest you try the first solution and wish you the
best of luck.
There's no magic rails support for this (i seem to recall there is a plugin that adds this, but I don't recall the name).
You can of course execute arbitrary sql in your migrations, so just write whatever your database requires, eg for mysql you could say
the third option is my favorite but was refuse from the core a few
months ago which is tho add a before/after_destroy_dependents hook into
active recored.
It seems odd to me that it has not been included in the :dependent
options, why not something like :dependent=>'constrained' which would
prohibit deletion if a dependent exists.