ActiveRecord 5 の dirty attribute methods （*_changed? とか）の挙動一覧

# この記事は何？

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

```ruby
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)
```

## 使用したスクリプト

```ruby: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](https://medium.com/@r7kamura/activerecord%E3%82%92%E8%A9%A6%E3%81%99%E3%81%A8%E3%81%8D%E3%81%AB%E4%BE%BF%E5%88%A9%E3%81%AA%E3%82%84%E3%81%A4-f5a10a8c17d8)を参考にしました。