楽観的ロックの使い方
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が発生する。