🫠

[Rails]ActiveRecord::AttributeMethods::Dirtyでオブジェクトやレコードの変更を追跡する

2023/11/25に公開

この記事はなに?

RailsでActiveRecordのattributeの状態や変更を追跡するActiveRecord::AttributeMethods::Dirtyモジュールについて簡単にまとめたもの。
実際に使ってみたらかなり便利でした。
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html

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)

https://github.com/issei-yoshi/sample-task-app-for-ActiveRecord-AttributeMethods-Dirty

今回は以下を実現します。

  • taskのstatusカラムをcompletedに変更すると、そのtaskに紐づくuserのlevelカラムが+1される
  • taskのnameカラムが更新されてもuserのlevelカラムは変わらない
task.rb
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