routes.rb をもう一度考えてみた

@tyabe さんに声をかけていただいて、18日のShibuya.rbでREST関連の話をさせてもらいました。
Shibuya.rb には前回のDeNAでの開催のとき初参加で、今回が2回目だったのですが、会場のAJITOはとても雰囲気がよくていいですね。ビールやドリンクがフリーなのもポイント高い。

補足

しゃべったことを補足すると、流れとしてはこんな感じです。

routes.rb の悪いところはどこか

I. resources が直感的でない
基本の7つのアクション、なぜこの名前なのか根拠が薄い。全部動詞かと思ったらそうでもない。indexとかどうなの?
パターンとして与えられるのはいいんだけど、やはり最初は「とにかく覚えろ」ということになるのか。

II. Convention over Configuration じゃない
もし7アクションが「規約」で、覚えることを前提とするなら、 resources も規約として省略して、 routes.rb 自体書かなくてすむのでは?

routes.rb を書かずにすむとしたら、どうなるか

Sinatraに近い形ということになりそう。直感的にはわかりやすい。ただし問題点もある。

とりあえずの結論

パターンとしてのメリットもあるので、 resources の基本7アクションは規約として受け入れるが、そのかわりほとんどすべて resources で書ける(書く)ようにすべきでは。

余談

最初、具体的に何について話すか考えていて、URL設計については来週やるので、もうちょっとRails寄り、ということで前の記事でも取り上げた routes.rb の話にしてみましたが、だいぶ実用とはかけ離れた話だったかもしれません…。
しかも「もう一度考えてみた」なので、 routes.rb に文句つけてこう改良すれば?という話にするつもりが、どうも今よりいい案が思いつかず。企画ミスの感が否めませんが、どうだったでしょうか。

発表後の話し合い?の時間では、思いがけずREST関連の話で盛り上がってうれしかったです。こういうのがやりたかった!とくに @okitan さん(かつてREST厨だったらしい)、@masata_masata さん、@joker1007 さん。
ありがとうございました。

宣伝

23日に、 Sendagaya.rb の拡大版?として「RailsにおけるRESTfulなURL設計勉強会」を開催します。すでに満員だったりするのですが…。
もし好評であれば2回目3回目とやりたいと思っていますので、よろしくお願いします。

Sendagaya.rb Drinkup 全員LT大会の巻」こっちでもいいと思いますよー。

routes.rb が必要なくなる Rails gem

Railsを使っている人からよく聞く話が、

config/routes.rb 書くのがめんどくさい・よくわからない

ということです。とくにSinatraのスタイルと比べると、Railsのルーティング定義は面倒なものに思えます。
基本的にはresources推奨なので、resourcesを書くだけではあるのですが、考えてみればRailsはConvention over Configuration (CoC) を標榜しているのに、推奨のルーティング設定も書かなきゃいけないというのはちょっと変な気もします。*1

だったら、コントローラ置いとけばresourcesとみなして勝手にルーティングしてくれるようにすればいいのでは、と思ってConventional Routesというgemを作ってみました。

Conventional Routes

これは何?

config/routes.rb に何も書かなくても、コントローラから自動的にルーティングを生成します。
すべてのルーティングはresourcesです。コントローラがサブディレクトリにある場合はnamespaceとみなします。

app/controllers/admin/special/licenses_controller.rb
app/controllers/admin/users_controller.rb
app/controllers/users_controller.rb

このコントローラがある場合は、このようなルーティング定義と同様になります。

namespace :admin do
  namespace :special do
    resources :licenses
  end
  resources :users
end
resources :users

生成されるルーティングはこのようになります。

    admin_special_licenses GET    /admin/special/licenses(.:format)          admin/special/licenses#index
                           POST   /admin/special/licenses(.:format)          admin/special/licenses#create
 new_admin_special_license GET    /admin/special/licenses/new(.:format)      admin/special/licenses#new
edit_admin_special_license GET    /admin/special/licenses/:id/edit(.:format) admin/special/licenses#edit
     admin_special_license GET    /admin/special/licenses/:id(.:format)      admin/special/licenses#show
                           PUT    /admin/special/licenses/:id(.:format)      admin/special/licenses#update
                           DELETE /admin/special/licenses/:id(.:format)      admin/special/licenses#destroy
               admin_users GET    /admin/users(.:format)                     admin/users#index
                           POST   /admin/users(.:format)                     admin/users#create
            new_admin_user GET    /admin/users/new(.:format)                 admin/users#new
           edit_admin_user GET    /admin/users/:id/edit(.:format)            admin/users#edit
                admin_user GET    /admin/users/:id(.:format)                 admin/users#show
                           PUT    /admin/users/:id(.:format)                 admin/users#update
                           DELETE /admin/users/:id(.:format)                 admin/users#destroy
                     users GET    /users(.:format)                           users#index
                           POST   /users(.:format)                           users#create
                  new_user GET    /users/new(.:format)                       users#new
                 edit_user GET    /users/:id/edit(.:format)                  users#edit
                      user GET    /users/:id(.:format)                       users#show
                           PUT    /users/:id(.:format)                       users#update
                           DELETE /users/:id(.:format)                       users#destroy

このルーティングは、config/routes.rbに記述したルーティングよりも後ろに追加されます。

これ必要?

実際、こんなのは少し複雑なルーティングが必要になったらすぐ破綻してしまうので、半分ぐらいジョークgemなわけですが、一応必要な部分だけ自分で書けばいいのでそんなに困りません。
また、今はコントローラのファイルの存在しか見ていませんが、中も見るようにすれば、Sinatra的なやり方も実現できそうです。
scaffoldを使うと自動的にroutes.rbにも追記されますが、それよりはデフォルトがこのしくみのほうがいいのではと思いますがどうでしょうか?

きっかけ

江島さんのツイート。

*1:昔はデフォルト設定が最後の行に書かれていたのですが、非推奨なので今はコメントアウトされています。これはactionがパスに含まれているので使うべきではありません。

次期RailsがPATCHメソッドを採用

先月、Railsのブログにこんな記事が載りました。

なぜか当初とURLが変わっているようで、ブックマークは Edge Rails: PATCH is the new primary HTTP method for updates | Riding Rails のほうが多いです。(たしかこっちはブログのコメントもいっぱいついてたと思うんだが消えている…)

これは簡単に言うと、

ということなんですが、僕が記事を読んだ限りの解釈をもう少し詳しく説明したいと思います。

PUTは冪等

Railsのroutes.rbで

resources :users

と書くと、いくつかのルーティングが生成されますが、その中に

PUT /users/:id(.:format) {:action=>"update", :controller=>"users"}

というものができます。リソース更新のため、HTTP PUTメソッドを使うとアクションメソッドupdateが呼び出されるというものです。
“RESTful”で一見問題ないように見えますが、そもそもPUTメソッドはどういう性質のものなのでしょうか。これはRFC2616に規定されています。

9.1.2 冪等{idempotent} なメソッド

メソッドは、(エラーや期限切れ発行とは別に) 同一のリクエストの N > 0 の副作用が単一のリクエストにおけるものと同じであるような際には "冪等{idempotence}" の性質を持つ事もできる。 GET, HEAD, PUT, DELETE 各メソッドはこの性質を共有する。

http://www.studyinghttp.net/cgi-bin/rfc.cgi?2616#Sec9.1.2

PUTは冪等なメソッドということです。「冪等(べきとう)」というのは数学的な用語で少し難しいですが、この説明がわかりやすいです。

例えば、ブラウザからある HTML ファイルに GET リクエストを行ったとします。
この場合、その HTML 文書がブラウザに出力されるでしょう。
この時、もう一度同じファイルに同じ GET リクエストを行ったとしたら、その結果はどうなるでしょうか?
その結果は、当然先程と同じ文書が、同じようにブラウザに出力される事でしょう。
一般に「同じデータについて何度操作が行なわれても、同じ結果になるような操作」の事を冪等な操作と言います。
すなわち、リソースを取得するための GET、リソースに関するヘッダを取得するための HEAD、リソースをアップロードするための PUT、リソースを削除するための DELETE 等は、同じメソッドを何度繰り返しても同様な結果を返すはずなので、これらは冪等なメソッドであると言えます。

