New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nested attributes with callbacks bugfix #5476
Nested attributes with callbacks bugfix #5476
Conversation
@@ -415,20 +415,14 @@ def assign_nested_attributes_for_collection_association(association_name, attrib | |||
unless reject_new_record?(association_name, attributes) | |||
association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) | |||
end | |||
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } | |||
unless association.loaded? || call_reject_if(association_name, attributes) | |||
elsif existing_record = begin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't use elsif existing_record = begin
. Could you break it apart?
This looks good but there are some style considerations to be done. From what I see, the code should be basically the same as before, except this line:
Should be rewritten to:
No? |
No! Aren't the two lines are equivalent, since "==" means same 'id'. I actually prefer the second version because it highlights the problem. Although "target_record == existing_record" they may be different objects, which is the root of the problem (IdentityMap could have helped ...). The problem with the previous code is the control structure. The lookup in the target cannot be constrained by "unless association.loaded?". So the alternative would be something like: elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
if target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
existing_record = target_record
else
association.target << existing_record # or association.add_to_target(existing_record)
end
if !call_reject_if(association_name, attributes)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy], assignment_opts)
end Do you like this better? |
Yes, that's what I had in mind. But please break this in two lines:
And continue using |
I see you have closed this, could you send a new pull request please? Thanks! |
I didn't mean to close - hit the wrong button. I don't agree about the semantics. Shouldn't an association with the target loaded from a previous call like this owner.associaton.to_a
owner.attributes = hash_with_nested_attributes call the callbacks the same number of times as just the nested attribute assignment: # target empty here
owner.attributes = hash_with_nested_attributes In the current implementation the callback is called in the second version but not in the first. And by the way, in the application it may not be obvious which of the two situations apply. Since the loading can happen at some completely different place. This action at a distance has made my job in hunting this down a lot more difficult. If we keep the current behavior, the implementation of the callback needs to check if the record is already in the association or if it is a really new record - which is short of yet another database lookup not quite so straightforward. |
I don't quite understand one of your comments:
The line above does the same thing:
So would you like
or split the "if" across two lines!?? |
Yeah, this:
Sorry for not making it clear before. :) |
OK. What about the comment above about "before_add callback" being called dependend on the association being loaded? I added a couple of tests (see commit below) highlighting the inconsistent behavior: With the current implementation the :before_add callback is called when a nested attribute assignment modifies an existing record if the association is not loaded before. The callback is not called if the association is loaded. I.e. on current state of master the last test fails, the one before does not. |
|
||
test "Assignment to nested attributes for existing record does not trigger callback when association is loaded" do | ||
@pirate_with_more_than_one_bird.birds_with_non_interfering_callback.load_target | ||
assert_equal(true,@pirate_with_more_than_one_bird.birds_with_non_interfering_callback.loaded?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test passes with the current implementation in master, because the association is loaded.
I just did a little research, when the second issue (about calling the before_add callback) was introduced into the code base. It was a side effect of the following commit: "Ensure not to load the entire association when bulk updating existing records using nested attributes" It explicitly adds a call to "add_to_target_with_callback" for an existing record. |
Is this still being worked on? |
@@ -416,17 +416,15 @@ def assign_nested_attributes_for_collection_association(association_name, attrib | |||
association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) | |||
end | |||
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } | |||
unless association.loaded? || call_reject_if(association_name, attributes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also ignore call_reject_if
? What if I am updating the record to a state that should be rejected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
call_reject_if
is called later in the same branch to decide if assign_to_or_mark_for_destruction
is called or not.
The lines following here deal with the existing record and its occurrence in the target array. This intention is obscured by checking call_reject_if
.
@josevalim what's the status of this one? ^^ |
Ping! We will need a rebase. |
Hi there! Seems like roughly another 4 months are over... I am sorry - I don't have the capacity to work on this right now - swamped by RL. Just in case - what's the procedure of a "rebase"? From what I understood, the procedure to include changes in master in a pull request goes like this:
If I instead rebase my local branch on master, I wouldn't be able to push to the same branch on github any more, since it's no longer a fast-forward!? So what do I do after the local rebase? |
You do this: And yes, you force push. |
@tandem-softworks how is it going? Do you need any help? |
@strzalek - thanks for asking. I just needed a little motiviation... |
@tandem-softworks thanks for getting back so quickly. Can you squash those two commits into one? |
I updated the pull request a month ago. - What's next? |
@tandem-softworks I want to review it but a lot of lines are causing horizontal scrolling . That is making it hard to review code. Can you split the lines so that the diff fits into the window without horizontal scrolling ? Thanks. |
Better? - I'd rather not rename too many things. |
@neerajdotname - do you still plan to review this pull request? |
@tandem-softworks I tried to review it. I could not understand the tests very well. And a lot of lines are still causing horizontal scrolling for me. |
…assignment assigns to existing record if the association is not yet loaded Issue rails#2: Nested Attributes assignment does not affect the record in the association target when callback triggers loading of the association Complete rewrite of tests
@neerajdotname - You are right - the tests did too many things at the same time. Please, have another go at it now. |
@tandem-softworks I think tests needs a bit more clarity. For example following method does not tell me what is happening in one glance.
|
This is a utility method, which provides attributes to update bird1 (b1_update), create a new bird (new_b), and destroy bird2 (b2_destroy). I can't name it short enough to avoid horizontal scroliing and make the name more telling than it is at the same time. (Are you familiar with assignments to nested attributes? This method strips the needed content down to the bare minimum.) The combination of changes is necessary to exhibit the second issue - so yes, things are a little complicated. But there is nothing I can do about it. Maybe you start by looking at the first set of tests about triggering the callback. |
@neerajdotname - did you have a chance to look at the first set of tests?
|
@neerajdotname - seems like this is not your cup of tea - fine ... |
The rebase dates back over two months - the fix is still the same as in March 2012. - What's next? |
@Empact - how about working together on this??? |
Hey @tandem-softworks - sorry that you feel otherwise, but from my perspective we are working together. You identified the problem and solution, and I better integrated it into the codebase and clarified the tests. That's the beauty of git - it's an open system where we can all improve on the work of others. Seems the last remaining bit is to add a changelog for your work. Would you be up for pulling from my branch, adding a changelog for your work, and updating this pull request? I would close my pull request if you did that. Otherwise I'm happy to write a changelog on your behalf. |
@Empact I can see your perspective - a little note to me about your engagement would have helped. - It's been a mixed experience with the community culture... Nevertheless, I am happy about you stepping in and about your improvements. I'll have a go at the remaining steps today - as you described including the changlog entry. |
@Empact - Could you have a look at nested_attributes_with_callbacks_bug_review - in particular the changelog entry? Are you happy with the commit message? If everythings OK I'll update the PR. What I did:
I made two minor changes in the test flle:
I get one regression failure running
|
Yep the the test changes you mention seem good - and I agree the sqlite failure is due to being and older version, I'm not seeing it over here. I would prefer you not squash my commits into yours, as keeping them separate preserves the line-by-line authorship of the changes and the motivation for them. I updated my PR #11525 to include your changelog and other changes (in your initial commit) while preserving authorship. Take a look! |
I understand your point about not squashing the commits. As I commented over at your PR - I close mine. |
…s_bug Improve #5476 - "Nested attributes with callbacks bugfix" to use add_to_target and clearer tests
I discovered a bug in the nested attributes assignment: It occurs when
an association callback changes the "loaded-ness" of the association
during the assignment. After association.loaded? changes from false to
true, the destroy flag fails to function. The first commit illustrates
the issue with a unit test. The test fails in the master branch -
cherry-picked to 3-2-stable, 3-1-stable, 3-0-stable fails the same way.
The second commit is the suggested fix. It replaces a section that
previously addressed the same issue of recognizing if a record was
already in the proxy_target array. When it wasn't, it called
add_to_target, which in turn calls the before_add callback. IMO this is
not correct, because the record concerned already exists in the database
as part of the association. The documentation of the callback states
that an exception may be raised to prevent the record from being added
to the association which contradicts the fact. Therefore I just plainly
appended the record to the proxy_target array. (The unit test in the
first commit pins down this behaviour). The fix is tested on sqlite on
master, 3-2-stable, 3-1-stable. The fix to 3-0-stable is slightly
different, so there is another pull request off of branch
nested_attributes_with_callbacks_bug_3-0 (if I manage to create one...).
Considering the whole method
assign_nested_attributes_for_collection_association - it almost
exclusively calls methods on the association and its logic is deeply
entangled with the implmentation details of the CollectionAssociation
class. I've seen some effort to augment the responsibilites of the
association classes, which IMO points in the right direction. A next
step to clear up this spot would be to move the main part of the method
to something like "CollectionAssociation.attributes=" - any comments!?