はじめに
今回はActiveRecordのattribute methodのうち、Dirtyメソッドをまとめていきます。
(2022年6月時点。Rails7.0対応。)
以前、BeforeTypeCastの内容をまとめましたが、今回のDirtyメソッドはBeforeTypeCastに比べて、より実用的なメソッドが多いと思います。
検証環境
以下のschemaとmodelに基づいて検証を行なっています。
enumを利用しているので、実際にDBに入っている値と出力される内容が異なる箇所が存在します。
create_table "todos", charset: "utf8mb4", force: :cascade do |t|
t.string "title"
t.datetime "scheduled_at"
t.integer "status"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
class Todo < ApplicationRecord
enum status: { not_yet: 0, complete: 1, in_progress: 2, cancel: 3 }
end
Dirty
Dirtyとは、オブジェクトの変更を検出し、変更前後の値を取得できるメソッド群です。
カラムの変更に応じてログを残したり、保存前後の値を比較してなにか処理をしたりなど、たびたびお世話になるメソッド達です。
何故「Dirty」と呼ばれるているのかは未知なのですが、持論では、オブジェクトに変更があるのに保存されてない状態を「汚れている」とみなし、その内容を検知するためのメソッドだから、「Dirty」と呼んでいるのではないかと考えています。
理由を知っている方は、是非教えてください。
attribute_before_last_save
指定したattributeの最終保存前の値を返却します。
保存時のコールバックの処理で、元のattributeの値を使用したい時に使えます。
[attribute名]_before_last_save
でも同様の呼出可能です。
保存前はnilを返却します。
todo = Todo.find(1) #=> #<Todo id: 1, status: "not_yet",...>
# 保存前はnilを返却
todo.attribute_before_last_save(:status) #=> nil
todo.update!(status: :in_progress)
# 保存前のattributeの値を返却
todo.attribute_before_last_save(:status) #=> "not_yet"
todo.update!(status: :cancel)
# [attribute名]_before_last_saveでも同様
# 最終保存前の値を取得するため、"in_progress"を返却
todo.status_before_last_save #=> "in_progress"
attribute_change_to_be_saved
指定したattributeの保存前の値と保存予定の値を配列形式で返却します。
配列の形式は[元の値, 保存予定の値]
となります。
レコードが保存されるときに発生するattributeの変更を確認するために使えます。
[attribute名]_change_to_be_saved
でも同様の呼出可能です。
保存後はnilを返却します。
途中で値が書き換えられても、最初の値と保存予定の値が返却されます。
todo = Todo.new #=> #<Todo id: nil, status: nil,...>
todo.status = :in_progress
# 新規作成なので、初期値はnil
todo.attribute_change_to_be_saved(:status) #=> [nil, "in_progress"]
todo.status = :cancel
# [attribute名]_change_to_be_savedでも同様
# 最初の値と保存予定の値を返却
todo.status_change_to_be_saved #=> [nil, "cancel"]
todo.save!
# 保存後はnilを返却
todo.attribute_change_to_be_saved(:status) #=> nil
attribute_in_database
変更に関わらず、指定したattributeのデータベース上の値を返却します。
ただし、型はキャストされた状態で返却されるので注意が必要です。
[attribute名]_in_database
でも同様の呼出可能です。
todo = Todo.find(3) #=> #<Todo id: 3, status: "in_progress",...>
# 型キャストされる前の値は2
todo.status_for_database #=> 2
todo.status = :complete
# 型キャストされた状態のデータベース上の値を返却
todo.attribute_in_database(:status) #=> "in_progress"
todo.save!
# [attribute名]_in_databaseでも同様
# 保存後なので、データベースに保存された"complete"を返却
todo.status_in_database #=> "complete"
attributes_in_database
変更保存予定のattributeのデータベース上の値をハッシュで返却します。
attribute_in_database
同様、型はキャストされた状態で返却されるので注意が必要です。
todo = Todo.find(3) #=> #<Todo id: 3, status: "complete",...>
# 変更がない場合は空ハッシュを返却
todo.attributes_in_database #=> {}
todo.status = :complete
# 同じ値を入れても、変更とは見なされない
todo.attributes_in_database #=> {}
todo.status = :cancel
# 変更保存予定のattributeのデータベース上の値を返却
todo.attributes_in_database #=> {"status"=>"complete"}
todo.save!
# 保存後は変更がない時と同様
todo.attributes_in_database #=> {}
changed_attribute_names_to_save
変更保存予定のattribute名の配列を返却します。
todo = Todo.find(3) #=> #<Todo id: 3, scheduled_at: 2022-03-10 00:00:00.000000000 +0900, status: "cancel", ...>
# 変更がない時は空配列を返却
todo.changed_attribute_names_to_save #=> []
todo.scheduled_at = Time.current.since(14.days)
todo.status = :complete
# 変更があるattribute名を返却
todo.changed_attribute_names_to_save #=> ["scheduled_at", "status"]
todo.save!
# 保存後は変更がない時と同様
todo.changed_attribute_names_to_save #=> []
changes_to_save
変更保存予定のattributeの変更前後の値を返却します。
todo = Todo.find(2) #=> #<Todo id: 2, scheduled_at: 2022-03-10 00:00:00.000000000 +0900, status: "in_progress", ...>
todo.attributes
# 変更がない時は空ハッシュを返却
todo.changes_to_save #=> {}
todo.scheduled_at = Time.current.since(14.days)
todo.status = :complete
# 変更保存予定の前後の値を返却
todo.changes_to_save
#=> {"scheduled_at"=>[Thu, 10 Mar 2022 00:00:00.000000000 JST +09:00, Sat, 19 Mar 2022 17:47:46.799374000 JST +09:00], "status"=>["in_progress", "complete"]}
todo.save!
# 保存後は変更がない時と同様に空ハッシュを返却
todo.changes_to_save #=> {}
has_changes_to_save?
changes_to_save
の値の有無でtrue
かfalse
を返却します。
todo = Todo.find(2) #=> #<Todo id: 2, scheduled_at: 2022-03-10 00:00:00.000000000 +0900, status: "in_progress", ...>
# 変更保存予定のattributeがないためfalseを返却
todo.has_changes_to_save? #=> false
todo.status = :complete
todo.scheduled_at = Time.current.since(14.days)
# 変更保存予定のattributeがあるためtrueを返却
todo.has_changes_to_save? #=> true
todo.save!
# 保存後は変更がない時と同様
todo.has_changes_to_save? #=> false
reload
attributeの変更を初期化し、設定されているプライマリーIDのデータを読み直します。
データの読み直しなので、select句のSQLが再実行されます。
オブジェクトが持っているIDを用いて、findが再実行されると考えると分かりやすいと思います。
そのため、新規のActiveRecordオブジェクトのように、プライマリーIDをもっていない状態のオブジェクトにreloadをするとRecordNotFoundの例外を吐きます。IDを指定せずにfindした時と同じ挙動です。
todo = Todo.find(2) #=> SELECT `todos`.* FROM `todos` WHERE `todos`.`id` = 2 LIMIT 1
todo.attributes #=> {..., "title"=>"初期タイトル",..., "status"=>"complete", ...}
todo.status = :cancel
todo.title = "reload前変更"
todo.attributes #=> {..., "title"=>"reload前変更",..., "status"=>"cancel", ...}
# reload時にselect再実行
todo.reload #=> SELECT `todos`.* FROM `todos` WHERE `todos`.`id` = 2 LIMIT 1
# 元の状態に戻る
todo.attributes #=> {..., "title"=>"初期タイトル",..., "status"=>"complete", ...}
# 新規のActiveRecordオブジェクトに対してreloadするとRecordNotFoundエラー
todo = Todo.new
todo.status = :cancel
# Todo.find()とした時と同じ挙動
todo.reload #=> ActiveRecord::RecordNotFound (Couldn't find Todo without an ID):
saved_change_to_attribute
指定したattributeの最終保存時に変更された値と保存前の値を配列で返却します。
配列は、[保存前の最初の値, 保存後の値]
の順になっています。
saved_change_to_[attribute名]
でも同様の呼出可能です。
@todo = Todo.find(3) #=> #<Todo id: 3, status: "complete", ...>
todo.status = :in_progress
# 未保存の状態はnilを返却
todo.saved_change_to_attribute(:status) #=> nil
todo.save!
todo.saved_change_to_attribute(:status) #=> ["complete", "in_progress"]
todo.status = :cancel
todo.update!(status: :complete)
# saved_change_to_[attribute名]でも同様
# 途中cancelを入れているが、保存前の最初の値はin_progressで保存後にcompleteに変わっているので、["in_progress", "complete"]を返却
todo.saved_change_to_status #=> ["in_progress", "complete"]
saved_change_to_attribute?
指定したattributeの前回保存時の変更有無でtrue
かfalse
を返却します。
saved_change_to_[attribute名]?
でも同様の呼出可能です。
from
やto
を指定して、変更前の値、変更後の値の条件を指定することも可能です。
todo = Todo.find(3) #=> #<Todo id: 3, status: "complete", ...>
todo.status = :in_progress
# 保存前なので、falseが返却される
todo.saved_change_to_attribute?(:status) #=> false
todo.save!
# 保存時に変更があるので、trueを返却
todo.saved_change_to_attribute?(:status) #=> true
# from, toと変更内容が一致してないとfalseを返却
todo.saved_change_to_attribute?(:status, from: "cancel") #=> false
todo.saved_change_to_attribute?(:status, to: "cancel") #=> false
# from, toと変更内容が一致しているので、trueを返却
todo.saved_change_to_attribute?(:status, from: "complete", to: "in_progress") #=> true
todo.status = :cancel
todo.update!(status: :in_progress)
# saved_change_to_[attribute名]?でも同様
# 保存時にstatusの値は変更がなかったので、falseを返却
todo.saved_change_to_status? #=> false
# saved_change_to_[attribute名]?でfrom,toを指定する場合は以下のように指定
# from, toを同じ値にしても、変更時に変化がない場合はfalseを返却
todo.saved_change_to_status?(from: "in_progress", to: "in_progress") #=> false
saved_changes
直前の保存による変更内容をハッシュで返却します。
ハッシュは、attributeをkeyにして、valueにsaved_change_to_attribute
と同様の値が入ります。
todo = Todo.find(2) #=> #<Todo id: 2, title: "変更前タイトル", status: "in_progress",...>
todo.title = "変更後タイトル"
todo.status = :complete
# 保存前は空ハッシュを返却
todo.saved_changes #=> {}
todo.save!
todo.saved_changes
#=> { "title"=>["変更前タイトル", "変更後タイトル"], "status"=>["in_progress", "complete"], "updated_at"=>[Wed, 08 Jun 2022 12:18:38.734546000 JST +09:00, Wed, 08 Jun 2022 12:20:10.658886000 JST +09:00]}
todo.save!
# 直前の保存では変更がないので、空ハッシュを返却
todo.saved_changes #=> {}
saved_changes?
直前の保存時に何か変更点があるかどうかで、true
かfalse
を返却します。
todo = Todo.find(2) #=> title: "変更前タイトル"
todo.title = "変更後タイトル"
# 保存してないので、falseを返却
todo.saved_changes? #=> false
todo.save!
todo.saved_changes? #=> true
todo.save!
# 直前の保存では、変更がないので、falseを返却
todo.saved_changes? #=> false
will_save_change_to_attribute?
指定したattributeが次の保存時に変更があるかどうかを返却します。
from
やto
を指定して、変更前の値、変更後の値の条件を指定することも可能です。
todo = Todo.find(3) #=> status: "in_progress"
todo.status = :in_progress
# 変更がないので、falseを返却
todo.will_save_change_to_attribute?(:status) #=> false
todo.status = :complete
todo.will_save_change_to_attribute?(:status) #=> true
todo.will_save_change_to_attribute?(:status, to: "complete") #=> true
# toの値が変更後の値と異なるので、falseを返却
todo.will_save_change_to_attribute?(:status, from: :in_progress, to: "cancel") #=> false
todo.save!
# 保存されていない変更がないので、falseを返却
todo.will_save_change_to_attribute?(:status) #=> false
おわりに
今回は、Dirtyメソッドについてまとめていきました。
ActiveRecordで、保存前後の値を使用して何かしたい、カラム/attributeの変更を検知したいといった時に参考になればと思います。
便利なメソッドは用法・用量を守って正しく使いましょう。
その他のメソッドについては、別記事でまとめています。↓↓
ActiveRecord attribute methodまとめ(その①: BeforeTypeCast編)
Active Record attribute methodまとめ(その③: PrimaryKey編)
Active Record attribute methodまとめ(その④: Query, Read, Write編)