🕸️
[Rails] has_one に対する `#assign_attributes` & `#save` の罠
ドーモ、株式会社ソーシャルPLUS CTO の サトウリョウスケ (@ryosuke_sato) です ✌︎('ω')✌︎
前回に引き続き、今回も簡単な備忘録です ✍️
別の大きなネタもあるんですが、時間が取れなかったのでまたの機会に。
以下のような User モデルに対してメモを記録するモデルとの関連があるとします。
has_one
と accepts_nested_attributes_for
を使っている点がポイントです。
class User < ApplicationRecord
has_one :memo
accepts_nested_attributes_for :memo
end
class Memo < ApplicationRecord
belongs_to :user
end
このモデルを使って更新処理を書いてみます。
class SomeController < ApplicationController
def update
user = User.find(params[:id])
user.assign_atteibutess(user_params)
if user.save
render status: :ok
else
# render an error response
end
end
private
def user_params
params.require(:user).permit!
# => {
# memo_attributes: {
# content: 'modified'
# }
# }
end
end
一見うまく動きそうなんですが、上記のコードだとバリデーションエラーの際に memo レコードが削除されてしまいます。
TRANSACTION (0.3ms) SAVEPOINT active_record_1
Memo Destroy (0.4ms) DELETE FROM `memos` WHERE `memos`.`id` = 1
TRANSACTION (0.2ms) RELEASE SAVEPOINT active_record_1
TRANSACTION (0.2ms) SAVEPOINT active_record_1
TRANSACTION (0.2ms) ROLLBACK TO SAVEPOINT active_record_1
これは #assign_attributes
を実行した際に Memo#destroy
が実行され、新しい Memo
インスタンスに置き換えられてしまうため。なお、 #save
に成功すれば新しい Memo
インスタンスの保存に成功するので、正常系では問題ないように見えます。
これを解決するには、 #assign_attributes
を使わず #update
を使えば OK です。
class SomeController < ApplicationController
def update
user = User.find(params[:id])
if user.update(user_params) # ← ここを変更
render status: :ok
else
# render an error response
end
end
end
発行される SQL にも変化があることがわかります。
TRANSACTION (0.2ms) SAVEPOINT active_record_1
Memo Destroy (0.3ms) DELETE FROM `memos` WHERE `memos`.`id` = 1
TRANSACTION (0.8ms) ROLLBACK TO SAVEPOINT active_record_1
複雑な代入が必要な際に #assign_attributes
を使いたいシーンがありますが、 has_one
を使っている場合はご注意ください。
Discussion