Help us understand the problem. What is going on with this article?

ActiveRecord 5 の dirty attribute methods (*_changed? とか)の挙動一覧

More than 1 year has passed since last update.

この記事は何?

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

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)

使用したスクリプト

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を参考にしました。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away