ある時、has_oneの関連レコードがすでに紐づいている状態で、build_association_nameで新たなレコードを作成しsaveをしても、特にエラーなどが起きずレコードが置き換わっていることに気づきました。
この挙動が奇妙だったため何が起きているのか調査をしてみることにしました。
has_oneのbuild_associationでDELETEが実行されていた
rails7.1.2で以下のようなモデルを使って検証をしてみます。
ポイントはhas_oneに対してdependent: :destroy
のオプションを与えている点です。
class Supplier < ApplicationRecord
has_one :account, dependent: :destroy
end
class Account < ApplicationRecord
belongs_to :supplier
end
Supplierのhas_oneの関連レコードであるAccountを永続化して紐付けた状態にしておきます。
irb(main):005> sup = Supplier.create!(name: 'hoge')
=> #<Supplier:0x0000000108691aa0 id: 2, name: "hoge", created_at: Sat, 25 Nov 2023 03:28:05.312517000 UTC +00:00, updated_at: Sat, 25 Nov 2023 03:28:05.312517000 UTC +00:00>
irb(main):006> sup.create_account!(account_number: '1')
=> #<Account:0x0000000109d6fb78 id: 2, supplier_id: 2, account_number: "1", created_at: Sat, 25 Nov 2023 03:29:36.497616000 UTC +00:00, updated_at: Sat, 25 Nov 2023 03:29:36.497616000 UTC +00:00>
この状態でbuild_accountを実行すると、以下のようにbuildの時点でDELETEのSQLが発行され既存のAccountのレコードを削除してしまいます。
どうやらbuild_association_nameメソッドが実行された際に、紐づく既存レコードがあれば暗黙的に削除されるようです。
irb(main):008> sup.build_account(account_number: '2')
TRANSACTION (0.1ms) begin transaction
Account Destroy (0.9ms) DELETE FROM "accounts" WHERE "accounts"."id" = ? [["id", 2]]
TRANSACTION (0.5ms) commit transaction
=> #<Account:0x0000000109c8f618 id: nil, supplier_id: 2, account_number: "2", created_at: nil, updated_at: nil>
もしbuild後のsaveの処理の際に例外が発生すれば、ただ既存レコードが削除されただけとなってしまうのは少し恐ろしいなと感じます...。
ちなみに、dependent: :destroy
が付与されていないと、buildしようとした時点で以下のように例外が起きます。
irb(main):016> sup.build_account(account_number: 1)
/Users/username/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/activerecord-7.1.2/lib/active_record/associations/has_one_association.rb:110:in `remove_target!': Failed to remove the existing associated account. The record failed to save after its foreign key was set to nil. (ActiveRecord::RecordNotSaved)
まとめ
既存のレコードが紐づいた状態でdependent: :destroy
が付与されたhas_one関連レコードをbuild_associationする際は、暗黙的に既存レコードが削除されてしまうことに注意しておきましょう。
buildが既存レコードに影響を及ぼすとは考えていなかったため、この挙動を知った時は驚きました。
もしこのような挙動となっている理由をご存知の方がいたら教えていただきたいです。
ちなみに以下の記事で紹介されているように、create_associationメソッドの挙動に関しても一悶着あったようです。
投稿者の方がおっしゃるように、全員が幸せになるような挙動を作るのはなかなか難しいのかもしれません・・・。