はじめに
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を使用することで、非同期ジョブを安全かつ確実に実行できます。
使い方を誤ると予期せぬ動作や意図しない結果を引き起こしてしまう可能性があるため、きちんと両者の挙動を理解して使い分けましょう。
参考資料