Reputation: 26313
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).
Upvotes: 4
Views: 2679
Reputation: 136
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
Upvotes: 0
Reputation: 13852
users = Group.first.users.select("*, memberships.data as memberships_data, users.*")
That will give you access to everything
Upvotes: 0
Reputation: 1
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:
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
Upvotes: 0
Reputation: 1396
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
Upvotes: 2