25
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-12

この記事は何?

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

25
16
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
25
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?