0

I've been trying to solve this problem for a while now. Looked here, but it is not exactly what I need.

I have three models: User, Group, GroupMembership. User can be a teacher and a student, so user has different Roles through UserRoles table. Groups can have multiple teachers. What I want is something like this:

Role.rb
has_and_belongs_to_many :users  #this works fine

User.rb
has_and_belongs_to_many :roles  # this works fine

has_many :group_memberships
has_many :groups, through: :group_memberships
has_many :teachers, through: :group_memberships
has_many :students, through: :group_memberships

Group.rb
has_many :students, through: :group_memberships
has_many :teachers, through: :group_memberships

GroupMembership.rb
belongs_to :student
belongs_to :teacher
belongs_to :group

User with roles works fine, there is no problem with the different roles for user. The problem is with group memberships. The above is just something that I want to work, but in some cases I need to supply source or class name and I am not sure what exactly to do. And what kind of migrations should I create?

Community
  • 1
  • 1
Dan
  • 39
  • 11
  • How do you know a user is a teacher or a student? Is there a field that set the user type? – Anezio Campos Feb 02 '16 at 13:56
  • @AnezioCampos Yes, there is a table Roles and a join table UserRoles – Dan Feb 02 '16 at 14:05
  • @Dan add `type` field to `User`, and make two inherits from `User` to student and teacher. – Малъ Скрылевъ Feb 02 '16 at 14:10
  • do you plan to use role to detect difference between students and teachers? – Малъ Скрылевъ Feb 02 '16 at 14:27
  • 1
    @МалъСкрылевъ he is using Roles to distinguish between the different types of User. IMHO thats a much better alternative than STI as STI makes it impossible to join. – max Feb 02 '16 at 14:40
  • You need [`source:`](http://stackoverflow.com/a/4632472/1143732) option but I don't have massive experience with it – Richard Peck Feb 02 '16 at 15:11
  • @Max you can join with STI, nay? – Richard Peck Feb 02 '16 at 15:18
  • 1
    @RichPeck since STI relies on information in the DB on what kinds of classes to load and what kinds of relations they have its pretty hard to create effective joins VS the normal case. Not impossible - but makes everything very *interesting*. Its pretty much the same as when using polymorphic relations. – max Feb 02 '16 at 15:29

2 Answers2

0

I'll delete this if required; I'm pretty sure you'd benefit from using source: in your User and Group models:

#app/models/user.rb
class User < ActiveRecord::Base
   has_and_belongs_to_many :roles  # this works fine

   has_many :group_memberships
   has_many :groups,   through: :group_memberships

   has_many :teachers, through: :group_memberships, source: :teacher
   has_many :students, through: :group_memberships, source: :student
end

#app/models/group_membership.rb
class GroupMembership < ActiveRecord::Base
  belongs_to :student, class_name: "User"
  belongs_to :teacher, class_name: "User"

  belongs_to :group
end

#app/models/group.rb
class Group < ActiveRecord::Base
   ...
   has_many :students, through: :group_memberships, source: :student
   has_many :teachers, through: :group_memberships, source: :teacher
end

This is not tested & I don't have super experience with source so it could be wrong.

Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • 1
    You're on the right track - however using two different columns on the `GroupMembership` join table is not a very good solution. – max Feb 02 '16 at 15:26
  • Certainly; I'll leave it until the OP looks. Appreciate the feedback! – Richard Peck Feb 02 '16 at 15:28
0

The modeling is a bit off - GroupMembership should be acting as a many to many join model. That means that each user and group needs a GroupMembership to tie them together.

Also you would alter the relation between users and roles to be a has_many through: so that you can have metadata in the join model.

class User
  has_many :user_roles
  has_many :roles, through: :user_roles
  has_many :group_memberships
  has_many :groups, through: :group_memberships
end

class Role
  has_many :user_roles
  has_many :users, through: :user_roles
end

class UserRole
  belongs_to :user
  belongs_to :role
end

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

class GroupMembership
  belongs_to :user
  belongs_to :group
end

To query for group users with a specific role you could now use:

teachers = group.users.where(users: { roles: [Role.find_by(name: 'teacher')])

To make this a bit more convenient we can setup special relations with conditions:

class Group
  has_many :group_memberships
  has_many :users, through: :group_memberships
  has_many :teachers, ->{ where(users: { roles: [Role.find_by(name: 'teacher')] }) } through: :group_memberships, source: :user
end

Note that we need to specify source as it tells AR that "teacher" is in fact group_memberships.user.

max
  • 96,212
  • 14
  • 104
  • 165
  • First of all, thank you for your help. With your solution there would be a problem if a student of a certain group will also have a role as a teacher. So I need to distinguish where users in a group are students and when they are teachers. – Dan Feb 02 '16 at 17:53
  • You might want to take a look at Rolify and how they solved it – max Feb 02 '16 at 21:43
  • Rolify uses a polymorpic relationship between roles and the resource that the role applies to. So in your case you would either add a group_id to users_roles or make the relation polymorphic so that roles can be applied to different resources. – max Feb 02 '16 at 21:46