環境
- Rails 7.0.0
- Ruby 2.7.5
- MySQL 5.7
楽観的ロック
lock_version
というカラムを用意するだけで利用できます。
$ rails g model user name:string lock_version:integer
$ bundle e rails b:migrate
$ bundle e rails c
irb(main):001:0> User.create!(name: 'Taro')
(0.9ms) SELECT sqlite_version(*)
TRANSACTION (0.1ms) begin transaction
User Create (0.5ms) INSERT INTO "users" ("name", "lock_version", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Taro"], ["lock_version", 0], ["created_at", "2021-12-20 23:14:45.459194"], ["updated_at", "2021-12-20 23:14:45.459194"]]
TRANSACTION (3.7ms) commit transaction
=> #<User:0x00007fe7449a47b8 id: 1, name: "Taro", lock_version: 0, created_at: Mon, 20 Dec 2021 23:14:45.459194000 UTC +00:00, updated_at: Mon, 20 Dec 2021 23:14:45.459194000 UTC +00:00>
lock_version
カラムが自動で更新されています。
悲観的ロック
$ rails g model user name:string
$ bundle e rails b:migrate
$ bundle e rails c
lock
発行するクエリに FOR UPDATE
句をつけてくれます。
irb(main):001:0> User.create!(name: 'Taro')
irb(main):002:0> User.lock.find(1)
User Load (0.9ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 FOR UPDATE
引数にデータベース固有のクエリを与えることもできます。
irb(main):003:0> User.lock('LOCK IN SHARE MODE').find(1)
User Load (0.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 LOCK IN SHARE MODE
lock!
id
カラムで行ロックをかけます。
irb(main):004:0> user = User.first
irb(main):005:0> user.lock!
User Load (0.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 FOR UPDATE
with_lock
トランザクションを開始してロックをかけます。
irb(main):006:1* user.with_lock do
irb(main):007:1* user.name = 'Jiro'
irb(main):008:1* user.save!
irb(main):009:0> end
TRANSACTION (0.6ms) BEGIN
User Load (0.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 FOR UPDATE
User Update (0.9ms) UPDATE `users` SET `users`.`name` = 'Jiro', `users`.`updated_at` = '2021-12-21 00:30:04.024824' WHERE `users`.`id` = 1
TRANSACTION (1.5ms) COMMIT
参考