こんにちは、鈴木です。
Techscore 本体の記事の下書きを書き進めています。
ということで、コールバックの記事の下書きを公開します。
コールバックの種類
コールバックとは、バリデーションの実行やデータベースへの保存などのタイミングで処理を行うための機能です。
あるタイミングで必ず実行する必要がある処理をコールバックに指定することで、モデルの一貫性を保つことができます。
Rails のコールバックの種類をまとめてみると、非常に多いことが分かります。
after_find
検索メソッドでオブジェクトが見つかったタイミングで実行されます。
検索条件を指定せずに all メソッドを呼び出すなど、検索結果が大量になる場合は注意が必要です。
検索結果の数だけコールバックが実行されますので、パフォーマンスに重大な影響を及ぼす可能性があります。
after_initialize
オブジェクトがインスタンス化されたタイミングで実行されます。
User.new のようにインスタンス化した場合は、after_find は実行されずに after_initialize のみが実行されます。
after_find と同様、検索条件を指定せずに all メソッドを呼び出すなど、検索結果が大量になる場合は注意が必要です。
インスタンス化された分だけコールバックが実行されますので、パフォーマンスに重大な影響を及ぼす可能性があります。
before_validation
バリデーションが行われる直前で実行されます。
主にバリデーション前に属性値を微調整する場合に用いられます。
このタイミングでは属性値にどのような型のどのような値が設定されているか保証は無いことに注意しましょう。
データベース上では文字列型であっても、数値が代入されているかもしれませんし、nil であるかもしれませんので、想定外のエラーが発生しないように気を付けましょう。
after_validation
バリデーションが行われた直後に実行されます。
バリデーションに成功した後ではなく、単純にバリデーションが行なわれた後に実行されます。
そのため、before_validation と同様に、属性値にどのような型の値が設定されているか分かりませんし、nil であるかもしれません。
before_save
バリデーションに成功し、実際にオブジェクトが保存される直前で実行されます。
INSERT される場合も、UPDATE される場合も呼び出されます。
INSERT もしくは UPDATE の場合だけ実行したい処理があるときは、後述する before_create / before_update を使用します。
before_create / before_update
before_save の後に実行されます。
オブジェクトが登録されるとき (new_record? が true のとき) は before_create が実行されます。
オブジェクトが更新されるとき (new_record? が false のとき) は before_update が実行されます。
登録と更新のどちらの場合にも同じ処理を行うのであれば、before_save を使用すると便利です。
after_create / after_update
オブジェクトが保存された直後 (after_save の直前) に実行されます。
オブジェクトが登録されたときは after_create、更新されたときは after_update が実行されます。
登録と更新のどちらの場合にも同じ処理を行うのであれば、after_save を使用すると便利です。
after_save
after_create / after_update の直後、データベースへの COMMIT の直前に実行されます。
保存されたオブジェクトの関連オブジェクトを操作するなど、データベースで言うトリガーのような処理を行なう場合に使用します。
after_commit
after_save の後(データベースに COMMIT された後)に実行されます。
after_rollback
バリデーションエラーや SQL 実行時にエラーが発生した場合に実行されます。
after_touch
touch メソッドが呼び出された直後に実行されます。
この場合は before_save や after_save など他のコールバックは実行されません。
before_destroy
destroy メソッドでオブジェクトが削除される直前に実行されます。
削除の場合は before_destroy → after_destroy → after_commit の順番で実行されます。
注意として delete/delete_all メソッドで削除した場合はコールバックが呼び出されません。
after_destroy
オブジェクトが削除された直後に実行されます。
データベース外で管理しているリソースを削除する場合などに使用することが多いでしょう。
コールバックが実行されるタイミング
コールバックは常に実行されるわけではありません。
オブジェクトを更新するメソッドであっても、コールバックが常に実行されないものもあります。
以下のメソッドはコールバックを実行します。
- create
- create!
- decrement!
- destroy
- destroy_all
- increment!
- save
- save!
- save(:validate => false)
- toggle!
- update
- update_attribute
- update_attributes
- update_attributes!
- valid?
以下のメソッドは after_find 及び after_initialize を実行します(after_initialize はオブジェクトを new したときにも実行されます)。
- all
- first
- find
- find_all_by_attribute
- find_by_attribute
- find_by_attribute!
- last
以下のメソッドはコールバックを実行しません。
- decrement
- decrement_counter
- delete
- delete_all
- find_by_sql
- increment
- increment_counter
- toggle
- touch
- update_column
- update_all
- update_counters
コールバックでビジネスロジックに関わる処理を行っている場合などには、コールバックがスキップされるメソッドがあることに注意しましょう。
コールバックの設定
コールバックを設定する方法は何通りかあります。
一つ目は実行したい処理をブロックで指定する方法です。
例えばオブジェクトが保存される前にパスワードをハッシュ化したい場合は以下のように指定します。
1 2 3 4 5 6 7 8 9 10 |
class User < ActiveRecord::Base before_save do # 入力された平文のパスワードをハッシュ化する. if self.password.present? self.hashed_password = ... # ハッシュ値を求める. end end end |
二つ目はコールバックしたいメソッド名をシンボルで指定する方法です。
処理内容が多い場合や、他の箇所からも呼び出される可能性がある場合はこの方法が良いでしょう。
1 2 3 4 5 6 7 8 9 10 11 |
class User < ActiveRecord::Base before_save :hash_password # 入力された平文のパスワードをハッシュ化する. def hash_password if self.password.present? self.hashed_password = ... # ハッシュ値を求める. end end end |
三つ目は、コールバック処理をもつクラスを定義し、そのインスタンスを指定する方法です。
この方法は、同じ処理を複数のモデルで共有したい場合に使用すると良いでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class User < ActiveRecord::Base before_save HashPassword.new end class HashPassword def before_save(object) if object.password.present? object.hashed_password = ... # ハッシュ値を求める. end end end |
ちなみにですが、コールバックは以下のように複数指定することもできます。
1 2 3 4 5 6 7 8 9 10 11 |
class User < ActiveRecord::Base before_save do puts 'before_save 1' end before_save do puts 'before_save 2' end end |
コールバックを複数指定した場合は、指定した順番に実行されます。
例えば、上記の通りにコールバックを設定した状態で User オブジェクトを save すると、「before_save 1」「before_save 2」の順に出力されます。
1 2 3 4 |
> user = User.new > user.save before_save 1 before_save 2 |
条件付きコールバック
ある条件を満たした場合だけコールバックを実行したい場合は :if オプションを使用します(:unless オプションもあります)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class User < ActiveRecord::Base before_save :hash_password, :if => :password_required? # 入力された平文のパスワードをハッシュ化する. def hash_password self.hashed_password = ... # ハッシュ値を求める. end # パスワードの入力が必要であるか判定する. def password_required? ... end end |
先ほどの例では hash_password の中でパスワードが指定されているか判定していましたが、ここでは判定処理を password_required? メソッドで行なわれます。
:if オプションにはメソッド名ではなく Proc オブジェクトを指定することもできます。
1 |
before_save :hash_password, :if => lambda { self.password.present? } |
保存処理の中断
before_validation で指定した処理が false を返した場合、オブジェクトの保存処理は中断されます。
最後に実行した処理が意図せず false とならないように注意しましょう。
オブジェクトの保存処理が中断されると、それ以降のコールバックはキャンセルされます。
そして、save メソッドが呼び出されていた場合は、呼び出し元に false が返されます(save が false を返します)。
save! が呼び出されていた場合は例外 ActiveRecord::RecordInvalid が発生します。
まとめ
コールバック処理を活用すると、モデルの一貫性を簡単に保つことができます。
一方で、destroy メソッドはコールバックを実行する、delete メソッドはコールバックを実行しない、update_attribute もコールバックを実行しない、・・・という決まりも理解しておくことが重要です。