カレーの恩返し

おいしいのでオススメ。

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! では以下の処理が順番に実行されている。

  1. User の create
  2. UserName の create
  3. UserName の after_create による User の update
  4. 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 難しい