RailsでのfavoriteのURL設計

http://d.hatena.ne.jp/r7kamura/20110505/1304577667がすごいなと思って、routes.rbの書き方の例についてコメントしたのですが、自分で書いておいて後で「unfavorite」はちょっとまずいかなと思ったので、favorite(いわゆるお気に入り、スター)はどういうふうに設計すればいいのか考えてみました。

構造はよくある感じの、

tweet has_many favorites
user has_many favorites

任意のツイートに任意のユーザーがお気に入りをつけられるというもの。別にツイートじゃなくても何でもOKです。

ブログのコメントにはこのように書きました。
(1)

resources :tweets do
  member do
    post 'favorite'
    post 'unfavorite'
  end
end

ルーティングはこうなります。

  favorite_tweet POST   /tweets/:id/favorite(.:format)   {:controller=>"tweets", :action=>"favorite"}
unfavorite_tweet POST   /tweets/:id/unfavorite(.:format) {:controller=>"tweets", :action=>"unfavorite"}
          tweets GET    /tweets(.:format)                {:controller=>"tweets", :action=>"index"}
                 POST   /tweets(.:format)                {:controller=>"tweets", :action=>"create"}
       new_tweet GET    /tweets/new(.:format)            {:controller=>"tweets", :action=>"new"}
      edit_tweet GET    /tweets/:id/edit(.:format)       {:controller=>"tweets", :action=>"edit"}
           tweet GET    /tweets/:id(.:format)            {:controller=>"tweets", :action=>"show"}
                 PUT    /tweets/:id(.:format)            {:controller=>"tweets", :action=>"update"}
                 DELETE /tweets/:id(.:format)            {:controller=>"tweets", :action=>"destroy"}

何がまずいのかというと、「"/tweets/:id/unfavorite"というURLがリソースの名前(基本的に名詞)になっていない」ということ。極端に言うと、GETできないURLがあったら何か変だというサインと思うのがいいかも。
"favorite"であれば名詞だし、これをPUTやDELETEすれば目的はかなうので、このようにするのがいいでしょう。
(2)

resources :tweets do
  resource :favorite, :only => [:update, :destroy]
end

ここで中のメソッドが単数形"resource"なのに注意。ユーザが持つfavoriteはtweetごとに1個ずつしかないので、ここは単数リソースを使います*1
:only をつけているのは、とりあえずPUTとDELETEしか必要ではないという理由です。さっきGETできないと変って言ってたんですが、例えばfavoriteをつけた日時などの属性情報を取得したいというニーズが出てくれば、GETで取得するのが自然なので、そのとき :only に :show を付け加えればいいわけです。

これによって、生成されるルーティングは以下の通り。

tweet_favorite PUT    /tweets/:tweet_id/favorite(.:format) {:controller=>"favorites", :action=>"update"}
               DELETE /tweets/:tweet_id/favorite(.:format) {:controller=>"favorites", :action=>"destroy"}
        tweets GET    /tweets(.:format)                    {:controller=>"tweets", :action=>"index"}
               POST   /tweets(.:format)                    {:controller=>"tweets", :action=>"create"}
     new_tweet GET    /tweets/new(.:format)                {:controller=>"tweets", :action=>"new"}
    edit_tweet GET    /tweets/:id/edit(.:format)           {:controller=>"tweets", :action=>"edit"}
         tweet GET    /tweets/:id(.:format)                {:controller=>"tweets", :action=>"show"}
               PUT    /tweets/:id(.:format)                {:controller=>"tweets", :action=>"update"}
               DELETE /tweets/:id(.:format)                {:controller=>"tweets", :action=>"destroy"}

と言ったけど

最初作るときは、あんまり深く考えず最初の(1)でもいいんではないかと。(1)から(2)はそんなに大きな変化ではないので、あとで見直して(2)に変更する手間もそんなにかからないと思います。

最初に大切なこと

それよりも、「できるだけresourcesを使う」ことのほうが大切だと思います。
routes.rbについて説明したページをいくつか読んだけど(代表はRails Routing from the Outside In — Ruby on Rails Guidesかな)、だいたい最初に基本という感じでmatchを説明しています。たしかに基本ではあるし直感的にわかりやすいのですが、多くの場合resourcesで用は足りるし、そういうときにmatch(get, post)を使うべきではありません*2
まずresourcesに沿う形でURL設計を考えてみるのが「Railに乗りやすくなる」方法ではないかと思います。

参考文献

いつもの信頼の2冊。

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

RESTful Webサービス

RESTful Webサービス

*1:全体としてはfavoriteは複数あるし、現在のユーザに依存するとpublicなURLとしてふさわしいといえなくなるので、これについては議論の余地はある

*2:もちろんリソースを理解してる人はガシガシmatchを使うこともあると思いますが