【问题标题】:how to create symmetrical, DRY, RESTful interface for has_many :through relations?如何为 has_many 创建对称、DRY、RESTful 接口:通过关系?
【发布时间】:2012-04-26 06:11:47
【问题描述】:

假设一个标准的 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 中是否有任何提供指导的约定?或者这是否偏离了轨道,我应该以任何我认为合适的方式实现它?

【问题讨论】:

  • 我之前也为此苦苦挣扎,最终暴露了“会员”表。没有特别的理由,除了它似乎比替代方案更直观

标签: ruby-on-rails-3 rest routing has-many-through


【解决方案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

【讨论】:

    【解决方案2】:

    附录

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

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

    我还没有读过Roy Fielding's thoughts关于此事的信息。所以这是下一步。

    【讨论】:

      【解决方案3】:

      不管你如何在 Rails 中执行此操作,以 REST 方式执行此操作的关键是您可以拥有超链接。从一个人的角度来看,什么是会员列表,而不是指向他们所属俱乐部的链接列表? (好吧,每个都有一些额外的数据。)除了指向会员的链接列表(同样,可能还有一些额外的数据)之外,俱乐部的会员列表是什么?

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

      【讨论】:

      • 如何用 Rails 写这个……不知道,很抱歉!我编写的 RESTful 应用程序使用的是完全不同的框架(也不是同样意义上的数据库支持)。你必须自己解决这部分问题。
      猜你喜欢
      • 1970-01-01
      • 2015-09-21
      • 2021-09-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多