Activeレコードのコールバック

このガイドでは、Activeレコードオブジェクトのサイクルに、任意の処理をフックする方法を説明します。 このガイドを読むことで、次の事が学べるはずです。

  • Activeレコードオブジェクトのライフサイクルについて
  • オブジェクトのライフサイクルのイベントに応答するコールバックメソッドの作成方法について
  • コールバックのために共通の処理をカプラセル化する特別なクラスの作成方法について

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

通常、Railsアプリケーションが運用されている間、オブジェクトは作成され、更新され、そして削除されます。 Activeレコードは、アプリケーションとデータの制御のために、このオブジェクトのライフサイクルにフックする機能を提供してくれます。

オブジェクトの状態が切り替わる前後に、コールバックによって任意の処理をトリガ出来るようになります。

2. コールバックの概要

コールバックはオブジェクトのライフサイクルの特定タイミングで呼び出されるメソッドです。 コールバックを使えば、Activeレコードが作成された時、保存された時、削除された時、検証された時、またデータベースから読み込まれた時など、 任意のタイミングで実行するコードを書くことが出来るようになります。

2.1 コールバックの登録

コールバックを使用できるようにするためには、コールバックの登録を行う必要があります。 任意のメソッドをコールバックとして実装することが可能で、これを登録するためにマクロ機能形式のクラスメソッドを使用します。

class User < ActiveRecord::Base
  validates :login, :email, presence: true

  before_validation :ensure_login_has_a_value

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

また、マクロ機能形式のクラスメソッドはブロックも受け付けます。 1行に収まるようなコードであれば、ブロック形式の使用を検討してみてください。

class User < ActiveRecord::Base
  validates :login, :email, presence: true

  before_create do |user|
    user.name = user.login.capitalize if user.name.blank?
  end
end

また、コールバックはライフサイクルの更に特定のイベントのみに対して登録することも可能です。 (検証の直前且つ「create」の場合のみ、検証の直後且つ「createとupdate」の場合のみ等)

class User < ActiveRecord::Base
  before_validation :normalize_name, on: :create

  # :on は配列での指定も可能
  after_validation :set_location, on: [ :create, :update ]

  protected
  def normalize_name
    self.name = self.name.downcase.titleize
  end

  def set_location
    self.location = LocationService.query(self)
  end
end

protectedまたは、privateのメソッドとして、 コールバックメソッドを定義することを検討するのは良い慣習と言えます。 publicにして、モデルの外側から呼び出させるようになるのは、カプセル化の原則にも違反します。

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

下記は利用可能Activeレコードのコールバックの一覧です。 各オペレーションで呼び出されるのと同じ順番で、それぞれ列挙しています。

3.1 オブジェクトの作成

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create
  • around_create
  • after_create
  • after_save

3.2 オブジェクトの更新

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_update
  • around_update
  • after_update
  • after_save

3.3 オブジェクトの削除

  • before_destroy
  • around_destroy
  • after_destroy

after_saveは、作成と更新の両方で実行しますが、 マクロが実行された順番に関わらず、常により具体的なコールバックであるafter_createafter_updateの、 後に呼び出されます。

3.4 after_initializeとafter_find

after_initializeコールバックは、Activeレコードのオブジェクトが、 newを使用した直接の場合、データベースからレコードが読み込まれた場合のどちらでも、 インスタンス化される度に呼び出されます。 これは、Activeレコードのinitializeメソッドを直接上書きされるのを避ける必要がある際に便利です。

after_findコールバックは、Activeレコードがデータベースからレコードを読み込んだ際に呼び出されます。 after_findは両方定義されていれば、after_initializeの前に呼び出されます。

after_initializeafter_findは、 before_*の形式を持ちませんが、他のActiveレコードのコールバックように登録することが可能です。

class User < ActiveRecord::Base
  after_initialize do |user|
    puts "You have initialized an object!"
  end

  after_find do |user|
    puts "You have found an object!"
  end
end

>> User.new
You have initialized an object!
=> #<User id: nil>

>> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>

4. コールバックの実行

下記のメソッドが、コールバックをトリガーします。

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

更に、after_findコールバックは下記のファインダーメッソからトリガーされます。

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

after_initializeコールバックは、クラスの新しいオブジェクトが初期化される毎にトリガされます。

find_by_*find_by_*!メソッドは、全属性を自動的に生成する動的ファインダーです。 詳細については、 動的ファインダーのセクションを確認してください。

5. コールバックのスキップ

バリデーションのように、コールバックをスキップすることも可能です。 ただし、コールバックで重要なビジネスルール、アプリケーションロジックを保持しているかもしれないので、 注意が必要です。 起こりえる影響を把握しないでコールバックを迂回してしますと、不正なデータを読み込んでしまうかもしれません。

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • increment
  • increment_counter
  • toggle
  • touch
  • update_column
  • update_columns
  • update_all
  • update_counters

6. 実行の停止

モデルのために新しいコールバックの登録を行うと、実行キューに登録されます。 このキューは、モデルのバリデーション、登録されたコールバック、実行されるデータベースオペレーションを全て含みます。

コールバック中のチェインはトランザクションにラップされます。 もし、任意のbeforeコールバックメソッドが、falseを返すか、例外を発生させれば、 実行チェインは中断され、ROLLBACKが発行されます。 一方、afterコールバックは、例外を発生させた場合のみです。

任意の例外を発生させると、saveとそれに近しい処理が失敗することを期待したコードが ActiveRecord::Rollback例外は、ロールバックが起こったことを明確にActiveレコードに伝えたと考えます。 Raising an arbitrary exception may break code that expects save and its friends not to fail like that. The ActiveRecord::Rollback exception is thought precisely to tell Active Record a rollback is going on. That one is internally captured but not reraised.

7. 関連性のあるコールバック

コールバックはモデルの関連性を通して動作し、更にその動作を定義することが出来ます。 例えば、ユーザーが多くの投稿を持つシステムがあったとして、このユーザーの投稿はユーザーが削除されたら一緒に削除されるべきです。 Postモデルのへの関連性を持たせることによって、 after_destroyコールバックをUserモデルに追加してみましょう。

class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
end

class Post < ActiveRecord::Base
  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、配列を指定可能です。 :ifオプションは、特定の条件下で実行したいコールバックがある場合に使用します。 反対に、:unlessオプションは特定の条件下で実行したくないコールバックがある場合に使用します。

8.1 :ifと:unlessをシンボルで使用する場合

:if:unlessオプションにコールバックが呼び出される直前に実行するメソッドを、 そのメソッド名と一致するシンボルで指定することが可能です。 もし、:ifオプションを使用する場合は、それに対応するメソッドがfalseを返すとコールバックが実行されません。 :unlessオプションを使用する場合は、それに対応するメソッドがtrueを返すとコールバックが実行されません。 この2つが最も一般的なオプションです。 これには、異なるメソッドを複数登録することも可能です。

class Order < ActiveRecord::Base
  before_save :normalize_card_number, if: :paid_with_card?
end

8.2 :ifと:unlessを文字列で使用する場合

evalで評価される文字列を指定することも可能で、そのため正しいRubyコードを指定する必要があります。 これを使用するのは、条件を少ない文字数で書ける場合にのみにすべきでしょう。

class Order < ActiveRecord::Base
  before_save :normalize_card_number, if: "paid_with_card?"
end

8.3 :ifと:unlessをProcで使用する場合

最後は、:if:unlessのProcオブジェクトを使った関連付けになります。 この方法は、条件が1行程で書けるような短い場合に最適です。

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

8.4 コールバックのための複数の条件の指定

コールバックの条件を書く際に、1つのコールバックに対して、 :if:unlessと両方を混ぜた定義を行うことが可能です。

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

9. コールバッククラス

時に、別のモデルのためい書いたコールバックメソッドが、他のモデルでも再利用でしたいというケースがあります。 Activeレコードはコールバックメソッドをカプセル化するクラスを作成可能で、それを使用することで再利用が容易になります。

下記は、after_destroyコールバックをPictureFileモデルのために作成した例です。

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

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

class PictureFile < ActiveRecord::Base
  after_destroy PictureFileCallbacks.new
end

この例ではインスタンスのメソッドとしてコールバックは定義されているため、 newを行ったPictureFileCallbacksのインスタンスが必要になることに注意してください。 コールバックはインスタンスオブジェクトの状態を利用して、何かを行う場合に特に便利です。 ただし、コールバックはクラスメソッドとして宣言する方が、理にかなっているケースが多いでしょう。 (上述したものと違い、self.が宣言の前に指定されています。)

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

この方法でコールバックメソッドを定義した場合、 PictureFileCallbacksをインスタンス化する必要はありません。

class PictureFile < ActiveRecord::Base
  after_destroy PictureFileCallbacks
end

コールバックのクラス内に、好きなだけコールバックを宣言することが可能です。

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

after_commitafter_rollbackという、データベーストランザクションの完了によってトリガされる、 2つのコールバックが存在します。 これらのコールバックは、データベースへの変更がコミットされたにせよ、ロールバックされたにせよ、 それまでは実行されないという点を除いて、after_saveに非常に似ています。 この機能は、データベースのトランザクションではない外部システムとの連携がActiveレコードモデルと必要になる場合に、 非常に重要になります。

前述したサンプルで考えると、PictureFileモデルは対になるレコードが削除された後に、ファイルを削除する必要があります。 もし、after_destroyコールバックが呼び出された後に何らかの例外が発生して、トランザクションがロールバックされると、 ファイルは削除されているのに、モデルの状態は矛盾したままになります。 例えば、下記のようにpicture_file_2がコードがあり検証で失敗したと仮定すると、save!メソッドはエラーを発生させます。

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

after_commitコールバックを使用することで、こういったケースに対応することが出来ます。

class PictureFile < ActiveRecord::Base
  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_commitafter_rollbackは、 全てのモデルのcreate、update、destroyedのトランザクションブロックから呼び出される事が保証されています。 もし、これらのコールバックのうちの1つから、何らかの例外が発生させられると、 他のコールバックの妨げをしないように無視されます。 そのため、コールバックで例外を発生させる場合は、rescueでそれをキャッチし、 コールバック内で適切に処理してください。

 Back to top

© 2010 - 2017 STUDIO KINGDOM