React x Railsでデータの同時編集ロック機能を実装する

検討

競合する可能性が大いにあり、かつデータの整合性が非常に重要な部分の実装なので、今回の場合は楽観的ロックではつらそう。では悲観的ロックではどうか。

悲観的ロックとは、

悲観的ロックとは、DBMSの行ロック機能(SELECT FOR UPDATE句)を利用して、並行した更新作業を制限する方法です。

ロックはレコード取得時にかかるので、同時にレコードを取得しようとした場合は他方がロック解除になるまで待機するため、同時に同じレコードを取得できないようになります。

via: http://kray.jp/blog/activerecord%E3%81%A7%E8%A1%8C%E3%83%AD%E3%83%83%E3%82%AF%E3%82%92%E3%81%8B%E3%81%91%E3%82%8B%E6%96%B9%E6%B3%95/

今回ロックしたいデータはReact componentからAjaxAPIを叩いているので、その点を考慮しながら悲観的ロックの実装を行う必要がある。

だけど、今回はロックをかけたいレコードがモデルをまたがるような構造になっているので、行ロックをテーブルをまたがって複数かけるひつようがありそうだ。

あと、SELECT FOR UPDATEが解除されるのがどのタイミングか調べてもいまいちわからなかったので、updateしたタイミングだとしたらユーザがupdateせずブラウザを離れる場合などもあり得るので、ちょっと怖いなとなった。

そして、そのページの中で、さらに紐付いてるデータも独立してばんばんcreateされることも考慮しないといけない。。。これは行ロックでは実現できない。

うーーんどうするか。。。

そこで今回は、ボタンをおした後に他のトランザクションが走っていることに気づく、という仕様ではなく、そもそも他の人の編集中には#editアクションのpathにアクセスできない、という仕様にすることにしました。

ですが、こちらの方法にも課題が合って、Webの仕組みというかさがというか、クライアントがそのページを離れたかどうかって、どうしてもWebの世界の仕組み上確証をとれないんですよね。。。

beforeunloadだってメッセージを表示するために特化した関数なのでそれでごちゃごちゃしようとした場合かなりブラウザごとに挙動の違いが出てくるみたいだし、そもそもPCの電源が落ちたときなど、このイベントさえ呼ばれないということだって全然ありえるだろうし。。

と、どうしようか悩んだのですが、Wordpressがこの方式を取っていることに気付き、ソースコードWordpressのミラー版がGitHubに公開されていたので参考にしてみることにしました。以下のコードあたりがそれっぽい。

https://github.com/WordPress/WordPress/blob/493f76a3d2ef8c030ab5dcd4333f9a401208f534/wp-admin/js/post.js#L156

heartbeatって名前が秀逸だな。。。

30秒ごとに脈を打って編集情報を更新してるみたい。

この方法とてもよい!と思い、この方法をRails + Reactで実装してみることにしました。

実装

今回の想定は、記事データを扱うPostというモデルの同時編集をロックするというもので実装していきます。Reactを使います。

編集情報を扱うモデルを作成

まずは編集情報を扱うPostUpdatingというモデルを作成します。

$ bin/rails g model PostUpdating user_id:integer post_id:integer

そしたら、PostとPostUpdatingは1:1の関係になるように各モデルのファイルとspecを実装しておいて下さい。(手抜きすみません。。)

APIを実装

続いてこのモデルのAPIを実装します

$ bin/rails g controller Api::V1::PostUpdating
class Api::V1::PostUpdatingController < ApplicationController
  def show
    post_updating = PostUpdating.find_by(id: params[:id])
    render json: post_updating
  end

  def update
    post_updating = PostUpdating.find_by(id: params[:id])

    # Update updated_at
    post_updating.touch

    if post_updating.update_attributes(post_updating_params)
      render json: post_updating, status: :ok
    else
      render json: post_updating.errors, status: :unprocessable_entity
    end
  end

  private

  def post_updating_params
    params.require(:post_updating).permit("user_id")
  end
end

ひとつつまずいたのは、Railsインスタンスが更新されていなかったらupdate_attributesなどでsaveされてもupdated_atを更新しない、ということでした。

これは post_updating.touch など、touchを使うことで解決できます。

React Componentを実装

setIntervalを使えば自動的にPostUpdatingの更新情報をupdateすることができそうです。

ReactでのsetIntervalの実用例はこちらとか参考になりました。

http://stackoverflow.com/questions/36299174/setinterval-in-a-react-app

