0
0

【Rails】after_createとafter_create_commitの挙動の違い

Posted at

はじめに

Railsのコールバックメソッドであるafter_createafter_create_commitの違いを備忘録としてまとめます。

最後まで読めば、両者の挙動の違いや使い分けが理解できるかなと思います。

この記事におけるバージョンはRails 7.1です。

after_createafter_create_commitの挙動の違い

2つのメソッドの挙動をそれぞれまとめます。

なおこの後でてくる「トランザクション」は、User.transaction doのような明示的に開始するトランザクションだけを指しているわけではありません。

User.createなどでデータベースに変更を加える際にRailsが自動的に管理する内部的なトランザクションも含めて指しています。

after_create

after_createはレコードがデータベースへ保存された直後、トランザクションがコミットされる前に実行されます。

そのためコールバック内でエラーが発生した場合、トランザクション全体がロールバックされるのです。

つまり、after_createはトランザクション内の処理の一部として扱われます。

after_create_commit

after_create_commitトランザクションが成功しコミットされた後に実行されます。

そのためコールバック内でエラーが発生しても、トランザクションには影響を及ぼしません。

after_createafter_create_commitの使い分け

after_createafter_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を使用することで、非同期ジョブを安全かつ確実に実行できます。

使い方を誤ると予期せぬ動作や意図しない結果を引き起こしてしまう可能性があるため、きちんと両者の挙動を理解して使い分けましょう。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0