test case to illustrate callbacks definitions and HABTM association are order dependent #8674

wants to merge 1 commit into


None yet

5 participants

senny commented Jan 1, 2013


The source issue is: #8672

This PR is only a failing test case as I am not completely sure how to tackle the problem.

I investigated and can confirm that the order in which after_create and has_and_belongs_to_many associations are defined does impact the functionality.

If the callback is defined before the association every HABTM record created inside the callback will be inserted twice. The reason is AutosaveAssociation#save_collection_association. It will pick up the already created records and save them again. This happens because @new_record_before_save will be true and since it's an after_create callback, the associations are inserted directly with <<.

My test case results in the following failure:

  1) Failure:
test_after_create_before_habtm_definition(HasAndBelongsToManyAssociationsTest) [test/cases/associations/has_and_belongs_to_many_associations_test.rb:616]:
Expected: 1
  Actual: 2
senny commented Jan 1, 2013

@jonleighton @rafaelfranca is it known and expected that HABTM associations and callbacks are order dependent? If this is the expected behavior, feel free to close. Otherwise we should look for a possible solution.


Definitely looks like unexpected behaviour to me.


No clue if I can squash this or not, but I'm starting to investigate. I'll update in here with whatever I find, at the least.

JonRowe commented Mar 12, 2013

Was any progress made on this?


I'll try to spend a little more time on it tonight, but no promises if I'll get anywhere. I was just trying to figure it out when I had to give up on it last time.


Status update: spent hours learning all the surrounding code, figured out that @senny is right on the money. No idea for a fix yet, but I'll give that a stab tomorrow.

senny commented Mar 13, 2013

I was thinking about this issue lately and it could be a tricky one to solve. Let me know if you find an angle.


@senny the problem is that AutosaveAssociation is using the public callback API to implement its functionality which means that it's subject to the definition order. There used to be a lot of these problems but most of them went away when @jonleighton refactored the association code.

The belongs_to :foo, :touch => true code is also using the callbacks api so that may be definition order dependent as well.


I basically came to the same conclusion as @pixeltrix. I think the best solution is to have separate callback chains for these purposes. Here's why:

  1. If we try to make save_collection_association smart enough to handle this situation, it'll be really ugly. The hack is certainly possible, but it's inexcusably ugly.
  2. If we try to force the right ordering by some sort of special case, that doesn't help at all in solving other instances where there might be problems of this sort.

The downsides I can see:

  1. By default, all callback chains are publicly exposed. If people start using the callback chains we add in their code, these sorts of problems could happen again. Then it turns into a sort of arms race for which callback chain is REALLY first or last.
  2. Aside from the public use scenario, in general proliferation of callback chains inside the same callback management code seems inelegant to me.

One final alternative that presents itself to me is to have an entirely separate, not publicly exposed, set of callback chains. Of course, this could be enforced more by code (of course, anyone can actually access them if they really want) or more by convention (documented as "don't touch unless you really know what you're doing"). A "separate set" is flexible concept, of course, so it could mean separate objects managing it, or a conceptually distinct set, managed by the same code, but distinct by convention.

I'm working on the separate by convention approach right now to see how the code turns out. If everyone is radically opposed to that, I'll stop and move on to something else.

senny commented Jul 16, 2013

I'm closing this one as it is just a test-case without a fix. It will remain linked in the issue.

@senny senny closed this Jul 16, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment