Active Record コールバック

このガイドでは、Active Recordオブジェクトのライフサイクルにフックをかける方法について説明します。

このガイドの内容:

  • Active Recordオブジェクトのどのライフサイクルでイベントが発生するか
  • それらのイベントに応答するコールバックを登録・実行・スキップする方法
  • リレーション/関連付け/条件付き/トランザクションのコールバックを作成する方法
  • コールバックを再利用するために共通の振る舞いをカプセル化するオブジェクトを作成する方法

1 オブジェクトのライフサイクル

Railsアプリケーションの通常の操作中に、オブジェクトが作成・更新・破棄されることがあります。Active Recordは、このオブジェクトのライフサイクルへのフックを提供することでアプリケーションとそのデータを制御できます。

コールバックを使うと、オブジェクトの状態の変更「前」または変更「後」にロジックをトリガーできます。コールバックとは、オブジェクトのライフサイクルの特定の瞬間に呼び出されるメソッドのことです。コールバックを使えば、Active Recordオブジェクトがデータベースで初期化・作成・保存・更新・削除・バリデーション・読み込みのたびに実行されるコードを記述できます。

 class BirthdayCake < ApplicationRecord after_create -> { Rails.logger.info("Congratulations, the callback has run!") } end 
 irb> BirthdayCake.create Congratulations, the callback has run! 

このように、ライフサイクルにはさまざまなイベントがあり、イベントの「前」「後」「前後」でフックするさまざまなオプションがあります。

2 コールバックを登録する

利用可能なコールバックを使うには、コールバックを実装して登録する必要があります。コールバックの実装は、通常のメソッド、ブロック、procを利用したり、クラスまたはモジュールでカスタムコールバックオブジェクトを定義するなど、さまざまな方法で行えます。これらの実装手法をそれぞれ見ていきましょう。

コールバックを登録するために、通常のメソッドを呼び出すマクロ形式のクラスメソッドを実装用に利用できます。

 class User < ApplicationRecord validates :username, :email, presence: true before_validation :ensure_username_has_value private def ensure_username_has_value if username.blank? self.username = email end end end 

このマクロスタイルのクラスメソッドはブロックも受け取れます。以下のようにコールバックしたいコードがきわめて短く、1行に収まるような場合にこのスタイルを検討しましょう。

 class User < ApplicationRecord validates :username, :email, presence: true before_validation do self.username = email if username.blank? end end 

以下のように、コールバックにprocを渡してトリガーさせることも可能です。

 class User < ApplicationRecord validates :username, :email, presence: true before_validation ->(user) { user.username = user.email if user.username.blank? } end 

最後に、独自のカスタムコールバックオブジェクトも定義できます。これについては後述します。

 class User < ApplicationRecord validates :username, :email, presence: true before_validation AddUsername end class AddUsername def self.before_validation(record) if record.username.blank? record.username = record.email end end end 

2.1 ライフサイクルイベントで実行されるコールバックを登録する

コールバックは、特定のライフサイクルイベントでのみ実行されるように登録することも可能です。:onオプションを指定することで、コールバックがいつ、どのようなコンテキストでトリガーされるかを完全に制御できます。

コンテキスト(context)とは、特定のバリデーションを適用するカテゴリまたはシナリオのようなものです。Active Recordモデルをバリデーションするときに、コンテキストを指定することでバリデーションをグループ化できます。これにより、さまざまな状況に適用される多種多様なバリデーションセットを作成できます。Railsには、:create:update:saveなどのバリデーション用コンテキストがデフォルトで用意されています。

 class User < ApplicationRecord validates :username, :email, presence: true before_validation :ensure_username_has_value, on: :create # :onは配列も受け取れる after_validation :set_location, on: [ :create, :update ] private def ensure_username_has_value if username.blank? self.username = email end end def set_location self.location = LocationService.query(self) end end 

コールバックはprivateメソッドとして宣言するのが好ましい方法です。コールバックメソッドがpublicな状態のままだと、このメソッドがモデルの外から呼び出され、オブジェクトのカプセル化の原則に違反する可能性があります。

コールバックメソッド内では、updatesaveなどのメソッドや、オブジェクトに副作用を引き起こすその他のメソッド呼び出しは避けてください。

たとえば、コールバック内でupdate(attribute: "value")を呼び出してはいけません。この方法はモデルの状態を変更してしまい、コミット中に思わぬ副作用を引き起こす可能性があります。

代わりに、より安全なアプローチとして、before_createbefore_update、またはそれ以前のタイミングでトリガーされるコールバックを使えば安全に値を直接代入できます(例: self.attribute = "value")。

