Summary of changes in behavior of create_association method in Rails 7.0.5 and later

willnet

Shinichi Maeshima

Posted on July 4, 2023

Summary of changes in behavior of create_association method in Rails 7.0.5 and later

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.

[7-0-stable] Revert singular association breaking changes by zzak · Pull Request #48809 · rails/rails

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

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. create a new record when the validation passes
  2. then delete the existing record
  3. retrieving an existing record for deletion is done via has_one related method.
  4. 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.
  5. if this newly created record is retrieved, it is treated as if there are no records to delete
  6. 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
Enter fullscreen mode Exit fullscreen mode

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...?

💖 💪 🙅 🚩
willnet
Shinichi Maeshima

Posted on July 4, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related