sub resources プラグインの紹介
こんにちは、大場です。
今日は、自作のプラグイン sub_resources (http://github.com/nay/sub_resources) を紹介します。
なお、これはRubyConf2009で初の英語プレゼンをした話題です。スライドは slideshare にあります。
この記事を書く動機
sub_resources はRails 2.3.x で使える、ルート作成を助けるプラグインです。そろそろ Rails 3 がやってきてルートが大きく変わるときにこの話題をするのは旬とはいえませんが、この話題の背景にはURLやコントローラの設計ニーズの話があります。この記事を通じてそのあたりのモヤモヤした領域を整理することができることは無駄ではないはずです。また、Rails 3 で私のニーズが解決しなければ sub_resources を改良して使って行くつもりだし、Rails 2.3.x で私としては sub_resources が役に立つと思っているので(一部、不足はあるのですが)、記事がまったく無駄になるということもないだろうと考えて、書いておくことにしました。
Rails2のURLマッピング(ルーティング)
原則として、単純な CRUD を行なうリソースと、そのリソースを単純に階層化していくだけなら、sub_resources プラグインは要りません。
例えば、グループのCRUDのルート(RailsにおけるURLとアクションのマッピング定義)は以下のようになります。
# /groups/3 のようなURLを作る
map.resources :groups
単純に階層化すると次のようになります。
# /groups/3/members/5 のようなURLを作る
map.resources :groups do |groups|
groups.resources :members
end
ここまでは非常にシンプルで素晴らしい。しかし、このようなシンプルさは、基本的には以下のルールの範囲内でしか成り立ちません。
- 1つのコントローラで1種類のリソースのCRUDのみを提供する
この原則から外れると、ルート定義は少しずつ複雑になっていきます。そこで、現実的にはどのような外れ方があるかを以下に説明していきます。
CRUD以外のアクションを作りたい
多くの問題がこの「CRUD以外のアクションを作りたい」に集約されます。しかし、結果としての形は同じでも、このニーズはさらに次のように分解できます。
- 動詞の追加 - そのコントローラの「リソース」に対するべつの「動詞」を追加したい
- 標準的でない対象範囲 - そのコントローラの「リソース」(ただし、範囲が標準の想定と違う)に対するCRUDを追加したい
- サブリソースの利用 - そのコントローラの「リソース」の付属的なリソース(ここでは「サブリソース」と呼んでおきます)に対する機能を追加したい
動詞の追加
動詞の追加の例としては、「ロック」「ロック解除」といった動詞を追加したいというケースがあります。この場合の解決方法には次のようなバリエーションがあります。
- resources に追加設定を記述する
- 「ロック状態」を別リソースと見なして、URLをさらに階層化する。「ロック状態」へのCRUDとして表現するので”動詞”部分は増やさない。
resourcesに追加設定を記述するには次のようにします。
map.resources :book, :members => {:lock => :post, :unlock => :post}
この方法は簡単ですが、難点として、URLやヘルパーメソッド名(ルート名)に動詞が入るということがあります。
POST books/:id/lock # lock_book_path で生成できる
POST books/:id/unlock # unlock_book_path で生成できる
本来、RESTなURLには動詞は含めないのでこれはあまり美しいとはいえません。
別の方法として、「ロック状態」を別リソースとして表現する方法があります。つまり、books/3/lock というURL(ここでいうlockは名詞)を考え、これへのPOSTを「ロックする」、DELETEを「アンロックする」と見なします。つまり、「動詞の追加」ではなく「サブリソースの利用」というニーズにシフトするということになります。これは美しいのですが、ルートの設定は面倒になってきます。
with_options({:controller => 'books'}) do |books|
books.book_lock 'books/:id/lock', :action => 'lock',
:conditions => {:method => :put}
books.connect 'books/:id/lock', :action => 'unlock',
:conditions => {:method => :delete}
end
sub_resourcesプラグインを使うと、この後者の設定を楽に行なうことができるようになります。この場合は、1つのBookに対して1つのLockしかないので、次のように sub_resource (単数系)のオプションを使って書くことになります。
map.resources :books, :sub_resource => :lock
こうすると、先ほどの2つのマッピングは以下のようになります。
# book_lock_path でURLを生成できる
PUT 'books/:id/lock' -> :action => 'update_lock'
DELETE 'books/:id/lock' -> :action => 'destroy_lock'
なお、sub_resource(s)オプション内には、通常のresourcesへ渡せるオプションがすべて使えます。例えば以下のように書くことができます。
map.resources :books,
:sub_resource => {:lock => {:only => [:update, :destroy]}}
アクションが単純なパターンに変えられますが、これはこれで全コントローラで統一されると読みやすいという利点があるのでいいと思っています。
標準的でない対象範囲
シンプルなマッピングから逸脱する一般的な要因にはもう一つ別の問題が挙げられます。それは、標準的でないリソースの範囲に対するアクションのマッピングです。この問題はさらに以下の2つに分けることができます。
- URL prefix (絞り込み条件)が標準的でない - groups/members といった :group_id を挟まない URL のカバー
- CRUDの対象範囲が標準的でない - 集合に対するDELETEなどを行ないたい
sub_resourcesプラグインは後者にしか対応していません(本当は前者にも踏み込みたいのですが)。
URL prefix (絞り込み条件)が標準的でない
まず、標準的な URL prefix の例を挙げましょう。「あるグループに所属するメンバー一覧」の一般的なルートは次のようになります。
- GET /groups/3/members
- GroupMembersController の index アクション
- groupmemberspath でURLを生成できる
上記のように ‘groups/:group_id/…..’ で始まる形は、Rails では標準的です。reousrces のネストなどで簡単に定義できます。
では、「グループに関係なくすべての新しいメンバーを一覧する機能」があったとしたらどうでしょうか? これが、標準的ではないリソースの範囲の例です。メンバーに対するほとんどの機能は、グループをURLで絞ることを前提としているのに、グループを超えた検索機能や閲覧機能を提供するとなると、戸惑ってしまいます。例えば、以下のような候補があるでしょう。
候補1
- GET /members
- MembersController の index アクション
- members_path でURLを生成できる
悪くありませんが、GroupMembersController との共存で混乱するかもしれませんし、URLの統一感としては多少迷うところです。メリットとしては、ルートの定義が多順ですし、フィルターなどのしがらみもなく実装がしやすいと思います。
map.resources :members
一方、メンバーというのはグループの下にあるのだから、「特定のグループ」を示している :group_id の部分を抜いて、’groups/members’ といったURLにするということも考えられます。
候補2
- GET /groups/members
- GroupMembersController の inter_groups アクション
- intergroupsmembers_path でURLを生成できる
この例では、同じコントローラにまとめられますが、’groups/:id’ ではなく ‘groups’ を冠するため、複雑なルート設定が必要になります。
map.inter_groups_members 'groups/members',
:controller => 'group_members', :action => 'index',
:conditions => {:method => :get}
残念ながら、sub_resourcesプラグインにはこれに対する有効な支援策は入っていません。Rails3の動向もみながら、いずれ何とかしたいとなと私が考えていることのひとつです。
CRUDの対象範囲が標準的でない
標準的でない対象範囲という話題ではもうひとつありがちな悩みがあります。それは、Railsの想定しているCRUDの対象範囲と違うものを提供したい場合です。例としては「すべての本を一括で削除したい」といった操作を(も)提供したい場合があげられます。実現したい内容は次のようになります。
- /books への DELETE で destroy_books アクションを呼ぶ
ごく単純なのですが、RAILSの標準ではないため、これの実現はまたもや手動でルートを定義しなければなりません。
# 手動定義の例
map.connect 'books',
:controller => 'books', :action => 'destroy_all',
:conditions => {:method => :delete}
map.resourcesへの追加設定で何とかなるのでは?と思われる方もいるかもしれません。以下のようにすると似たことが実現できますが、URLとヘルパーメソッド名が犠牲になります。
# reosurcesを使った定義の例
map.resources :books,
:collection => {:destroy_all => :delete}
この場合、destroyallbookspath で /books/destroyall URLが生成されます。合理的ですが、前の例ほど美しくはありません。
しかし、sub_resources プラグインを入れていると、上記のようなresourcesを利用した記述で、手動定義とまったく同様の効果を得られるようになります。というのは、destroyall、updateall といったアクション名を、全体へのdestoryやupdateであると心得て、自動的にきれいなURLにしてくれる機能を入れているからです。命名規則を守って書くだけできれいなURLを実現できます。なお、この機能は、sub_resource(s)をつかった定義内でも利用することができます。
サブリソースの利用
ここまでで、sub_resources プラグインの意味をだいぶ掴んでいただけたと思うのですが、冒頭に挙げた「標準的なCRUD」を逸脱する最後の例についての説明が残っています。これが、サブリソースの利用です。「ロック」の例でも挙げましたが、1つのコントローラで、メインのリソースのほかに、副次的なリソースを扱いたいことがあります。これについてはいろいろな動機が考えられます。
- 前述の「ロック」の例のように、CRUD以外の動詞を使わないようにするために「名詞」であるリソースを増やすケース
- 日記の画像のように、ほぼ日記の一部であるものを手軽に同じコントローラで制御したいケース
- 多くのコントローラに共通するサブリソースの実装をモジュールなどに切り出したいケース
最後の例はRubyConf用の資料で説明しています。簡単にいうと、updatetags、destroytags といったアクション名にしておけば、モジュールに切り出して色々なコントローラからincludeするだけですむが、ArticleTagsController、UserTagsController、などとタグの対象ごとにコントローラを作るのは大変だ、というようなニーズです。これについてはここでは割愛して、単純に、日記の画像の例で説明しておきます。
なお、1つのコントローラで複数のリソースを扱うのは、原則的には良くないといえます。Railsアプリケーションとしては基本的には1コントローラ1リソースで表現するようにし、何か理由のあるときにサブリソースを検討するとよいと思います。
日記の画像についての要件として、以下があるとします。「日記には3つ画像がつけられる。作成や変更は日記と一緒に行なわれるので、それぞれの画像の表示機能だけを BlogsController につけたい。」
このようなとき、sub_resources プラグインを使うと以下のように記述することができます。
map.resources :blogs, :sub_resources => :images
このようにすると、’blogs/3/images/17’ といった URL が BlogsController の image アクションに対応づけられます。このとき、:id には ブログのid 、:image_id に画像のidが入ります。このように、コントローラの主たるリソースのid がつねに :id になるのは、フィルターなどで統一的に処理がしやすくなって便利です。
sub_resources プラグインの機能のまとめ
以上、Rails2.3.xのルーティングで標準的に書きづらいギャップの話と、それをカバーするための sub_resources プラグインの話をしました。まとめると、sub_resources プラグインは以下の機能を持っています。
- 1つのコントローラに、付属的なリソースへの操作を追加するためのきれいなルート設定を簡単にする。アクション命名が統一的になる。 ** 付属的なリソースがメインリソース1つに対して複数の場合は sub_resources オプションを使う ** 付属的なリソースがメインリソース1つに対して1つの場合は sub_resource オプションを使う
- リソースの集合へのupdate、destroyをきれいなURLで実現できるようにする
もし興味のある方はぜひ使ってみてください。また、Rails 3での最適解(もし、Rails3でこれらがすべて満たされるのでなければ)について一緒に取り組んでいただければと思います。
Posted by Yasuko Ohba on Sunday, April 18, 2010








