楽観的ロックの使い方

webアプリケーションでは、二人以上のユーザーが同じデータを編集中という状況が発生する。その場合、特に対策をとらなければ、最後に送信ボタンを押したユーザーのデータで更新されるはず。しかし、これではお互いの意思を確認できないまま、結果として、先に登録したユーザーのデータが、知らないうちに破棄されてしまうことになるので、あまり良くない。そこで、編集する時に何らかの制御をかける。その制御方法に、以下の二つがある。

  • 悲観的ロック(データ編集を開始した時にロック)
    • 既に誰かが編集中であれば、他の人はそのデータの編集ページを開くことが出来ないようにする。
  • 楽観的ロック(データ更新した時にロック)
    • 編集ページは複数ユーザーで同時に開くことが出来るが、途中で誰かが更新した場合は、その後の更新が制限される。

悲観的ロックが安全確実な気がするが、不要なロックが長時間続いてしまう可能性もあり、厳密過ぎるロックは操作性を悪くする。大抵は楽観的ロックで十分運用できるようだ。そして、Railsはcreated_at、updated_atと同じくらい手軽に楽観的ロックをサポートしてくれている。フィールド名に「lock_version」を追加するだけでOK。以下のように試してみた。

作成

  • まずはターミナルからモデルを作成して...
script/generate model lock
  • データベースを以下のように設定して...
    • 大事なことは:default => 0で、デフォルト値を0(ゼロ)に設定しておくこと。
    • これを忘れてNULLが保存された状態では、Railsの楽観的ロックは機能しない。
# db/migrate/001_create_locks.rb

class CreateLocks < ActiveRecord::Migration
  def self.up
    create_table :locks do |t|
      t.column :name, :string
      t.column :lock_version, :integer, :default => 0
    end
  end

  def self.down
    drop_table :locks
  end
end
  • その後、ターミナルで、scaffoldまで実行する。
    • 更新する時に、params[:lock_version]で取得できるパラメーターを送信するようになっていればOK。(例: <% hidden_field :lock, :lock_version %>)
script/generate scaffold lock

実験

  • 適当にデータを1件登録して、ブラウザでウィンドウを二つ開いて、どちらもhttp://localhost:3000/locks/edit/1にアクセスしておく。
  • 片方のウィンドウでEditボタンを押してデータを更新する。
  • もう片方のウィンドウでも同じ操作をしてみる。すると...
ActiveRecord::StaleObjectError in LocksController#update

Attempted to update a stale object

RAILS_ROOT: script/../config/..
  • 上記のようなエラーが発生して、更新できない。


楽観的ロックによる制限がちゃんと働いている!フィールドを追加するだけで実現できるので手間いらず。あとはrescueで、エラー処理をするだけだ。

追記

lock_versionが思い通りに動かないと思ったときのチェックポイント

  • lock_versionの初期値には必ず数値が入っていること。「default => 0」オプションを忘れずに。
  • lock_versionの値を、text_field、hidden_field、またはパラメーターに含めて送信すること。
  • ActiveRecord::Base.lock_optimistically = false」を記述すると、lock_versionが無効になってしまう。(自分で記述しない限りlock_versionが有効な状態がデフォルトだが。)

以上で正常に機能すれば、editで取得したlock_versionの値がupdateで送信され、DBの値と同じならlock_versionに1加算して保存、正常終了。違っていればActiveRecord::StaleObjectErrorが発生する。