简体   繁体   中英

How can I access a has_many :through association object's relevant join model without additional queries?

This is something I've come across a number of times now and I'd love to either figure out how to do what I'm wanting or build and submit a patch to Rails that does it. Many times in my apps I'll have some models that look something like this:

class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, through: :memberships
end

class Membership
  belongs_to :user
  belongs_to :group

  def foo
    # something that I want to know
  end
end

class Group
  has_many :memberships
  has_many :users, through: :memberships
end

What I want to be able to do is access the relevant membership from a call to the association without doing additional queries. For instance, I want to do something like this:

@group = Group.first
@group.users.each do |user|
  membership = user.membership # this would be the membership for user in @group
end

Is there anything in Rails that allows this? Because the only methods I know to achieve the result I'm talking about are terribly ugly and inefficient, something like this:

@group.users.each do |user|
  membership = Membership.where(group_id: @group.id, user_id:user.id).first
end

Does ActiveRecord have some arcane in-built facility to achieve this? It seems like it wouldn't be too hard, it's already having to fetch the join model in order to properly retrieve the association anyway so if this functionality doesn't exist it seems to me it should. I've run into this a number of times and am ready to roll up my sleeves and solve it for good. What can I do?

Update: The other pattern for this that I could use that basically gets what I want is something like this:

@group.memberships.includes(:user).each do |membership|
  user = membership.user
end

But aesthetically I don't like this solution because I'm not actually interested in the memberships so much as I am the users and it feels wrong to be iterating over the join model instead of the association target. But this is better than the other way I pointed out above (thanks to Ian Yang of Intridea for reminding me of this one).

If you just want to access some attributes in membership, there is an ugly trick

group.users.select('*, memberships.some_attr as membership_some_attr')

It works because memberships is included in JOIN implicitly.

Update

What's more, if you add another ugly trick in User

class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, through: :memberships
  belongs_to :membership #trick, cheat that we have membership_id
end

Now:

group.users.select('*, memeberships.id as membership_id').includes(:membership).each do |user|
  membership = user.membership
end

You can do it this way:

Get all users within specific group memberships:

Where group_array is an array of IDs for groups that you want the @user to be a member of.

@user = User.all(
  include: :groups, conditions: ["memberships.group_id in (?)", group_array]
).first

Reverse it with:

@group = Group.all(
  include: :users, conditions: ["memberships.user_id in (?)", user_array]
).first
   users = Group.first.users.select("*, memberships.data as memberships_data, users.*")

这将使您能够访问所有内容

If the data you wish to access is an attribute on the join table, then includes is a pretty clean way to do it.

However, from your post it seems like you have a method on the membership that wants to do some intelligent work with the underlying data. Also, it seems like you want to do two things with one query:

  1. Output user information (which is why you're iterating on the user object in the first place, if you just wanted memberships you'd just iterate on those).
  2. Output some intelligent and processed information for your membership object.

As you've noticed, anything you do here feels weird because no matter where you put that code, it doesn't feel like the right model.

I usually identify this feeling as the need for another abstraction layer. Consider creating a new model called MembershipUsers (it's a terrible name, but you can think of a different one).

The following is my ad-hoc coding attempt that is untested but should give you an idea of the solution:

class MembershipUser < User
  def self.for_group(group)
    select('memberships.*, users.*').
    joins('join memberships on memberships.user_id = users.id').
    where('memberships.group_id = ?', group.id)
  end
  def foo
    # now you have access to the user attributes and membership attributes
    # and you are free to use both sets of data for your processing
  end
end

By creating a class that represents the User and their Membership to a specified Group, you've created a context where the foo method feels appropriate. I'm guessing that foo didn't mean much without being in the context of a specific user, and that you references the associated user in the foo method.

-Nick (@ngauthier)

EDIT: forgot to bring this full-circle:

class Group
  def membership_users
    MembershipUser.for_group(self)
  end
end

# then iterate
group.membership_users.each do |membership_user|
  membership_user.user_name # a pretend method on user model
  membership_user.foo # the foo method that's only on MembershipUser
end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM