Rails 2.3.2でロックの挙動が変わっている

 Ruby on Rails 2.3.2がリリースされたので、さっそくインストールしました。自分の作っているアプリケーションでテストを実行してみると、2.3.1のときに出ていなかった"ActiveRecord::StaleObjectError: Attempted to delete a stale object"というエラーが出てきました。この ActiveRecord::StaleObjectError は lock_version を使ったロック機能によるものですが、いつもは更新時に起きるのに、今回はどうもレコードの削除時に起きているようです。また、エラーを出しているのはいずれもこのロックに加えて、関連先のモデルに対して counter_cache と":dependent => :destroy"による連鎖削除を指定しているモデルのようです。

 原因を調べるために、新しく以下のふたつのモデルを作りました。なお、環境は Mac OS X 10.5.6 と Ruby 1.8.7, データベースには SQLite を使っています。

  • app/models/user.rb
class User < ActiveRecord::Base
  has_many :blogs, :dependent => :destroy
end
  • app/models/blog.rb
class Blog < ActiveRecord::Base
  belongs_to :user, :counter_cache => true
end

 migration は以下のような定義です。

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :name
      t.integer :lock_version, :default => 0
      t.integer :blogs_count, :default => 0

      t.timestamps
    end
  end

  def self.down
    drop_table :users
  end
end

class CreateBlogs < ActiveRecord::Migration
  def self.up
    create_table :blogs do |t|
      t.references :user
      t.string :title

      t.timestamps
    end
  end

  def self.down
    drop_table :blogs
  end
end

 migration を実行した後、console でレコードを操作してみます。2.3.2だとこうなります。

mac:blog nabeta$ script/console
Loading development environment (Rails 2.3.2)
>> u = User.create(:name => 'nabeta') # Userを作成する
=> #<User id: 1, name: "nabeta", lock_version: 0, blogs_count: 0, created_at: "2009-03-16 15:09:40", updated_at: "2009-03-16 15:09:40">
>> u.blogs
=> []
>> u.blogs.create(:title => 'my blog') # Userに関連づけられたBlogを作成する
=> #<Blog id: 1, user_id: 1, title: "my blog", created_at: "2009-03-16 15:10:00", updated_at: "2009-03-16 15:10:00">
>> u
=> #<User id: 1, name: "nabeta", lock_version: 0, blogs_count: 0, created_at: "2009-03-16 15:09:40", updated_at: "2009-03-16 15:09:40">
>> u.reload
=> #<User id: 1, name: "nabeta", lock_version: 1, blogs_count: 1, created_at: "2009-03-16 15:09:40", updated_at: "2009-03-16 15:09:40">
>> u.destroy # Userを削除しようとする。以下のとおり失敗する
ActiveRecord::StaleObjectError: Attempted to delete a stale object
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/locking/optimistic.rb:127:in `destroy_without_callbacks'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/callbacks.rb:337:in `destroy_without_transactions'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/transactions.rb:229:in `send'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/transactions.rb:229:in `with_transaction_returning_status'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/connection_adapters/abstract/database_statements.rb:136:in `transaction'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/transactions.rb:182:in `transaction'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/transactions.rb:228:in `with_transaction_returning_status'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/transactions.rb:192:in `destroy'
	from (irb):6
>> 

 User を削除しようとすると、問題の ActiveRecord::StaleObjectError が発生します。2.3.1だとこうなります。

mac:blog nabeta$ script/console
Loading development environment (Rails 2.3.1)
>> u = User.create(:name => 'nabeta') # Userを作成する
=> #<User id: 1, name: "nabeta", lock_version: 0, blogs_count: 0, created_at: "2009-03-16 15:21:23", updated_at: "2009-03-16 15:21:23">
>> u.blogs
=> []
>> u.blogs.create(:title => 'my blog') # Userに関連づけられたBlogを作成する
=> #<Blog id: 1, user_id: 1, title: "my blog", created_at: "2009-03-16 15:21:29", updated_at: "2009-03-16 15:21:29">
>> u
=> #<User id: 1, name: "nabeta", lock_version: 0, blogs_count: 0, created_at: "2009-03-16 15:21:23", updated_at: "2009-03-16 15:21:23">
>> u.reload
=> #<User id: 1, name: "nabeta", lock_version: 1, blogs_count: 1, created_at: "2009-03-16 15:21:23", updated_at: "2009-03-16 15:21:23">
>> u.destroy # Userを削除しようとする。以下のとおり成功する
=> #<User id: 1, name: "nabeta", lock_version: 1, blogs_count: 1, created_at: "2009-03-16 15:21:23", updated_at: "2009-03-16 15:21:23">
>> u.blogs
=> [#<Blog id: 1, user_id: 1, title: "my blog", created_at: "2009-03-16 15:21:29", updated_at: "2009-03-16 15:21:29">]
>> u.reload
ActiveRecord::RecordNotFound: Couldn't find User with ID=1
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.1/lib/active_record/base.rb:1595:in `find_one'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.1/lib/active_record/base.rb:1578:in `find_from_ids'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.1/lib/active_record/base.rb:616:in `find'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.1/lib/active_record/base.rb:2695:in `reload_without_dirty'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.1/lib/active_record/dirty.rb:94:in `reload_without_autosave_associations'
	from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.3.1/lib/active_record/autosave_association.rb:191:in `reload'
	from (irb):7
>> Blog.find(:all)
=> []
>> 

 User はエラーを起こさずに削除されています。

 もう少し2.3.2の動作やソースコードを調べてみると、最近 ActiveRecord でロックを扱っている部分に変更があったことがわかりました。また、上記の例では counter_cache と ":dependent => :destroy"による連鎖削除の両方を指定しているときに ActiveRecord::StaleObjectError が発生し、どちらか片方だけでは発生しないこともわかりました。