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

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Better Rails Performance with JSON Patch 原文公開日: 2017/11/19 著者: Form API サイト: https://formapi.io/ — PDF生成APIサービスの会社です 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 … Continue reading Rails: JSON Patchでパフォーマンスを向上(翻訳)