ActiveRecord の dup の挙動

ひとことで言うと、 ActiveRecord には dirtry な attribute を追跡するための便利なメソッド群が定義されていますが、dup とともにこれらを扱うときには要注意ですよ、というお話です。

_was とか _changed? は便利

dirty な attribute を追跡するってのはどういうことかっていうと、たとえばこういうことです。

# DBからuserをひとり引いてくる
user = User.first
user.name # => "shinpei"
user.name_was # => "shinpei"
user.name_changed? # => false

# name atribute を書き換える
user.name = "nekogata"
user.name # => "nekogata"
user.name_was # => "shinpei"
user.name_changed? # => true

# DBに保存する
user.save!
user.name # => "nekogata"
user.name_was # => "nekogata"
user.name_changed? # => false

要するに「書き変わってるんだけど、未だにDBに保存されていないattributeはどれですか、書き変わる前の値はなんですか」みたいなことを記録してくれてるんですね。オー、便利。

dup すると面白い挙動する

# DBからuserをひとり引いてくる
user = User.first
duped = user.dup

user.name # => "shinpei"
user.name_was # => "shinpei"
user.name_changed? # => false

duped.name #=>"shinpei"
deped.name_was # => ""
deuped.name_changed? # => true

おおお?という感じの挙動ですね。ちょっとバグっぽい感じもする。しかしこれは意図された挙動です。それは ActiveRecord のテストからも確認できます。

rails/dup_test.rb at master · rails/rails · GitHub

test_dup_not_persistedtest_dup_has_no_id、きわめつけに test_dup_with_changes を見てみましょう。

このあたりを読むと、ActiveRecorddup は以下のような使われた方をすることを想定したメソッドであることが見て取れます。

# DBからユーザーを引く
user = User.first 

# (pk以外は) 同じ値を持ったユーザーをDBに保存
user.dup.save!

なるほど? つまり、ActiveRecorddup は、「オブジェクトのコピー」を作るメソッドではなくて、「レコードのコピー」を作るメソッドとして意図されているわけですね。

オブジェクトのコピーが欲しいなら clone を使おう

よけいなことしないでくれ!俺は単にオブジェクトのコピーが欲しいんや!というときにはどうすればいいかというと、clone メソッドを使ってあげましょう。こうすれば普通の挙動をします。

# DBからuserをひとり引いてくる
user = User.first
cloned = user.clone

user.name # => "shinpei"
user.name_was # => "shinpei"
user.name_changed? # => false

cloned.name #=>"shinpei"
cloned.name_was # => "shinpei"
cloned.name_changed? # => false

API docも読もう

dup (ActiveRecord::Core) - APIdock

clone (ActiveRecord::Core) - APIdock