RailsでのURL設計を考えてみる(2) follow
前回の「RailsでのfavoriteのURL設計」が思いがけなくそこそこ見てもらったようなので、いろんなパターンのURL設計を考えてみるシリーズをやってみたいと思います。(続くかどうかは未定)
こんどはMioからは離れて、といってもほとんど同じようなものですが、Twitterのfollowのような機能を考えてみます。
Twitterの設計
考える前に、TwitterのWebサイトとAPIではフォロー関係の設計がどうなっているか参考に見てみましょう。*1
Webサイト
URL | |
---|---|
フォローしている ユーザ(ツイート) |
/:screen_name/following |
フォローしている ユーザ |
/:screen_name/following/people |
フォローされている ユーザ |
/:screen_name/followers |
API*2
URL | 追加パラメータ | |
---|---|---|
フォローしている ユーザ(ID) |
/friends/ids | :screen_name |
フォローされている ユーザ(ID) |
/followers/ids | :screen_name |
フォローする | (POST) /friendships/create | :screen_name |
フォローを外す | (DELETE) /friendships/destroy | :screen_name |
フォロー関係 | /friendships/show | :source_screen_name, :target_screen_name |
なんかこの2つはだいぶ違いますね…。「Webサービス(サイト)とWeb APIを分けて考えないことが大切」*3なんですが。
読み取りと書き込みがだいたい別になっているようです。読み取り系リソースは「following(friends)」「followers」。書き込み系は「friendships」というところでしょうか。できればどちらかに統一したいところです。
「following」「followers」は直感的で意味がわかりやすいですね。
「friendships」というリソースはモデル構造からきていて、これはフォローする人・される人というような多対多(m:n)の関係をモデリングするときに、
class Friendship < ActiveRecord::Base belongs_to :following, :class_name => 'User' belongs_to :follower, :class_name => 'User' end class User < ActiveRecord::Base has_many :friendships, :foreign_key => "follower_id" has_many :reverse_friendships, :foreign_key => "following_id", :class_name => "Friendship" has_many :followings, :through => :friendships, :source => :following # essence has_many :followers, :through => :reverse_friendships, :source => :follower ... end
のように「has_many :through」の定石として使われる中間モデルをほぼそのままリソースにしたものです(上記の場合自己参照なのでちょっと複雑ですが、essenceの行がポイントです)。
resourcesにマッピング
さて、どちらがいいのでしょう。モデルをもとにリソースをつくるとうまくいくことが多いのですが、この場合は「followings」「followers」でつくるのがいいのではないかと思います。つくってみるとこのようになります。
resources :users do resources :followings, :only => [:index, :show, :update, :destroy] resources :followers, :only => [:index, :show] end
ルーティングはこうなります。(users部分は省略)
user_followings GET /users/:user_id/followings(.:format) {:controller=>"followings", :action=>"index"} user_following GET /users/:user_id/followings/:id(.:format) {:controller=>"followings", :action=>"show"} PUT /users/:user_id/followings/:id(.:format) {:controller=>"followings", :action=>"update"} DELETE /users/:user_id/followings/:id(.:format) {:controller=>"followings", :action=>"destroy"} user_followers GET /users/:user_id/followers(.:format) {:controller=>"followers", :action=>"index"} user_follower GET /users/:user_id/followers/:id(.:format) {:controller=>"followers", :action=>"show"}
「フォローする」をPUT、「フォローを外す」をDELETEで表現しています。自然ですね。
なぜ「friendships」ではよくないのかという理由ですが、今回のパターンは「フォローする」「フォローされる」が別の意味を持っている(関係が片方向)ので「followings」「followers」という2つのリソースに分けたほうがいいと判断しました。これが等価である(関係は常に双方向)場合、単純に「friendships」だけでいいかもしれません。
モデルとリソース
もっと一般的には、少し考えた限りですがどうも正規化を進めていくとモデルとリソースが乖離してくる気がします。リレーショナルモデルは正規化していくことでほとんどid(外部キー)のみを持つテーブルができてきますが、こういうテーブルはリソースとして扱う意味がないことがあります*4。リソースが持つデータを決めるときも、あえて正規化を崩して冗長にデータを持たせるのが好ましいです*5。
考えてみる
どういう設計がいいかはいろんな考えがあると思うので、アイデアください。