1

3つのモデル間の標準の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 / peoples /:person_id/membershipsと同じです。
  • とてもドライです。
  • 通常のユーザーには、Membership:idは表示されません。代わりに、:person_idと:club_idは、特定のメンバーシップを参照する複合キーとして機能します。
  • :idによるメンバーシップオブジェクトへの直接アクセスは許可されますが、これは管理者用に予約されています。

認識されるルートの一部を次に示します。

/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は複合キーをどのように処理するのか」として再構成できます。

/ customers /2141などのリソースを見つけるための単一のIDがある場合、RESTの哲学は明確です。リソースが複合キーによって一意に定義されている場合の動作はあまり明確ではありません。上記の例では、/ clubs /:club_idと/ peoples /:person_idがメンバーシップを一意に識別する複合キーを形成しています。

私はこの問題に関するロイ・フィールディングの考えを読んでいません。これが次のステップです。

于 2012-04-14T17:03:00.040 に答える
0

Railsでこれをどのように行うかに関係なく、これをRESTfullyに行うための鍵は、ハイパーリンクを設定できることです。個人の観点からのメンバーシップのリストとは何ですか?しかし、彼らがメンバーであるクラブへのリンクのリストは何ですか?(まあ、それぞれにいくつかの追加データがあります。)メンバーである人々へのリンクのリスト以外のクラブのメンバーシップのリストは何ですか(ここでも、おそらくいくつかの追加データがあります)?

それを考えると、個人の観点からのメンバーシップは、実際にはメンバーシップテーブルのビューです(同様に、クラブの観点からも、この抽象化レベルでは違いはありません)。トリッキーな点は、人のメンバーシップを変更するときに、ビューを介して変更を基になるテーブルにプッシュして戻す必要があることです。それでもRESTfulです。RESTfulリソースがあるからといって、直接変更の指示がない場合でもリソースが変更されないというわけではありません。(これにより、共有リソースなど、あらゆる種類の便利な機能が禁止されます!)実際、この領域でのRESTの本当の意味は、ホスティングサービスが明示的に許可しない限り、クライアントがすべてを安全にキャッシュできると想定してはならないということです。 (適切なメタデータ/ HTTPヘッダーを介して)。

于 2012-04-14T17:35:41.503 に答える