ActiveRecord6 の  dirty attribute methods （will_save_change_to_* とか）の挙動一覧

[ActiveRecord::AttributeMethods::Dirty](https://github.com/rails/rails/blob/v6.0.2.2/activerecord/lib/active_record/attribute_methods/dirty.rb)モジュールで定義している以下メソッドの挙動をまとめました。

- `*_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 の返り値を表にまとめました。

```ruby
o = Obj.create(foo: "V1")
o.foo = "V2"
o.save!
o.foo = "V3"
```

# 一覧

## active_record 6.0.2.2

### Create record

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

```ruby
  o.foo = "V2"
```

| 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

```ruby
  o.save!
```

| 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

```ruby
  o.foo = "V3"
```

| 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"}` |

## 検証用に使用したスクリプト

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

## 参考
- [ActiveRecord 5 の dirty attribute methods （*_changed? とか）の挙動一覧 - Qiita](https://qiita.com/uasi/items/e00edc0fdf101a7b6f7d)
- [Active Model の基礎 - Railsガイド](https://railsguides.jp/active_model_basics.html#dirty%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB)
- [Active Record コールバック - Railsガイド](https://railsguides.jp/active_record_callbacks.html)