web-dev-qa-db-ja.com

has_many:through関係の子オブジェクトのフィルタリングRails 3

ご挨拶、

CompaniesUsersCompanyMembershipモデルを介して相互に属する必要があるアプリケーションがあります。このモデルには、メンバーシップに関する追加情報(具体的には、ユーザーがブール値adminを介した会社の管理者)。コードの単純なバージョン:

class CompanyMembership < ActiveRecord::Base
  belongs_to :company
  belongs_to :user
end

class Company < ActiveRecord::Base
  has_many :company_memberships
  has_many :users, :through => :company_memberships
end

class User < ActiveRecord::Base
  has_many :company_memberships
  has_many :companies, :through => :company_memberships
end

もちろん、これにより、company.users.allなどを介して会社のすべてのメンバーを簡単に取得できます。ただし、その会社の管理者である会社のすべてのユーザーのリストを取得しようとしています(また、ユーザーが特定の会社の管理者であるかどうかをテストしようとしています)。私の最初の解決策は、company.rbで次のとおりでした。

def admins
  company_memberships.where(:admin => true).collect do |membership|
    membership.user
  end
end

def is_admin?(user)
    admins.include? user
end

これは機能しますが、何かが非効率に感じられ(各メンバーシップを繰り返し、毎回SQLを実行しますか?それともRelationはそれよりも賢いですか?)、これを実行するためのより良い方法があるかどうかはわかりません(おそらくRails 3が使用する?)スコープまたは新しいRelationオブジェクト。

続行するための最良の方法(できればRails 3つのベストプラクティスを使用))に関するアドバイスをいただければ幸いです。

16
Michelle Tilley

私はこれを間違った方法で行っていたと思います。実際に必要だったusersの代わりに_company_memberships_の条件を指定しました(UsersのリストではなくCompanyMembershipsのリスト)。私が探していたと思う解決策は次のとおりです。

_users.where(:company_memberships => {:admin => true})
_

これにより、次のSQLが生成されます(IDが1の会社の場合)。

_SELECT "users".* FROM "users"
  INNER JOIN "company_memberships"
    ON "users".id = "company_memberships".user_id
  WHERE (("company_memberships".company_id = 1))
    AND ("company_memberships"."admin" = 't')
_

必要かどうかはまだわかりませんが、includes()メソッドは、必要に応じてSQLクエリの数を抑えるために積極的な読み込みを実行します。

Active Recordを使用すると、ロードされるすべての関連付けを事前に指定できます。これは、_Model.find_呼び出しのincludesメソッドを指定することで可能になります。インクルードを使用すると、Active Recordは、指定されたすべての関連付けが、可能な限り最小限のquerys.queriesを使用してロードされるようにします。 RoRガイド:ActiveRecordクエリ

(これが最善/最も効果的/正しい方法ではないと考える人からの提案はまだ受け付けています。)

17
Michelle Tilley

さらにクリーンな方法は、次のような関連付けを会社モデルに追加することです。

has_many :admins, :through => :company_memberships, :class_name => :user, :conditions => {:admin => true}

正確な構文を正しく取得するには、 Rails doc を掘り下げる必要があるかもしれません。

ビューで参照する可能性のある:userに関連付けられた他のクラスがない限り、:includeは必要ありません。

8
Ed Haywood

このようなものはどうですか:

Company.find(:id).company_memberships.where(:admin => true).joins(:user)
3
Ed Haywood

私はこの答えに出くわし、今日ではhas_many association scopesを使用するより良い方法があると信じています( has_manyドキュメント ):

has_many :admins, -> { where(admin: true) }, through: :company_memberships, class_name: :user

Has_manyアソシエーションの2番目のパラメーターは、フィルターを含むprocまたはlambdaにすることができます。

2
snrlx

これを行うために activerecord_where_assoc gemを作成しました。

これにより、結合やインクルードは不要です。積極的な読み込みを行うことは、義務ではなく、あなたの選択のままです。

users.where_assoc_exists(:company_memberships, admin: true, company_id: @company.id)

会社をどのようにフィルタリングするかについての質問は不明確なので、私はそれを自分で追加しました。これがないと、ユーザーが複数の会社に所属できる場合、ある会社の管理者であるということは、別の会社の管理者として扱われることを意味する可能性があります。

このgemはRails 3では機能しませんが、9年後、Rails 4.1以降がサポートされています。

はじめに です。詳細については、 ドキュメント をご覧ください。

0
Maxime Lapointe