Active Record コールバック

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

このガイドの内容:

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

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

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

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.nil?
        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

コールバックは、特定のライフサイクルのイベントでのみ呼び出されるように登録することもできます。

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な状態のままだと、このメソッドがモデルの外から呼び出され、オブジェクトのカプセル化の原則に違反する可能性があります。

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

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

3.1 オブジェクトの作成

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create
  • around_create
  • after_create
  • after_save
  • after_commit/after_rollback

3.2 オブジェクトの更新

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_update
  • around_update
  • after_update
  • after_save
  • after_commit/after_rollback

3.3 オブジェクトのdestroy

  • before_destroy
  • around_destroy
  • after_destroy
  • after_commit/after_rollback

after_saveコールバックは作成と更新の両方で呼び出されますが、コールバックマクロの呼び出し順にかかわらず、必ず、より詳細なafter_createコールバックやafter_updateコールバックより に呼び出されます。

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

3.4 after_initializeafter_find

after_initializeコールバックは、Active Recordオブジェクトが1つインスタンス化されるたびに呼び出されます。インスタンス化は、直接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

>> User.new
オブジェクトは初期化されました
=> #<User id: nil>

>> 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

>> 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">

>> u.touch
オブジェクトにtouchしました
=> true

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

class Employee < ApplicationRecord
  belongs_to :company, touch: true
  after_touch do
    puts 'Employeeモデルにtouchされました'
  end
end

class Company < ApplicationRecord
  has_many :employees
  after_touch :log_when_employees_or_company_touched

  private
    def log_when_employees_or_company_touched
      puts 'Employee/Companyがtouchされました'
    end
end

>> @employee = Employee.last
=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">

# @employee.company.touchをトリガーする
>> @employee.touch
Employee/Companytouchされました
Employeetouchされました
=> true

4 コールバックの実行

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

  • create
  • create!
  • destroy
  • destroy!
  • destroy_all
  • save
  • save!
  • 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 コールバックをスキップする

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

  • decrement!
  • decrement_counter
  • delete
  • delete_all
  • increment!
  • increment_counter
  • update_column
  • update_columns
  • update_all
  • update_counters

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

6 コールバックの停止

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

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

throw :abort

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

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

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

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

class Post < ApplicationRecord
  after_destroy :log_destroy_action

  def log_destroy_action
    puts 'Post destroyed'
  end
end

>> user = User.first
=> #<User id: 1>
>> user.posts.create!
=> #<Post id: 1, user_id: 1>
>> user.destroy
Post destroyed
=> #<User id: 1>

8 条件付きコールバック

検証と同様、与えられた述語の条件を満たす場合に実行されるコールバックメソッドの呼び出しを作成することもできます。これを行なうには、コールバックで:ifオプションまたは:unlessオプションを使います。このオプションはシンボル、Proc、またはArrayを引数に取ります。特定の状況でのみコールバックが呼び出される必要がある場合は、:ifオプションを使います。特定の状況ではコールバックを呼び出してはならない場合は、:unlessオプションを使います。

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

:ifオプションまたは:unlessオプションは、コールバックの直前に呼び出される述語メソッド(訳注: trueかfalseのいずれかの値のみを返すメソッド)の名前に対応するシンボルと関連付けることができます。:ifオプションを使う場合、述語メソッドがfalseを返せばコールバックは実行されません。:unlessオプションを使う場合、述語メソッドがtrueを返せばコールバックは実行されません。これはコールバックで最もよく使われるオプションです。この方法で登録することで、いくつもの異なる述語メソッドを登録して、コールバックを呼び出すべきかどうかをチェックすることができます。

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

8.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

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

1つの条件付きコールバック宣言内で、:ifオプションと:unlessオプションを同時に使えます。

class Comment < ApplicationRecord
  after_create :send_email_to_author, if: :author_wants_emails?,
    unless: Proc.new { |comment| comment.post.ignore_comments? }
end

8.4 コールバックの条件を結合する

コールバックが行われるべきかどうかを定義する条件が複数ある場合は、Arrayを使えます。同じコールバックで:if:unlessを両方適用することも可能です。

class Comment < ApplicationRecord
  after_create :send_email_to_author,
    if: [Proc.new { |c| c.user.allow_send_email? }, :author_wants_emails?],
    unless: Proc.new { |c| c.article.ignore_comments? }
end

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

9 コールバッククラス

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

以下の例では、PictureFileモデル用にafter_destroyコールバックを持つクラスを作成しています。

class PictureFileCallbacks
  def after_destroy(picture_file)
    if File.exist?(picture_file.filepath)
      File.delete(picture_file.filepath)
    end
  end
end

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

class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks.new
end

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

class PictureFileCallbacks
  def self.after_destroy(picture_file)
    if File.exist?(picture_file.filepath)
      File.delete(picture_file.filepath)
    end
  end
end

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

class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks
end

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

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

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

例として、直前の例に用いたPictureFileモデルで、対応するレコードがdestroyされた後にファイルを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_create_commit
  • after_update_commit
  • 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_commitコールバックおよびafter_rollbackコールバックは、1つのトランザクションブロック内で作成/更新/destroyされたすべてのモデルで呼び出されます。ただし、これらのコールバックのいずれかで何らかの例外が発生すると、その例外のせいで以後のafter_commitコールバックやafter_rollbackコールバックのメソッドは実行されなくなります。このため、もし自作のコールバックが例外を発生する可能性がある場合は、自分のコールバック内でrescueして適切にエラー処理を行い、他のコールバックが停止しないようにする必要があります。

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

同一のモデル内でafter_create_commitafter_update_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 'User was saved to database'
  end
end

# 何も出力されない
>> @user = User.create

# @userを更新する
>> @user.save
=> User was saved to database

作成や更新の両方の操作にコールバックを登録するには、代わりにafter_commitをお使いください。以下のエイリアスも、作成や更新の両方で使えるafter_commitコールバックとして用いることができます。

  • after_save_commit
class User < ApplicationRecord
  after_commit :log_user_saved_to_db, on: [:create, :update]
  after_save_commit :log_user_saved_to_db

  private
  def log_user_saved_to_db
    puts 'User was saved to database'
  end
end

# ユーザーを1人作成する
>> @user = User.create
=> User was saved to database

# @userを更新する
>> @user.save
=> User was saved to database

支援・協賛

Railsガイドは下記のサポーターから継続的な支援を受けています。