http://www.studyinghttp.net/method

冪等だと何がうれしいのかということですが、ものすごく大ざっぱに言うと「リンク・ボタンを連打してもだいじょうぶ」みたいな感じです。例えばモバイル環境などネットワークが不安定でレスポンスがちゃんと返ってこなかった、リクエストが受け付けられたかどうかわからない、という場合でも、冪等が保証されているメソッドであればただもう一度リクエストを送ればいいのです。

updateは冪等?

さて、Railsのupdateアクションは本当に冪等でしょうか?
それはまあ「実装による」わけですが、多くの場合Railsのモデルではcreated_at, updated_atというタイムスタンプ機能を利用します。この機能では更新日時は自動的に書き込まれます。そして、その日時はGETで取得できたり、リソース内のデータとして普通に使っています。
ということは、PUTを2回実行したら、日時が違うわけだから違うデータになってしまいます。あれ、冪等じゃないじゃん。困ったね。

冪等でない更新はPATCH

そこで、昔に提案されていたHTTPメソッドがPATCHです。
PATCHは、部分更新のためのメソッドですが、冪等でないと規定されています*1。ちょうどいいね、ということで、次のバージョンのRails 4ではupdateアクションはPATCHメソッドに結びつくようになりました。ただし、互換性を考えて、PUTメソッドでもupdateアクションが呼び出されます。ルーティングは

PATCH|PUT /users/:id(.:format) {:action=>"update", :controller=>"users"}

となるでしょう。

本当にそれは適切なのかどうか

説明は以上です。この変更は概ね好意的に受け止められているようです。
ただし、上記ではPATCH採用のもう1つの理由である「リソースの部分更新」あたりの説明をあえて省略しました。これはちょっと個人的に思うところがあるからです。

という感じなのですが、この詳細は次回に続くということで…。

*1:冪等であってはいけない、という意味ではない。冪等でもよい

複数のリソースに一度にアクセスしたいときのURL設計

RESTとRailsスタイル]」のときに、@shu_0115さんから「複数同時に書き込みたいときはどうするか」という質問がありました。これは実用上はなかなか重要な点だと思うので、少しまとめます。

親子関係のリソースを更新

例えば /users/123 と /users/123/profile を両方変更したいなど。
この場合は、親に対するリクエス

PUT /users/123

だけですませるのが一般的です。POSTでユーザを新規作成するときも、自動的に子のリソースが作られたとみなしますよね。

複数のMemberリソースを更新

例えば /posts/1 /posts/2 /posts/3 の3つの投稿に同時に「rest」タグをつける、というUIがあるかもしれません。
この場合、とくにアトミックな必要はないので、Ajaxでリクエストを3回送ってもかまわないのですが、3個ならまだしも10個などになるとひどいので、パフォーマンスの観点から、そういう用途が必要ならCollectionリソースを使って

POST /posts

で、リクエストボディでIDとタグデータを送る、というのがいいと思います。このようなインターフェイスをバルク更新もしくはバッチ更新といいます。

複数のMemberリソースを取得

実は、複数のMemberリソースは、更新(作成)よりも取得のほうが需要があります。例えばCollection内に非常に多くのMemberリソースがある状況で、特定の複数のリソースをIDからピックアップしたい、などです。10個ピックアップしたいときに10回リクエストするのはとても非効率です。
この場合は、IDを列挙するURLを使って

GET /posts/1,2,3

とすることができます(コンマの代わりにセミコロンでも可)。これをバルク取得といいます。
前節の更新は、このURLを使ってPUTメソッドで設計することもできます。

ステータスコード

バルク取得、バルク更新は、複数の処理が行われるということなので、一部だけエラーという可能性があります。アトミックな処理なら全部がエラーとすればいいのですが。
この場合は、しょうがないので 200 OK または 207 Multi-Status を返して、レスポンスボディ内にどれが成功してどれが失敗したのかを記述します。

複数のリソースをアトミックに作成・更新

トランザクションリソースを使います。
(詳細省略)

所感

Twitterは、バルク取得APIをつくってよ。1個1個取ってたらすぐAPI制限に引っかかるんだよ…。
と前から思っていたこともあって書いてみました。

第5回デザイナー向けプログラム部 #p4d に参加しました

昨年の10月ぐらいから参加している「デザイナー向けプログラム部」の第5回が開催されたので、エンジニア枠で行ってきました。
基本的には「気軽に教えてくれるメンター(先生役)のいるハッカソン」ということで、実際に手を動かしてやってみようというイベントなのですが、今回は時間を30分もらってRESTについて話をさせてもらいました。

「しっかり準備します」って言ったわりにはあまりちゃんと準備できなくて、スライドの完成度がいまいち、内容もまだ途中(ステータスコードの話が全くない)、という感じなのですが、質疑応答もあってちょうど30分ぐらいになりました。
対象がデザイナーということで、できるだけ専門用語を使わずに、重要なものだけ、とか思っていたのですがスライド見るとそんな感じはないですね…。「リソース」という言葉は使ったほうがよかったかも。

参加されたみなさんありがとうございました。すばらしい会場を提供していただいたKDDIウェブコミュニケーションズ様ありがとうございました。

次回は3/20に開催されます。プログラミングを学んでみたいデザイナーの方はぜひどうぞ。

