Edited at

ActiveRecord 5 の dirty attribute methods (*_changed? とか)の挙動一覧

More than 1 year has passed since last update.


この記事は何?

ActiveRecord 5系の dirty attribute methods (attribute_changed? とか attribute_was とか)の挙動を一覧にしたものです。以下のコードを1行ずつ実行し、各時点で dirty method の返り値を表にまとめました。

o = Obj.create(foo: "V1")

o.foo = "V2"
o.save!
o.foo = "V3"

5.1.xでは attribute_changed? など一部のメソッドが非推奨となり、5.2.0から異なる挙動のメソッドとして復活しました。そのため両バージョンの挙動を併記しています。


一覧


active_record 5.2.0


Create record

o = Obj.create(foo: "V1")

method
before_save
after_save
created

foo
"V1"
"V1"
"V1"

foo_was
nil
"V1"
"V1"

foo_before_last_save
nil
nil
nil

foo_in_database
nil
"V1"
"V1"

saved_change_to_foo
nil
[nil, "V1"]
[nil, "V1"]

foo_changed?
true
false
false

will_save_change_to_foo?
true
false
false

saved_change_to_foo?
false
true
true


Change record

o.foo = "V2"

method
changed

foo
"V2"

foo_was
"V1"

foo_before_last_save
nil

foo_in_database
"V1"

saved_change_to_foo
[nil, "V1"]

foo_changed?
true

will_save_change_to_foo?
true

saved_change_to_foo?
true


Save record

o.save!

method
before_save
after_save
save finished

foo
"V2"
"V2"
"V2"

foo_was
"V1"
"V2"
"V2"

foo_before_last_save
nil
"V1"
"V1"

foo_in_database
"V1"
"V2"
"V2"

saved_change_to_foo
[nil, "V1"]
["V1", "V2"]
["V1", "V2"]

foo_changed?
true
false
false

will_save_change_to_foo?
true
false
false

saved_change_to_foo?
true
true
true


Change record

o.foo = "V3"

method
changed

foo
"V3"

foo_was
"V2"

foo_before_last_save
"V1"

foo_in_database
"V2"

saved_change_to_foo
["V1", "V2"]

foo_changed?
true

will_save_change_to_foo?
true

saved_change_to_foo?
true


active_record 5.1.6


Create record

o = Obj.create(foo: "V1")

method
before_save
after_save
created

foo
"V1"
"V1"
"V1"

foo_was
nil
nil
"V1"

foo_before_last_save
nil
nil
nil

foo_in_database
nil
"V1"
"V1"

saved_change_to_foo
nil
[nil, "V1"]
[nil, "V1"]

foo_changed?
true
true
false

will_save_change_to_foo?
true
false
false

saved_change_to_foo?
false
true
true


Change record

o.foo = "V2"

method
changed

foo
"V2"

foo_was
"V1"

foo_before_last_save
nil

foo_in_database
"V1"

saved_change_to_foo
[nil, "V1"]

foo_changed?
true

will_save_change_to_foo?
true

saved_change_to_foo?
true


Save record

o.save!

method
before_save
after_save
save finished

foo
"V2"
"V2"
"V2"

foo_was
"V1"
"V1"
"V2"

foo_before_last_save
nil
"V1"
"V1"

foo_in_database
"V1"
"V2"
"V2"

saved_change_to_foo
[nil, "V1"]
["V1", "V2"]
["V1", "V2"]

foo_changed?
true
true
false

will_save_change_to_foo?
true
false
false

saved_change_to_foo?
true
true
true


Change record

o.foo = "V3"

method
changed

foo
"V3"

foo_was
"V2"

foo_before_last_save
"V1"

foo_in_database
"V2"

saved_change_to_foo
["V1", "V2"]

foo_changed?
true

will_save_change_to_foo?
true

saved_change_to_foo?
true


Deprecation warnings in 5.1.6

DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `attribute_before_last_save` instead. (called from public_send at dirty_method.rb:51)

DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from public_send at dirty_method.rb:51)
DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. (called from public_send at dirty_method.rb:51)
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from public_send at dirty_method.rb:51)
DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `attribute_before_last_save` instead. (called from public_send at dirty_method.rb:51)
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from public_send at dirty_method.rb:51)
DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. (called from public_send at dirty_method.rb:51)
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from public_send at dirty_method.rb:51)


使用したスクリプト


dirty_method.rb

require "bundler/inline"

$stdout = open(File::NULL, "w") # Suppress migration log

gemfile(true) do
source "https://rubygems.org"

gem "activerecord", ENV['ACTIVERECORD_VERSION'], require: "active_record"
gem "sqlite3"
end

ActiveRecord::Base.establish_connection(
adapter: "sqlite3",
database: ":memory:",
)

ActiveRecord::Schema.define do
create_table :objs do |t|
t.string :foo, null: false
end
end

$stdout = STDOUT

class Obj < ActiveRecord::Base
METHODS = %i[
foo
foo_was
foo_before_last_save
foo_in_database
saved_change_to_foo
will_save_change_to_foo?
saved_change_to_foo?
]

Captured = Struct.new(:context, :results)

after_initialize :clear

before_save do
capture("before_save")
end

after_save do
capture("after_save")
end

def capture(context)
results = METHODS.map do |method|
[method, public_send(method).inspect]
end.to_h

@captured << Captured.new(context, results)
end

def clear
@captured = []
end

def inspect
table = []

table << "| method |" + @captured.map { |cap| " #{cap.context} " }.join("|") + "|"
table << "|---" + ("|---" * @captured.length) + "|"

METHODS.each do |method|
table << "| `#{method}` |" + @captured.map { |cap| " `#{cap.results[method]}` " }.join("|") + "|"
end

table.join("\n") + "\n"
end
end

BINDING = binding

def action(desc, code)
puts <<~END
###
#{desc}

#{code}

END

BINDING.eval(code)
end

puts "## active_record #{ENV["ACTIVERECORD_VERSION"]}\n\n"

o = action("Create record", %q{o = Obj.create(foo: "V1")})
o.capture("created")
p o
o.clear

action("Change record", %q{o.foo = "V2"})
o.capture("changed")
p o
o.clear

action("Save record", %q{o.save!})
o.capture("save finished")
p o
o.clear

action("Change record", %q{o.foo = "V3"})
o.capture("changed")
p o
o.clear



所感

5.1.6までの attribute_was の挙動が分かりづらい。属性を変更して保存したとき、 after_save コールバック内では変更前の値を返すが、コールバックを抜けると 現在の値 を返すようになる。 attribute_changed? も同様。


謝辞

1枚のスクリプトに Gemfile と ActiveRecord スキーマを詰め込む方法はActiveRecordを試すときに便利なやつ – r7kamura – Mediumを参考にしました。