ActiveRecord::AttributeMethods::Dirtyモジュールで定義している以下メソッドの挙動をまとめました。
*_before_last_save
*_in_database
saved_change_to_*
will_save_change_to_*?
saved_change_to_*?
*_change_to_be_saved
saved_changes?
saved_changes
has_changes_to_save?
changes_to_save
changed_attribute_names_to_save
attributes_in_database
※順番はテキトーです
以下のコードを1行ずつ実行し、各時点の dirty method の返り値を表にまとめました。
o = Obj.create(foo: "V1")
o.foo = "V2"
o.save!
o.foo = "V3"
一覧
active_record 6.0.2.2
Create record
o = Obj.create(foo: "V1")
method |
before_save |
after_save |
created |
foo |
"V1" |
"V1" |
"V1" |
foo_before_last_save |
nil |
nil |
nil |
foo_in_database |
nil |
"V1" |
"V1" |
saved_change_to_foo |
nil |
[nil, "V1"] |
[nil, "V1"] |
will_save_change_to_foo? |
true |
false |
false |
saved_change_to_foo? |
false |
true |
true |
foo_change_to_be_saved |
[nil, "V1"] |
nil |
nil |
saved_changes? |
false |
true |
true |
saved_changes |
{} |
{"id"=>[nil, 1], "foo"=>[nil, "V1"]} |
{"id"=>[nil, 1], "foo"=>[nil, "V1"]} |
has_changes_to_save? |
true |
false |
false |
changes_to_save |
{"foo"=>[nil, "V1"]} |
{} |
{} |
changed_attribute_names_to_save |
["foo"] |
[] |
[] |
attributes_in_database |
{"foo"=>nil} |
{} |
{} |
Change record
method |
changed |
foo |
"V2" |
foo_before_last_save |
nil |
foo_in_database |
"V1" |
saved_change_to_foo |
[nil, "V1"] |
will_save_change_to_foo? |
true |
saved_change_to_foo? |
true |
foo_change_to_be_saved |
["V1", "V2"] |
saved_changes? |
true |
saved_changes |
{"id"=>[nil, 1], "foo"=>[nil, "V1"]} |
has_changes_to_save? |
true |
changes_to_save |
{"foo"=>["V1", "V2"]} |
changed_attribute_names_to_save |
["foo"] |
attributes_in_database |
{"foo"=>"V1"} |
Save record
method |
before_save |
after_save |
save finished |
foo |
"V2" |
"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"] |
will_save_change_to_foo? |
true |
false |
false |
saved_change_to_foo? |
true |
true |
true |
foo_change_to_be_saved |
["V1", "V2"] |
nil |
nil |
saved_changes? |
true |
true |
true |
saved_changes |
{"id"=>[nil, 1], "foo"=>[nil, "V1"]} |
{"foo"=>["V1", "V2"]} |
{"foo"=>["V1", "V2"]} |
has_changes_to_save? |
true |
false |
false |
changes_to_save |
{"foo"=>["V1", "V2"]} |
{} |
{} |
changed_attribute_names_to_save |
["foo"] |
[] |
[] |
attributes_in_database |
{"foo"=>"V1"} |
{} |
{} |
Change record
method |
changed |
foo |
"V3" |
foo_before_last_save |
"V1" |
foo_in_database |
"V2" |
saved_change_to_foo |
["V1", "V2"] |
will_save_change_to_foo? |
true |
saved_change_to_foo? |
true |
foo_change_to_be_saved |
["V2", "V3"] |
saved_changes? |
true |
saved_changes |
{"foo"=>["V1", "V2"]} |
has_changes_to_save? |
true |
changes_to_save |
{"foo"=>["V2", "V3"]} |
changed_attribute_names_to_save |
["foo"] |
attributes_in_database |
{"foo"=>"V2"} |
検証用に使用したスクリプト
# frozen_string_literal: true
require "bundler/inline"
ACTIVERECORD_VERSION = '6.0.2.2'
$stdout = open(File::NULL, "w") # Suppress migration log
gemfile(true) do
source "https://rubygems.org"
gem "activerecord", 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_before_last_save
foo_in_database
saved_change_to_foo
will_save_change_to_foo?
saved_change_to_foo?
foo_change_to_be_saved
saved_changes?
saved_changes
has_changes_to_save?
changes_to_save
changed_attribute_names_to_save
attributes_in_database
).freeze
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}
```ruby
#{code}
```
END
BINDING.eval(code)
end
puts "## active_record #{ACTIVERECORD_VERSION}\n\n"
o = action("Create record", 'o = Obj.create(foo: "V1")')
o.capture("created")
p o
o.clear
action("Change record", 'o.foo = "V2"')
o.capture("changed")
p o
o.clear
action("Save record", 'o.save!')
o.capture("save finished")
p o
o.clear
action("Change record", 'o.foo = "V3"')
o.capture("changed")
p o
o.clear
参考