Open5
Railsの`parent.children = children`で書き込みが走る件について調べる

問題提起: childrenはいつ保存されている?
今回のイメージコード
何故か、parent.children = children
の時点でchildrenが保存されてしまっていたのが不思議だった
# Read Only APIの処理の中で
class ParentAPI
def call
parent = Parent.find(id)
children = [
Child.build(name: '子1', parent_id: parent.id),
Child.build(name: '子2', parent_id: parent.id),
]
# この時点でchildrenが保存されている!?
parent.children = children
# ここで、parentとchildrenがまとめて保存されると思っていた
parent.save!
end
end
こっちのコードは想定通り parent.save!
でまとめて保存された
(この違いを見つけた時点でなんとなく察しは付いた)
# Read Only APIの処理の中で
class ParentAPI
def call
parent = Parent.find(id)
children = [
Child.build(name: '子1'),
Child.build(name: '子2'),
]
# この時点では、childrenは保存されていない
parent.children = children
# ここで、parentとchildrenがまとめて保存される
parent.save!
end
end
ActiveRecord::AutosaveAssociation
この問題を理解するには、どうやらAutosaveAssocitationを理解する必要がありそう

parent.save!の前に保存されるのは、id直接指定かassociation名指定か
-
ActiveRecord::AutosaveAssociation
は、親オブジェクトの保存時に、子オブジェクトも自動で保存してくれる - しかし、子オブジェクトの関連を手動(
parent_id: parent.id
)で設定すると、Railsが子オブジェクトを独立したエンティティとして扱い、即座に保存を試みる動作となる
なぜ挙動が変わる?
parent.children = children
の処理が呼ばれた時点で、Railsが「childrenはすでに永続化可能」と判断する
has_manyの関連付けの仕組み
parent.children = children
が行われた時の手順
- parent.children (has_many :children) の関連をクリア(既存の関連がある場合)
- 渡された children の各要素に対して parent_id を設定
- この時点で Rails は「新たに関連付けられたオブジェクトは永続化が必要」と判断し、暗黙的に保存処理を実行
保存のトリガーはなに?
前提:
- Rails の関連付けには、自動保存を行う コールバック(内部的なトリガー処理) がある
- データベース上でも一貫性を持たせようとする
条件:
- 子オブジェクトの主キー(id)が nil(未保存状態)
- 子オブジェクトに parent_id が手動で割り当てられている

混合だと挙動がおかしい
parent指定なしと、parentを関連で指定するケースを混合にすると、
何故かparentを指定された方だけレコードが保存される。
(ちなみに、混合ではなく両方 parent: を指定する or しないにするとレコードは作られなかった)
# Read Only APIの処理の中で
class ParentAPI
def call
parent = Parent.find(id)
children = [
Child.build(name: '子1'),
Child.build(name: '子2', parent:),
]
parent.children = children
binding.pry # ここで止めて検証
parent.save!
end
end
binding.pryで止めて確認すると、何故か片方だけレコードが作られている
> parent.children
=> [#<Child
id: nil,
body: "子1",
parent_id: 1,
created_at: nil,
updated_at: nil>,
#<Child
id: 1, # parentを指定した方だけレコードが作成される
body: "子2",
parent_id: 1,
created_at: "2024-11-24 14:04:39.204987000 +0900",
updated_at: "2024-11-24 14:04:39.204987000 +0900">]
> parent.children[0].new_record?
=> false
> parent.children[1].new_record?
=> true

原因がわからないけど、おそらく内部の条件分岐の問題なんだろう、、、
基本的には親の関連は指定しなくていいし
混合で指定するケースなんて無いから問題なさそうなので、頭の片隅にこの挙動を入れておくで済ませる。誰か強い人とかソースコード読みに行った偉い人いたら教えて下さい。

この動作、なぜか再現できなくなっていた
あれ?間違っていたのか