sass-rails-bootstrap を使ったら職が(ry

twitter-bootstrap-railsがとても便利そうです。generatorでレイアウトの雛型が用意されてるのもすばらしい。
でもやっぱりLESSじゃなくてSassを使いたいんですよね。

ということでsass-rails-bootstrapというのを見つけたので、使えるかどうか同じ手順で試してみました。

railsアプリを作りましょう。

rvmでgem set作ります。

rvm --create 1.9.3-p125@tbrsample

railsアプリ生成します。

gem install rails --no-ri --no-rdoc
rails new tbrsample

twitter bootstrap railsを使ったら職が見つかり彼女も出来て背も3センチ伸びました。 - pblog

同じようにやります。

gemを指定しましょう。

gem "twitter-bootstrap-rails"

のかわりにこう。

gem 'sass-rails-bootstrap', :git => 'https://github.com/voidseeker/sass-rails-bootstrap.git'

bootstrapをgenerateしましょう。

必要なjsやcssをapp/assets配下に配置します。

generatorはないので、ソース GitHub - ppworks/tbrsample を参考にして、こんな感じにファイルを作成。

/* app/assets/stylesheets/bootstrap.css.sass */
@import "twitter/bootstrap/variables"
@import "twitter/bootstrap/bootstrap"
@import "twitter/bootstrap/responsive"

たぶんカスタマイズしたいときはこの中に書けばOK?

レイアウトファイルを作成しよう。

layouts/application.html.erbにひな形のレイアウトを作りましょう。 ここでは、スマートフォン対応もしたいなーと思うので、可変レイアウトにしようと思います。

これもソース tbrsample/application.html.erb at master · ppworks/tbrsample · GitHub から丸コピペ。

画面を見てみよう。

ここまででtwitter bootstrapの準備はokなので実際に画面を見てみましょう。 というわけで、とりあえず適当にページを作ります。

同じようにやります。
できました。


カスタマイズは試してません。

感想

やっぱり元のtwitter-bootstrap-railsのほうが便利ですね。とくにSassにこだわりがなければLESSでもいいし。
あと、Sass移植とは言ってもSCSSのものは他にいろいろあるようなので、そっちでもいいと思います。たぶんもっと使いやすいものがある可能性大。

@ppworks さんに勝手に便乗して書かせてもらいました。ありがとうございました!

RailsのURL設計を考えてみる(6) 設計の選択肢

今回は直接Railsとは関係ないのですが、先日こういうページを見つけました。

これは2009年に書かれたもののようですが、URL設計に関する重要な考え方がいろいろ書かれています。
その中に、このブログのシリーズの(4) スラッシュと「持っている」関係(5) Railsのリソースパターンの前段階になるような“Choosing a URI schemes for resource hierarchies”という話があったので、かいつまんで紹介したいと思います。

リソースの階層に対して、どういうURLを設計するか?

前提として、設計するWebサイトには、conversation(会話)というリソースが複数あり、それぞれのリソースはユニークなIDで区別されるとします。(IDは数字とは限らない識別子)

/conversations/conversation/{id}
  • /conversations ですべての会話
  • /conversation/{id} でID=idの会話

全体の会話は複数形、特定の1つの会話は単数形という考え方。
英語としては自然なのかもしれませんが、わざわざ使い分けるほどの意義は薄いし、後者でパスを削った /conversation は何を指すのかという問題が残ります。

/conversations/conversation/{id}
  • /conversations ですべての会話
  • /conversations/conversation/{id} でID=idの会話

先ほどと比べると1種類のパスに統合されましたが、やはり /conversations/conversation が何を指すのかが謎です。

/conversations/{id}
  • /conversations ですべての会話
  • /conversations/{id} でID=idの会話

Rails方式です。これでパスを削っても問題なくなりました。ただし、新しい会話を作成するための画面に用いる /conversations/new という補助的リソースが、ID=newのリソースと区別できなくなるという問題があります。これは比較的小さな問題ではありますが、前の2つの方式ではこの問題は起こりません。

/conversations/conversation-{id}
  • /conversations ですべての会話
  • /conversations/conversation-{id} でID=idの会話

それを解決した方法。ただしちょっと冗長ではあります。

深い階層

次に、それぞれのconversationがtodo list(やることリスト)というリソースを複数持っているとします。

/conversations/{cid}/todo-lists/{tid}
  • /conversations/{cid}/todo-lists でID=cidの会話が持つすべてのやることリスト
  • /conversations/{cid}/todo-lists/{tid} でID=cidの会話が持つID=tidのやることリスト

Rails方式。前節と同じ問題は引き継ぎます。
また、 /conversations/{cid}/todo-lists (やることリストの全体)は実際には必要なく、 /conversations/{cid} があればそれで十分なことがあります((日付をURLに入れるとき、わざわざ /years/2012/months/01/days/03 とする必要がないのにも似ています))。

/conversations/{cid}/{tid}
  • /conversations/{cid}/{tid} でID=cidの会話が持つID=tidのやることリスト

tidが何を指しているのかがよくわかりません。日付など明らかにわかるような場合なら短くていい方式でしょう。

/conversations/conversation-{cid}/todo-list-{tid}
  • /conversations/conversation-{cid}/todo-lists-{tid} でID=cidの会話が持つID=tidのやることリスト

前節の最後の方法を採用しつつ、todo listの全体はいらないのだから、そのパス階層だけなくした方式。この方式だと、全体のリソースと個別のリソースのどれを提供しているかが明確になります。

私見

Rails方式でいいじゃん(笑)
と言ってしまうと元も子もないのですが、こういうような議論を経て、今のRailsのリソースパターン(Memberリソース、Collectionリソースなど)が定まってきたのではないか、と想像できます。

Rails方式における /conversations/new の問題点については、実は過去には /conversations;new となっていた時期がありました。これなら問題は起きないのですが、セミコロンが不自然であまり受け入れられなかったのか、すぐにスラッシュに変わりました。
個人的な考えでは、こういう補助的なリソースは“_”で始めることとして、 /conversations/_new とすればいいのではと思っています*1。ただしRailsの規約で“_”はpartial viewを指すので、ちょっと相性が悪いかもしれません。

もちろん、Railsを使うときもこれらの方式のどれを実際に採用するかは自由です。routes.rbの書き方次第で、どのような方式にすることもできます。ただ、Railsのパターンに従ってresourcesで書くのが楽ですよ。

*1:GitHubがこの方式を採用しています