[Rails]ActiveRecord::AttributeMethods::Dirtyでオブジェクトやレコードの変更を追跡する
この記事はなに?
RailsでActiveRecordのattributeの状態や変更を追跡するActiveRecord::AttributeMethods::Dirty
モジュールについて簡単にまとめたもの。
実際に使ってみたらかなり便利でした。
ActiveRecord::AttributeMethods::Dirty
モジュールとは
Provides a way to track changes in your Active Record models. It adds all methods from ActiveModel::Dirty and adds database-specific methods.
Active Recordモデルの変更を追跡する方法を提供します。
ActiveModel::Dirty
からすべてのメソッドを追加し、データベース固有のメソッドを追加します。
公式でこのように書かれています。
つまり、オブジェクトに変更があった時に、変更前後の値を取得したりすることができる優れものというわけです。
今回私は「特定のカラムが変更された場合に、とあるコールバックを呼びたい」という実現したいことがあり、本記事で紹介するActiveRecord::AttributeMethods::Dirty
モジュールにたどり着きました。
ActiveRecord::AttributeMethods::Dirty
モジュールによって提供されるメソッド(一部省略)
前提として以下のようなデータが存在する場合に提供されるメソッドを想定します。
今回はnameのカラムを主として扱います。
#<Task:0x0000000107419030
id: 1,
user_id: 1,
name: "タスク1",
status: "initial"
...
>
attribute_before_last_save
更新前の値を取得することが可能
attribute_before_last_save("name")
もしくはname_before_last_save
を使用することが可能
# taskのnameを更新
task.update!(name: "タスク名変更")
# 変更前の値を取得
task.name_before_last_save
=> "タスク1"
attribute_change_to_be_saved
更新する前段階で使用可能
更新される前の値とこれから更新する予定の値を取得することが可能
attribute_change_to_be_saved("name")
もしくはname_change_to_be_saved
を使用することが可能
# この段階ではnameをsaveしていない
task.name = "タスク名変更"
=> "タスク名変更"
task.name_change_to_be_saved
=> ["タスク1", "タスク名変更"]
attribute_in_database
現時点でDBに保存されている値を取得することが可能
attribute_in_database("name")
もしくはname_in_database
を使用することが可能
# この段階ではnameをsaveしていない
task.name = "タスク名変更"
=> "タスク名変更"
task.name_in_database
=> "タスク1"
attributes_in_database
これから更新する予定のカラム名と元の値を取得することが可能
# 更新しようとしていないカラムの場合は空のhashが返る
task.attributes_in_database
=> {}
task.name = "タスク名変更"
task.attributes_in_database
=> {"name"=>"タスク1"}
changed_attribute_names_to_save
これから更新する予定のカラム名を配列で取得することが可能
# 更新しようとしていないカラムの場合は空の配列が返る
task.changed_attribute_names_to_save
=> []
task.name = "タスク名変更"
=> "タスク名変更"
task.changed_attribute_names_to_save
=> ["name"]
task.status = :in_progress
=> :in_progress
# 更新しようとしていないカラムが複数の場合はその配列が返る
task.changed_attribute_names_to_save
=> ["name", "status"]
changes_to_save
これから更新する予定のカラムの更新前・更新後の値を取得することが可能
task.name = "タスク名変更"
=> "タスク名変更"
task.changes_to_save
=> {"name"=>["タスク1", "タスク名変更"]}
has_changes_to_save?
更新されるカラム・値があるかどうかをbooleanで返すことが可能
task.name = "タスク名変更"
=> "タスク名変更"
task.has_changes_to_save?
=> true
saved_change_to_attribute
更新後にそのカラムの更新前・更新後の値を取得することができる
saved_change_to_attribute("name")
もしくはsaved_change_to_name
を使用することが可能
task.update!(name: "タスク名変更")
task.saved_change_to_name
=> ["タスク1", "タスク名変更"]
saved_change_to_attribute?
更新後にそのカラムが更新されたかどうかをbooleanで返すことが可能
saved_change_to_attribute?("name")
もしくはsaved_change_to_name?
を使用することが可能
task.update!(name: "タスク名変更")
task.saved_change_to_name?
=> true
試しに使ってみる
こちらのリポジトリに作っています。(めっちゃざっくりですがw)
今回は以下を実現します。
- taskの
status
カラムをcompleted
に変更すると、そのtaskに紐づくuserのlevel
カラムが+1される - taskの
name
カラムが更新されてもuserのlevel
カラムは変わらない
class Task < ApplicationRecord
belongs_to :user
enum :status, %w[initial in_progress completed], prefix: true
# taskモデルがsaveされたらコールバックでメソッドを呼び出す(コールバックはまた別議題のため今回は詳細割愛)
after_save_commit :update_user_level!
def update_user_level!
# \\\ここでActiveRecord::AttributeMethods::Dirtyのsaved_change_to_attribute?///を使用
# statusが更新された時以外はreturnする
return unless saved_change_to_status?
# statusがcompletedの場合のみ処理を行う(enumのメソッドなので今回は詳細割愛)
if status_completed?
user.level += 1
user.save!
end
end
end
最後に
今回はActiveRecord::AttributeMethods::Dirty
モジュールについて簡単にまとめてみました。
実務で使用することがあったのですが、すごく便利に使わせていただきました。
今回はコールバックとうまく組み合わせることでメリットを感じられましたが、今後も使えそうなシチュエーションがあれば加筆していきたいなと思います。
最後まで読んでいただきありがとうございました。
こんなメソッドを自動で作ってくれるなんて、、
改めて思いますが、Rails便利すぎますね。。。🫠
Discussion