zenet_logo

-株式会社ゼネット技術ブログ-

Railsの属性変更検知を極める!ActiveRecord::DirtyとActiveModel::Dirtyの徹底ガイド


こんにちは。ゼネットの張です。業務でRuby on Railsを扱っています。
今回はRailsの変更検知に関するモジュールとメソッドを紹介したいと思います!

はじめに

最近の開発で、画面入力値とデータベースに保存された値を比較するような処理を書くことが増えてきました。その中で、「Railsには属性の変更を検知する便利なメソッドがたくさんあるけど、どれを使えばいいのか分からない……」と悩む場面がありました。
調べてみると、ActiveRecordやActiveModelにいろいろな“Dirty”メソッドが用意されていて、それぞれ微妙に用途や返す値が異なっていました。この記事では、それらのメソッドの違いや使い方を自分なりに整理してまとめています。
もし漏れや誤りがあれば、ぜひコメントでご指摘いただけると嬉しいです。

ActiveRecord::AttributeMethods::Dirtyモジュール

ActiveRecordのモデルに属性変更の追跡機能を用意するモジュールです。ActiveModel::Dirtyモジュールを拡張しており、データベースに依存するモデルでよく使われています。

attribute_before_last_save

直前の保存を実行する前の保存値を取得する。

user = User.create!(last_name: "田中")
# 新規作成されたため、nilを返します
user.attribute_before_last_save(:last_name) #=> nil
user.update!(last_name: "鈴木")
user.attribute_before_last_save(:last_name) #=> "田中"
# {カラム名}_before_last_saveでも呼び出せます
user.last_name_before_last_save #=> "田中"
attribute_change_to_be_saved

次の保存で、保存前と保存後の内容を配列形式で取得する。

user = User.create!(last_name: "田中")
user.last_name = "鈴木"
user.attribute_change_to_be_saved(:last_name) #=> ["田中", "鈴木"]
# {カラム名}_change_to_be_savedでも呼び出せます
user.last_name_change_to_be_saved #=> ["田中", "鈴木"]
# 保存済みで値の更新がない場合はnilを返す
user.save!
user.attribute_change_to_be_saved(:last_name) #=> nil
attribute_in_database

enum例 指定の属性の実際のデータベースに保存されている値を取得する。

class User < ApplicationRecord
  enum role: { general: 0, admin: 1 }
end

User.roles  # => { "general" => 0, "admin" => 1 }
user = User.create(role: :general)
user.role # => "general"(マッピング後の値)
user[:role] # => 0(DBに保存されている整数)
user.attribute_in_database(:role) # => 0(DBに保存されている整数)

user.role = :admin # 保存しない
user.role  # => "admin"
user[:role] # => 1
user.attribute_in_database(:role) # => 0(DBの値のまま)
attributes_in_database

次の保存で変更されるすべての属性の実際のデータベースに保存されている値をHash形式で取得する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.assign_attributes(last_name: "鈴木", first_name: "次郎")
user.attributes_in_database #=> { "last_name" => "田中", "first_name" => "太郎" }
changed_attribute_names_to_save

次の保存で変更されるすべての属性の属性名を配列形式で取得する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.assign_attributes(last_name: "鈴木", first_name: "次郎")
user.changed_attribute_names_to_save #=> [ "last_name", "first_name" ]
changes_to_save

次の保存でDB保存値に変更がある属性すべてを、{ "属性名" => [変更前の値, 変更後の値] }のHash形式で取得する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.assign_attributes(last_name: "鈴木", first_name: "次郎")
user.changes_to_save #=> { "last_name" => ["田中", "鈴木"], "first_name" => ["太郎", "次郎"] }
has_changes_to_save?

次の保存でDB保存値に変更がある属性があるかどうかを判断する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.assign_attributes(last_name: "鈴木", first_name: "次郎")
user.has_changes_to_save? #=> true
user.save!
user.has_changes_to_save? #=> false
reload

データベースのレコードを再度取得し、保存されていない変更をすべて削除する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.assign_attributes(last_name: "鈴木", first_name: "次郎")
user.attributes #=> { last_name: "鈴木", first_name: "次郎" }
user.reload
user.attributes #=> { "last_name" => "田中", "first_name" => "太郎" }
saved_change_to_attribute

直前の保存による変更を、[ 保存前の値, 保存後の値 ]の配列形式で取得する。

user = User.create!(last_name: "田中")
# 変更が保存されていない場合はnilを返す
user.last_name = "鈴木"
user.saved_change_to_attribute(:last_name) #=> nil
user.save!
user.saved_change_to_attribute(:last_name) #=> ["田中", "鈴木"]
# saved_change_to_{カラム名}でも呼び出せます
user.saved_change_to_last_name #=> ["田中", "鈴木"]
saved_change_to_attribute?

直前の保存で、変更があるかどうかを判断する。

user = User.create!(last_name: "田中")
user.last_name = "鈴木"
user.saved_change_to_attribute?(:last_name) #=> false
user.save!
user.saved_change_to_attribute?(:last_name) #=> true
# saved_change_to_{カラム名}でも呼び出せます
user.saved_change_to_last_name? #=> true
saved_changes

直前の保存で変更された属性(updated_atなど自動更新の属性も含む)をすべて、"属性名" => [ "保存前の値", "保存後の値" ]のHash形式で取得する。

user = User.create!(last_name: "田中")
user.update!(:last_name: "鈴木")
user.saved_changes
#=> { "last_name" => [ "田中", "鈴木" ], "updated_at" => [ Thu, 15 May 2025 13:33:55.641000000 JST +09:00, Thu, 15 May 2025 14:20:12.241000000 JST +09:00 ] }
saved_changes?

直前の保存でupdated_atなど自動更新の属性を含めて、変更があるかどうかを判断する。

user = User.create!(last_name: "田中")
user.last_name = "鈴木"
user.saved_changes? #=> false
save!
user.saved_changes? #=> true
will_save_change_to_attribute?

次の保存で、属性が変更されるかどうかを判断する。

user = User.create!(last_name: "田中")
user.will_save_change_to_attribute?(:last_name) #=> false
user.last_name = "鈴木"
user.will_save_change_to_attribute?(:last_name) #=> true
save!
# will_save_change_to_{カラム名}でも呼び出せます
user.will_save_change_to_last_name #=> false

ActiveModel::Dirtyモジュール

ActiveModel系のモデルに属性変更の追跡機能を用意するモジュールです。フォームオブジェクトなどデータベースに依存しないモデルでよく使われています。

*_change

属性の変更内容を取得する。

user = User.new
user.last_name = "田中"
user.last_name_change #=> [ nil, "田中" ]
*_changed?

属性が変更されたかどうかを判断する。

user = User.new(last_name: "田中")
user.last_name = "田中"
user.last_name_changed? #=> true
*_previous_change

直前の保存による保存値の変更を、[ 保存前の値, 保存後の値 ]の配列形式で取得する。

user = User.create!(last_name: "田中")
user.update!(last_name: "鈴木")
user.last_name_previous_change #=> ["田中", "鈴木"]
*_previous_changed?

直前の保存で保存値が変更されたかどうかを判断する。また、オプションを追加することにより、変更前後の値を指定することができる。

user = User.create!(last_name: "田中")
user.update!(last_name: "鈴木")
user.last_name_previous_changed? #=> true
user.last_name_previous_changed?(from: nil, to: "田中") #=> false
user.last_name_previous_changed?(from: "田中", to: "鈴木") #=> true
*_previously_was

直前の保存を行う前の保存値を取得する。

user = User.create!(last_name: "田中")
user.last_name_previous_was #=> nil
user.update!(last_name: "鈴木")
user.last_name_previous_was #=> "田中"
*_was

属性が変更される前の値(データベースの保存値)を取得する。

