Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

HABTM join tables cleared on destroy even if a before_destroy says don't

#6562

Given the following models:


class Disesase < ActiveRecord::Base
  has_and_belongs_to_many :fleas
  before_destroy :not_if_fleas_still_carry_it

  protected
  def not_if_fleas_still_carry_it
    raise ActiveRecord::Rollback unless fleas.empty?
  end
end

class Flea < ActiveRecord::Base
  has_and_belongs_to_many :diseases
end

Then if we run the following code


plague = Disease.create
plague.fleas << Flea.create
plague.fleas << Flea.create

plague.fleas.reload.empty? #=> false

plague.destroy
plague.destroyed? # => false

plague.fleas.reload.empty? #=> true :(

e.g. a before_destroy that halts destruction of a model instance doesn't halt the clearing of the habtm join table for that instance. The current implementation will only halt the clearing of the join table if the before_destroy throws an exception other than ActiveRecord::Rollback which is not the standard way of doing things.

This is mildly related to #6191 which fixed it so that the join table wasn't cleared before before_destroy callbacks were run. The attached patch moves the clearing into an after_destroy instead of redefining the destroy method.

Reported by Murray Steele · March 11th, 2011 @ 12:37 PM

State: committed
Milestone: 3.0.6
Assigned to: Aaron Patterson Aaron Patterson
Importance: Low

Activity

  1. Santiago Pastorino
    Santiago Pastorino
    • Assigned user set to Aaron Patterson
    • Importance changed from to Low

    March 11th, 2011 @ 02:27 PM

  2. Murray Steele
    Murray Steele

    I've also attached is a patch for 3-0-stable (the original patch is for master)

    March 11th, 2011 @ 02:55 PM

  3. Murray Steele
    Murray Steele
    • No changes were found…

    March 11th, 2011 @ 02:56 PM

  4. Aaron Patterson
    Aaron Patterson
    • State changed from new to needs-more-info

    It seems this patch changes the current behavior such that the exception isn't re-raised. Can you possibly provide an implementation that doesn't change the existing test?

    I'm OK to change the behavior of the existing test, but not in a bugfix release.

    March 22nd, 2011 @ 10:14 PM

  5. Kane
    Kane

    It seems this patch changes the current behavior such that the exception isn't re-raised.

    isnt this the expceted behaviour if ActiveRecord::Rollback is raised?

    ActiveRecord::Transactions::ClassMethods.transaction uses this exception to distinguish a deliberate rollback from other exceptional situations. Normally, raising an exception will cause the transaction method to rollback the database transaction and pass on the exception. But if you raise an ActiveRecord::Rollback exception, then the database transaction will be rolled back, without passing on the exception.

    Also im concerned with the order of destruction.
    Destroying the associated objects after the destroy violates foreign key constraints as stated in #4386.
    We seem to have a major inconsistency between has_many and has_and_belongs_to_many. In has_many the dependant destroy is hooked as a before_callback

    rails / activerecord / lib / active_record / associations / builder / has_many.rb

    23 model.before_destroy dependency_method_name

    and has_and_belongs_to_many runs it afterwards

    rails / activerecord / lib / active_record / associations / builder / has_and_belongs_to_many.rb

    23 super # super

    24 #{name}.clear # posts.clear

    I thing the goal should be to do the destruction of the associated objects after the before_destroy callbacks of the model and before the actual destroy, so before_destroy callbacks dont see any change and fk constraints can be honored.

    March 23rd, 2011 @ 01:00 AM

  6. Murray Steele
    Murray Steele

    Attached reworked versions of both patches.

    It seems this patch changes the current behavior such that the exception isn't re-raised. Can you possibly provide an implementation that doesn't change the existing test?

    I'm OK to change the behavior of the existing test, but not in a bugfix release.

    Aaron - I've redone the first commit in both patches to not change the behaviour of the Lesson object so it's before_destroy still raises a LessonError and added a (for this test only) before_destroy to Student that raises an ActiveRecord::Rollback and then tested both cases. I think that should address your concerns, although adding a one-time before_destroy callback to Student is ugly - I'm happy to change that to something better if I can without adding a new model.

    I thing the goal should be to do the destruction of the associated objects after the before_destroy callbacks of the model and before the actual destroy, so before_destroy callbacks dont see any change and fk constraints can be honored.

    Kane - I think this would be a worthy goal, but I don't know how to tackle it. I'll have a think about how we might ensure our before_destroy is always the last before_destroy. However, should wanting to fix foreign key constraints hold up fixing this bug? Maybe it's a separate issue, or we just need to make sure that whatever fixes #4386 also addresses what we've done here? Aaron?

    March 23rd, 2011 @ 12:32 PM

  7. Murray Steele
    Murray Steele

    ps. I removed the old patches so we don't get confused (as they had the same names). I hope this isn't bad form.

    March 23rd, 2011 @ 12:34 PM

  8. Kane
    Kane

    You are right, this is a seperate issue or should be addressed in #4386.

    March 23rd, 2011 @ 02:24 PM

  9. Aaron Patterson
    Aaron Patterson
    • State changed from needs-more-info to open

    @Murray awesome, thank you. I think the foreign key fix should be dealt with in the other ticket. :-)

    March 23rd, 2011 @ 09:30 PM

  10. Aaron Patterson
    Aaron Patterson
    • State changed from open to committed
    • Milestone set to 3.0.6

    Applied and pushed. Thanks a bunch!

    March 23rd, 2011 @ 09:49 PM