Railsで使うデータベース上の特別な意味を持つ列名 その弐 lock_version
前回Timestamping関係のカラムを試してみた。
今回はOptimistic Lockingだ。
列名は"lock_version"だ。
適当な、プロジェクトと、モデル、Scaffoldを作る。
モデルはDiaryという名で、こんな感じのmigrationファイル。
class CreateDiaries < ActiveRecord::Migration def self.up create_table :diaries do |t| t.column :title, :string t.column :text, :text t.column :lock_version, :integer end end def self.down drop_table :diaries end end
データベースはこんな感じだ。
mysql> desc diaries; +--------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +--------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | title | varchar(255) | YES | | NULL | | | text | text | YES | | NULL | | | lock_version | int(11) | YES | | NULL | | +--------------+--------------+------+-----+---------+----------------+
"lock_version"がユーザにより入力されると調子が悪そうな気がするので、app/views/diaries/_form.rhtmlを編集。その前に、ちょっと読む。
Optimistic Locking
Active Records support optimistic locking if the field
lock_version
is present. Optimistic Locking requires that at least one item in the model requires uniqueness validation (e.g. validates_uniqueness_of :foo). If you don’t have validates_uniqueness_of you will run into call back errors.Ensure that the attribute
lock_version
is passed in and can be evaluated between conflicting posts. i.e. put it in your view as a hidden field.
む。hiddenフィールドで渡してほしいのか。じゃあ、
<%= error_messages_for 'diary' %> <!--[form:diary]--> <p><label for="diary_title">Title</label><br/> <%= text_field 'diary', 'title' %></p> <p><label for="diary_text">Text</label><br/> <%= text_area 'diary', 'text', :rows => 5 %></p> <%= hidden_field 'diary', 'lock_version' %> <!--[eoform:diary]-->
しかし!
記事を書いて、それをupdateしようとすると、nil.+ができない!NoMethodErroが出て、お亡くなりになる。
どやら、モデルのupdate_attributesで失敗しているようだ。
困った。
edit画面のHTMLソースを見るとlock_versionのvalueが無い。
MySQLのプロンプトから"lock_version"を見ると、NULLだ。
試行錯誤の結果、lock_versionにはデフォルト値を設定してやるとうまく動くようだ。
こんな感じにmaigrate。
mysql> desc diaries; +--------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +--------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | title | varchar(255) | YES | | NULL | | | text | text | YES | | NULL | | | lock_version | int(11) | YES | | 1 | | +--------------+--------------+------+-----+---------+----------------+
これで、動くはずです。
一人二役をIEとFireFox(以下FX)を使って再現します。
- 何かしらの記事を作っておく。
- FXでEdit画面に行く。(Submitは、まだしない。)
- IEでEdit画面に行く。(Submitは、まだしない。)
- IEでSubmitし、更新を確定する。
- FXでSubmitし、更新を確定しようとする。
例外発生!FXでは更新できませんでした。
例外がドバーッと描画されるので、ハンドリングしてやります。
app/controllers/diaries_controller.rbを変更。
(略) def update @diary = Diary.find(params[:id]) begin if @diary.update_attributes(params[:diary]) flash[:notice] = 'Diary was successfully updated.' redirect_to :action => 'show', :id => @diary else render :action => 'edit' end rescue ActiveRecord::StaleObjectError flash[:notice] = '何者かに先に書き込まれました。やり直してください。' @diary = Diary.find(params[:id]) render :action => 'edit' end end (略)
MagicFieldNames in Ruby on Railsには、validates_uniqueness_ofを書かないと、call back errorsになるよ。っぽいことが書かれてあるような気がするんですが。書いてないけどcall back errorsは無い。idがプライマリーキーでユニークだからだろうか?
まぁいいや。
要点
"lock_version"の列にはデフォルトを設定してやるといいよ。