user = User.new(last_name: "田中")
user.last_name_was #=> nil # 保存していないため、nilを返す
user.save!
user.last_name_was #=> "田中" # 保存したため、DBの保存値を返す
user.last_name = "鈴木"
user.last_name_was #=> "田中" # DBの保存値を返す
*_will_change!

ActiveModelの属性を破壊的に変更する場合、ActiveModelが継続的にその属性をトラッキングできるよう、属性名_will_change!でその属性をマーキングする必要がある。ただ、ActiveRecordではそういう変更を自動的に検出するため、ActiveRecordのモデルの場合は属性名_will_change!を呼び出す必要がない。

user = User.new(last_name: "Bill")
user.last_name_will_change!
user.last_name_change #=> [ "Bill", "Bill" ]
user.last_name << 'y'
user.last_name_change #=> [ "Bill", "Billy" ]
attribute_changed?(attr_name, **options)

*_changed?と同じであり、属性名とオプションを指定できる。

user = User.new
user.last_name = "田中"
user.attribute_changed?(:last_name, from: nil, to: "田中") #=> true
attribute_previously_changed?(attr_name, **options)

*_previously_changed?と同じであり、属性名とオプションを指定できる。

user = User.create!(last_name: "田中")
user.update!(last_name: "鈴木")
user.attribute_previously_changed?(:last_name) #=> true
user.attribute_previously_changed?(:last_name, from: nil, to: "田中") #=> false
user.attribute_previously_changed?(:last_name, from: "田中", to: "鈴木") #=> true
attribute_previously_was(attr_name)

*_previously_wasと同じである。

user = User.create!(last_name: "田中")
user.attribute_previously_was(:last_name) #=> nil
user.update!(last_name: "鈴木")
user.attribute_previously_was(:last_name) #=> "田中"
attribute_was(attr_name)

*_wasと同じである。

user = User.new(last_name: "田中")
user.attribute_was(:last_name) #=> nil # 保存していないため、nilを返す
user.save!
user.attribute_was(:last_name) #=> "田中" # 保存したため、DBの保存値を返す
user.last_name = "鈴木"
user.attribute_was(:last_name) #=> "田中" # DBの保存値を返す
changed

まだ保存していない変更がある属性の名前を配列形式で取得する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.last_name = "鈴木"
user.changed #=> ["last_name"]
user.save!
user.changed #=> []
changed?

まだ保存していない変更があるかどうかを判断する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.last_name = "鈴木"
user.changed? #=> true
user.save!
user.changed? #=> false
changed_attributes

まだ保存していない変更がある属性を、{ 属性名 => DBの保存値 }のHash形式で取得する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.last_name = "鈴木"
user.changed_attributes #=> { "last_name" => "田中" }
user.save!
user.changed_attributes #=> {} # 保存後は取得できなくなる
changes

まだ保存していない変更がある属性を、{ 属性名 => [DBの保存値, 変更後の値] }のHash形式で取得する。

user = User.create!(last_name: "田中", first_name: "太郎")
user.last_name = "鈴木"
user.changes #=> { "last_name" => ["田中", "鈴木"] }
user.save!
user.changes #=> {} # 保存後は取得できなくなる
changes_applied

Dirtyデータを削除し、changesの内容をprevious_changesに、mutations_from_databaseの内容をmutations_before_last_saveに移動する。変更内容を保存した後(saveメソッド内)に呼び出す。

clear_*_change

指定の属性のすべてのDirtyデータ(現在の変更、以前の変更)を削除する。

user = User.new(last_name: "田中")
user.last_name = "鈴木"
user.last_name_change #=> ["田中", "鈴木"]
user.clear_last_name_change
user.last_name_change #=> nil
clear_attribute_changes(attr_names)

clear_*_changeと同じである。

user = User.new(last_name: "田中")
user.last_name = "鈴木"
user.last_name_change #=> ["田中", "鈴木"]
user.clear_attribute_changes(:last_name)
user.last_name_change #=> nil
clear_changes_information

すべての属性のすべてのDirtyデータ(現在の変更、以前の変更)を削除する。

previous_changes