3 利用可能なコールバック

Active Recordで利用可能なコールバックの一覧を以下に示します。これらのコールバックは、実際の操作中に呼び出される順序に並んでいます

3.1 オブジェクトの作成

この2つのコールバックについて詳しくは、after_commitafter_rollbackセクションを参照してください。

これらのコールバックの利用方法を示す例を以下に示します。コールバックは関連する操作ごとにグループ化されており、最後に組み合わせて使う方法を示します。

3.1.1 バリデーション時のコールバック

バリデーション時のコールバックは、レコードがvalid?(またはエイリアスのvalidate)、またはinvalid?メソッドで直接バリデーションされるか、もしくはcreateupdatesaveで間接的にバリデーションされるたびにトリガーされます。

before_validationはバリデーションフェーズの直前に呼び出され、after_validationはバリデーションフェーズの直後に呼び出されます。

 class User < ApplicationRecord validates :name, presence: true before_validation :titleize_name after_validation :log_errors private def titleize_name self.name = name.downcase.titleize if name.present? Rails.logger.info("Name titleized to #{name}") end def log_errors if errors.any? Rails.logger.error("Validation failed: #{errors.full_messages.join(', ')}") end end end 
 irb> user = User.new(name: "", email: "john.doe@example.com", password: "abc123456") #=> #<User id: nil, email: "john.doe@example.com", created_at: nil, updated_at: nil, name: ""> irb> user.valid? Name titleized to Validation failed: Name can't be blank #=> false 
3.1.2 保存時のコールバック

保存時のコールバックは、レコードがcreateupdate、またはsaveメソッドで背後のデータベースに永続化(保存)されるたびにトリガーされます。

before_saveはオブジェクトが保存される直前に呼び出され、after_saveは保存の直後に、around_saveは保存の直前直後に呼び出されます。

 class User < ApplicationRecord before_save :hash_password around_save :log_saving after_save :update_cache private def hash_password self.password_digest = BCrypt::Password.create(password) Rails.logger.info("Password hashed for user with email: #{email}") end def log_saving Rails.logger.info("Saving user with email: #{email}") yield Rails.logger.info("User saved with email: #{email}") end def update_cache Rails.cache.write(["user_data", self], attributes) Rails.logger.info("Update Cache") end end 
 irb> user = User.create(name: "Jane Doe", password: "password", email: "jane.doe@example.com") Password hashed for user with email: jane.doe@example.com Saving user with email: jane.doe@example.com User saved with email: jane.doe@example.com Update Cache #=> #<User id: 1, email: "jane.doe@example.com", created_at: "2024-03-20 16:02:43.685500000 +0000", updated_at: "2024-03-20 16:02:43.685500000 +0000", name: "Jane Doe"> 
3.1.3 作成時のコールバック

作成時のコールバックは、レコードが背後のデータベースに初めて保存されるたびに、つまり、createまたはsaveメソッドで新規レコードを保存するときにトリガーされます。

before_createはオブジェクトが作成される直前に呼び出され、after_createは作成の直後に、around_createは作成の直前直後に呼び出されます。

 class User < ApplicationRecord before_create :set_default_role around_create :log_creation after_create :send_welcome_email private def set_default_role self.role = "user" Rails.logger.info("User role set to default: user") end def log_creation Rails.logger.info("Creating user with email: #{email}") yield Rails.logger.info("User created with email: #{email}") end def send_welcome_email UserMailer.welcome_email(self).deliver_later Rails.logger.info("User welcome email sent to: #{email}") end end 
 irb> user = User.create(name: "John Doe", email: "john.doe@example.com") User role set to default: user Creating user with email: john.doe@example.com User created with email: john.doe@example.com User welcome email sent to: john.doe@example.com #=> #<User id: 10, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe"> 

3.2 オブジェクトの更新

更新時のコールバックは、既存のレコードが背後のデータベースで永続化(保存)されるたびにトリガーされます。これらは、オブジェクトが更新される直前、更新された直後、および更新の直前直後に呼び出されます。

after_saveコールバックはcreateupdateの両方で実行されますが、マクロ呼び出しの実行順序にかかわらず、常にafter_createafter_updateという特定のコールバックよりもに呼び出されます。同様に、保存前と保存前後のコールバックも同じルールに従います。before_saveは作成・更新よりもに実行され、around_saveは作成・更新操作の直前直後で実行されます。保存コールバックは常に、より具体的な作成・更新コールバックの直前/直前直後/直後に実行されることに注意しておくことが重要です。

