この記事は何?
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
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
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
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
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
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
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を参考にしました。