...

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

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

どうもおはようポテト(@ohayoupoteto22)です。

Railsで排他制御ってどうやるん?

ということで今回は

「Railsの排他制御についてざっくり知りたい」

「楽観ロックってなんぞや」

という方に向けてRailsで「lock_versionを使って排他制御(楽観ロック)を行う方法」をまとめました。

初学者の備忘録ゆえ至らない点もあると思いますが参考になれば幸いです⸝⸝- ̫ -⸝⸝

ブログ主

早速いってみよう!

 

そもそも楽観ロックって?

競合とはなんぞや」という方に向けて、図にするとこんな感じ。

ユーザー二人が同じタイミングで商品の在庫を取得する

右の人が2つ購入して商品の在庫は2つに更新。

左の人が1つ購入して商品の在庫は3つに… ファッ !?

ってな具合です。

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

私もりんご食べたい…

ソロモン

それを防ぐのが排他処理であり、今回利用するのはそのうちの一つである「楽観ロック」。

データの参照時には何もせず、更新時に競合しているかチェックしてくれるものです。

ブログ主

実際に体験した方が早いよね

 

手順

そこまで難しいものではありません。

手順
step1
排他制御を行うモデルを作成
step2
lock_versionカラムを定義
step3
lock_versionの値を受け渡しできるようにする
レッツゴー!

ソロモン

 

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

下準備

$ rails new opti_test

まずアプリを作成します。今回は「opti_test」という名前で。

$ cd opti_test

アプリのディレクトリに移動。

 

$ rails generate scaffold food

便利なScaffolding機能を使ってサクッと簡易なアプリを作ります。

テーブル名は「foods」で。料理を取り扱うモデルです。

参考 rails g scaffoldとは?Qiita

 

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

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

今回は料理の名前と値段用のカラム、nameとpriceを定義。さらに今回の楽観ロックの肝となるlock_virsionも定義します。

lock_virsionとは

行のバージョンを管理するためのレコード

更新時に値が異なっていた(他のユーザーによる更新と競合した)場合にエラーを発生してくれる

簡単にまとめるとこんな感じ。

 

$ rails db:migrate

マイグレーションを実行してあげます。

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

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

 

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

そしてレコードが一つも無いと何もできないので、適当に一つ作ってあげます。

lock_versionはデフォルトで0だから指定しなくていいんだね

ソロモン

 

ビューをいじる

<%= 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に上のように追記してあげます。

nameとpriceに対しては普通にフィールドを設置してあげて、lock_versionはhidden_fieldにして値をコントローラーに受け渡せるようにします。

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

更にfoods_controller.rbのfood_paramsメソッドに上のように記述してあげれば、パラメーターとして受け渡しができるはずです。

 

例外処理

def update
  begin
    (省略)
    rescue ActiveRecord::StaleObjectError
    render plain:"競合しちゃってるよ"
  end
end

次にupdateメソッド内に例外処理を記述します。

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

Railsのrenderメソッドで出来ることまとめ【Rails】renderメソッドで出来ることまとめ

 

試してみる

実際に試してみましょう。

まずlocalhost:3000/foodsにアクセス。

すると料理一覧のページで、先ほど追加した唐揚げが(見えないけど)あるので「Edit」をクリック。

 

これを2つのタブで同時に行ってあげて同じ編集画面を開きます

ブログ主

競合させなきゃいけないからね

 

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

エラーメッセージが表示されればおけ!

これで終了です。

お疲れ様!

ソロモン

 

(ちなみに素のエラー画面はこんな感じ。)

 

以上となります。

参考になれば幸いです!では⸝⸝- ̫ -⸝⸝

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA