Ruby on Railsでは、あるActiveRecordのModelの関連先のインスタンスのメソッドを作るためのメソッドとして build メソッド(build_association メソッド)があります。
例えば以下のようなUserモデルにおいて
class User < ActiveRecord::Base
has_one :project, dependent: :destroy
end関連先であるproject のインスタンスを以下のように作ることができます。
user = User.find(1)
project = user.build_project # similar to user.project = Project.newbuild なので、DBにはまだ保存されていない状態のインスタンスであり、save 等行えばDBに保存されます。
しかし、「DBに保存されていない状態」インスタンスではあるのですが、DBの操作が行われるケースがあったので、備忘的に残しておきます。
buildでDB操作が行われるケースとは?
それは既に user に紐づくprojectが存在している場合です。
この状態で user.build_project すると、実行時に既に紐づいている project を削除してしまいます。
$ user = User.find(1)
User Load (6.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 /*application='Myapp'*/
=> #<User:0x0000ffff5dc581b0 id: 3, name: "Bob", created_at: "2025-02-04 12:09:01.621168000 +0000", updated_at: "2025-02-04 12:09:01.621168000 +0000">
$ user.project
Project Load (2.5ms) SELECT `projects`.* FROM `projects` WHERE `projects`.`user_id` = 1 LIMIT 1 /*application='Myapp'*/
=>
#<Project:0x0000ffff66560368
id: 2,
user_id: 1,
created_at: "2025-02-04 12:17:57.823714000 +0000",
updated_at: "2025-02-04 12:17:57.823714000 +0000">
$ user.build_project # Destroyが走ってる!
Project Destroy (3.2ms) DELETE FROM `projects` WHERE `projects`.`id` = 2 /*application='Myapp'*/
=> #<Project:0x0000ffff5dc30b60 id: nil, content: nil, user_id: 3, created_at: nil, updated_at: nil>build という言葉の意味から、DBの操作はてっきりしないと思い込んでしまうため、注意が必要です。
私が遭遇したケースでは、build をトランザクション外で行っていたため、異常時ロールバックができないという事態が発生してしまっていました。
project = Project.find(params[:id])
# project に紐づく user が存在していたが、build により destroyされてしまった
user = project.build_user
user.name = 'John Doe'
ActiveRecord::Base.transaction do
# バリデーションエラー等が発生しても destroy はトランザクション外で
# 既に実行済みなのでロールバックされない!
user.save!
endどこで削除が走っている?
なぜこんな挙動なのかは分からないので、ActiveRecordのソースを読んでみました。
原因は has_one_association.rb 内の remove_target! にありました。
def remove_target!(method)
case method
when :delete
target.delete # ここで delete もしてる!
when :destroy
target.destroyed_by_association = reflection
if target.persisted?
target.destroy # ここで destroy してる!
end
else
...dependent: :destroy だけでなく dependent: :delete も同様に削除されてしまうようです。
なお例えばdependent: :nullifyといった他のオプションの場合は else 内を通りますが、build の場合はいずれもエラーが発生するようになっていそうでした。
※ build から順に追うと以下の流れ。
- https://github.com/rails/rails/blob/593dfca100eb1bf5f83adebde94b4e01770d67c4/activerecord/lib/active_record/associations/builder/singular_association.rb#L32-L34
- https://github.com/rails/rails/blob/593dfca100eb1bf5f83adebde94b4e01770d67c4/activerecord/lib/active_record/associations/singular_association.rb#L29-L33
- https://github.com/rails/rails/blob/593dfca100eb1bf5f83adebde94b4e01770d67c4/activerecord/lib/active_record/associations/has_one_association.rb#L91-L93
- https://github.com/rails/rails/blob/593dfca100eb1bf5f83adebde94b4e01770d67c4/activerecord/lib/active_record/associations/has_one_association.rb#L59-L69
なお、project.build_user といった逆向きは削除されません。
あくまで has_one の関連付けだけのようです。
なぜこんな挙動になっているかというところは結局分からなかったのですが、どういう意図からこうなったのかすごく気になります…
とりあえず has_one での build には気をつけるようにしましょう。

