このガイドでは、Active Recordオブジェクトのライフサイクルにフックをかける方法について説明します。
このガイドの内容:
Railsアプリケーションを普通に操作すると、その内部でオブジェクトが作成・更新・削除(destroy)されます。Active Recordはこのオブジェクトライフサイクルへのフックを提供しており、これを用いてアプリケーションやデータを制御できます。
コールバックは、オブジェクトの状態が切り替わる「前」または「後」にロジックをトリガします。
class Baby < ApplicationRecord after_create -> { puts "Congratulations!" } end
irb> @baby = Baby.create Congratulations!
このように、ライフサイクルにはさまざまなイベントがあり、イベントの「前」「後」「前後」のいずれかにフックできます。
コールバックとは、オブジェクトのライフサイクル期間における特定の瞬間に呼び出されるメソッドのことです。コールバックを利用することで、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.blank? 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
以下のように、コールバックにprocを渡してトリガーさせることも可能です。
class User < ApplicationRecord before_create ->(user) { user.name = user.login.capitalize if user.name.blank? } end
最後に、独自のカスタムコールバックオブジェクトも定義できます。これについては後述します。
class User < ApplicationRecord before_create MaybeAddName end class MaybeAddName def self.before_create(record) if record.name.blank? record.name = record.login.capitalize end 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な状態のままだと、このメソッドがモデルの外から呼び出され、オブジェクトのカプセル化の原則に違反する可能性があります。
コールバック内のオブジェクトに副作用を与えるupdate
、save
などのメソッド呼び出しは避けてください。たとえば、コールバック内でupdate(attribute: "value")
を呼び出してはいけません。これはモデルのステートを変更する可能性があり、コミット時に予期せぬ副作用が発生する可能性があります。代わりに、before_create
やbefore_update
、またはそれより前にトリガーされるコールバックで安全に値を直接代入できます(例: self.attribute = "value"
)。
Active Recordで利用可能なコールバックの一覧を以下に示します。これらのコールバックは、実際の操作中に呼び出される順序に並んでいます。
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit
/ after_rollback
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit
/ after_rollback
after_save
はcreate
とupdate
の両方で実行されますが、マクロ呼び出しの実行順序にかかわらず、常にafter_create
やafter_update
という特定のコールバックよりも後に呼び出されます。
before_destroy
コールバックは、dependent: :destroy
よりも前に配置すること(またはprepend: true
オプションをお使いください)。理由は、そのレコードがdependent: :destroy
関連付けによって削除されるよりも前にbefore_destroy
コールバックが実行されるようにするためです。
after_commit
の保証は、after_save
やafter_update
やafter_destroy
の保証とは大きく異なります。たとえば、after_save
で例外が発生した場合、トランザクションはロールバックされ、データは永続化されません。
一方、after_commit
で発生したものは、トランザクションが既に完了し、データがデータベースに永続化されたことを保証できます。詳しくはトランザクションのコールバックで後述します。
after_initialize
とafter_find
after_initialize
コールバックは、Active Recordオブジェクトがインスタンス化されるたびに呼び出されます。インスタンス化は、直接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_touch
after_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 Book < ApplicationRecord belongs_to :library, touch: true after_touch do puts 'Bookがtouchされました' end end class Library < ApplicationRecord has_many :books after_touch :log_when_books_or_library_touched private def log_when_books_or_library_touched puts 'Book/Libraryがtouchされました' end end
irb> @book = Book.last => #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05"> irb> @book.touch # @book.library.touchがトリガーされる Bookがtouchされました Book/Libraryがtouchされました => true
以下のメソッドはコールバックをトリガします。
create
create!
destroy
destroy!
destroy_all
destroy_by
save
save!
save(validate: false)
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のセクションを参照してください。
バリデーション(検証)の場合と同様、以下のメソッドでもコールバックをスキップできます。
decrement!
decrement_counter
delete
delete_all
delete_by
increment!
increment_counter
insert
insert!
insert_all
insert_all!
touch_all
update_column
update_columns
update_all
update_counters
upsert
upsert_all
ただし、重要なビジネスルールやアプリケーションロジックがコールバックに設定されている可能性もあるので、これらのメソッドの利用には十分注意すべきです。この点を理解せずにコールバックをバイパスすると、データの不整合が発生する可能性があります。
モデルに新しくコールバックを登録すると、コールバックは実行キューに入ります。このキューには、あらゆるモデルに対するバリデーション、登録済みコールバック、実行待ちのデータベース操作が置かれます。
コールバックチェーン全体は、1つのトランザクションにラップされます。コールバックの1つで例外が発生すると、実行チェーン全体が停止してロールバックが発行されます。チェーンを意図的に停止するには次のようにします。
throw :abort
ActiveRecord::Rollback
やActiveRecord::RecordInvalid
を除く例外は、その例外によってコールバックチェインが停止した後も、Railsによって再び発生します。さらに、ActiveRecord::Rollback
やActiveRecord::RecordInvalid
以外の例外が発生すると、save
やupdate
のようなメソッド(つまり通常true
かfalse
を返そうとするメソッド)が例外を発生することを想定していないコードが中断する恐れがあります。
after_destroy
、before_destroy
、またはaround_destroy
コールバック内で ActiveRecord::RecordNotDestroyed
例外が発生した場合、destroy
メソッドは再度例外を発生せずに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>
関連付けのコールバックは通常のコールバックと似ていますが、コレクションのライフサイクル内で発生するイベントによってトリガーされる点が異なります。利用可能な関連付けコールバックは以下のとおりです。
before_add
after_add
before_remove
after_remove
関連付けコールバックを定義するには、関連付けの宣言で以下のようにオプションを追加します。
class Author < ApplicationRecord has_many :books, before_add: :check_credit_limit def check_credit_limit(book) # ... end end
Railsは、追加または削除されるオブジェクトをコールバックに渡します。
以下のようにコールバックを配列として渡すことで、単一のイベントに複数のコールバックを登録できます。
class Author < ApplicationRecord has_many :books, before_add: [:check_credit_limit, :calculate_shipping_charges] def check_credit_limit(book) # ... end def calculate_shipping_charges(book) # ... end end
before_add
コールバックが:abort
をスローした場合、オブジェクトはコレクションに追加されません。 同様に、before_remove
コールバックが`:abort
をスローした場合、オブジェクトはコレクションから削除されません。
# limit_reached?がtrueの場合はbookが追加されない def check_credit_limit(book) throw(:abort) if limit_reached? end
これらのコールバックは、関連付けられるオブジェクトが関連付けのコレクションを介して追加または削除された場合にのみ呼び出されます。
# この場合は`before_add`コールバックがトリガーされる author.books << book author.books = [book, book2] # この場合は`before_add`コールバックがトリガーされない book.update(author_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
条件リストではprocを手軽に利用できます。
class Comment < ApplicationRecord before_save :filter_content, if: [:subject_to_parental_control?, Proc.new { untrusted_author? }] 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
条件もtrue
と評価されなかった場合にのみ実行されます。
有用なコールバックメソッドを書いた後で、他のモデルでも使い回したいことがあります。Active Recordは、コールバックメソッドをカプセル化したクラスを作成できるので、手軽に再利用できます。
ここでは、ファイルシステム上で破棄されたファイルのクリーンアップを行うためにafter_destroy
コールバックを持つクラスを作成する例を示します。
この振る舞いはPictureFile
モデルに固有のものではなく、共有したい場合もあるので、これを別のクラスにカプセル化するのは良い考えです。これにより、その振る舞いのテストや変更がずっと簡単になります。
class FileDestroyerCallback def after_destroy(file) if File.exist?(file.filepath) File.delete(file.filepath) end end end
上のようにクラス内で宣言すると、コールバックメソッドはモデルオブジェクトをパラメータとして受け取れるようになります。これで、このコールバッククラスをモデルで使えます。
class PictureFile < ApplicationRecord after_destroy FileDestroyerCallbacks.new end
コールバックをインスタンスメソッドとして宣言したので、FileDestroyerCallback
オブジェクトを新しくインスタンス化する必要があったことにご注意ください。これは、インスタンス化されたオブジェクトの状態をコールバックメソッドで利用したい場合に特に便利です。ただし、コールバックをクラスメソッドとして宣言する方が理にかなうこともよくあります。
class FileDestroyerCallback def self.after_destroy(file) if File.exist?(file.filepath) File.delete(file.filepath) end end end
コールバックメソッドを上のように宣言した場合は、モデル内でFileDestroyerCallback
オブジェクトのインスタンス化が不要になります。
class PictureFile < ApplicationRecord after_destroy FileDestroyerCallback end
コールバッククラスの内部では、いくつでもコールバックを宣言できます。
after_commit
コールバックとafter_rollback
コールバックデータベースのトランザクションが完了したときにトリガされるコールバックが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
コールバックまたはafter_rollback
コールバックが呼び出されます。
ただし、これらのコールバックのいずれか内で例外が発生した場合、例外はバブルアップし、残りのafter_commit
メソッドやafter_rollback
メソッドは実行されません。
そのため、コールバックのコードで例外が発生する可能性がある場合は、他のコールバックを実行できるように、その例外をrescue
してコールバック内で処理する必要があることにご注意ください。
after_commit
コールバックやafter_rollback
コールバック内で実行されるコード自体は、トランザクション内に含まれない点にご注意ください。
単一トランザクションのコンテキストで、データベース内の同じレコードを表す複数の読み込み済みオブジェクトを操作する場合、after_commit
コールバックやafter_rollback
コールバックの振る舞いには重要な注意点があります。
これらのコールバックは、トランザクション内で変更される特定のレコードの「最初のオブジェクト」に対してのみトリガーされます。読み込まれている他のオブジェクトは、同じデータベースレコードを表しているにもかかわらず、after_commit
コールバックやafter_rollback
コールバックをトリガーしません。
この微妙な振る舞いによる影響が特に大きいシナリオは、「同じデータベースレコードに関連付けられるオブジェクトごとに、コールバックが独立して実行されることが予想される」場合です。この振る舞いは、コールバックシーケンスのフローや予測可能性に影響を与える可能性があり、トランザクションに沿って動作するアプリケーションロジックに不整合が生じる可能性があります。
after_commit
コールバックのエイリアス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_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_save_commit
作成と更新の両方でafter_commit
コールバックを使う場合のエイリアスとして、after_save_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を更新 ユーザーはデータベースに保存されました
デフォルトでは、コールバックは定義された順序で実行されます。
ただし、トランザクショナルなafter_
コールバック(after_commit
やafter_rollback
など)を複数定義すると、定義時と逆順で実行される可能性があります。
class User < ActiveRecord::Base after_commit { puts("これは実際には2番目に実行される") } after_commit { puts("これは実際には1番目に実行される") } end
これは、after_destroy_commit
などを含む、すべてのafter_*_commit
のバリエーションにも当てはまります。
この実行順序は以下の設定で変更できます。
config.active_record.run_after_transaction_callbacks_in_order_defined = false
true
(Rails 7.1以降のデフォルト)に設定すると、コールバックは定義された順序で実行されます。
false
に設定すると、上の例と同様に、実行順序が逆になります。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。