komatsu(@nomnel)です。
小ネタですがタイトルの事象でハマってしまったので記事にします。
どういうこと?
下のコードで説明します。
irb(main):001:0> foo = Foo.first => #<Foo id: 1, name: "foo", created_at: "2018-08-31 06:25:39", updated_at: "2018-08-31 06:25:39"> irb(main):002:0> foo.name = "#{foo.name}_bar" => "foo_bar" irb(main):003:0> foo.instance_variable_get(:@bar) => nil irb(main):004:0> foo.instance_variable_set(:@bar, 1) => 1 irb(main):005:0> foo.instance_variable_get(:@bar) => 1 irb(main):006:0> foo.reload => #<Foo id: 1, name: "foo", created_at: "2018-08-31 06:25:39", updated_at: "2018-08-31 06:25:39"> irb(main):007:0> foo.name => "foo" irb(main):008:0> foo.instance_variable_get(:@bar) => 1
irb(main):002 で 変更した foo.name
は foo.reload
で変更が無かったことになるのに、 irb(main):004 でセットしたインスタンス変数 @bar
は foo.reload
後もそのままの値を持ち続けるので注意しましょう…ということです。
今回は既存コードを下のようにインスタンス変数でメモ化して高速化を図ったところ、意図しないところでテストが落ちてしまいました。*1
# 変更前 def bar # 重い処理 end # 変更後 def bar @bar ||= # 重い処理 end
(結局 foo.reload
ではなく foo = foo.class.find(foo.id)
のように再取得して上書きました)
実装を追う
同じようなハマりを繰り返さないように ActiveRecord::Base#reload の実装を確認しておきます。
新規に Rails 5.2.1 のアプリケーションを用意し、
# app/models/foo.rb class Foo < ApplicationRecord def reload byebug super end end
byebug でステップ実行しながら実装を追いました。
def reload(*) # :nodoc: clear_aggregation_cache super end
https://github.com/rails/rails/blob/v5.2.1/activerecord/lib/active_record/aggregations.rb#L13
def reload(options = nil) @marked_for_destruction = false @destroyed_by_association = nil super end
def reload(*) # :nodoc: clear_association_cache super end
https://github.com/rails/rails/blob/v5.2.1/activerecord/lib/active_record/associations.rb#L253
# <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new @mutations_from_database = nil end end
def reload(options = nil) self.class.connection.clear_query_cache fresh_object = if options && options[:lock] self.class.unscoped { self.class.lock(options[:lock]).find(id) } else self.class.unscoped { self.class.find(id) } end @attributes = fresh_object.instance_variable_get("@attributes") @new_record = false self end
https://github.com/rails/rails/blob/v5.2.1/activerecord/lib/active_record/persistence.rb#L601
(ちなみに ActiveRecord::Persistence は、ここで ActiveRecord::Base に include
されています)
というわけで、ActiveRecord::Base#reload では
- キャッシュを破棄したり各種のインスタンス変数を初期化(っぽい)したりしつつ、
@attributes
を置き換えている - メモ化用のものなど、自前(?)でセットしたインスタンス変数達は変更されない
ということがわかりました。
共有は以上です。
*1:本来は別インスタンスが行う処理を1つのインスタンスを使い回して行っていた