Shinichi Maeshima
Posted on July 4, 2023
This article is an English translation from https://blog.willnet.in/entry/2023/07/04/113321
There are several related PRs and Issues on this matter and comments are scattered, and it is complicated to explain to people, so this is a summary as a blog. Please comment if you find any mistakes or opinions!
Update 2023/08/02
A commit has been made to revert this change to the 7-0-stable branch. It seems that the policy is to revert back and start over because some people had trouble with the new behavior.
I don't know when 7.0.7 will be released, but if it is released in its current state, it will revert to 7.0.4 behavior; those who are having trouble with changes after 7.0.5 may want to point to 7-0-stable once it is released.
Overview
- Behavior of create_association method generated when defining has_one association is different in Rails 7.0.4 and 7.0.5 or later.
- If you think that the behavior of create_association is only "create a new association record", you may need to modify your code to use 7.0.5 or later.
Assumptions
- If you try to create a new model via has_one association when the association is already persisted, the existing association will be deleted(behavior of
association=
is written in http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one butbuild_association
andcreate_association
do not seem to be described, including in the Rails guide). - The way the deletion is done depends on the dependent option of has_one (This is where the code is applicable. https://github.com/rails/rails/blob/89508b95d8d289b430cca7db05109d8171ff7a5f/activerecord/lib/active_record/associations/has_one_association.rb#L94-L116 )
- When
:nullify
or:dependent
option is not set, set NULL to the foreign key and UPDATE - When
:delete
, delete with thedestroy
method - When
:delete
, delete withdelete
method.
- When
For example, in all of (1)~(3) in the following code, if there is a record associated with an existing associate, it will be deleted.
class User < ApplicationRecord
has_one :address, dependent: :destroy
end
class Address
belongs_to :user
end
user = User.find(42)
user.address = Address.new # (1)
user.create_address # (2)
user.build_address # (3)
Behavior up to Rails 7.0.4
Previously, the behavior of create_association was to insert and then delete in separate transactions as described in this PR.
# begin transaction
INSERT new_record; # commit transaction
# commit transaction
# begin transaction
DELETE old_record; # commit transaction
# commit transaction
Furthermore, even if the preceding insert (save method) fails due to a validation error, the subsequent delete will still be executed. This behavior caused a bug in which a validation error would result in the deletion of an existing record when execute create_association!
Related Issue: has_one association getting deleted on using create_association & validation fails - Issue #46737 - rails/rails
There was another issue where deletion of existing records did not work in the following way
- create a new record when the validation passes
- then delete the existing record
- retrieving an existing record for deletion is done via has_one related method.
- when the order by related to has_one is not set (in most cases, order by is not set for has_one), the newly created record is retrieved in rare cases.
- if this newly created record is retrieved, it is treated as if there are no records to delete
- as a result, there are two or more records without deletion.
Related Issue: Sometimes create_association
does not delete existing records - Issue #47554 - rails/rails
Behavior since Rails 7.0.5
In 7.0.5, the behavior has changed to delete and then insert in the same transaction, as shown below, and the above bug has been mostly resolved.
# begin transaction
DELETE old_record;
INSERT new_record; # commit transaction
# commit transaction
However, the code would need to be modified for an application that originally had a unique constraint on the has_one foreign key and wanted to make a validation error if create_association was performed when there was an existing record.
Related Issue: has_one relation start deleting existing record even when new record fails passing validation - Issue #48330 - rails/rails
Thoughts
I personally feel that the changes in 7.0.5 were necessary for the consistency of the behavior described in the "Assumptions" section, but on the other hand, I understand that it may be surprising to people who do not know that association=
, build_association
, or create_association
will delete existing records. On the other hand, I can understand why some people might find it hard to modify their code to accommodate this change, as it might be a surprise to those who are not familiar with this behavior.
I wonder what kind of behavior would make everyone happy...?
Posted on July 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.