バリデーション時のコールバック保存時のコールバックについては既に説明しました。これら2つのコールバックの利用例については、after_commitafter_rollbackセクションを参照してください。

3.2.1 更新時のコールバック
 class User < ApplicationRecord before_update :check_role_change around_update :log_updating after_update :send_update_email private def check_role_change if role_changed? Rails.logger.info("User role changed to #{role}") end end def log_updating Rails.logger.info("Updating user with email: #{email}") yield Rails.logger.info("User updated with email: #{email}") end def send_update_email UserMailer.update_email(self).deliver_later Rails.logger.info("Update email sent to: #{email}") end end 
 irb> user = User.find(1) #=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "user" > irb> user.update(role: "admin") User role changed to admin Updating user with email: john.doe@example.com User updated with email: john.doe@example.com Update email sent to: john.doe@example.com 
3.2.2 コールバックを組み合わせる

欲しい振る舞いを実現するには、コールバックを組み合わせて使う必要が生じることがよくあります。

たとえば、ユーザーが作成された後に確認メールを送信したいが、そのユーザーが新規で更新されていない場合のみ確認メールを送信したい場合や、ユーザー更新時に重要な情報が変更された場合は管理者に通知したい場合があります。

この場合、after_createコールバックとafter_updateコールバックを組み合わせて使えます。

 class User < ApplicationRecord after_create :send_confirmation_email after_update :notify_admin_if_critical_info_updated private def send_confirmation_email UserMailer.confirmation_email(self).deliver_later Rails.logger.info("Confirmation email sent to: #{email}") end def notify_admin_if_critical_info_updated if saved_change_to_email? || saved_change_to_phone_number? AdminMailer.user_critical_info_updated(self).deliver_later Rails.logger.info("Notification sent to admin about critical info update for: #{email}") end end end 
 irb> user = User.create(name: "John Doe", email: "john.doe@example.com") Confirmation email sent to: john.doe@example.com #=> #<User id: 1, email: "john.doe@example.com", ...> irb> user.update(email: "john.doe.new@example.com") Notification sent to admin about critical info update for: john.doe.new@example.com #=> true 

3.3 オブジェクトの破棄

破棄(destroy)時のコールバックは、レコードが破棄されるたびにトリガーされますが、レコードが削除(delete)されるときは無視されます。

before_destroyはオブジェクトが破棄される直前に呼び出され、after_destroyは破棄された直後に、around_destroyは破棄される直前直後に呼び出されます。

利用例については、after_commitafter_rollbackを参照してください。

3.3.1 破棄時のコールバック
 class User < ApplicationRecord before_destroy :check_admin_count around_destroy :log_destroy_operation after_destroy :notify_users private def check_admin_count if admin? && User.where(role: "admin").count == 1 throw :abort end Rails.logger.info("Checked the admin count") end def log_destroy_operation Rails.logger.info("About to destroy user with ID #{id}") yield Rails.logger.info("User with ID #{id} destroyed successfully") end def notify_users UserMailer.deletion_email(self).deliver_later Rails.logger.info("Notification sent to other users about user deletion") end end 
 irb> user = User.find(1) #=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "admin"> irb> user.destroy Checked the admin count About to destroy user with ID 1 User with ID 1 destroyed successfully Notification sent to other users about user deletion 

3.4 after_initializeafter_find

after_initializeコールバックは、Active Recordオブジェクトがnewで直接インスタンス化されるたびに、またはレコードがデータベースから読み込まれるたびに呼び出されます。これを利用すれば、Active Recordのinitializeメソッドを直接オーバーライドせずに済みます。

after_findコールバックは、Active Recordがデータベースからレコードを読み込むたびに呼び出されます。

after_findafter_initializeが両方定義されている場合は、after_findが先に呼び出されます。

after_initializeafter_findコールバックには、対応するbefore_*メソッドはありません。

これらも、他のActive Recordコールバックと同様に登録できます

 class User < ApplicationRecord after_initialize do |user| Rails.logger.info("オブジェクトは初期化されました") end after_find do |user| Rails.logger.info("オブジェクトが見つかりました") end end 
 irb> User.new オブジェクトは初期化されました #=> #<User id: nil> irb> User.first オブジェクトが見つかりました オブジェクトは初期化されました #=> #<User id: 1> 

3.5 after_touch

after_touchコールバックは、Active Recordオブジェクトがtouchされるたびに呼び出されます。詳しくはAPIドキュメントのtouchを参照してください。

 class User < ApplicationRecord after_touch do |user| Rails.logger.info("オブジェクトにtouchしました") end end 
 irb> user = User.create(name: "Kuldeep") #=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49"> irb> user.touch オブジェクトにtouchしました #=> true 

このコールバックはbelongs_toと併用できます。

 class Book < ApplicationRecord belongs_to :library, touch: true after_touch do Rails.logger.info("Bookがtouchされました") end end class Library < ApplicationRecord has_many :books after_touch :log_when_books_or_library_touched private def log_when_books_or_library_touched Rails.logger.info("Book/Libraryがtouchされました") end end 
 irb> book = Book.last #=> #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05"> irb> book.touch # book.library.touchがトリガーされる Bookがtouchされました Book/Libraryがtouchされました #=> true 

4 コールバックの実行

以下のメソッドはコールバックをトリガーします。

  • create
  • create!
  • destroy
  • destroy!
  • destroy_all
  • destroy_by
  • save
  • save!
  • save(validate: false)
  • save!(validate: false)
  • toggle!
  • touch
  • update_attribute
  • update_attribute!
  • update
  • update!
  • valid?
  • validate

また、after_findコールバックは以下のfinderメソッドを実行すると呼び出されます。

  • all
  • first
  • find
  • find_by
  • find_by!
  • find_by_*
  • find_by_*!
  • find_by_sql
  • last
  • sole
  • take

after_initializeコールバックは、そのクラスの新しいオブジェクトが初期化されるたびに呼び出されます。

find_by_*メソッドとfind_by_*!メソッドは、属性ごとに自動的に生成される動的なfinderメソッドです。詳しくは動的finderセクションを参照してください。

5 条件付きコールバック

バリデーションのときと同様に、指定された述語の条件を満たす場合に実行されるコールバックメソッドの呼び出しも作成可能です。これを行なうには、コールバックで:ifオプションまたは:unlessオプションを使います。このオプションはシンボル、Proc、またはArrayを引数に取ります。

特定の状況でのみコールバックを呼び出す必要がある場合は、:ifオプションを使います。 特定の状況でコールバックを呼び出してはならない場合は、:unlessオプションを使います。

5.1 :ifおよび:unlessオプションでシンボルを使う

:ifオプションまたは:unlessオプションは、コールバックの直前に呼び出される述語メソッド名に対応するシンボルと関連付けることが可能です。

:ifオプションを使う場合、述語メソッドがfalseを返せばコールバックは実行されません:unlessオプションを使う場合、述語メソッドがtrueを返せばコールバックは実行されません。これはコールバックで最もよく使われるオプションです。

 class Order < ApplicationRecord before_save :normalize_card_number, if: :paid_with_card? end 

この方法で登録すれば、さまざまな述語メソッドを登録して、コールバックを呼び出すべきかどうかをチェックできるようになります。詳しくはコールバックで複数の条件を指定するで後述します。

5.2 :ifおよび:unlessオプションでProcを使う

:ifおよび:unlessオプションではProcオブジェクトも利用できます。このオプションは、1行以内に収まるワンライナーでバリデーションを行う場合に最適です。

 class Order < ApplicationRecord before_save :normalize_card_number, if: ->(order) { order.paid_with_card? } end 

procはそのオブジェクトのコンテキストで評価されるので、以下のように書くこともできます。

 class Order < ApplicationRecord before_save :normalize_card_number, if: -> { paid_with_card? } end 

5.3 コールバックで複数の条件を指定する

:if:unlessオプションは、procやメソッド名のシンボルの配列を受け取ることも可能です。

 class Comment < ApplicationRecord before_save :filter_content, if: [:subject_to_parental_control?, :untrusted_author?] end 

条件リストではprocを手軽に利用できます。

 class Comment < ApplicationRecord before_save :filter_content, if: [:subject_to_parental_control?, -> { untrusted_author? }] end 

5.4 :if:unlessを同時に使う

コールバックでは、同じ宣言の中で:if:unlessを併用できます。

 class Comment < ApplicationRecord before_save :filter_content, if: -> { forum.parental_control? }, unless: -> { author.trusted? } end 

このコールバックは、すべての:if条件がtrueと評価され、どの:unless条件もtrueと評価されなかった場合にのみ実行されます。

6 コールバックをスキップする

バリデーションの場合と同様、以下のメソッドを使うとコールバックはスキップされます。

Userモデルのbefore_saveコールバックがユーザーのメールアドレスの変更を記録する場合を考えてみましょう。

 class User < ApplicationRecord before_save :log_email_change private def log_email_change if email_changed? Rails.logger.info("Email changed from #{email_was} to #{email}") end end end 

ここで、メールアドレスの変更を記録するbefore_saveコールバックをトリガーせずにユーザーのメールアドレスを更新したいというシナリオがあるとします。これはupdate_columnsメソッドを使えば可能です。

 irb> user = User.find(1) irb> user.update_columns(email: 'new_email@example.com') 

上は、before_saveコールバックをトリガーせずにユーザーのメールアドレスを更新しています。

コールバックによっては、スキップしてはならない重要なビジネスルールやアプリケーションロジックが設定されている可能性もあるので、これらのメソッドの利用には十分注意すべきです。この点を理解せずにコールバックをバイパスすると、データの不整合が発生する可能性があります。

7 保存を抑制する

ある種のシナリオでは、コールバック内でレコードが保存されないように一時的に保存を抑制する必要が生じることがあります。保存の抑制は、レコードの関連付けが複雑にネストしている状況で、コールバックを恒久的に無効にしたり複雑な条件付きロジックを導入したりせずに、特定の操作中にのみ特定のレコードの保存をスキップしたい場合に役立ちます。

Railsは、ActiveRecord::Suppressorモジュールでコールバックを抑制する(suppress)メカニズムを提供しています。コールバックを抑制したいコードブロックをこのモジュールでラップすると、その操作中はコールバックが実行されなくなります。

ユーザーに対してさまざまな通知が行われるシナリオを考えてみましょう。 以下のUserを作成すると、Notificationレコードも自動的に作成されます。

 class User < ApplicationRecord has_many :notifications after_create :create_welcome_notification def create_welcome_notification notifications.create(event: "sign_up") end end class Notification < ApplicationRecord belongs_to :user end 

ユーザーを作成するときに通知を作成しないようにするには、次のようにActiveRecord::Suppressorモジュールを利用します。

 Notification.suppress do User.create(name: "Jane", email: "jane@example.com") end 

上のコードは、Notification.suppressブロックにより、ユーザー"Jane"の作成中は Notificationを保存しなくなります。

ActiveRecord::Suppressorを利用すると、コールバックの実行を選択的に制御できるメリットがある反面、コードが複雑になって思わぬ振る舞いが発生する可能性もあります。コールバックを抑制すると、アプリケーションで意図したフローがわかりにくくなり、今後のコードベースの理解やメンテナンスが困難になる可能性があります。ActiveRecord::Suppressorを利用した場合の影響の大きさを慎重に検討し、ドキュメント作成やテストを入念に実施して、意図しない副作用やテストの失敗のリスクを軽減する必要があります。

8 コールバックを停止する

モデルに新しくコールバックを登録すると、コールバックは実行キューに入ります。このキューには、あらゆるモデルに対するバリデーション、登録済みコールバック、実行待ちのデータベース操作が置かれます。

コールバックチェーン全体は、1つのトランザクションにラップされます。コールバックの1つで例外が発生すると、実行チェーン全体が停止(halt)してロールバックが発行され、エラーが再度raiseします。

 class Product < ActiveRecord::Base before_validation do raise "Price can't be negative" if total_price < 0 end end Product.create # "Price can't be negative"がraiseする 

これによって、createsaveなどのメソッドで例外が発生することが想定されていないコードが、予期せず壊れます。

コールバックチェインの途中で例外が発生した場合は、ActiveRecord::RollbackまたはActiveRecord::RecordInvalid例外でない限り、Railsは例外を再度raiseします。代わりにthrow :abortを用いてコールバックチェインを意図的に停止する必要があります。いずれかのコールバックが:abortをスローすると、プロセスは中止し、createはfalseを返します。

 class Product < ActiveRecord::Base before_validation do throw :abort if total_price < 0 end end Product.create # => false 

ただし、(createではなく)create!を呼び出した場合はActiveRecord::RecordNotSavedが発生します。この例外は、コールバックの中断によりレコードが保存されなかったことを示します。

 User.create! # => ActiveRecord::RecordNotSavedをraiseする 

throw :abortがdestroy系のコールバックで呼び出された場合は、destroyはfalseを返します。

 class User < ActiveRecord::Base before_destroy do throw :abort if still_active? end end User.first.destroy # => false 

ただし、(destroyではなく)destroy!を呼び出した場合はActiveRecord::RecordNotDestroyedが発生します。

 User.first.destroy! # => ActiveRecord::RecordNotDestroyedをraiseする 

9 関連付けのコールバック

関連付けのコールバックは通常のコールバックと似ていますが、コレクションのライフサイクル内で発生するイベントによってトリガーされる点が異なります。利用可能な関連付けコールバックは以下のとおりです。

  • before_add
  • after_add
  • before_remove
  • after_remove

関連付けのコールバックは、関連付けの宣言でオプションを追加することで定義できます。

Authorモデルにhas_many :booksが定義されている例を考えてみましょう。ただし、authorsコレクションに本を追加する前に、その著者が本の個数制限に達していないことを確認する必要があります。個数制限を確認するためのbefore_addコールバックを追加することで、これを実行できます。

 class Author < ApplicationRecord has_many :books, before_add: :check_limit private def check_limit if books.count >= 5 errors.add(:base, "この著者には本を5冊までしか追加できません") throw(:abort) end end end 

before_addコールバックが:abortをスローした場合、オブジェクトはコレクションに追加されません。

関連付けられているオブジェクトに対して複数の操作を実行したい場合があります。この場合はコールバックを配列として渡せば、単一のイベントに複数のコールバックを積み上げられます。さらにRailsは、追加または削除されるオブジェクトをコールバックに渡して利用可能にしてくれます。

 class Author < ApplicationRecord has_many :books, before_add: [:check_limit, :calculate_shipping_charges] def check_limit if books.count >= 5 errors.add(:base, "この著者には本を5冊までしか追加できません") throw(:abort) end end def calculate_shipping_charges(book) weight_in_pounds = book.weight_in_pounds || 1 shipping_charges = weight_in_pounds * 2 shipping_charges end end 

同様に、before_removeコールバックが:abortをスローした場合、オブジェクトはコレクションから削除されません。

これらのコールバックは、関連付けられているオブジェクトが関連付けコレクションを通じて追加・削除された場合にのみ呼び出されます。

 # `before_add`コールバックがトリガーされる author.books << book author.books = [book, book2] # `before_add`コールバックはトリガーされない book.update(author_id: 1) 

10 関連付けのコールバックをカスケードする

コールバックは、関連付けられたオブジェクトが変更されたタイミングで実行できます。コールバックはモデルの関連付けを通じて機能し、ライフサイクルイベントが関連付けにカスケードする形でコールバックを起動できます。

Userモデルにhas_many :articlesが定義されている例を考えてみましょう。ユーザーが破棄(destroy)された場合、そのユーザーの記事も合わせて破棄する必要があります。Articleモデルへの関連付けを介して、Userモデルにafter_destroyコールバックを追加してみましょう。

 class User < ApplicationRecord has_many :articles, dependent: :destroy end class Article < ApplicationRecord after_destroy :log_destroy_action def log_destroy_action Rails.logger.info("Article destroyed") end end 
 irb> user = User.first #=> #<User id: 1> irb> user.articles.create! #=> #<Article id: 1, user_id: 1> irb> user.destroy Article destroyed #=> #<User id: 1> 

before_destroyコールバックを使う場合は、レコードがdependent: :destroyで削除される前に実行されるように、dependent: :destroy関連付けの前に配置する(またはprepend: trueオプションを指定する)必要があります。

11 トランザクションのコールバック

11.1 after_commitコールバックとafter_rollbackコールバック

データベースのトランザクションが完了したときにトリガーされるコールバックが2つあります。after_commitafter_rollbackです。

これらのコールバックはafter_saveコールバックときわめて似ていますが、データベースの変更のコミットまたはロールバックが完了するまでトリガーされない点が異なります。これらのメソッドは、Active Recordのモデルから、データベーストランザクションの一部に含まれていない外部のシステムとやりとりしたい場合に特に便利です。

例として、PictureFileモデルで、対応するレコードが削除された後にファイルを1つ削除する必要があるとしましょう。

 class PictureFile < ApplicationRecord after_destroy :delete_picture_file_from_disk def delete_picture_file_from_disk if File.exist?(filepath) File.delete(filepath) end end end 

after_destroyコールバックの直後に何らかの例外が発生してトランザクションがロールバックすると、ファイルが削除され、モデルの一貫性が損なわれたままになってしまいます。 ここで、以下のコードにあるpicture_file_2オブジェクトが無効で、save!メソッドがエラーを発生するとします。

 PictureFile.transaction do picture_file_1.destroy picture_file_2.save! end 

after_commitコールバックを使えば、このような場合に対応できます。

 class PictureFile < ApplicationRecord after_commit :delete_picture_file_from_disk, on: :destroy def delete_picture_file_from_disk if File.exist?(filepath) File.delete(filepath) end end end 

:onオプションは、コールバックがトリガーされるタイミングを指定します。:onオプションを指定しないと、すべてのアクションでコールバックがトリガーされます。詳しくは:onの利用方法を参照してください。

トランザクションが完了すると、そのトランザクション内で作成・更新・破棄されたすべてのモデルに対してafter_commitコールバックまたはafter_rollbackコールバックが呼び出されます。 ただし、これらのコールバックのいずれかで例外が発生した場合、その例外はバブルアップされ、残りのafter_commitafter_rollbackメソッドは実行されません。

 class User < ActiveRecord::Base after_commit { raise "Intentional Error" } after_commit { # 1つ上のafter_commitで例外が発生するため、これは呼び出されない Rails.logger.info("This will not be logged") } end 

コールバックコードで例外が発生した場合は、他のコールバック実行が中断されないよう、その例外をrescueしてコールバック内で処理する必要があります。

after_commitの保証は、after_saveafter_updateafter_destroyの保証とはまったく異なります。たとえば、以下のafter_saveで例外が発生した場合、トランザクションはロールバックし、データは保持されません。

 class User < ActiveRecord::Base after_save do # これが失敗したらユーザーは保存されない EventLog.create!(event: "user_saved") end end 

しかし、データはafter_commit中に既にデータベースに保存されているため、例外が発生しても何もロールバックしなくなります。

 class User < ActiveRecord::Base after_commit do # これが失敗したらユーザーは既に保存済み EventLog.create!(event: "user_saved") end end 

after_commitコールバックやafter_rollbackコールバック内で実行されるコード自体は、トランザクション内に囲まれません。

データベース内の同じレコードを単一のトランザクションのコンテキストで表現する場合、after_commitコールバックやafter_rollbackコールバックで注意すべき重要な動作があります。 これらのコールバックは、トランザクション内で変更される特定のレコードの最初のオブジェクトに対してのみトリガーされます。読み込まれている他のオブジェクトは、同じデータベースレコードを表現しているにもかかわらず、after_commitコールバックやafter_rollbackコールバックはどのオブジェクトでもトリガーされません。

 class User < ApplicationRecord after_commit :log_user_saved_to_db, on: :update private def log_user_saved_to_db Rails.logger.info("ユーザーはデータベースに保存されました") end end 
 irb> user = User.create irb> User.transaction { user.save; user.save } # ユーザーはデータベースに保存されました 

この微妙な振る舞いは、同じデータベースレコードに関連付けられている個別のオブジェクトに対して独立したコールバック実行が予想されるシナリオで、特に大きな影響を及ぼします。コールバックシーケンスのフローや予測可能性に影響し、そのトランザクションの後のアプリケーションロジックに不整合が生じる可能性があります。

11.2 after_commitコールバックのエイリアス

after_commitコールバックは作成・更新・削除でのみ用いることが多いので、それぞれのエイリアスも用意されています。場合によっては、createupdateの両方に単一のコールバックを使わなければならなくなることもあります。これらの操作の一般的なエイリアスを次に示します。

いくつか例を見てみましょう。

以下は、onオプションを指定したafter_commitdestroyに使っています。

 class PictureFile < ApplicationRecord after_commit :delete_picture_file_from_disk, on: :destroy def delete_picture_file_from_disk if File.exist?(filepath) File.delete(filepath) end end end 

上と同じことをafter_destroy_commitを使ってもできます。

 class PictureFile < ApplicationRecord after_destroy_commit :delete_picture_file_from_disk def delete_picture_file_from_disk if File.exist?(filepath) File.delete(filepath) end end end 

同じ要領でafter_create_commitafter_update_commitも使えます。

ただし、after_create_commitコールバックとafter_update_commitコールバックに同じメソッド名を指定すると、両方とも内部的にafter_commitにエイリアスされ、同じメソッド名で以前に定義されたコールバックをオーバーライドするため、最後に定義されたコールバックだけが有効になってしまいます。

 class User < ApplicationRecord after_create_commit :log_user_saved_to_db after_update_commit :log_user_saved_to_db private def log_user_saved_to_db # これは1回しか呼び出されない Rails.logger.info("ユーザーはデータベースに保存されました") end end 
 irb> user = User.create # 何も出力しない irb> user.save # userを更新する ユーザーはデータベースに保存されました 

この場合は、代わりにafter_save_commitを使う方が適切です。これは、作成と更新の両方でafter_commitコールバックを利用するためのエイリアスです。

 class User < ApplicationRecord after_save_commit :log_user_saved_to_db private def log_user_saved_to_db Rails.logger.info("ユーザーはデータベースに保存されました") end end 
 irb> user = User.create # Userを作成 ユーザーはデータベースに保存されました irb> user.save # userを更新 ユーザーはデータベースに保存されました 

11.3 トランザクショナルなコールバックの順序

Rails 7.1以降のコールバックは、デフォルトでは定義された順序で実行されます。

 class User < ActiveRecord::Base after_commit { Rails.logger.info("これは1番目に実行される") } after_commit { Rails.logger.info("これは2番目に実行される") } end 

ただし、それより前のバージョンのRailsでは、トランザクショナルなafter_コールバック(after_commitafter_rollbackなど)を複数定義すると、コールバックの実行順序が定義と逆順になりました。

何らかの理由で引き続き逆順に実行したい場合は、以下の設定をfalseに設定することで、コールバックが逆順で実行されます。詳しくは、Active Recordの設定オプションを参照してください。

 config.active_record.run_after_transaction_callbacks_in_order_defined = false 

これは、after_destroy_commitなどを含むすべてのafter_*_commitコールバックに適用されます。

11.4 トランザクションごとのコールバックを登録する

特定のトランザクションに対してbefore_commitafter_commitafter_rollbackなどのトランザクションコールバックを登録することも可能です。これは、アクションをモデル固有ではなく、作業単位として実行する必要がある状況で便利です。

ActiveRecord::Base.transactionActiveRecord::Transactionオブジェクトを返すので、これに対してコールバックを登録できます。

 Article.transaction do |transaction| article.update(published: true) transaction.after_commit do PublishNotificationMailer.with(article: article).deliver_later end end 

11.5 ActiveRecord.after_all_transactions_commit

[ActiveRecord.after_all_transactions_commit][after_all_transactions_commit]は、すべての現在のトランザクションがデータベースに正常にコミットされた「後に」コードを実行するためのコールバックです。

 def publish_article(article) Article.transaction do Post.transaction do ActiveRecord.after_all_transactions_commit do PublishNotificationMailer.with(article: article).deliver_later # メールは、最も外側のトランザクションがコミットされた後に送信される end end end end 

after_all_transactions_commitに登録したコールバックは、最も外側のトランザクションがコミットされた後にトリガーされます。現在開いているトランザクションのいずれかがロールバックされた場合、そのブロックは呼び出されません。 コールバックが登録された時点でオープン中のトランザクションが存在しない場合、そのブロックは直ちに実行されます。

[after_all_transactions_commit] : https://api.rubyonrails.org/classes/ActiveRecord.html#method-c-after_all_transactions_commit

12 コールバックオブジェクト

作成したコールバックメソッドが便利なので、他のモデルで再利用したくなる場合があります。Active Recordでは、コールバックメソッドをカプセル化するクラスを作成することでコールバックを再利用可能にできます。

以下は、ファイルシステム上で破棄したファイルのクリーンアップを処理するafter_commitコールバッククラスの例です。この振る舞いはPictureFileモデルに固有とは限らず、他でも共有したい場合もあるため、これを別のクラスにカプセル化することをオススメします。こうすることで、この振る舞いのテストや変更がずっと簡単になります。

 class FileDestroyerCallback def after_commit(file) if File.exist?(file.filepath) File.delete(file.filepath) end end end 

クラス内で上のように宣言すると、コールバックメソッドはモデルオブジェクトをパラメーターとして受け取ります。

これは次のように、そのクラスを利用するすべてのモデルで機能します。

 class PictureFile < ApplicationRecord after_commit FileDestroyerCallback.new end 

ここではコールバックをインスタンスメソッドとして宣言しているので、FileDestroyerCallbackオブジェクトをnewでインスタンス化する必要があることにご注意ください。これは、コールバックがインスタンス化されたオブジェクトのステートを利用する場合に特に便利です。

ただし多くの場合、以下のようにコールバックをクラスメソッドとして宣言する方が合理的です。

 class FileDestroyerCallback def self.after_commit(file) if File.exist?(file.filepath) File.delete(file.filepath) end end end 

コールバックメソッドがこのようにクラスメソッドとして宣言されていれば、モデル内でFileDestroyerCallbackオブジェクトをnewでインスタンス化せずに済みます。

 class PictureFile < ApplicationRecord after_commit FileDestroyerCallback end 

コールバックオブジェクト内では、コールバックを必要なだけいくつでも宣言できます。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加