同時に同一のレコードにアクセスして編集しがちなテーブルにはlock_versionが使えるかも


はじめに

Rails Guide読んでて気になったので実際に触ってみました。そのメモです。

アプリにてAさんとBさんが同じレコードを編集する際、全く同じタイミングで同じレコードにアクセスした場合、Aさんがレコードをupdateしてその後にBさんがupdateするとBさんはAさんの編集内容を確認せずに上書きしてしまいます。
これが同じ属性のupdateならまだしも別の属性である場合は編集が巻き戻されるので良くない状況です。

この問題に対しActive Recordにlock_versionが用意されています。
lock_versionはテーブルにそのインスタンスの編集履歴カウントの役割をするカラムを追加し、その値を参照することで
lock_versionを利用すると上記の様なケースに置いてエラーを発生させることができます。

使用方法

利用するにはテーブルにlock_versionというカラムを作成すればいい。

t.integer :lock_version, default: 0

動作確認

p1 = Memo.find(1)
p2 = Memo.find(1)
# => #<Memo id: 1, text: "hello", lock_version: 0>

当然ですがまだ呼び出した時点ではlock_versionは0
次にp1をupdateします。

p1.update(text:'good bye')
# => #<Memo id: 1, text: "good bye", lock_version: 1>
SQL
   (0.3ms)  BEGIN
  Memo Update (13.4ms)  UPDATE `memos` SET `text` = 'good bye', `lock_version` = 1 WHERE `memos`.`id` = 1 AND `memos`.`lock_version` = 0
   (3.9ms)  COMMIT

look_versionが1に書き換わりました。
またupdateのSQLの条件にlock_version = 0があることも確認できます。
次はp2をupdateしてみましょう。reloadしていないのでp2のインスタンス のlock_versionは0のままです。

p2.update(text:'say hello')
# => ActiveRecord::StaleObjectError (Attempted to update a stale object: Memo.)
SQL
   (0.7ms)  BEGIN
  Memo Update (1.0ms)  UPDATE `memos` SET `text` = 'say hello', `lock_version` = 1 WHERE `memos`.`id` = 1 AND `memos`.`lock_version` = 0
   (2.4ms)  ROLLBACK

ちゃんとエラーでていますね。
SQL自体はエラー実行しても出ないと思うのでrails側で影響与えた行がないことをトリガーとしてエラーを返している感じかな?
今回ソースまで読まないので読んだ方は共有していただけると助かります!

動作確認(Web)

RailsApi通り、formにhiddenパラメータとしてlock_versionを入れましょう。
これがないとバックエンドでlock_versionの比較ができないのでエラーを発生しません。

<input type="hidden" value="2" name="memo[lock_version]" id="memo_lock_version">

上記を追記し、同じレコードを編集するタブを2つ開いてそれぞれupdateすると後からupdateした方に無事エラーが出ました。

あとはこれまたRailsApiにある通りエラーハンドリングして対応しましょう。
対応の仕方としては色々考えられますね。

  • 2人が別々の属性を編集したのであればそのまま順番にupdateして同一の属性を編集した時は画面に表示してユーザーに確認
  • 自分よりも前の人がupdateした他の属性には影響が及ばない様にした上でupdateさせる

とか。この辺はデータがアプリ上でどの様に使用されているかによりけりと言ったところでしょうか。

最後に

僕は実務で利用したことないので「ウチではこうやってるよ!」的な共有がいただけると嬉しいです!

参考記事

Rails Guide
What is Optimistic Locking