ActiveModel::Dirty を使うときに気をつけること
最近ハマってしまったのでメモとして残しておく。
TL;DR
自身のmodel と association を同時に save する場合は dirty attribute が上書きされる可能性があるので注意する必要がある。
本編
雑なサンプルを提示する。
- User : ユーザー作成時に新規ユーザー向けのメール送信する機能を持っている
- UserName : User と has_one の関係で UserName 作成時に親の User model の
filled_name
を更新する
# == Schema Information # # Table name: user_names # # id :integer not null, primary key # user_id :integer indexed # name :string(32) # created_at :datetime # updated_at :datetime # class UserName < ApplicationRecord belongs_to :user after_create :mark_as_filled_name private def mark_as_filled_name user.filled_name = true user.save! end end
# == Schema Information # # Table name: users # # id :integer not null, primary key # filled_name :boolean default(FALSE), not null, indexed # created_at :datetime # updated_at :datetime # class User < UsersModel::User has_one :user_name after_save :send_mail_for_new_user private def send_mail_for_new_user MailService.send_new_user_mail(user) if saved_change_to_id? end end
上記のような model が存在したとき、以下のような実装で User と UserName の作成を同時に行うとメールが送信されない。
user = User.new user.user_name.build(name: 'this is name') user.save!
User と UserName の作成を別々に行うときちんとメールは送信される。(同一トランザクションでも問題ない)
user = User.new user.save! user.user_name.create!(name: 'this is name')
どうしてこうなるかを解説していく。
まず、 User と UserName の作成を同時に行う場合の user.save!
では以下の処理が順番に実行されている。
- User の create
- UserName の create
- UserName の after_create による User の update
- User の after_save
4. を実行するタイミングでは 1. の dirty attribute(id column の変化) が残っていることを期待するが 3. の User update(filled_name column の変化) によって 1. の dirty attribute が上書きされ、サンプルコードで言うところのsaved_change_to_id?
がfalse
を返してしまう。
学び
- model の同時 save を行う必要がないときはなるべく別々での save を心がける
- dirty attribute 難しい