Railsでコンフリクトをvalidate

Railsで同じデータを2人のユーザがほぼ同時に編集したとき、何も対処しないと片方のユーザがもう片方のユーザの変更を確認しないまま上書きしてしまうことになるので、更新時刻を元に一方が競合していることを判定する方法について説明します。

要点

  1. サーバ側でHTMLに最終更新日時を埋め込む
  2. クライアント側で更新リクエスト時に最終更新日時も同時に送る
  3. サーバ側で送られてきた最終更新日時とDB上の最終更新日時を比較して検証する
  4. これらの実装に validate_confirmation の機能を使う

Viewに最終更新日時を埋め込む

日時をUnixtimeで表現して埋め込むならこんな感じになります。

erb <%= form_for @model do |f| %> <%= f.hidden_field :updated_at_confirmation_in_unixtime, value: @model.updated_at_confirmation_in_unixtime %> ... <% end %>

ModelにValidationを定義する

View用にupdatedatのunixtime表現を提供するメソッドと、クライアントから送られてきたunixtimeを保持しておくプロパティ、それからupdatedatconfirmationがnil以外の値を返したときにupdatedatと等価かどうか比較するvalidationを定義します。confirmation: trueを利用すると等値比較のvalidationとエラーメッセージを定義してくれるので少し楽できます。

```rb

Public: A concern module to validate conflict.

To validate conflict, set updatedatconfirmationinunixtime attribute to the model object.

Example:

model.update(body: "foo", updatedatconfirmationinunixtime: time.to_i)

module ConflictValidatable extend ActiveSupport::Concern

included do attraccessor :updatedatconfirmationin_unixtime

validates :updated_at, confirmation: true, if: :has_updated_at_confirmation_in_unixtime?

end

def hasupdatedatconfirmationinunixtime? updatedatconfirmationin_unixtime.present? end

def updatedatconfirmation Time.at(updatedatconfirmationinunixtime.toi) if hasupdatedatconfirmationinunixtime? end

def updatedatinunixtime updatedat.toi unless updatedat.nil? end end ```

Ajaxで更新する場合の注意点

Ajaxで更新するようなフォームの場合は、更新成功後にサーバが最終更新日時を返すようにし、クライアント側でView内の最終更新日時を書き換える処理を入れる必要があるので注意が必要です。

おしまい

今回は更新時刻で判定しましたが、本文のハッシュ値などにも利用できると思います。