Activeレコードのコールバック
このガイドでは、Activeレコードオブジェクトのサイクルに、任意の処理をフックする方法を説明します。 このガイドを読むことで、次の事が学べるはずです。
- Activeレコードオブジェクトのライフサイクルについて
- オブジェクトのライフサイクルのイベントに応答するコールバックメソッドの作成方法について
- コールバックのために共通の処理をカプラセル化する特別なクラスの作成方法について
- 1. オブジェクトのライフサイクル
- 2. コールバックの概要
- 3. 利用可能なコールバック
- 4. コールバックの実行
- 5. コールバックのスキップ
- 6. 実行の停止
- 7. 関連性のあるコールバック
- 8. 条件によるコールバック
- 9. コールバッククラス
- 10. トランザクション・コールバック
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_create
とafter_update
の、
後に呼び出されます。
3.4 after_initializeとafter_find
after_initialize
コールバックは、Activeレコードのオブジェクトが、
new
を使用した直接の場合、データベースからレコードが読み込まれた場合のどちらでも、
インスタンス化される度に呼び出されます。
これは、Activeレコードのinitialize
メソッドを直接上書きされるのを避ける必要がある際に便利です。
after_find
コールバックは、Activeレコードがデータベースからレコードを読み込んだ際に呼び出されます。
after_find
は両方定義されていれば、after_initialize
の前に呼び出されます。
after_initialize
とafter_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_commit
とafter_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_commit
とafter_rollback
は、
全てのモデルのcreate、update、destroyedのトランザクションブロックから呼び出される事が保証されています。
もし、これらのコールバックのうちの1つから、何らかの例外が発生させられると、
他のコールバックの妨げをしないように無視されます。
そのため、コールバックで例外を発生させる場合は、rescue
でそれをキャッチし、
コールバック内で適切に処理してください。
© 2010 - 2017 STUDIO KINGDOM