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

考えてみる

どういう設計がいいかはいろんな考えがあると思うので、アイデアください。

*1:URLは必要な部分だけ抜き出し、パラメータをRailsのルーティング記述風に書き直した

*2:http://dev.twitter.com/doc

*3:Webを支える技術」p.242, 310

*4:思いつきだがテーブル(モデル)に着目するよりリレーションに着目したほうがいいような気がしてきた

*5:Webを支える技術」p.299