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

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 Biedenharn http://agileconsultingllc.com Rob@AgileConsultingLLC.com

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.

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)

Fred

Sorry, ignore the line:

        format.xml { head :bad_request }

return something more appropriate for the XML format when errors occur.

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.

Aaron

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

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

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

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

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

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