🦓

[Rails]ActiveRecord::Dirtyモジュール

に公開

はじめに

データ変更に関連する処理を実装したいのでActiveRecord::Dirtyについてまとめてみました。
https://api.rubyonrails.org/classes/ActiveModel/Dirty.html

ActiveRecord::Dirtyとは

ActiveRecord::Dirtyモジュールは、RailsのActive Recordライブラリに組み込まれたモジュールの一つであり、データベースレコードの変更状態を追跡するために使用されます。
データベースレコードが変更されたかどうかを検出し、変更内容を追跡するための便利なメソッドとイベントを提供しています。

ActiveRecord::Dirtyモジュールの主な機能:

  1. 変更の追跡: データベースレコードが変更された場合、どの属性が変更されたかを知ることができます。
  2. 変更内容の取得: レコードの変更された属性の以前の値と新しい値を取得するためのメソッドが提供されます。
  3. 変更イベント: データベースレコードの特定の属性が変更された場合に、カスタムメソッドをトリガーすることができます。変更時のカスタム処理を実装するには便利です。

ActiveRecord::AttributeMethods::Dirtyモジュールで提供される主要なメソッド:

メソッド 説明
changed_attribute_names_to_save? 変更されたすべての属性の配列を返します。 (changedから)
has_changes_to_save? 1つ以上の属性が変更された場合にtrueを返します。 (changed?から)
changes_to_save 変更された属性とその変更前後の値を含むハッシュを返します。(changesから)
<attribute>_change_to_be_saved 特定のカラムの変更前後の値を含むハッシュを返します。(<attribute>_changesから)
will_save_change_to_attribute? 保存予定の変更があるか判定しtrue/falseを返します。変更前後の値を指定できますます。(<attribute>_changed?から)
saved_changes 最後の保存から変更された属性とその変更前後の値を含むハッシュを返します(previous_changesから)。
restore_attributes 変更前の値で属性を復元します。
previous_changes 最後の保存から変更された属性とその変更前後の値を含むハッシュを返します。
clear_changes_information 属性の変更情報をクリアします。
clear_attribute_changes 特定の属性の変更情報をクリアします。
attribute_in_database 変更された属性とその変更前後の値を含むハッシュを返します(changed_attributesから)。
changes_applied 属性変更を適用し、変更情報をクリアします。
reset_attribute 特定の属性の変更情報をリセットします。
reset_changes すべての属性の変更情報をリセットします。

これらのメソッドは、Active Recordモデル内でデータの変更状態を追跡し、属性の変更前後の値を取得したり、変更があったかどうかを確認したりするのに使えます。

ActiveRecord::Dirtyモジュールのクラスメソッド一覧

https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html

早速ActiveRecord::Dirtyを使ってみます。

モデルにActiveRecord::Dirtyモジュールをincludeする

Todoモデルがあるとします。
一つのTodoにステータスをつけられます、ActiveRecord::Dirtyモジュールを導入しステータスの変更を記録する例です。
また、before_saveコールバックを使用して、status属性が変更されたときにカスタム処理を実行させることもできます。

class Todo < ApplicationRecord

  enum status: { planned: 0, started: 1, completed: 2, archived: 3 }
  # includeする
  include ActiveModel::Dirty
  
  before_save :announce_status_changes

  private

  def announce_status_changes
    if status_changed?
      # status属性が変更された場合に実行する処理
      puts "ステータスが #{name_was} から #{name}"に変更されました。
    end
  end
end

ステータスを変更する

irb(main):016:0> todo = Todo.last
  Todo Load (0.3ms)  SELECT "todos".* FROM "todos" ORDER BY "todos"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> 
#<Todo:0x00000001122927d8
...
irb(main):017:0> todo.status = 2
=> 2
irb(main):018:0> todo.changed?
=> true
irb(main):019:0> todo.status_changed?
=> true
irb(main):020:0> todo.status_was
=> "planned"
irb(main):021:0> todo.status_change
=> ["planned", "started"]
irb(main):022:0> todo.status = 3
=> 3
irb(main):023:0> todo.status_change
=> ["planned", "archived"]

