How to prevent a parent with children from being deleted

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?

Many thanks…

On Jan 16, 2008, at 5:33 PM, Decker wrote:

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?

Many thanks…

Since save and destroy are automatically wrapped in a transaction,
perhaps something like this:
http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#M001115

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

-Rob

Rob B. http://agileconsultingllc.com
[email protected]

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

DELETE /people/1

DELETE /people/1.xml

def destroy
@person = Person.find(params[:id])

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.

On Jan 16, 6:30 pm, Rob B. [email protected]

On 16 Jan 2008, at 22:33, Decker wrote:

Why not just use a foreign key constraint (and then you don’t have to
worry about category.children having changed behind your back)

Fred

Sorry, ignore the line:

    format.xml  { head :bad_request }

from my previous post. I’m sure that’s not really correct. It should
return something more appropriate for the XML format when errors
occur.

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.

Aaron

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…

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.

-K

On 17 Jan 2008, at 04:12, Decker wrote:

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…

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

execute “ALTER TABLE some_table ADD CONSTRAINT
fk_some_table_other_tables FOREIGN KEY (other_table_id) REFERENCES
other_tables(id)”

Fred

On Jan 17, 2008, at 12:03 PM, Frederick C. wrote:

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).

Yes, that’s foreign_key_migrations:

 http://www.redhillonrails.org/#foreign_key_migrations

We[*] use it in all projects whose database supports FKs.

– fxn

[*] ASPgems

support ticket http://dev.rubyonrails.org/ticket/10843 with a +1 in the
comments

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.

Tonypm

I had a similar problem. I wanted children categories to become deleted
record’s siblings, then destroy it.

A sensible solution is to add a method to your model to use instead of
destroy:

def soft_destroy
children.each {|ch| ch.update_attribute(:parent_id, parent ? parent.id
: nil) }
reload
destroy
end

I hope you find it useful…

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs