rails: Autosaving associations twice in Rails 6.0.3 violates uniqueness
Steps to reproduce
This behavior is new as of Rails 6.0.3.
If you updated a just-created record with a has_many through association where the join table has a uniqueness constraint on the two foreign keys the associations get saved twice, resulting in a uniqueness violation:
class Team < ApplicationRecord
has_many :memberships
has_many :players, through: :memberships
end
class Player < ApplicationRecord
has_many :memberships
has_many :teams, through: :memberships
end
class Membership < ApplicationRecord
# memberships has a unique index on player_id and team_id
belongs_to :player
belongs_to :team
end
player = Player.create!(teams: team])
player.update!(updated_at: Time.current)
Expected behavior
In Rails 6.0.2.2, this would save a new Player
and a Membership
linking the Player
to the Team
successfully. The subsequent update would be successful.
Actual behavior
An ActiveRecord::RecordNotUnique
exception is raised when a second Membership
is written at the time of the #update!
.
I think the issue is related to this change: rails/rails#39124. This can be worked around, but it’s an unexpected change from prior behavior in Rails.
System configuration
Rails version: 6.0.3
Ruby version: 6.0.2.2
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 1
- Comments: 30 (18 by maintainers)
Thinking this through a bit more, I think we would ideally be able to handle all of these edge cases but since the autosave and callback system relies on instance variables on the model instance, we’re probably going to end up playing a long winded game of whack of mole for all eternity as these callbacks can logically collide with one another in many ways (since they work on the instance variables on the same instance and assume some level of isolation).
Therefore, I’d propose:
raise ActiveRecord::NestedCallback
error (or similar)This will make sure that we help people remove problemsome code (nested callbacks) while giving them time to act on the warnings.
Any update on this? There were security updates bundled into 6.0.3.1 that I can’t update to because of this issue. Any chance of pushing forward @jules2689 proposition?
I understand that we should be trying to remove nested callbacks, but when dealing with large apps, it is very difficult to track them all down. I have tried deploying the update a couple of times, every time running into this problem in a subtly new scenario.
We really shouldn’t have to be playing games with our apps’ existing functionality and security so I humbly request that this be pushed out with urgency.
Thanks for the reproduction steps! Both bisect to https://github.com/rails/rails/pull/38166. I’ll take a closer look later this evening.
I believe https://github.com/rails/rails/pull/39771 fixes the problem reported here. @geoffharcourt (or anyone else affected): would you mind testing it against your application? https://github.com/eugeneius/rails/tree/6_0_stable_autosave_exactly_once is a branch containing that commit on top of 6-0-stable, in case that makes it easier.
@danielricecodes: https://github.com/rails/rails/pull/38166 wasn’t backported to Rails 5.2, so I don’t think you’re seeing the same problem as the other reports here, despite the similar-looking error. Rails 5.2 is no longer supported for bug fixes, but if you can reproduce your problem in a sample application or an executable test case that uses the 6-0-stable or master branch, please do open a new issue.
👍 not breaking existing apps on a minor update but giving a warning instead, so users can fix it until 6.1, is the best approach imo.
Hi all - bumping the request here. Since ‘callbacks within callbacks’ is part of the public API (even if it’s heavily discouraged) it’s really frustrating that this occurred during a patch update. Are there are any updates on when this might be fixed? Like @hashwin mentions above, having to choose between security compliance and existing functionality is not a fun place to suddenly be.
This is being caused by a documented limitation of the Rails system. On associations, there exist 2 callbacks in ActiveRecord for associations: https://github.com/rails/rails/blob/0571da95934ff8004e0116cf6a91d9182707fb23/activerecord/lib/active_record/autosave_association.rb#L367-L373
What this means is that
before_save
we tell ActiveRecord that we are a new record (this means we should create the association), and in theafter_save
we reset that to false.after_create
happens beforeafter_save
, so we aren’t set tofalse
yet byafter_save_collection_association
. By defining a method that updates the model with callbacks (e.g. update, save, save!, etc) you are invoking the callbacks with@new_record_before_save
set to true still. This will save the record twice.This is why @yosiat had luck using
after_commit
because that happens afterafter_save
.There is a warning in the ActiveRecord documentation that states:
While this probably shouldn’t cause issues, it is still not advisable to run methods with callbacks in callbacks.
In the bug report here (https://github.com/rails/rails/issues/39173#issuecomment-625178248), the
after_create
should be changed to abefore_save
orbefore_validation
to be something likeself.last_post_creation_time = Time.now
and let the normal save callbacks work (bonus is a saved SQL query!)Unfortunately undoing some recent changes re-introduces a different bug of a similar nature that causes children records to not be saved if added to an unsaved parent that is subsequently saved (e.g.
comment = Comment.create!; post = Post.new(comments: [comment]); post.save; # comment does not have "post_id" assigned
).I’ll leave the ultimate decision of which bug is worse up to the Rails team, but this could be fixed with this diff, which would re-introduce the bug that introduced this one 🙃
Hello, I’m experiencing this issue after upgrading to Rails 6.1.0-alpha. Has this fix been reverted?
@eugeneius I confirmed that your fix resolves the issue for us where it fails in 6.0.3.
@eugeneius can confirm that branch works for my test cases 👍
hi @adipasquale !
I am seeing the same issue in my rails app, when upgrading to rails 6.0.3, If I do an extra
save!
in after_create I will get the same record being written twice. But If I change the after_create to be after_commit on create it will solve the issue for me.Wrote reproduction script with failing test on rails 6.0.3 but passing on 6.0.2:
@eugeneius can you take a look?
No, the fix is still present on the master branch. Please open a new issue with an executable test case.
Hi @eugeneius I haven’t been at a computer in a day, but I should be able to test this later today. Thank you for your work to help resolve!
@maxvlc for my application, changing to
after_commit on: :create
fixed the issue. You can try this on your app, it might work for you use case.I’ve tried reproducing with your script by switching to
pg
, and using something close to my case (settingteam_ids
instead ofteams
) but it still does not reproduce 😕 I’ll let you know if I manage to reproduce.