ひとことで言うと、 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_persisted
や test_dup_has_no_id
、きわめつけに test_dup_with_changes
を見てみましょう。
このあたりを読むと、ActiveRecord の dup は以下のような使われた方をすることを想定したメソッドであることが見て取れます。
# DBからユーザーを引く user = User.first # (pk以外は) 同じ値を持ったユーザーをDBに保存 user.dup.save!
なるほど? つまり、ActiveRecord の dup は、「オブジェクトのコピー」を作るメソッドではなくて、「レコードのコピー」を作るメソッドとして意図されているわけですね。
オブジェクトのコピーが欲しいなら 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