バージョン情報
$ ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]
$ bin/rails -v
Rails 7.0.3
内容
ActiveRecord オブジェクトの属性 (attributes) を引き継いだ別のオブジェクトを作成し、データベースに保存したい場合に ActiveRecord::Core#dup を使用することがある。この場合、たしかに属性は引き継がれるが、同時に Object#dup の振る舞いによりインスタンス変数の値も引き継ぐ。
https://docs.ruby-lang.org/en/3.1/Object.html#method-i-dup より
Produces a shallow copy of obj—the instance variables of obj are copied, but not the objects they reference.
そのため、例えば以下のようにインスタンス変数を使って特定の値をメモ化している場合、コピー先にもその値を意図せず引き継ぐ場合があるので注意が必要だ。
app/models/monster.rb
class Monster < ApplicationRecord
def monster_itself
@monster_itself ||= self
end
end
monster = Monster.find_by!(name: 'リオレウス')
#=> #<Monster:0x00000001098be980 id: 1, name: "リオレウス", category: "飛竜種", habitat: "森丘", created_at: ...>
monster.monster_itself.name
#=> "リオレウス"
subsp_monster = monster.dup
#=> #<Monster:0x000000010b0af508 id: nil, name: "リオレウス", category: "飛竜種", habitat: "森丘", created_at: nil, updated_at: nil>
subsp_monster.assign_attributes(name: 'リオレウス亜種')
subsp_monster.name
#=> "リオレウス亜種"
subsp_monster.monster_itself.name # あれ? 🤔
#=> "リオレウス"
# subsp_monster.monster_itself が自分自身ではなく、コピー元のオブジェクトを返す 🤔
subsp_monster.monster_itself == monster
#=> true
subsp_monster.monster_itself == subsp_monster
#=> false
これを防ぐには、dup メソッドを使用せずにコピー元のオブジェクトの属性を使用して新しいオブジェクトを作成するようにする。
monster = Monster.find_by!(name: 'リオレウス')
monster.monster_itself.name
#=> "リオレウス"
attributes = monster.attributes.symbolize_keys.except(:id, :created_at, :updated_at)
#=> {:name=>"リオレウス", :category=>"飛竜種", :habitat=>"森丘"}
subsp_monster = Monster.new(attributes.merge(name: 'リオレウス亜種'))
#=> #<Monster:0x00000001073bb408 id: nil, name: "リオレウス亜種", category: "飛竜種", habitat: "森丘", created_at: nil, updated_at: nil>
subsp_monster.name
#=> "リオレウス亜種"
subsp_monster.monster_itself.name
#=> "リオレウス亜種"
# subsp_monster.monster_itself が自分自身を返すようになった 😉
subsp_monster.monster_itself == monster
#=> false
subsp_monster.monster_itself == subsp_monster
#=> true
参考
- Object#dup
-
ActiveRecord::Core#dup
-
ActiveRecord::Core#initialize_dup の実装
- initialize_dup メソッドは dup メソッドの内部で呼び出されるフックメソッド。dup の振る舞いを独自に定義したい場合にオーバーライドする目的で定義されている。
-
ActiveRecord::Core#initialize_dup の実装