このガイドでは、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_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
before_destroy
around_destroy
after_destroy
after_commit/after_rollback
after_save
コールバックは作成と更新の両方で呼び出されますが、コールバックマクロの呼び出し順にかかわらず、必ず、より詳細なafter_create
コールバックやafter_update
コールバックより 後 に呼び出されます。
before_destroy
コールバックは、dependent: :destroy
よりも前に配置する(またはprepend: true
オプションを用いる)べきです。理由は、そのレコードがdependent: :destroy
によって削除されるよりも前にbefore_destroy
コールバックが実行されるようにするためです。
after_initialize
とafter_find
after_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 >> User.new オブジェクトは初期化されました => #<User id: nil> >> User.first オブジェクトが見つかりました オブジェクトは初期化されました => #<User id: 1>
after_touch
after_touch
コールバックは、Active Recordオブジェクトがtouchされるたびに呼び出されます。
class User < ApplicationRecord after_touch do |user| puts "オブジェクトにtouchしました" end end >> 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"> >> 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 >> @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"> # @employee.company.touchをトリガーする >> @employee.touch Employeeにtouchされました Employee/Companyにtouchされました => true
以下のメソッドはコールバックをトリガします。
create
create!
destroy
destroy!
destroy_all
save
save!
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のセクションを参照してください。
検証(validation)の場合と同様、以下のメソッドでコールバックをスキップできます。
decrement
decrement_counter
delete
delete_all
increment
increment_counter
toggle
update_column
update_columns
update_all
update_counters
重要なビジネスルールやアプリケーションロジックはたいていコールバックに仕込まれますので、これらのメソッドの利用には十分注意すべきです。コールバックをうかつにバイパスすると、データの不整合が発生する可能性があります。
モデルに新しくコールバックを登録すると、コールバックは実行キューに入ります。このキューには、あらゆるモデルに対する検証、登録済みコールバック、実行待ちのデータベース操作が置かれます。
コールバックチェイン全体は、1つのトランザクションにラップされます。コールバックの1つで例外が発生すると、実行チェイン全体が停止してロールバックが発行されます。チェインを意図的に停止するには次のようにします。
throw :abort
ActiveRecord::Rollback
やActiveRecord::RecordInvalid
を除く例外は、その例外によってコールバックチェインが停止した後も、Railsによって再び発生します。このため、ActiveRecord::Rollback
やActiveRecord::RecordInvalid
以外の例外を発生させると、save
やupdate_attributes
のようなメソッド (つまり通常true
かfalse
を返そうとするメソッド) が例外を発生させることを想定していないコードが中断する恐れがあります。
コールバックはモデルのリレーションシップを経由して動作できます。また、リレーションシップを用いてコールバックを定義することすらできます。1人のユーザーが多数の投稿(post)を持っている状況を例に取ります。あるユーザーが所有する投稿は、そのユーザーがdestroyされたらdestroyされる必要があります。User
モデルにafter_destroy
コールバックを追加し、このコールバックでPost
モデルへのリレーションシップを経由すると以下のようになります。
class User < ApplicationRecord has_many :posts, dependent: :destroy end class Post < ApplicationRecord 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>
検証と同様、与えられた述語の条件を満たす場合に実行されるコールバックメソッドの呼び出しを作成することもできます。これを行なうには、コールバックで:if
オプションまたは:unless
オプションを使います。このオプションはシンボル、Proc
、またはArray
を引数に取ります。特定の状況でのみコールバックが呼び出される必要がある場合は、:if
オプションを使います。特定の状況ではコールバックを呼び出してはならない場合は、:unless
オプションを使います。
:if
および:unless
オプションでシンボルを使う:if
オプションまたは:unless
オプションは、コールバックの直前に呼び出される述語メソッド(訳注: trueかfalseのいずれかの値のみを返すメソッド)の名前に対応するシンボルと関連付けることができます。: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
1つの条件付きコールバック宣言内で、:if
オプションと:unless
オプションを同時に使えます。
class Comment < ApplicationRecord after_create :send_email_to_author, if: :author_wants_emails?, unless: Proc.new { |comment| comment.post.ignore_comments? } 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
モデルで、対応するレコードがdestroyされた後にファイルを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_create_commit
after_update_commit
after_destroy_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つのトランザクションブロック内におけるあらゆるモデルの作成/更新/destroy時に呼び出されます。これらのコールバックのいずれかで何らかの例外が発生すると、その例外のせいで以後のafter_commit
コールバックやafter_rollback
コールバックのメソッドは実行されなくなります。このため、もし自作のコールバックが例外を発生する可能性がある場合は、自分のコールバック内でrescue
して適切にエラー処理を行い、他のコールバックが停止しないようにする必要があります。
同一のモデル内でafter_create_commit
とafter_update_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 'User was saved to database' end end # 何も出力されない >> @user = User.create # @userを更新する >> @user.save => User was saved to database
create
アクションのコールバックとupdate
アクションのコールバックを両方とも登録するには、代わりにafter_commit
を使います。
class User < ApplicationRecord after_commit :log_user_saved_to_db, on: [:create, :update] end
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。