假设标准has_many:通过三个模型之间的关系
class Person < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :clubs, :through => :memberships
end
class Club < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :persons, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :club
end
在API驱动的应用程序中,您希望公开以下URI:
我的第一个想法是实现一对映射到MembersController的嵌套路由,如:
GET /clubs/:club_id/memberships => members_controller#index
GET /persons/:person_id/memberships => members_controller#index
......但这里有点奇怪。
两个路由都映射到相同的members_controller方法(索引)。这没问题 - 我可以查看params
哈希,看看是否给出了:club_id或a:person_id,并在members_controller表上应用了适当的范围。
但我不确定我们是否希望将Member对象暴露给最终用户。更直观的一对路线(至少从用户的角度来看)可能是:
GET /clubs/:club_id/persons
GET /persons/:person_id/clubs
......将返回一份人员名单和一份俱乐部名单(分别)。
但是如果你这样做,你会将这些路线映射到哪个控制器和动作? Rails中是否有任何提供指导的约定?或者这是否偏离了轨道,我应该以任何我认为合适的方式实施它?
答案 0 :(得分:1)
我最终实现了完成以下任务的路由和控制器:
以下是它识别的一些路线:
/persons/:person_id/clubs/:club_id/memberships - a specific membership association
/clubs/:club_id/persons/:person_id/memberships - equivalent
/persons/:person_id/clubs - all the clubs that a specific person belongs to
/clubs/:club_id/persons - all the persons that belong to a specific club
/memberships/:id - access to a specific membership (accessible only to admin)
请注意,在以下示例中,我没有包含用于身份验证和授权的Devise和CanCan构造。它们很容易添加。
这是路由文件:
# file: /config/routes.rb
Clubbing::Application.routes.draw do
resources :persons, :except => [:new, :edit] do
resources :clubs, :only => :index
end
resources :clubs, :except => [:new, :edit] do
resources :persons, :only => :index
end
resources :memberships, :except => [:new, :edit]
# there may be clever ways to specify these routes using #resources and
# #collections and #member, but this ultimately is more straightforward
match "/persons/:person_id/clubs/:club_id/memberships" => "memberships#create", :via => :post
match "/persons/:person_id/clubs/:club_id/memberships" => "memberships#show", :via => :get
match "/persons/:person_id/clubs/:club_id/memberships" => "memberships#update", :via => :put
match "/persons/:person_id/clubs/:club_id/memberships" => "memberships#destroy", :via => :delete
match "/clubs/:club_id/persons/:person_id/memberships" => "memberships#create", :via => :post
match "/clubs/:club_id/persons/:person_id/memberships" => "memberships#show", :via => :get
match "/clubs/:club_id/persons/:person_id/memberships" => "memberships#update", :via => :put
match "/clubs/:club_id/persons/:person_id/memberships" => "memberships#destroy", :via => :delete
控制器非常简单。对于PersonsController和ClubsController,唯一不标准的是:index方法,我们在参数和范围中寻找:club_id或:person_id:
# file: /app/controllers/persons_controller.rb
class PersonsController < ApplicationController
respond_to :json
before_filter :locate_collection, :only => :index
before_filter :locate_resource, :except => [:index, :create]
def index
respond_with @persons
end
def create
@person = Person.create(params[:person])
respond_with @person
end
def show
respond_with @person
end
def update
if @person.update_attributes(params[:person])
end
respond_with @person
end
def destroy
@person.destroy
respond_with @person
end
private
def locate_collection
if (params.has_key?("club_id"))
@persons = Club.find(params[:club_id]).persons
else
@persons = Person.all
end
end
def locate_resource
@person = Person.find(params[:id])
end
end
# file: /app/controllers/clubs_controller.rb
class ClubsController < ApplicationController
respond_to :json
before_filter :locate_collection, :only => :index
before_filter :locate_resource, :except => [:index, :create]
def index
respond_with @clubs
end
def create
@club = Club.create(params[:club])
respond_with @club
end
def show
respond_with @club
end
def update
if @club.update_attributes(params[:club])
end
respond_with @club
end
def destroy
@club.destroy
respond_with @club
end
private
def locate_collection
if (params.has_key?("person_id"))
@clubs = Person.find(params[:person_id]).clubs
else
@clubs = Club.all
end
end
def locate_resource
@club = Club.find(params[:id])
end
end
MembershipsController只是稍微复杂一点:它在参数哈希中检测到:person_id和/或:club_id并相应地应用范围。如果同时存在:person_id和:club_id,我们可以假设它引用了唯一的成员资格对象:
# file: /app/controllers/memberships_controller.rb
class MembershipsController < ApplicationController
respond_to :json
before_filter :scope_collection, :only => [:index]
before_filter :scope_resource, :except => [:index, :create]
def index
respond_with @memberships
end
def create
@membership = scope_collection.create(params[:membership])
respond_with @membership
end
def show
respond_with @membership
end
def update
if @membership.update_attributes(params[:membership])
end
respond_with @membership
end
def destroy
@membership.destroy
respond_with @membership
end
private
# apply :person_id and/or :club_id scoping if present in params hash
def scope_collection
@memberships = scope_by_parameters
end
def scope_resource
@membership = scope_by_parameters.first
end
def scope_by_parameters
scope_by_param_id(scope_by_param_id(Membership.scoped, :person_id), :club_id)
end
def scope_by_param_id(relation, scope_name)
(id = params[scope_name]) ? relation.where(scope_name => id) : relation
end
end
答案 1 :(得分:0)
(不是答案 - 只是观察)。这个问题的大部分都可以重新定义为“REST如何处理复合键?”
当您使用单个ID查找资源(如/ customers / 2141)时,REST理念很明确。当资源由复合键唯一定义时,做什么不太清楚:在上面的示例中,/ clubs /:club_id和/ persons /:person_id形成唯一标识成员资格的复合键。
关于此事,我还没有看过Roy Fielding's thoughts。这就是下一步。
答案 2 :(得分:0)
不管你在Rails中如何做到这一点,RESTful的关键是你可以拥有超链接。从一个人的角度来看会员名单是什么,但是他们所属的俱乐部的链接列表是什么? (好吧,每个都有一些额外的数据。)俱乐部的会员资格列表是什么,而不是作为会员的人的链接列表(同样,可能还有一些额外的数据)?
鉴于此,从一个人的角度来看,成员资格实际上是对会员资格表的看法(同样从俱乐部的角度来看; 在这个抽象层面没有区别)。棘手的是,当您更改某个人的成员资格时,您必须将更改通过视图推送回基础表。那仍然是RESTful;拥有RESTful资源并不意味着当没有给出直接的更改指令时资源永远不会改变。 (这会禁止各种有用的东西,比如共享资源!)确实,REST在这个领域的真正含义是客户端不应该假设它可以安全地缓存所有东西,除非托管服务明确说明它可以(通过合适的元数据/ HTTP标头)。