views:

417

answers:

3

I've been stuck on this all day. I have a setup like the one below. I'm trying to define friends using the group_memberships association.

class User < ActiveRecord::Base
  has_many :group_memberships
  has_many :groups, :through => :group_memberships
  has_many :friends # what goes here? <<
end

class GroupMembership < ActiveRecord::Base
  belongs_to :user
  belongs_to :role
  belongs_to :group
end

class Role < ActiveRecord::Base
  has_many :group_memberships
end

class Group < ActiveRecord::Base
  has_many :group_memberships
  has_many :users, :through > :group_memberships
end

I'd like to do this without creating a join table for friends, unless it's completely crazy to do it without.

The group_membership table contains user_id and group_id linking one user to one group.

I'd trying to get

@user.friends

to return users with common group_memberships using the group_id.

has_many :friends, :through => :group_memberships, :source => :group

Nothing I've tried works, but I'll chalk that up to my complete misunderstanding of the above code.

+3  A: 

Unfortunately Rails doesn't let you nest has_many's more than 2 deep.. Forgetting about naming it friends for a moment (let's call it users instead), this would theoretically be what you'd want:

has_many :group_memberships
has_many :groups, :through => :group_memberships
has_many :users, :through => groups

Except that this doesn't work. If you try it you'll see this not-so-helpful error message which comes from this bit of code, specifically source_reflection.options[:through].nil?. That is, the through isn't allowed to have a through itself.

Instead, you may want to do something like this:

Solution 1

class User < ActiveRecord::Base
  has_many :group_memberships
  has_many :groups, :through => :group_memberships

  def friends
    groups.with_users.map(&:users).flatten.uniq.reject{|u| u == self}
  end
end

class Group < ActiveRecord::Base
  has_many :group_memberships
  has_many :users, :through => :group_memberships

  named_scope :with_users, :include => :users
end

Solution 2

Use the nested_has_many_through plugin that Radar mentioned. It looks like at least one fork of it on github has been updated to work on the latest Rails.

Solution 3 (just for kicks)

or, just for kicks, you could do it with one big SQL query:

class User < ActiveRecord::Base
  has_many :group_memberships
  has_many :groups, :through => :group_memberships

  def friends
    sql = <<-SQL
      SELECT users.* FROM users, (
        SELECT DISTINCT gm2.user_id AS user_id
        FROM group_memberships gm, groups g, group_memberships gm2
        WHERE gm.user_id = ? AND g.id = gm.group_id AND gm2.group_id = g.id AND gm2.user_id != ?
      ) AS user_ids
      WHERE users.id = user_ids.user_id
    SQL
    User.find_by_sql([sql, id, id])
  end
end
Jordan Brough
You don't have to do ugly sql. Check out my answer.
Ryan Bigg
The solution w/ ugly SQL was just for fun. The first solution just has a named scope and some filtering, no SQL at all. The plugin you mentioned does look cool though.
Jordan Brough
+1  A: 

Use the nested_has_many_through plugin.

Ryan Bigg
Link broken, try this: http://github.com/ianwhite/nested_has_many_through/tree/master
Aaron Hinni
A: 

delegate :users, :to => 'group'

Could you explain that a little more?
MediaJunkie
`delegate` would only work if it were `group`, not `groups`. Since `user.groups.users` doesn't work, `delegate :users, :to => 'groups'` wouldn't work either. http://api.rubyonrails.org/classes/Module.html#M000102
Jordan Brough