はじめに
Railsのコールバックメソッドであるafter_create
とafter_create_commit
の違いを備忘録としてまとめます。
最後まで読めば、両者の挙動の違いや使い分けが理解できるかなと思います。
この記事におけるバージョンはRails 7.1です。
after_create
とafter_create_commit
の挙動の違い
2つのメソッドの挙動をそれぞれまとめます。
なおこの後でてくる「トランザクション」は、User.transaction do
のような明示的に開始するトランザクションだけを指しているわけではありません。
User.create
などでデータベースに変更を加える際にRailsが自動的に管理する内部的なトランザクションも含めて指しています。
after_create
after_create
はレコードがデータベースへ保存された直後、トランザクションがコミットされる前に実行されます。
そのためコールバック内でエラーが発生した場合、トランザクション全体がロールバックされるのです。
つまり、after_create
はトランザクション内の処理の一部として扱われます。
after_create_commit
after_create_commit
はトランザクションが成功しコミットされた後に実行されます。
そのためコールバック内でエラーが発生しても、トランザクションには影響を及ぼしません。
after_create
とafter_create_commit
の使い分け
after_create
とafter_create_commit
の違いを意識せずに使っていると、データ整合性にまつわるエラーを引き起こす可能性があります。
そこでここからは以下の2つのケースについて、説明していきます。
-
after_create_commit
よりafter_create
が適しているケース -
after_create
よりafter_create_commit
が適しているケース
after_create_commit
よりafter_create
が適しているケース
ユーザー情報とプロフィール情報を別のテーブルで管理している状況を想定します。
下記のサンプルを見てください。
# NG例
class User < ApplicationRecord
after_create_commit :create_profile
private
def create_profile
Profile.create!(user_id: id)
end
end
after_create_commit
を使用しているため、ユーザーの作成が成功し、create_profile
によるプロフィール作成が失敗した場合に、ユーザーの作成処理はロールバックされません。
これにより、プロフィールのないユーザーが作成される可能性があります。
after_create
を使えば、プロフィールの作成が失敗した場合に、ユーザーの作成もロールバックされるように実装できます。
# OK例
class User < ApplicationRecord
after_create :create_profile
private
def create_profile
Profile.create!(user_id: id)
end
end
このようにデータの整合性を担保した上で関連レコードを操作する必要がある場合は、トランザクション内で処理が行われるafter_create
が適しています。
※説明のためにこのケースをサンプルとして用意しましたが、こういったケースではそもそもコールバックを使用せずに明示的なトランザクションの中で処理をした方が良いと思われます。
after_create
よりafter_create_commit
が適しているケース
ユーザー登録時にメールを送信する状況を想定します。
# NG例
class User < ApplicationRecord
after_create :send_welcome_email
private
def send_welcome_email
WelcomeEmailJob.perform_later(id)
end
end
class WelcomeEmailJob < ApplicationJob
def perform(user_id)
user = User.find_by(id: user_id)
UserMailer.welcome(user).deliver_now
end
end
この実装だとuser = User.find_by(id: user_id)
で問題が発生するかもしれません。
after_create
を使用しているためトランザクションのコミット前に非同期ジョブがキューへ投入されるからです。
ジョブの実行時点でトランザクションがコミットされているとは限りません。
もしトランザクションのコミットよりも先にジョブが実行された場合、ユーザーのレコードがまだデータベースに保存されていないため、user = User.find_by(id: user_id)
の結果はnil
になります。
その結果、UserMailer.welcome(user).deliver_now
がエラーとなってしまうのです。
after_create_commit
を使えば、こういった懸念を排除できます。
# OK例
class User < ApplicationRecord
after_create_commit :send_welcome_email
private
def send_welcome_email
WelcomeEmailJob.perform_later(id)
end
end
class WelcomeEmailJob < ApplicationJob
def perform(user_id)
user = User.find_by(id: user_id)
UserMailer.welcome(user).deliver_now
end
end
このケースではトランザクションがコミットされたことを確認できてからジョブを実行すべきなのでafter_create_commit
が適切です。
おわりに
最後にまとめです。
-
after_create
: トランザクションがコミットされる前に実行 -
after_create_commit
: トランザクションがコミットされた後で実行
after_create
を使用することで、関連レコードの作成に失敗した場合でもトランザクション全体をロールバックしてデータの不整合を防げます。
after_create_commit
を使用することで、非同期ジョブを安全かつ確実に実行できます。
使い方を誤ると予期せぬ動作や意図しない結果を引き起こしてしまう可能性があるため、きちんと両者の挙動を理解して使い分けましょう。
参考資料