-
Notifications
You must be signed in to change notification settings - Fork 21.8k
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
Loading has_many :through
association in a before_save
callback creates duplicate records
#54176
Comments
Hi, @jenshenny. Supposedly, ActiveRecord don't understand which records need to create and therefore make two identical records. Maybe code below resolve your problem.
or even it will be better
|
Thanks @jenshenny, autosave is something I didn't consider. I'm pretty sure this is the patch we want: main...joshuay03:rails:fix-54176 I'll add some coverage and get a PR up soon. |
Hey @joshuay03 ! Thanks for looking at this so quickly. I was pairing with @jenshenny to investigate this behaviour. One thing we were debating while investigating was how the ordering of building the parent / children vs the join record impacts a potential duplicated join record. The example Jenny provided in the repro is definitely solved by your patch: class BugTest < ActiveSupport::TestCase
def test_association_stuff
# Doctor as parent, build the join record off of the parent, then build the associated patient off the join record
# With patch, we persist the appointment as part of saving doctor, and no longer save an additional join record
# when saving the patient
doctor = Doctor.new(name: "Dr Dolittle")
appointment = doctor.appointments.build
patient = appointment.build_patient(name: "John")
doctor.save!
assert_equal(1, doctor.appointments.size)
end ✅ Passes However, when the patient is built first, and then the join record as a child of patient, we still see two appointments: def test_association_stuff_2
doctor = Doctor.new(name: "Dr Dolittle")
patient = doctor.patients.build(name: "John")
appointment = patient.appointments.build(doctor: doctor)
doctor.save!
assert_equal(1, doctor.reload.appointments.size)
end
1) Failure:
BugTest#test_association_stuff_2 [scripts/save_collection_association_bug.rb:170]:
Expected: 1
Actual: 2 ❌ Fails If we build the appointment with doctor as the parent, we see one: def test_association_stuff_3
doctor = Doctor.new(name: "Dr Dolittle")
patient = doctor.patients.build(name: "John")
appointment = doctor.appointments.build(patient: patient)
doctor.save!
assert_equal(1, doctor.appointments.size)
end Passes ✅ So scenarios 1 + 3 are solved by the patch because the join record was built with Does the behaviour in scenario 2 seem correct, or would we expect it to behave the same way as the other two examples? 🤔 |
It seems that scenario 1 is the only one that passes on 7.2 and 8.0, but fails on main i.e. is a regression. 2 and 3 also fail on those releases, but 3 just so happens to be addressed by my patch. I think they're all related regardless, and it makes sense to address them all together where possible. Just thought it was worth mentioning.
Hmm I'm not sure tbh. Having a look. |
Just realised I didn't answer your question in my previous reply.
Not really. So The behaviour of |
I think it makes sense that scenario 3 also now passes with this patch. There we explicitly build both I think we could argue that scenario 2 is slightly different, and much trickier to 'fix' if we wanted to. Fixing that would mean that we need to traverse the opposite direction on the join record to associate the appointment with the doctor, which is not easy. The docs for I'll aim to send a PR for the patch this week. |
Hey @joshuay03 ! Thanks for the detailed reply, you're great ❤️
Aha, okay got it -- that makes sense. The extra details there (which might be apparent, but this is what was tripping me up before so I'll elaborate in case it helps others 😅 ) -- as a part of autosaving I'm on board with marking scenario 2 as out of scope -- as you've expressed, having If you could get a patch up ASAP that would be great, this would unblock us being able to upgrade to Rails main at Shopify. Thank you for your work! |
Oof my bad 😅 I've got a PR up: #54238 |
Steps to reproduce
Expected behavior
1 appointment is created.
Actual behavior
2 appointments are created. Without the
before_save
, 1 appointment is being created.After a bit of digging, this is what I believe is happening:
AutosaveAssociation#save_collection_association
callsActiveRecord::AutosaveAssociation#associated_records_to_validate_or_save
to get the associated records to save, which gets them via the target method on the association. So, previously, the target was not set and no records were returned, and thus no records were saved. But now, the target is set properly and the method does return a record (the patient) to save, which leads to another appointment being created.before_save
, it sets the has many through association. Without the before save doing this,AutosaveAssociation#save_collection_association
would be a no-op for the has many through association betweenDoctor
andPatient
. This is becauseassociation_instance_get(reflection.name)
would returnnil
andActiveRecord::AutosaveAssociation#associated_records_to_validate_or_save
would never be run.cc: @joshuay03
The text was updated successfully, but these errors were encountered: