Active Record コールバック

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

このガイドの内容:

  • Active Recordオブジェクトのライフサイクルでいつイベントが発生するか
  • オブジェクトのライフサイクルにおけるイベントに応答するコールバックメソッドを作成する方法
  • コールバックで共通となる振る舞いをカプセル化する特殊なクラスの作成方法

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

Railsアプリケーションを普通に操作すると、その内部でオブジェクトが作成・更新・削除(destroy)されます。Active Recordはこのオブジェクトライフサイクルへのフックを提供しており、これを用いてアプリケーションやデータを制御できます。

コールバックは、オブジェクトの状態が切り替わる「前」または「後」にロジックをトリガします。

class Baby < ApplicationRecord
  after_create -> { puts "Congratulations!" }
end
irb> @baby = Baby.create
Congratulations!

このように、ライフサイクルにはさまざまなイベントがあり、イベントの「前」「後」「前後」のいずれかにフックできます。

2 コールバックの概要

コールバックとは、オブジェクトのライフサイクル期間における特定の瞬間に呼び出されるメソッドのことです。コールバックを利用することで、Active Recordオブジェクトが作成・保存・更新・削除・検証・データベースからの読み込み、などのイベント発生時に常に実行されるコードを書けるようになります。

2.1 コールバックの登録

コールバックを利用するためには、コールバックを登録する必要があります。コールバックの実装は普通のメソッドと特に違うところはありません。これをコールバックとして登録するには、マクロのようなスタイルのクラスメソッドを使います。

class User < ApplicationRecord
  validates :login, :email, presence: true

  before_validation :ensure_login_has_a_value

  private
    def ensure_login_has_a_value
      if login.blank?
        self.login = email unless email.blank?
      end
    end
end

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

class User < ApplicationRecord
  validates :login, :email, presence: true

  before_create do
    self.name = login.capitalize if name.blank?
  end
end

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

class User < ApplicationRecord
  before_create ->(user) { user.name = user.login.capitalize if user.name.blank? }
end

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

class User < ApplicationRecord
  before_create MaybeAddName
end

class MaybeAddName
  def self.before_create(record)
    if record.name.blank?
      record.name = record.login.capitalize
    end
  end
end

コールバックは、特定のライフサイクルのイベントでのみ呼び出される形で登録することも可能です。 これにより、コールバックがトリガーされるタイミングやコンテキストを完全に制御できるようになります。

class User < ApplicationRecord
  before_validation :normalize_name, on: :create

  # :onは配列も受け取れる
  after_validation :set_location, on: [ :create, :update ]

  private
    def normalize_name
      self.name = name.downcase.titleize
    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 オブジェクトの作成

3.2 オブジェクトの更新

after_savecreateupdateの両方で実行されますが、マクロ呼び出しの実行順序にかかわらず、常にafter_createafter_updateという特定のコールバックよりもに呼び出されます。

3.3 オブジェクトのdestroy

before_destroyコールバックは、dependent: :destroyよりもに配置すること(またはprepend: trueオプションをお使いください)。理由は、そのレコードがdependent: :destroy関連付けによって削除されるよりも前にbefore_destroyコールバックが実行されるようにするためです。

after_commitの保証は、after_saveafter_updateafter_destroyの保証とは大きく異なります。たとえば、after_saveで例外が発生した場合、トランザクションはロールバックされ、データは永続化されません。 一方、after_commitで発生したものは、トランザクションが既に完了し、データがデータベースに永続化されたことを保証できます。詳しくはトランザクションのコールバックで後述します。

3.4 after_initializeafter_find

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

after_findコールバックは、Active Recordがデータベースからレコードを1件読み込むたびに呼び出されます。after_findafter_initializeが両方定義されている場合は、after_findが先に呼び出されます。

after_initializeafter_findコールバックには、対応するbefore_*メソッドはありませんが、他のActive Recordコールバックと同様に登録できます。

class User < ApplicationRecord
  after_initialize do |user|
    puts "オブジェクトは初期化されました"
  end

  after_find do |user|
    puts "オブジェクトが見つかりました"
  end
end
irb> User.new
オブジェクトは初期化されました
=> #<User id: nil>

irb> User.first
オブジェクトが見つかりました
オブジェクトは初期化されました
=> #<User id: 1>

3.5 after_touch

after_touchコールバックは、Active Recordオブジェクトがtouchされるたびに呼び出されます。

class User < ApplicationRecord
  after_touch do |user|
    puts "オブジェクトにtouchしました"
  end
end
irb> u = 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> u.touch
オブジェクトにtouchしました
=> true

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

class Book < ApplicationRecord
  belongs_to :library, touch: true
  after_touch do
    puts '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
      puts '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
  • update!
  • valid?

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

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

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

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

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

バリデーション(検証)の場合と同様、以下のメソッドでもコールバックをスキップできます。

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

6 コールバックの停止

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

コールバックチェーン全体は、1つのトランザクションにラップされます。コールバックの1つで例外が発生すると、実行チェーン全体が停止してロールバックが発行されます。チェーンを意図的に停止するには次のようにします。

throw :abort

ActiveRecord::RollbackActiveRecord::RecordInvalidを除く例外は、その例外によってコールバックチェインが停止した後も、Railsによって再び発生します。さらに、ActiveRecord::RollbackActiveRecord::RecordInvalid以外の例外が発生すると、saveupdateのようなメソッド(つまり通常truefalseを返そうとするメソッド)が例外を発生することを想定していないコードが中断する恐れがあります。

after_destroybefore_destroy、またはaround_destroyコールバック内で ActiveRecord::RecordNotDestroyed例外が発生した場合、destroyメソッドは再度例外を発生せずにfalseを返します。

7 リレーションシップのコールバック

コールバックはモデルのリレーションシップを経由して動作できます。また、リレーションシップを用いてコールバックを定義することも可能です。1人のユーザーが多数の記事(article)を持っている状況を例に取ります。ユーザーが削除されたら、ユーザーの記事も削除する必要があります。Userモデルにafter_destroyコールバックを追加し、このコールバックでPostモデルへのリレーションシップを経由すると以下のようになります。

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  after_destroy :log_destroy_action

  def log_destroy_action
    puts '記事を削除しました'
  end
end
irb> user = User.first
=> #<User id: 1>
irb> user.articles.create!
=> #<Article id: 1, user_id: 1>
irb> user.destroy
記事を削除しました
=> #<User id: 1>

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

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

  • before_add
  • after_add
  • before_remove
  • after_remove

関連付けコールバックを定義するには、関連付けの宣言で以下のようにオプションを追加します。

class Author < ApplicationRecord
  has_many :books, before_add: :check_credit_limit

  def check_credit_limit(book)
    # ...
  end
end

Railsは、追加または削除されるオブジェクトをコールバックに渡します。

以下のようにコールバックを配列として渡すことで、単一のイベントに複数のコールバックを登録できます。

class Author < ApplicationRecord
  has_many :books,
    before_add: [:check_credit_limit, :calculate_shipping_charges]

  def check_credit_limit(book)
    # ...
  end

  def calculate_shipping_charges(book)
    # ...
  end
end

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

# limit_reached?がtrueの場合はbookが追加されない
def check_credit_limit(book)
  throw(:abort) if limit_reached?
end

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

# この場合は`before_add`コールバックがトリガーされる
author.books << book
author.books = [book, book2]

# この場合は`before_add`コールバックがトリガーされない
book.update(author_id: 1)

9 条件付きコールバック

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

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

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

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

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

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

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

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

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

class Order < ApplicationRecord
  before_save :normalize_card_number,
    if: Proc.new { |order| order.paid_with_card? }
end

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

class Order < ApplicationRecord
  before_save :normalize_card_number, if: Proc.new { paid_with_card? }
end

9.3 :if:unlessを同時に使う

コールバックでは、以下のように同じ宣言内で:if:unlessを併用できます。

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

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

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

: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?, Proc.new { untrusted_author? }]
end

9.5 :if:unlessを同時に使う

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

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

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

10 コールバッククラス

有用なコールバックメソッドを書いた後で、他のモデルでも使い回したいことがあります。Active Recordは、コールバックメソッドをカプセル化したクラスを作成できるので、手軽に再利用できます。

