このガイドでは、Active Recordオブジェクトのライフサイクルにフックをかける方法について説明します。
このガイドの内容:
Railsアプリケーションを普通に操作すると、その内部でオブジェクトが作成・更新・削除(destroy)されます。Active Recordはこのオブジェクトライフサイクルへのフックを提供しており、これを用いてアプリケーションやデータを制御できます。
コールバックは、オブジェクトの状態が切り替わる「前」または「後」にロジックをトリガします。
コールバックとは、オブジェクトのライフサイクル期間における特定の瞬間に呼び出されるメソッドのことです。コールバックを利用することで、Active Recordオブジェクトが作成・保存・更新・削除・検証・データベースからの読み込み、などのイベント発生時に常に実行されるコードを書けるようになります。
コールバックを利用するためには、コールバックを登録する必要があります。コールバックの実装は普通のメソッドと特に違うところはありません。これをコールバックとして登録するには、マクロのようなスタイルのクラスメソッドを使います。
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な状態のままだと、このメソッドがモデルの外から呼び出され、オブジェクトのカプセル化の原則に違反する可能性があります。
Active Recordで利用可能なコールバックの一覧を以下に示します。これらのコールバックは、実際の操作中に呼び出される順序に並んでいます。
before_validationafter_validationbefore_savearound_savebefore_createaround_createafter_createafter_saveafter_commit / after_rollback
before_validationafter_validationbefore_savearound_savebefore_updatearound_updateafter_updateafter_saveafter_commit / after_rollback
after_saveコールバックは作成と更新の両方で呼び出されますが、コールバックマクロの呼び出し順序にかかわらず、常に、より具体的なafter_createコールバックやafter_updateコールバックより後に呼び出されます。
コールバック内では属性の更新や保存は行わないようにしてください。たとえば、コールバック内でupdate(attribute: "value")を呼び出してはいけません。このような操作はモデルのステートを変化させて、コミット時に思わぬ副作用が生じる可能性があります。before_create、before_update、およびそれより前に発火するコールバックで値を(self.attribute = "value"のように)直接代入するのは安全です。
before_destroyコールバックは、dependent: :destroyよりも前に配置すること(またはprepend: trueオプションをお使いください)。理由は、そのレコードがdependent: :destroy関連付けによって削除されるよりも前にbefore_destroyコールバックが実行されるようにするためです。
after_initializeとafter_findafter_initializeコールバックは、Active Recordオブジェクトが1つインスタンス化されるたびに呼び出されます。インスタンス化は、直接newを実行する他にデータベースからレコードが読み込まれるときにも行われます。これを利用すれば、Active Recordのinitializeメソッドを直接オーバーライドせずに済みます。
after_findコールバックは、Active Recordがデータベースからレコードを1つ読み込むたびに呼び出されます。after_findとafter_initializeが両方定義されている場合は、after_findが先に実行されます。
after_initializeとafter_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>
after_touchafter_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 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
irb> @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"> irb> @employee.touch # triggers @employee.company.touch Employeeがtouchされました Employee/Companyがtouchされました => true
以下のメソッドはコールバックをトリガします。
createcreate!destroydestroy!destroy_alldestroy_bysavesave!save(validate: false)toggle!touchupdate_attributeupdateupdate!valid?また、after_findコールバックは以下のfinderメソッドを実行すると呼び出されます。
allfirstfindfind_byfind_by_*find_by_*!find_by_sqllastafter_initializeコールバックは、そのクラスの新しいオブジェクトが初期化されるたびに呼び出されます。
find_by_*メソッドとfind_by_*!メソッドは、属性ごとに自動的に生成される動的なfinderメソッドです。詳しくは動的finderのセクションを参照してください。
バリデーション(検証)の場合と同様、以下のメソッドでもコールバックをスキップできます。
decrement!decrement_counterdeletedelete_alldelete_byincrement!increment_counterinsertinsert!insert_allinsert_all!touch_allupdate_columnupdate_columnsupdate_allupdate_countersupsertupsert_allただし、重要なビジネスルールやアプリケーションロジックがコールバックに設定されている可能性もあるので、これらのメソッドの利用には十分注意すべきです。この点を理解せずにコールバックをバイパスすると、データの不整合が発生する可能性があります。
モデルに新しくコールバックを登録すると、コールバックは実行キューに入ります。このキューには、あらゆるモデルに対するバリデーション、登録済みコールバック、実行待ちのデータベース操作が置かれます。
コールバックチェーン全体は、1つのトランザクションにラップされます。コールバックの1つで例外が発生すると、実行チェーン全体が停止してロールバックが発行されます。チェーンを意図的に停止するには次のようにします。
throw :abort
ActiveRecord::RollbackやActiveRecord::RecordInvalidを除く例外は、その例外によってコールバックチェインが停止した後も、Railsによって再び発生します。このため、ActiveRecord::RollbackやActiveRecord::RecordInvalid以外の例外を発生させると、saveやupdateのようなメソッド(つまり通常trueかfalseを返そうとするメソッド)が例外を発生することを想定していないコードが中断する恐れがあります。
コールバックはモデルのリレーションシップを経由して動作できます。また、リレーションシップを用いてコールバックを定義することも可能です。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>
検証と同様、与えられた述語の条件を満たす場合に実行されるコールバックメソッドの呼び出しも作成可能です。これを行なうには、コールバックで:ifオプションまたは:unlessオプションを使います。このオプションはシンボル、Proc、またはArrayを引数に取ります。特定の状況でのみコールバックが呼び出される必要がある場合は、:ifオプションを使います。特定の状況でコールバックを呼び出してはならない場合は、:unlessオプションを使います。
:ifおよび:unlessオプションでシンボルを使う:ifオプションまたは:unlessオプションは、コールバックの直前に呼び出される述語メソッドの名前に対応するシンボルと関連付けることが可能です。:ifオプションを使う場合、述語メソッドがfalseを返せばコールバックは実行されません。:unlessオプションを使う場合、述語メソッドがtrueを返せばコールバックは実行されません。これはコールバックで最もよく使われるオプションです。この方法で登録すれば、さまざまな述語メソッドを登録して、コールバックを呼び出すべきかどうかをチェックできるようになります。
class Order < ApplicationRecord before_save :normalize_card_number, if: :paid_with_card? end
: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
: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と評価されない場合にのみ実行されます。
:ifと:unlessオプションは、procやメソッド名のシンボルの配列を受け取ることも可能です。
class Comment < ApplicationRecord before_save :filter_content, if: [:subject_to_parental_control?, :untrusted_author?] end
有用なコールバックメソッドを書いた後で、他のモデルでも使い回したいことがあります。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
コールバッククラスの内部では、いくつでもコールバックを宣言できます。
データベースのトランザクションが完了したときにトリガされるコールバックが2つあります。after_commitとafter_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コールバックは作成/更新/削除でのみ用いることが多いので、それぞれのエイリアスも用意されています。
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つのトランザクションブロック内で作成・更新・削除されたすべてのモデルで呼び出されます。ただし、これらのコールバックのいずれかで何らかの例外が発生すると、その例外のせいで以後のafter_commitコールバックやafter_rollbackコールバックのメソッドは実行されなくなります。このため、もし自作のコールバックで例外が発生する可能性がある場合は、他のコールバックが停止しないように自分のコールバック内でrescueして適切にエラー処理を行う必要があります。
after_commitコールバックやafter_rollbackコールバックの中で実行されるコードそのものは、トランザクションで囲まれません。
同一のモデル内で同じメソッド名を引数に取るafter_create_commitとafter_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を更新する ユーザーはデータベースに保存されました
コールバックを作成と更新の両方の操作に登録するには、代わりにafter_commitをお使いください。以下のエイリアスも、作成や更新の両方で使えるafter_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を更新 ユーザーはデータベースに保存されました
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。