Filtering child objects in a has_many :through relationship in Rails 3

Viewed 28729

Greetings,

I have an application where Companies and Users need to belong to each other through a CompanyMembership model, which contains extra information about the membership (specifically, whether or not the User is an admin of the company, via a boolean value admin). A simple version of the code:

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

Of course, this makes it simple to get all the members of a company via company.users.all, et al. However, I am trying to get a list of all Users in a Company who are admins of that Company (and also to test whether a user is an admin of a given company). My first solution was the following in company.rb:

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

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

While this works, something feels inefficient about it (it's iterating over each membership, executing SQL each time, right? Or is Relation smarter than that?), and I'm not sure if there's a better way to go about this (perhaps using scopes or the fancy new Relation objects that Rails 3 uses?).

Any advice on the best way to procede (preferably using Rails 3 best practices) would be greatly appreciated!

5 Answers

I stumbled across this answer and believe that nowadays there is a nicer way using has_many association scopes (has_many documentation):

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

The second parameter of a has_many association can be a proc or lambda that contains your filter.

I made the activerecord_where_assoc gem to do this.

With it, no need for joins or includes. Doing eager loading remains your choice, not an obligation.

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

The question is unclear on how to filter the company, so I added it myself. Without this, if a user can be in multiple company, it being admin in one company could mean being treated as an admin in another company.

The gem doesn't work in Rails 3, but we are 9 years later and Rails 4.1 and more are supported.

Here are the introduction and examples. Read more details in the documentation.

Related