変更を取得する

irb(main):001:0> todo.has_changes_to_save?
=> true
irb(main):002:0> todo.changed
=> ["status"]
irb(main):003:0> todo.changed_attributes
=> {"status"=>"planned"}
irb(main):004:0> todo.changes
=> {"status"=>["planned", "done"]}
irb(main):005:0> todo.changes_to_save
=> {"status"=>["planned", "done"]}

同じステータスで更新しても変更にならない

irb(main):048:0> todo.status = 2
=> 2
# 同じステータスで更新する
irb(main):049:0> todo.status = 2
=> 2
irb(main):050:0> todo.status_changed?
=> false
irb(main):051:0> todo.status_change
=> nil

変更をトラッキングする

irb(main):052:0> todo.status_will_change!
=> "done"
irb(main):053:0> todo.status_change
=> ["done", "done"]

status_will_change!メソッドは、Active Recordモデル内で属性の変更をトラッキングするために使用されるメソッドの一つです。属性の変更をトラッキングするためのフラグを立てます。
このメソッドを呼び出すことで、Active Recordは指定した属性が変更されたことを認識し、後でその変更をデータベースに反映できるようになります。

class Todo < ApplicationRecord
  # status属性が変更される前にstatus_will_change!メソッドを呼び出す
  def mark_as_completed
    status_will_change!
    self.status = 2 # ステータスを「完了」に変更
    save
  end
end

mark_as_completedメソッド内でstatus_will_change!メソッドを呼び出して、status属性が変更されたことをActive Recordに通知しています。その後、status属性を変更してレコードを保存します。

変更をリセットする

手動で変更をリセットします。

irb(main):059:0> todo.changes_applied
=> nil
irb(main):060:0> todo.changes
=> {}
irb(main):061:0> todo.changed
=> []
irb(main):062:0> todo.has_changes_to_save?
=> false
# 全ての変更を消す
irb(main):063:0> todo.clear_changes_information
=> nil

ステータスを保存する

ステータスを保存すると変更をリセットされます。

irb(main):026:0> todo.save
  TRANSACTION (0.2ms)  BEGIN
  Todo Update (0.4ms)  UPDATE "todos" SET "status" = $1, "updated_at" = $2 WHERE "todos"."id" = $3  [["status", 3], ["updated_at", "2023-09-26 15:18:08.129468"], ["id", 4]]
  TRANSACTION (0.5ms)  COMMIT
=> true
irb(main):027:0> todo.changed?
=> false
irb(main):028:0> todo.status_changed?
=> false
irb(main):029:0> todo.status_was
=> "planned"
irb(main):030:0> todo.status_for_database
=> 1

ステータスが保存された後に変更を取得する

irb(main):001:0> todo.status = 1
=> 1
irb(main):002:0> todo.status
=> "started"
irb(main):003:0> todo.save
  TRANSACTION (0.2ms)  BEGIN
  Todo Update (1.7ms)  UPDATE "todos" SET "status" = $1, "updated_at" = $2 WHERE "todos"."id" = $3  [["status", 1], ["updated_at", "2023-09-26 15:44:21.531908"], ["id", 4]]
  TRANSACTION (0.6ms)  COMMIT
=> true
irb(main):004:0> todo.previous_changes
=> 
{"status"=>["planned", "started"],
 "updated_at"=>
  [Tue, 26 Sep 2023 15:24:44.736906000 UTC +00:00, Tue, 26 Sep 2023 15:44:21.531908000 UTC +00:00]}
irb(main):005:0> todo.status_before_last_save
=> "planned"

終わりに

RailsのActiveRecord::Dirtyモジュールでした。
ActiveRecordのコールバックと組み合わせて使うことでTodoの変更状態に応じて特定のメソッドを実行できます。状態変更の通知などを送れると良さそうです。

https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
https://tech.stmn.co.jp/entry/2021/04/22/100133

Discussion