Rails: JSON Patchでパフォーマンスを向上(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: JSON Patchでパフォーマンスを向上(翻訳)

FormAPIはPDFを生成するサービスです。バックエンドはRuby on Railsで書かれており、PDFテンプレートエディタはReactで作られています。PostgreSQLを用いてテンプレートのフィールドデータをjsonbカラムに保存しています。

テンプレートエディタの最初のバージョンでは、素朴なアプローチでフィールドデータを保存していました。フィールドが変更されるたびにarray全体をサーバーにpostしていたのです。立ち上げ当初のMVP(Minimum Viable Product)はこの方法で十分動きましたが、やがて顧客がPDFテンプレートに500個ものフィールドを設定するようになり、実行に5〜10秒も要するリクエストにアラートが出始めました。

明らかな解決法は、変更点だけをサーバーに送信することです。ReactアプリではReduxを使っているので、Reduxのアクションを送信することも考えましたが、Reduxのアクションには複雑なロジックが若干含まれているため、私たちのFormAPIではその手が使えませんでした。コードをRubyで書き直したくなかったのですが、仮にNode.jsを使っていたなら同じJavaScriptコードをサーバー側で再利用できたかもしれません。

他の選択肢としては、純粋なJSONオブジェクトの差分を取り出してそれをサーバーに送信する方法が考えられます。その場合JSON Patchを利用できます。

JSON Patchは、JSONドキュメントの変更点を記述するフォーマットです。JSONの一部だけが変更された場合にドキュメント全体の送信を避けるために利用できます。

こういうものは独自にこしらえる1よりも、IETF標準(RFC6902)を利用する方がほとんどの場合よい結果が得られると思います。以下のオープンソースライブラリを利用しました。

  • fast-json-patch — クライアント側でJSON Patchを生成するNPMパッケージです
  • hana — RailsサーバーでJSON Patchを適用するRuby gemです

以下のようにしてRailsモデルのjsonカラムにJSON Patchを適用しました。

class Template < ApplicationRecord
  # エラーが発生したらここでエラーを保存し、
  # バリデーション中にそれらを追加する
  attr_accessor :json_patch_errors
  validate :add_json_patch_errors
  after_save :clear_json_patches

  attr_reader :fields_patch

  # メソッドが呼び出されると即座にJSON Patchが適用される
  def fields_patch=(patch_data)
    # 後でアクセスしたい場合
    @fields_patch = patch_data
    self.json_patch_errors ||= {}
    json_patch_errors.delete :fields

    unless patch_data.is_a?(Array)
      json_patch_errors[:fields] =
        'JSON patch data was not an array.'
      return
    end

    hana_patch = Hana::Patch.new(patch_data)
    begin
      hana_patch.apply(fields)
    rescue Hana::Patch::Exception => ex
      json_patch_errors[:fields] =
        "Could not apply JSON patch to \"fields\": #{ex.message}"
    end
  end

  # データ再読み込み時にすべてのJSON Patchとエラーをクリア
  def reload
    super
    clear_json_patches
  end

  private

  def add_json_patch_errors
    return unless json_patch_errors.present?
    json_patch_errors.each do |attribute, errors|
      errors.add(attribute, errors)
    end
  end

  def clear_json_patches
    @fields_patch = nil
    self.json_patch_errors = nil
  end
end

こちらのRSpecテストをコピーして実装が正しいことを確認できます。そのうちこれをgemとしてリリースするかもしれません。

permittedパラメータとしてfields_patchをコントローラに追加しました。

params.require(:template).permit(
  fields: {},
).tap do |permitted|
  # arrayやhashのネストはややこしい
  if params[:template][:fields_patch].is_a?(Array)
    permitted[:fields_patch] = params[:template][:fields_patch].
      map(&:permit!)
  end
end

上のコードは、:fields_patchを通常の属性として扱い、update_attributesの間にJSON Patchを適用することを示しています。JSON Patchの適用に失敗すると、バリデーション中にエラーが追加されます。

フロントエンド側の実装は実に簡単です。改修前のコードは次のようになっていました。

if (!Immutable.is(template.fields, previousTemplate.fields)) {
  data.fields = template.fields.toJS()
}

新しいコードでは、JSON Patchをfields_patch属性として送信します。

import { compare as jsonPatchCompare } from 'fast-json-patch'

if (!Immutable.is(template.fields, previousTemplate.fields)) {
  data.fields_patch = jsonPatchCompare(
    previousTemplate.fields.toJS(), template.fields.toJS())
}

以下は新しいコードから送信されるAJAXリクエストの例です。

{
  "template": {
    "fields_patch": [
      {
        "op": "replace",
        "path": "/2/name",
        "value": "image_field_2"
      }
    ]
  }
}

コードの変更はわずか数行で済んだにもかかわらず、送信されるデータを大きく削減できました。

JSON Patchのもうひとつのメリットは、複数ユーザーによる同時編集をずっと楽にサポートできることです。JSON Patchは多くの場合任意の順序で適用することができます。replaceinsert操作しか含まれていない場合は特にそうです。衝突が発生した場合、最新のデータをリロードしてユーザーにやり直してもらうだけで済みます。また、WebSocketを使えばサーバーからブラウザにJSON Patchを送信して全クライアントを同期することもできます。

お読みいただきありがとうございました。コメントはHacker Newsまでどうぞ。

関連記事

Rails: ActiveModelSerializersでAPIを作る–Part 1(翻訳)

RailsでGraphQL APIをつくる: Part 1 – GraphQLとは何か(翻訳)

Ruby on Railsで使ってうれしい19のgem(翻訳)


  1. FormAPIの最初のバージョンではフィールド名記述に独自文法を使っていましたが、その後でJSON Pointersというものを発見しました。
    それまでスラッシュ文字/をエスケープするなど思いもよりませんでしたが、仕様の一部に含まれています。 
デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ

BPSアドベントカレンダー