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.

API documentation for Locking

む。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       |                |
+--------------+--------------+------+-----+---------+----------------+

これで、動くはずです。
一人二役IEFireFox(以下FX)を使って再現します。

  1. 何かしらの記事を作っておく。
  2. FXでEdit画面に行く。(Submitは、まだしない。)
  3. IEでEdit画面に行く。(Submitは、まだしない。)
  4. IEでSubmitし、更新を確定する。
  5. 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"の列にはデフォルトを設定してやるといいよ。