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の実装をしてる人はご検討いただければ