#322 RABL
- Download:
- source codeProject Files in Zip (100 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (10.2 MB)
- webmFull Size VP8 Video (12.2 MB)
- ogvFull Size Theora Video (23.6 MB)
Railsアプリケーションで、モデルデータに基づいてカスタムのJSONデータを返したい場合、これを行う方法はいくつかあります。モデルのas_json
メソッドをオーバーライドするか、エピソード320でおこなったようにJbuilder gemを使用することができます。もう一つよく使われるのが、RABL gemを用いる方法です。RABLはRuby API Builder Languageの略で、その他のツールと比べると若干機能が豊富です。
今回のエピソードではRABLの機能を紹介しますが、Jbuilderのエピソードで使用したのと同じサンプルアプリケーションを使うことによって、これら2つのライブラリの違いを理解する視点を提供します。このアプリケーションは複数の記事を持つブログアプリケーションで、記事のURLに.json
を加えることで個別の記事のデータをJSONとして表示させたいとします。これを今試してみると、アプリケーションがJSONリクエストにどう応答すればわからないのでエラーメッセージが表示されるだけです。
RABLを使う
これをおこなうためにArticlesController
を修正し、showアクションがJSONリクエストに応答することを確認し、選択された記事のJSON形式のデータを返すこともできますが、その代わりに今回はRABLを使用します。これはgemとして提供されているので、インストールは通常の方法で、gemfileに追加してbundle
コマンドを実行します。
gem 'rabl'
Jbuilderと同じようにRABLにはテンプレートハンドラが含まれるのでビュー層でJSONレスポンスを定義できます。記事をJSON形式で出力するために、ここでそれを一つ作成します。RABLのDSLを利用してRubyコードを使ってそれをおこないます。
object @article attributes :id, :name, :published_at
通常RABLテンプレートの最初に来るのはobject
の呼び出しで、これに対して操作の対象とするオブジェクトを渡します。続いてattributes
を呼び出して返したいオブジェクトの属性を定義します。ここで記事のJSONのURLにアクセスすると、テンプレートで定義したJSONが表示されます。
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z"}}
単純な属性ではないものを指定したい場合は、node
メソッドを使用してそれに名前とブロックを渡します。ブロックが返すものが値として使用されます。
object @article attributes :id, :name, :published_at node(:edit_url) { "..." }
これはJSONにedit_url属性を追加します。
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"..."}}
RABLテンプレートからヘルパーメソッドにアクセスできるので、edit_article_url
を使って記事の編集用URLとcurrent_user
を取得して、(使用している認証のしくみがcurrent_user
ヘルパーメソッドを持っていると仮定して)現在のユーザがadminの場合のみURLが表示されるようにします。
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { edit_article_url(@article) } end
しかしこれには問題があります。edit_article_url
でarticleインスタンス変数を使用していますが、これはいい方法ではありません。その代わりに、ブロックに渡されるarticleオブジェクトを使うべきです。
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end
ブロックに渡されるオブジェクトはobject
メソッドに渡されるのと同じオブジェクトですが、ブロックに渡される方のオブジェクトを使うべきであるということには明白な理由があり、それについては後ほど説明します。ここで記事のJSONを見てみると、edit_url
属性が、正しい値と共に表示されています。
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit"}}
関連レコード
関連レコードのデータを含めたい場合も簡単です。今回のアプリケーションではArticle
はAuthor
に属し(belongs to)、複数のComment
を持ち(has many)ます。RABLのchild
メソッドを使用して、これらの関連するモデルから情報を含めることが可能です。
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end child :author do attributes :id, :name node(:url) { |author| author_url(author) } end
関連する著者のJSONを生成するコードは、記事のJSONを生成するコードに非常に似ています。唯一の違いは、著者は記事にではなくノードのブロックに渡されるという点です。ここでJSONを見てみると、希望通りに記事の著者の詳細情報が表示されています。
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"}}}
Comment
の関連についても同じように処理できますが、has_many
の関連を扱うので複数形を使用します。
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end child :author do attributes :id, :name node(:url) { |author| author_url(author) } end child :comments do attributes :id, :name, :content end
JSONレスポンスでコメントは、著者のように一つのレコードとして表示されるのではなく、配列としてネストされます。
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}}
テンプレートの再利用
記事のJSONデータをきれいに定義できましたが、この出力をアプリケーションの別のところで利用したい場合はどうすればいいでしょうか?
collection @articles extends "articles/show"
複数の記事を対象にしているので、ここではobject
ではなくcollection
を使用します。showテンプレートのときにおこなったのと同じように、ここで使用したい属性を定義することもできますが、個別の記事のときと同じJSONが欲しいので、ここではそのテンプレートを再利用するためにextends
を呼び出してその名前を渡します。articles.json
を見てみると、すべての記事のJSONデータが表示されます。
$ curl http://localhost:3000/articles.json [{"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}},{"article":{"id":2,"name":"Krypton","published_at":"2012-01-05T18:38:50Z","edit_url":"http://localhost:3000/articles/2/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"":[]}},{"article":{"id":3,"name":"Batman & Robin","published_at":"2012-01-26T18:38:50Z","edit_url":"http://localhost:3000/articles/3/edit","author":{"id":1,"name":"Bruce Wayne","url":"http://localhost:3000/authors/1"},"comments":[{"comment":{"id":3,"name":"The Joker","content":"Haha, Batman, you will see your bat signal tonight!"}},{"comment":{"id":4,"name":"Robin","content":"Enough with the games Joker."}},{"comment":{"id":5,"name":"Riddler","content":"Did someone say games?"}}]}}]
このようにテンプレートを再利用できる機能こそが、RABLテンプレートでインスタンス変数の使用を最小限にするべき理由です。オブジェクトを定義するときにshowテンプレートの@article
変数を使用しているだけなので、indexテンプレートの中でこのテンプレートを再利用する方がより簡単です。これは、インスタンス変数ではなくノードの呼び出しのブロックに渡される記事を使用しているからです。
ルートノード
RABLが出力するJSONの特徴として、ルートノードにモデル名が含まれている点に気づいた方もいるでしょう。
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}}
これでいい場合もあれば、都合が悪い場合もあります。Rails 3.1ではデフォルトのJSON出力にはルート要素は含まれず、RABLをこれに合わせるには設定が必要です。これをおこなうには/config/initializers
ディレクトリに新規ファイルを作成し、include_json_root
をfalseに設定します。
Rabl.configure do |config| config.include_json_root = false end
初期化ファイルを作成したので、この変更を有効化させるためにアプリケーションを再起動します。すると最初の記事のJSONにはルートノードが含まれていません。
$ curl http://localhost:3000/articles/1.json {"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"},{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}]}
RABLのREADMEファイルのConfigurationセクションに、指定できる設定オプションの詳細が記載されています。ここで触れることができなかった機能が他にもあります。XMLやMessage Packなどその他のserialization(直列化)のオプションについても触れられています。
WebページにJSONを埋め込む
別のコントローラアクションを呼び出すのではなく、JSONデータをHTMLドキュメントに埋め込まなくてはいけない場合があります。RABLなどを利用する場合、これはどうすればいいでしょうか? indexアクションのHTMLテンプレートは次のようになります。
<h1>Articles</h1> <div id="articles"> <% @articles.each do |article| %> <h2> <%= link_to article.name, article %> <span class="comments">(<%= pluralize(article.comments.size, 'comment') %>)</span> </h2> <div class="info"> by <%= article.author.name %> on <%= article.published_at.strftime('%b %d, %Y') %> </div> <div class="content"><%= article.content %></div> <% end %> </div>
ラッパーのdiv
のdata-
属性に記事のJSONを追加します。単に@articles.to_json
を呼び出す方法もありますが、これはRABLテンプレートを使用しません。代わりにrender(:template)
を呼び出してテンプレート名を渡すことができます。
<div id="articles" data-articles="<%= render(template: "articles/index.json.rabl") %>" >
ページをリロードしてソースを表示させると埋め込まれたJSONを見ることができます。
<div id="articles" data-articles="[{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"},{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}]},{"id":2,"name":"Krypton","published_at":"2012-01-05T18:38:50Z","edit_url":"http://localhost:3000/articles/2/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"":[]},{"id":3,"name":"Batman & Robin","published_at":"2012-01-26T18:38:50Z","edit_url":"http://localhost:3000/articles/3/edit","author":{"id":1,"name":"Bruce Wayne","url":"http://localhost:3000/authors/1"},"comments":[{"id":3,"name":"The Joker","content":"Haha, Batman, you will see your bat signal tonight!"},{"id":4,"name":"Robin","content":"Enough with the games Joker."},{"id":5,"name":"Riddler","content":"Did someone say games?"}]}]" >
このテクニックを使用する場合は、出力されるテンプレートで使われるインスタンス変数はコントローラのアクションで設定されるので注意してください。