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