#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
endJSONレスポンスでコメントは、著者のように一つのレコードとして表示されるのではなく、配列としてネストされます。
$ 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?"}]}]" >
このテクニックを使用する場合は、出力されるテンプレートで使われるインスタンス変数はコントローラのアクションで設定されるので注意してください。


