maedamaのブログ

アプリケーションエンジニアです。最近は主に設計を担当しています。

jbuilder_deferred_render作ってみました

Rails等でAPIを書いてると、いくつか問題にぶちあたります。

n+1問題がその一つなんですけど、Railsではこれをcontroller側で解決しろと話をしてます。

元々はWebページを作るフレームワークなので、そもそもAPIを作るのはどうなのみたいな話はありますが、まぁ色々総合的に別にRailsでもいいんじゃないかなと思ってるので、それはいいんですがn+1問題は結構めんどいです。

Webページの場合はViewの階層構造とかって比較的シンプルなので、includeを都度かいていくのは問題ないと思いますが、RESTFul APIをかいてると、client friendlyでつくる複雑なjsonつくっちゃいがちです。

i.e

 {
    "id": 1,
    "kind": "activity",
    "actor": {
      "kind": "user",
      "id": 1, 
      "displayName": "ほげほげ"
    },
    "verb": "like",
    "target": {
      "id": 3,
      "kind": "Page",
      "owner": {
        "id": 4,
        "kind": "user",
        "displayName": "げほげほ"
      }
    }
 }
 

階層構造が深くなると、includeしていく方法は結構つらくなりがちです。そもそも、

  • リソースをどのようにRenderingするかはViewが知っている(Pageのownerをrenderingするかしないかとか)
  • Viewが、モデルの様々な属性にアクセスするのは論理的には間違ってない
  • だけど、モデルの属性のbackendの特性として、一つ一つアクセスするのは現実的に遅い。
  • Viewは深さ優先renderingをするので、モデルの属性に一つ一つアクセスする => パフォーマンス劣化

みたいな流れで、そもそも Controllerがviewがどういうrenderingをするかに応じて includeするのはちょっとつらいだろうなと、そこでgemかきました。まだ完全にアルファ qualityですがすでに本番で限定的に稼働しています。

maedama/jbuilder_deferred_render · GitHub

こんな感じで、jbuilderのviewをかくと

@users = User.take(10)
@jbuilder = Jbuilder.new do |json|
json.array! @users do |user|
  json.name user.name
  json.when(
    user.deferred_load.books
  ).then do |books|
    json.books books, :title
  end
end

こんな感じでqueryされます。

SELECT * FROM users ORDER BY id DESC LIMIT 10;
SELECT * FROM books where user_id in (?, ?, ?, ?, ?, ?, ?, ?, ?)
user.deferred_load.books

の部分は、任意の q-deferの promiseを入れられるんですが、必要なタイミングでまとめてとるための実装は deferred_loaderというgemで実装したので、特に何もせずとも使用できます。

https://github.com/maedama/deferred_loader jclem/q-defer · GitHub

深さ優先探索を前提としたjbuilderの上に無理矢理のっけてるのでちょっとつらいんですけど利用側からするともうincludeとかかかなくていいので個人的な需要は満たしています。階層構造の深いJSONを返すAPIの実装をしてる人はご検討いただければ