ここでは、ファイルシステム上で破棄されたファイルのクリーンアップを行うためにafter_destroyコールバックを持つクラスを作成する例を示します。 この振る舞いはPictureFileモデルに固有のものではなく、共有したい場合もあるので、これを別のクラスにカプセル化するのは良い考えです。これにより、その振る舞いのテストや変更がずっと簡単になります。

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

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

class PictureFile < ApplicationRecord
  after_destroy FileDestroyerCallbacks.new
end

コールバックをインスタンスメソッドとして宣言したので、FileDestroyerCallbackオブジェクトを新しくインスタンス化する必要があったことにご注意ください。これは、インスタンス化されたオブジェクトの状態をコールバックメソッドで利用したい場合に特に便利です。ただし、コールバックをクラスメソッドとして宣言する方が理にかなうこともよくあります。

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

コールバックメソッドを上のように宣言した場合は、モデル内でFileDestroyerCallbackオブジェクトのインスタンス化が不要になります。

class PictureFile < ApplicationRecord
  after_destroy FileDestroyerCallback
end

コールバッククラスの内部では、いくつでもコールバックを宣言できます。

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

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

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

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

例として、直前の例で用いたPictureFileモデルで、対応するレコードが削除された後にファイルを1つ削除する必要があるとしましょう。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オプションを指定しないと、すべてのアクションでコールバックがトリガされます。

トランザクションが完了すると、そのトランザクション内で作成・更新・破棄されたすべてのモデルに対してafter_commitコールバックまたはafter_rollbackコールバックが呼び出されます。 ただし、これらのコールバックのいずれか内で例外が発生した場合、例外はバブルアップし、残りのafter_commitメソッドやafter_rollbackメソッドは実行されません。 そのため、コールバックのコードで例外が発生する可能性がある場合は、他のコールバックを実行できるように、その例外をrescueしてコールバック内で処理する必要があることにご注意ください。

after_commitコールバックやafter_rollbackコールバック内で実行されるコード自体は、トランザクション内に含まれない点にご注意ください。

単一トランザクションのコンテキストで、データベース内の同じレコードを表す複数の読み込み済みオブジェクトを操作する場合、after_commitコールバックやafter_rollbackコールバックの振る舞いには重要な注意点があります。 これらのコールバックは、トランザクション内で変更される特定のレコードの「最初のオブジェクト」に対してのみトリガーされます。読み込まれている他のオブジェクトは、同じデータベースレコードを表しているにもかかわらず、after_commitコールバックやafter_rollbackコールバックをトリガーしません。 この微妙な振る舞いによる影響が特に大きいシナリオは、「同じデータベースレコードに関連付けられるオブジェクトごとに、コールバックが独立して実行されることが予想される」場合です。この振る舞いは、コールバックシーケンスのフローや予測可能性に影響を与える可能性があり、トランザクションに沿って動作するアプリケーションロジックに不整合が生じる可能性があります。

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

after_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_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
      puts 'ユーザーはデータベースに保存されました'
    end
end
irb> @user = User.create # 何も出力しない

irb> @user.save          # @userを更新する
ユーザーはデータベースに保存されました

11.3 after_save_commit

作成と更新の両方でafter_commitコールバックを使う場合のエイリアスとして、after_save_commitも利用できます。

class User < ApplicationRecord
  after_save_commit :log_user_saved_to_db

  private
    def log_user_saved_to_db
      puts 'ユーザーはデータベースに保存されました'
    end
end
irb> @user = User.create # Userを作成
ユーザーはデータベースに保存されました

irb> @user.save # @userを更新
ユーザーはデータベースに保存されました

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

デフォルトでは、コールバックは定義された順序で実行されます。 ただし、トランザクショナルなafter_コールバック(after_commitafter_rollbackなど)を複数定義すると、定義時と逆順で実行される可能性があります。

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

これは、after_destroy_commitなどを含む、すべてのafter_*_commitのバリエーションにも当てはまります。

この実行順序は以下の設定で変更できます。

config.active_record.run_after_transaction_callbacks_in_order_defined = false

true(Rails 7.1以降のデフォルト)に設定すると、コールバックは定義された順序で実行されます。 falseに設定すると、上の例と同様に、実行順序が逆になります。

フィードバックについて

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

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

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

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

支援・協賛

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

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