はじめに
環境: Ruby on Rails 5.1.1
Railsの楽観的ロックで、どういうSQLが発行されるかを検証する。
楽観的ロックについては http://qiita.com/merrill/items/d9d41d64df292bd6432a などを参照。
もしRailsの実装で "saveする直前にselectしてlock_versionを確認し、同じならupdate" となっていたら、このselectとupdateの間に横から更新が入ると問題になる。そうなっていないかどうか念のため確認したかった。
先に結論
インスタンスをupdateするとき、1回のSQL文で行われる。したがって上のような問題は起きない。
where + update_all をした場合は楽観的ロックの対象外になるので注意。
準備
migration
class CreateDogs < ActiveRecord::Migration[5.1]
def change
create_table :dogs do |t|
t.string :name, null: false
t.integer :lock_version, default: 0
t.timestamps
end
end
end
lock_version カラムを用意するだけでよいのがポイント。
model
class Dog < ApplicationRecord
end
そして、rails consoleから試す。
先にデータを準備
dog1 = Dog.create!(name: 'poti') # id: 1
結果
インスタンスに対するupdate
irb(main):010:0> dog1.update(name: 'shige')
(5.2ms) BEGIN
SQL (0.6ms) UPDATE `dogs` SET `dogs`.`name` = 'shige', `dogs`.`updated_at` = '2017-06-06 01:16:27', `dogs`.`lock_version` = 1 WHERE `dogs`.`id` = 1 AND `dogs`.`lock_version` = 0
(2.8ms) COMMIT
=> true
where句にlock_versionを追加し、1クエリで行っている。
次に横から更新してみる
irb(main):011:0> dog11 = Dog.find(1)
irb(main):012:0> dog11.update(name: "yokoyari")
=> true
irb(main):014:0> dog1.update(name: "poti")
(5.7ms) BEGIN
SQL (1.3ms) UPDATE `dogs` SET `dogs`.`name` = 'poti', `dogs`.`updated_at` = '2017-06-06 01:22:40', `dogs`.`lock_version` = 2 WHERE `dogs`.`id` = 1 AND `dogs`.`lock_version` = 1
(0.4ms) ROLLBACK
ActiveRecord::StaleObjectError: Attempted to update a stale object: Dog.
from (irb):14
lock_versionが更新されていたので ActiveRecord::StaleObjectError が出た。
最後に、横からこのdogを削除してみる。
irb(main):015:0> dog1 = Dog.find(1)
irb(main):016:0> dog11 = Dog.find(1)
irb(main):017:0> dog11.delete
irb(main):018:0> dog1.update(name: 'poti')
(3.2ms) BEGIN
SQL (0.8ms) UPDATE `dogs` SET `dogs`.`name` = 'poti', `dogs`.`updated_at` = '2017-06-06 01:26:45', `dogs`.`lock_version` = 3 WHERE `dogs`.`id` = 1 AND `dogs`.`lock_version` = 2
(0.6ms) ROLLBACK
ActiveRecord::StaleObjectError: Attempted to update a stale object: Dog.
from (irb):18
同じ ActiveRecord::StaleObjectError エラーになる。仕組みを考えると当然。(このクエリの結果からはwhereにひっかからなかったことだけがわかり、消されてるかlock_versionが更新されてるかは分からない)
where.update_all
where + update_allの場合は、lock_versionが更新されない。
したがって、横からwhere.update_allで更新されたレコードに対してupdateをかけると ActiveRecord::StaleObjectError が発生しない(正常にupdateされてしまう) ということがわかった。注意が必要。
irb(main):024:0> dog3 = Dog.create!(name: 'inu')
irb(main):025:0> Dog.where(name: 'inu').update_all(name: 'neko')
SQL (3.0ms) UPDATE `dogs` SET `dogs`.`name` = 'neko' WHERE `dogs`.`name` = 'inu'
=> 1
irb(main):026:0> dog3.update(name: 'kuma')
(4.7ms) BEGIN
SQL (0.9ms) UPDATE `dogs` SET `dogs`.`name` = 'kuma', `dogs`.`updated_at` = '2017-06-06 01:34:13', `dogs`.`lock_version` = 1 WHERE `dogs`.`id` = 3 AND `dogs`.`lock_version` = 0
(0.7ms) COMMIT
=> true