
こんにちは。ゼネットの張です。業務でRuby on Railsを扱っています。
今回はRailsの変更検知に関するモジュールとメソッドを紹介したいと思います!
- はじめに
- ActiveRecord::AttributeMethods::Dirtyモジュール
- ActiveModel::Dirtyモジュール
- *_change
- *_changed?
- *_previous_change
- *_previous_changed?
- *_previously_was
- *_was
- *_will_change!
- attribute_changed?(attr_name, **options)
- attribute_previously_changed?(attr_name, **options)
- attribute_previously_was(attr_name)
- attribute_was(attr_name)
- changed
- changed?
- changed_attributes
- changes
- changes_applied
- clear_*_change
- clear_attribute_changes(attr_names)
- clear_changes_information
- previous_changes
- restore_*!
- restore_attributes
- ※注意点
- ActiveRecord::Persistenceモジュール
- よく使うメソッドとその使用例
- 最後に
- 参考リンク
はじめに
最近の開発で、画面入力値とデータベースに保存された値を比較するような処理を書くことが増えてきました。その中で、「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?、changed、changed?、changes、changed_attributes、*_was、attribute_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の属性変更検知に関するメソッドを一通り紹介しました。
普段の開発ではあまり意識せず使っているメソッドも多いと思いますが、改めて整理してみると、それぞれの用途や違いがよく分かって勉強になりました。特にバリデーションやログ出力、条件付きの処理などで役立つ場面が多いので、この記事が皆さんの開発のヒントになれば嬉しいです。
最後までお読みいただき、ありがとうございました!
