ActiveRecordのdestroyメソッドの挙動について調べてみた

本日のよちよち.rbでdestroyメソッドの挙動が気になったので調べてみました。

問題提起

destroyは、一度DBに作成したデータを削除してくれるメソッドです。
例えば(Railsチュートリアル6.1.1引用)

1
$ rails generate model User name:string email:string

で、Userモデルを作ったとします。
で、migration upをした後にrails consoleとかでいろいろ触ってみました。

で、Railsチュートリアルには

destroyはcreateの逆です。

と書いてあります。
また、

奇妙なことに、destroyはcreateと同じようにそのオブジェクト自身を返しますが、その返り値を使用しても、もう一度destroyを呼ぶことはできません。そして、おそらくさらに奇妙なことに、destroyされたオブジェクトは以下のようにまだメモリ上に残っています。

と書かれています。

じゃあ、一度destroyしたオブジェクトを再度saveすればDBに保存されるんじゃね?

結果

1
2
3
4
5
6
7
8
irb(main):001:0> foo = User.create(name: "Foo", email: "foo@bar.com")
(0.0ms) SAVEPOINT active_record_1
SQL (0.3ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2015-03-30 16:19:45.964740"], ["email", "foo@bar.com"], ["name", "Foo"], ["updated_at", "2015-03-30 16:19:45.964740"]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Foo", email: "foo@bar.com", created_at: "2015-03-30 16:19:45", updated_at: "2015-03-30 16:19:45">
irb(main):002:0> User.all
User Load (0.2ms) SELECT "users".* FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1, name: "Foo", email: "foo@bar.com", created_at: "2015-03-30 16:19:45", updated_at: "2015-03-30 16:19:45">]>

Userクラスのレコードを作って。。。

1
2
3
4
5
6
7
8
irb(main):003:0> foo.destroy
(0.1ms) SAVEPOINT active_record_1
SQL (1.1ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Foo", email: "foo@bar.com", created_at: "2015-03-30 16:19:45", updated_at: "2015-03-30 16:19:45">
irb(main):004:0> User.all
User Load (0.1ms) SELECT "users".* FROM "users"
=> #<ActiveRecord::Relation []>

消えました。
ただし、fooにはまだオブジェクトが残っているんですよね。

1
2
irb(main):005:0> foo
=> #<User id: 1, name: "Foo", email: "foo@bar.com", created_at: "2015-03-30 16:19:45", updated_at: "2015-03-30 16:19:45">

じゃあこいつをもう一度saveするとどうなる?

1
2
3
4
irb(main):006:0> foo.save
(0.1ms) SAVEPOINT active_record_1
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true

trueは返ってきたが、SQLが発行されてないですね。。。

1
2
3
irb(main):007:0> User.all
User Load (0.2ms) SELECT "users".* FROM "users"
=> #<ActiveRecord::Relation []>

DBにも保存されていないようです。

Railsのコードを読んでみた

ググり力がなく、Google先生に聞いてもわからなかったので、本家のソースコードを見ることにしました。

GithubがDOS攻撃にあっている中、あのデカいリポジトリをForkしてローカルにcloneしました。

destroyは、ActiveRecordクラスのメソッドのはずなので(modelのクラスでActiveRecord::Baseを継承しているから)
ActiveRecord::Baseをまず見ました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
require 'yaml'
require 'set'
require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/class/subclasses'
require 'arel'
require 'active_record/attribute_decorators'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
require 'active_record/relation/delegation'
require 'active_record/attributes'
require 'active_record/type_caster'
module ActiveRecord #:nodoc:
...

いっぱいrequireしてる下に。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
...
class Base
extend ActiveModel::Naming
extend ActiveSupport::Benchmarkable
extend ActiveSupport::DescendantsTracker
extend ConnectionHandling
extend QueryCache::ClassMethods
extend Querying
extend Translation
extend DynamicMatchers
extend Explain
extend Enum
extend Delegation::DelegateCache
include Core
include Persistence
include ReadonlyAttributes
include ModelSchema
include Inheritance
include Scoping
include Sanitization
include AttributeAssignment
include ActiveModel::Conversion
include Integration
include Validations
include CounterCache
include Attributes
include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
include Callbacks
include Timestamp
include Associations
include ActiveModel::SecurePassword
include AutosaveAssociation
include NestedAttributes
include Aggregations
include Transactions
include NoTouching
include Reflection
include Serialization
include Store
include SecureToken
include Suppressor
end
ActiveSupport.run_load_hooks(:active_record, Base)
end

Baseクラスがいました。
この中から当たりをつけて探していきます。。。

面倒くさかったので、$ git grep 'def destroy'で検索をかけてみると、
activerecord/lib/active_record/persistence.rb
にありそうですね。

destroyの挙動はこうなっていました!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Deletes the record in the database and freezes this instance to reflect
# that no changes should be made (since they can't be persisted).
#
# There's a series of callbacks associated with #destroy. If the
# <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled
# and #destroy returns +false+.
# See ActiveRecord::Callbacks for further details.
def destroy
raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
destroy_associations
self.class.connection.add_transaction_record(self)
destroy_row if persisted?
@destroyed = true
freeze
end

コメントアウトに、レコードを削除するのと、再度作って変更できないようにインスタンスをfreezeするって書いてますね。
ちょっと上のdeleteメソッドも同様みたいです。
(夜も遅いので細かい挙動は終えてないですが、後でメモ書くかもです)

結論

一度destroyされたオブジェクトはfreezeしちゃうんですね。
なので、さっきのfoo変数に対して、frozen?と呼ぶと

1
2
irb(main):008:0> foo.frozen?
=> true

trueが返ります。
ただし、全く同じ内容でも新しいオブジェクトで上書きすると再度使えるようになります。

1
2
3
4
5
6
7
irb(main):009:0> foo = User.create(name: "Foo", email: "foo@bar.com")
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2015-03-30 16:29:33.786538"], ["email", "foo@bar.com"], ["name", "Foo"], ["updated_at", "2015-03-30 16:29:33.786538"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 2, name: "Foo", email: "foo@bar.com", created_at: "2015-03-30 16:29:33", updated_at: "2015-03-30 16:29:33">
irb(main):010:0> foo.frozen?
=> false

なるほど!
という感じでした。