15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ActiveRecord attribute methodまとめ(その②: Dirty編)

Last updated at Posted at 2022-06-10

はじめに

今回はActiveRecordのattribute methodのうち、Dirtyメソッドをまとめていきます。
(2022年6月時点。Rails7.0対応。)
以前、BeforeTypeCastの内容をまとめましたが、今回のDirtyメソッドはBeforeTypeCastに比べて、より実用的なメソッドが多いと思います。

検証環境

以下のschemaとmodelに基づいて検証を行なっています。
enumを利用しているので、実際にDBに入っている値と出力される内容が異なる箇所が存在します。

db/schema.rb
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
app/model/todo.rb
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の値の有無でtruefalseを返却します。

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の前回保存時の変更有無でtruefalseを返却します。
saved_change_to_[attribute名]?でも同様の呼出可能です。
fromtoを指定して、変更前の値、変更後の値の条件を指定することも可能です。

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?

直前の保存時に何か変更点があるかどうかで、truefalseを返却します。

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が次の保存時に変更があるかどうかを返却します。
fromtoを指定して、変更前の値、変更後の値の条件を指定することも可能です。

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編)

参考

15
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?