【Rails】lock_versionを使って排他制御(楽観的ロック)を行う方法

楽観的ロックとは?

複数のユーザーが同じレコードを編集する際に、後のリクエストに対してエラーを投げるロックのこと。

図解してみるとこのような流れです。

楽観ロックの流れ

  1. ユーザー2人が同じタイミングで商品の在庫(4つ)を取得する
  2. 右のユーザーが2つ購入して商品の在庫は2つに更新される
  3. 左の人が1つ購入し商品の在庫を1つ減らして3つに更新しようとするが、このタイミングでエラーになる

右の人が購入し終わってから左の人が商品の在庫数を取得すればこんなことにはならないのですが、同じタイミングで在庫数を取得しているためこのように矛盾が生じてしまいます。

それを防ぐのが排他処理で、今回利用するのはその一種の「楽観的ロック」。データの参照時には何もせず、更新時に競合しているかチェックしてくれるものです。

Railsで排他制御(楽観的ロック)を行う方法

1. Railsアプリを作成

Terminal window
$ rails new opti_test
$ cd opti_test
$ rails generate scaffold food

Scaffolding機能を使い、サクッと簡易的なアプリを作ります。

アプリ名は適当にopti_test。テーブル名はfoods、料理を取り扱うモデルです。

参考:rails generate scaffold | Railsドキュメント

2. lock_versionカラムを定義

class CreateFoods < ActiveRecord::Migration[5.2]
def change
create_table :foods do |t|
t.string :name # 料理名
t.integer :price # 値段
t.integer :lock_version, default: 0
t.timestamps
end
end
end

マイグレーションファイルにカラムを定義していきます。

今回楽観的ロックを行うのに必要なのがlock_version。これは行のバージョンを管理するためのレコードで、更新時に値が異なっていた(他のユーザーによる更新と競合した)場合にエラーを投げます。

3. マイグレーション実行

Terminal window
$ rails db:migrate

マイグレーションを実行。

Terminal window
$ rails c
irb(main):001:0> Food.column_names
=> ["id", "name", "price", "lock_version", "created_at", "updated_at"]

コンソールモードでカラムを確認してあげて、ちゃんと登録されてればOK。

4. レコード作成

Terminal window
irb(main):001:0> Food.create(name: "竜田揚げ", price: 700)

サンプルのレコードを1件作成しておきます。(lock_versionにはデフォルト値が入るので指定は不要)

5. ビューを編集

<%= form_with(model: food, local: true) do |form| %>
(省略)
<%= form.text_field :name%>
<%= form.number_field :price%>
<%= form.hidden_field :lock_version%>
<%= form.submit("値段を修正する") %>
<%end%>

_form.html.erbに上のように追記します。

namepriceに対しては通常通りフィールドを設置し、lock_versionhidden_fieldにして値をコントローラーに受け渡せるようにします。

6. コントローラー実装

def food_params
params.require(:food).permit(:name, :price, :lock_version)
end

foods_controller.rbfood_paramsメソッドを上のようにすることで、パラメーターとして受け渡しができるようになります。

def update
begin
(省略)
rescue ActiveRecord::StaleObjectError
render plain: "競合しています。"
end
end

updateメソッド内に例外処理を追加。

ActiveRecord::StaleObjectErrorはまさしく競合が起きた時のエラーで、rescue文でその際にエラー文を表示します。

動作確認

動作確認のためにローカル環境にアクセスする

実際に試してみるためにまずlocalhost:3000/foodsにアクセスし、「Edit」をクリックします。

編集画面が表示されるので、これを2つのウィンドウで開く

すると編集画面が表示されるので、これを2つのウィンドウで開きます。

そして片方を適当に編集してあげて、もう片方でも編集してあげようとすると⋯

競合の旨を伝えるエラーメッセージが表示された

競合の旨を伝えるエラーメッセージが表示されました 🎉