const YourComponent = React.createClass({
  ...
  componentWillMount() {
     var result = this.check_if_multiple_user_editing();
     if(result == false){
       this.heartbeat();
       // 30 seconds
       setInterval(this.heartbeat, 30000);
     }
  },

  // 記事の更新情報をupdateする
  heartbeat() {
    var result = $.ajax({
      url: "/your/api/url/" + this.props.post_updating_id + "/",
      dataType: 'json',
      type: 'PATCH',
      timeout: 10000,
      data: { post_updating: { user_id: this.props.user_id, updated_at: Date.now() } },
      success: function(result) {
        // console.log("throb!"); // debug
      }.bind(this),
      error: function(_xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    }).responseJSON;
    return result;
  },

  fetchPostUpdating(post_updating_id) {
    var result = $.ajax({
      url: "/your/api/url/" + post_updating_id + "/",
      async: false,
      dataType: 'json',
      type: 'GET',
      timeout: 10000,
      success: function(result) {
        // console.log("Success");
      }.bind(this),
      error: function(_xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    }).responseJSON;
    return result;
  },

  check_if_multiple_user_editing() {
    var result = this.fetchPostUpdating(this.props.post_updating_id);
    editing_date = new Date(result["updated_at"]);
    now = Date.now();
    var diff = now - editing_date.getTime();
    // 秒
    var diff_second = diff / 1000;

    // 10秒以内にheartbeatにより更新されていて、編集ユーザが異なる場合は
    // 警告を出して前のページにリダイレクト
    if(diff_second < 10 && this.props.user_id != result["user_id"]) {
      alert("このページは他ユーザによって編集されています");
      location.href = "/go/ahead/your/favorite/url";
    }
  },
  
  ...

  render: function() {
    return (
      <div>
        <YourComponent />
      </div>
    );
  }
});

これで、このComponentをmountしてるViewで、他の人が同時にアクセスすることを防ぐ機能を実装できました。わーーーい。

参考資料

楽観的ロックと悲観的ロック - Qiita

Rails4で楽観的ロックを実装する - Rails Webook

Rails4で悲観的ロックを実装する - Rails Webook

Railsで悲観的ロックできないの? - babie steps

Reactでロード中indicatorを表示するcomponentをRailsに組み込む

react-spinkitを使ってみます。

github.com

以下にデモも用意してくれてます。

React Spinkit

classnamesとobject-assignというライブラリを使用しているので、こちらをnpmでインストールしておくのを忘れないように。

GitHub - JedWatson/classnames: A simple javascript utility for conditionally joining classNames together

$ npm install classnames object-assign --save

ソース中ではclassnamesはcx, object-assignはassignとして使われてるので、どこかでrequireしておきます。

cx = require("classnames");
assign = require("object-assign");

こちらのソースから直接componentをassetsのディレクトリにぶち込みました。

https://github.com/KyleAMathews/react-spinkit/blob/master/src/index.jsx

そして、必要なcssファイルも一通りRailsのasset pipelineに乗るようにしておきます。

https://github.com/KyleAMathews/react-spinkit/tree/master/css

ここまでくれば後は、Viewを呼び出すだけです。hamlから直接このComponentを呼び出すなり、子componentとして呼び出すなり。

私の今回の場合だと、render()メソッドでreturnする内容をstateによって切り替えて、load中ならSpinner componentが挿されるような感じで使いました。

これでloadingのanimationを簡単に実装することができました。便利!

Railsで標準クラスへメソッドを追加してNumericの桁数を求めれるようにする

Numericクラスに、桁数を返すメソッドを追加したいと思ったので、Railsに乗っていて標準クラスをオーバーライドするプラクティスあるのかなーと思って調べてみましたが、特に決まったものはなさそうです。

ruby on rails - Railsで既存クラスへのメソッド追加をする時のファイルの置き場所 - スタック・オーバーフロー

In Ruby on Rails, to extend the String class, where should the code be put in? - Stack Overflow

ruby - adding a method to built-in class in rails app - Stack Overflow

ruby - In Rails, how to add a new method to String class? - Stack Overflow

In Ruby on Rails, to extend the String class, where should the code be put in? - Stack Overflow

ということで lib/core_ext/ 以下にどんどんつくっていくような感じにしてみます。

# lib/core_ext/numeric.rb
class Numeric
  def length
    self.zero? ? 1 : Math.log10(self.abs).to_i + 1
  end
end
# config/initializers/core_ext.rb
Dir[File.join(Rails.root, "lib", "core_ext", "*.rb")].each {|l| require l }

数値の桁数を求めるメソッドはこちらを参考にさせていただきました。

qiita.com

これで桁数を返せるようになりました!わーーーい

pry> 3.length
=> 1
pry> 10000.length
=> 5