LoginSignup
31
20

More than 3 years have passed since last update.

ActiveRecord6 の dirty attribute methods (will_save_change_to_* とか)の挙動一覧

Last updated at Posted at 2020-04-24

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

  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

  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

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

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

# 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

参考

31
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
20