1

假设一个标准的 has_many :through 三个模型之间的关系

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:

  • 列出人 x 所属的俱乐部
  • 列出属于俱乐部 y 的成员
  • (......以及通常的 CRUD 方法集合......)

我的第一个想法是实现一对映射到 MembersController 的嵌套路由,例如:

GET /clubs/:club_id/memberships     => members_controller#index
GET /persons/:person_id/memberships => members_controller#index

...但是这里有点奇怪。

两条路由都映射到相同的 members_controller 方法(索引)。没问题——我可以查看params哈希以查看是否给出了 :club_id 或 :person_id ,并在 members_controller 表上应用适当的范围。

但我不确定我们是否要向最终用户公开 Member 对象。一对更直观的路线(至少从用户的角度来看)可能是:

GET /clubs/:club_id/persons   
GET /persons/:person_id/clubs 

...这将返回一个人员列表和一个俱乐部列表(分别)。

但是,如果您这样做,您会将这些路由映射到哪个控制器和操作?Rails 中是否有任何提供指导的约定?或者这是否偏离轨道足够远,我应该以任何我认为合适的方式实施它?

4

3 回答 3

1

我最终实现了完成以下任务的路由和控制器:

  • 它是完全 RESTful 的。
  • 它是完全对称的: /persons/:person_id/clubs/:club_id/memberships 与 /clubs/:club_id/persons/:person_id/memberships 相同
  • 它非常干燥。
  • 普通用户永远不会看到会员身份:id。相反, :person_id 和 :club_id 用作引用特定成员资格的复合键。
  • 它确实允许通过 :id 直接访问 Membership 对象,但这是为管理员保留的。

以下是它识别的一些路线:

/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
于 2012-04-19T06:56:39.613 回答
0

附录

(不是答案——只是观察)。这个问题的大部分可以重新定义为“REST 如何处理复合键?”

当您使用单个 ID 来定位资源(例如 /customers/2141)时,REST 理念很明确。当资源由复合键唯一定义时,我们不太清楚该怎么做:在上面的示例中,/clubs/:club_id 和 /persons/:person_id 形成唯一标识成员资格的复合键。

我还没有读过罗伊菲尔丁对此事的看法。所以这是下一步。

于 2012-04-14T17:03:00.040 回答
0

不管你如何在 Rails 中做到这一点,以 RESTful 方式做到这一点的关键是你可以拥有超链接。从一个人的角度来看,什么是会员列表,而不是指向他们所属俱乐部的链接列表?(好吧,每个都有一些额外的数据。)除了指向会员的链接列表之外,俱乐部的会员列表是什么(再次,可能带有一些额外的数据)?

鉴于此,从一个人的角度来看,会员资格实际上是 Membership 表上的一个视图(从俱乐部的角度来看也是如此;在这个抽象级别上没有区别)。棘手的一点是,当您更改一个人的成员资格时,您必须通过视图将更改推送回基础表。那仍然是 RESTful 的;拥有 RESTful 资源并不意味着在没有直接指示更改时资源永远不会更改。(这将禁止各种有用的东西,例如共享资源!)事实上,REST 在这方面的真正含义是客户端不应该假设它可以安全地缓存所有内容,除非托管服务明确表示它可以(通过合适的元数据/HTTP 标头)。

于 2012-04-14T17:35:41.503 回答