LoginSignup
18
10

More than 5 years have passed since last update.

Railsの楽観的ロックで発行されるSQL

Last updated at Posted at 2017-06-06

はじめに

環境: 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
18
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
10