トップ画像イメージ

楽観的ロックとは?

こんにちは。 今回は、Railsでの楽観的ロックの実装に当たるTIPSについて、まとめておきます。

そもそも、楽観的ロックって何?データベースの共有ロックや、排他ロックと同じなの?

というところなのですが、今回で言う楽観的ロックとは

複数人が同じ画面で、更新・変更処理を行うことを想定した時に、どっちかが先に更新処理をかけたせいでデータの状態に不整合がおきないように制御しよう!

とか言う考え方のつもりでまとめていきます。

例えば、次のようなusersテーブルというテーブルがあったとしましょう。

users.id users.name users.kana_name contents.id contents.user_id contents.meigen
1 飯塚 イイヅカ 1 1 蓄積に操られたモンスターなんだよー!
2 豊本 トヨモト 1 2 お尻舐めたげる
3 角田 カクタ 1 3 若者の逆ギレが怖ぇ〜!

このテーブルのレコードを編集するために、users/edit画面があるとして、

Aさんが先に飯塚さんの情報を変更したとしましょう。

Aさん:飯塚と書いて、「コントオバケ」という読みにしよう

users.id users.name users.kana_name contents.id contents.user_id contents.meigen
1 飯塚 コントオバケ 1 1 蓄積に操られたモンスターなんだよー!

ところがAさんがeditの画面を開くのと同時にBさんが同じ画面を開いて、情報の修正をしようとしていました

Bさん:さて、飯塚さんの名言を変えとこうっと!

users.id users.name users.kana_name contents.id contents.user_id contents.meigen
1 飯塚 イイヅカ 1 1 ピント来いよ!

そう! Aさんが画面を開くのと同時に、Bさんも同じ画面を開いていたので、どちらの変更もこの時点ではDBに登録されていないのです

これを二人同時に保存ボタンを押して、たまたまAさんの変更がなされた直後に、Bさんの変更が適用されたとします。すると、レコード的には以下のような状態になります。

users.id users.name users.kana_name contents.id contents.user_id contents.meigen
1 飯塚 イイヅカ 1 1 ピント来いよ!

するとAさん的には

Aさん:「あれ?俺の変更が適用されていない!読み仮名変えたはずなのに!!」

ということになってしまうので、DBの状態に不整合が起きてしまいます。

それを防止するために、楽観ロック・悲観ロックという手法が使われるようです。

言葉については、こちらのサイトをご参照ください。

排他制御(楽観ロック・悲観ロック)の基礎

要するに楽観ロックっていうのは、滅多なことではほぼ同時に編集なんてされないから、もしそれが起こった時に、不整合が起きるのを防いであげよう!

という考え方になるのかと思います。

Railsの実装方法

上のサイトで紹介されていたように、楽観ロックを実装するために、各レコードにバージョン値を持たせてやります。

これでレコードの現在の更新状況を判断するのですが、具体的には、

Aさんが画面を開いた時点 ⇨ 飯塚さんのレコードのバージョン0(一回も更新されたことがない)

Bさんが画面を開いた時点 ⇨ 飯塚さんのレコードのバージョン0(一回も更新されたことがない)

という状況だとします。

ここで二人が同時に保存ボタンを押した時に、たまたまAさんの変更が先になされたとしましょう。 すると、レコードのバージョンを1に上げてしまいます。

そうすることにより、

Bさんが画面を開いた時点 ⇨ バージョン0(一回も更新されたことがない)

Bさんの更新処理(トランザクション)を始めた時 ⇨ バージョン1(一回更新されている)

という状況になるため、

「ごめん!Bさん!Aさんが先に変更をしたみたいで、データの整合性が取れなくなるから、もう1回データを最新の状態にして、やり直して!てへぺろ⭐️」

という安全措置を取ることができるようになります。

lock_version

紹介したような、テーブルに持たせるバージョン値について、railsではlock_versionというカラムを持たせてやることで、上手いことやってくれます。

migrationファイルで次のように、lock_versionというカラムを追加します。

class AddLockVersionToUser < ActiveRecord::Migration
  def change
    add_column :users, :lock_version, :integer, default: 0, null: false
  end
end

このlock_versionというカラムを追加することで、Railsでレコードの更新が行われると、

lock_versionの値を1づつ増やしてくれ、競合した更新処理が実行されそうになると、ActiveRecord::StaleObjectErrorという例外を発生させて、競合を防いでくれます。

つまり、

「ごめん!Bさん!Aさんが先に変更をしたみたいで、データの整合性が取れなくなるから、もう1回データを最新の状態にして、やり直して!てへぺろ⭐️」

を実行してくれるという訳です。(実際は、ちゃんとエラーメッセージを返して、ユーザーに教えてあげる必要はあります。)

Railsでlock_versionの値をみて、楽観ロックをしてもらうためには、画面を開いた時に持っていたlock_versionの値を飛ばしてあげるだけです。

formを使ったsubmitであれば、こんな感じ。

<%= f.hidden_field :lock_version ,id:"members_lock_version"   %>

また、ajaxとかでもlock_versionというキー名で、値を飛ばしてあげれば実装できます。

なお、updateやsaveメソッドが走っても、そのレコードに変更するべき属性がなければ、lock_versionの値は上がりません。(update文が走らない)

ですので、ユーザーが何も変更しないで、更新のボタンを押したところで、無闇矢鱈にlock_versionの値はあげないし、無駄にSQLも発行されないです。

逆に何も変更がなくても、lock_versionやupdated_atを更新したい時

まぁ、無駄にSQLを発行させないのは良いのですが、逆に何も変更がなくても、テーブルのupdated_atlock_versionだけを更新して欲しい時があります。

そのテーブルのレコード自体は何の変更もされていないけど、リレーション的にはそのテーブルの子となるようなテーブルのレコードが更新されたら、親テーブルも更新がされたとして処理をしたい場合とかですね

例えば、 売上管理テーブル 1 - * 売り上げ明細テーブル などのように、売り上げ管理テーブルでは、売り上げ日の変更などはあっていないが、実際にどんな商品が何個売れたか、変更が行われた場合、売上管理テーブル側でも更新があったものとして処理を行いたい時などですね。

そんな時にはtouchメソッドを利用します。

user = User.find(1)
user.touch

これを実行することで、usersテーブルのid1について、updated_atやlock_versionを更新してくれて、レコードの属性的には他に何の変更がなくても、一度更新がされたものとして処理を行ってくれることになります。

まとめ

  • Railsで楽観ロックはlock_versionを使えば、簡単に実装できます
  • とはいえやっていることは結構単純で、ただ単にレコードのバージョン値を見てやっているだけ
    • 悲観ロックであれば、同じ画面に複数ユーザーが利用しようとした瞬間、画面を使えないように制御してやる感じかな?