直前の保存による変更を、{ 属性名 => ["保存前の保存値", "保存後の保存値"] }のHash形式で取得する。

user = User.create!(last_name: "田中")
user.last_name = "鈴木"
user.previous_changes #=> { "last_name" => [nil, "田中"] }
user.save!
user.previous_changes #=> { "last_name" => ["田中", "鈴木"] }
restore_*!

属性を変更前の値に戻す。

user = User.new(last_name: "田中")
user.last_name = "鈴木"
user.restore_last_name!
user.last_name #=> "田中"
restore_attributes

すべての属性を変更前の値に戻す。

※注意点

*_change*_changed?attribute_changed?changedchanged?changeschanged_attributes*_wasattribute_was、こちらのメソッドはRails5.1、5.2で挙動変更があり、加えてActiveRecordのDirtyモジュールが追加された関係で、非推奨警告が出るようになりました。Rails6.0以降はActiveModelとActiveRecord両方のDirtyモジュールが併存しており、警告が出ないようになりましたが、ActiveRecordのDirtyモジュールの使用が推奨されています。
また、上記メソッドの推奨されている新しいメソッドは以下になります。

旧メソッド 新メソッド
*_change attribute_change_to_be_saved
*_changed?、attribute_changed? will_save_change_to_attribute?
changed changed_attribute_names_to_save
changed? has_changes_to_save?
changes changes_to_save
changed_attributes attributes_in_database
attribute_was、*_was attribute_in_database

ActiveRecord::Persistenceモジュール

こちらはRailsがActiveRecordモデルが永続化、つまりデータベースとやり取りするためのモジュールになりますが、レコード自体の状態(新規?既存?)に関するメソッドもあったので紹介したいと思います。

new_record?

オブジェクトがまだデータベースに保存されていない場合、trueを返す。

user = User.new
user.new_record? #=> true
user.save!
user.new_record? #=> false
persisted?

オブジェクトがすでにデータベースに保存されていた場合、trueを返す。

user = User.new
user.persisted? #=> false
user.save!
user.persisted? #=> true
previously_new_record?

オブジェクトが直前の保存でデータベースに新規作成された場合、trueを返す。

user = User.create!(last_name: "田中")
user.previously_new_record? #=> true # 保存直後はtrueを返す
user.update!(last_name: "鈴木")
user.previously_new_record? #=> false # updateなどの処理を挟んだ場合はfalseを返す
previously_persisted?

オブジェクトはデータベースに保存されていたが、現在は削除された場合、trueを返す。

user = User.create!
user.previously_persisted? #=> false # 削除されず、存在している場合はfalseを返す
user.destroy!
user.previously_persisted? #=> true # 削除されたあとはtrueを返す

よく使うメソッドとその使用例

メソッドが色々ありますが、よく使うメソッドを抜粋し、その使用例をまとめてみました。

メソッド 使用例
will_save_change_to_attribute? 特定の属性が変更されたときのみ、バリデーションを実行する
saved_change_to_attribute? 特定の属性が変更されたときのみ、関連するオブジェクトを更新したい/属性の変更ログを出力したい場合に利用する
attribute_before_last_save 属性の変更履歴をログとして出力したいときに利用する
changes_to_save 複数の属性の変更履歴を作成したいときに利用する
has_changes_to_save? オブジェクトの変更を検知し、変更があった場合のみ、処理を実行したいときに利用する
new_record? テストコード作成で、オブジェクトが新規作成されたかを検証したいときに利用する

最後に

以上、Railsの属性変更検知に関するメソッドを一通り紹介しました。
普段の開発ではあまり意識せず使っているメソッドも多いと思いますが、改めて整理してみると、それぞれの用途や違いがよく分かって勉強になりました。特にバリデーションやログ出力、条件付きの処理などで役立つ場面が多いので、この記事が皆さんの開発のヒントになれば嬉しいです。
最後までお読みいただき、ありがとうございました!

参考リンク

api.rubyonrails.org

api.rubyonrails.org

techracho.bpsinc.jp

api.rubyonrails.org