Reputation: 3160
Two models Organization and User have a 1:many relationship, where an organization has multiple users (members; a user can also not be associated to any organization):
class Organization < ActiveRecord::Base
has_many :users, dependent: :destroy
accepts_nested_attributes_for :users, :reject_if => :all_blank, :allow_destroy => true
validates_associated :users
end
class User < ActiveRecord::Base
belongs_to :organization, inverse_of: :users
end
Everything worked, all sorts of tests pass.
Now I added an additional relationship for a moderator function, where users can have moderator rights for (multiple) organizations. Hence, a many:many relationship through a third model, which I named Moderator:
class Organization < ActiveRecord::Base
has_many :users, dependent: :destroy
accepts_nested_attributes_for :users, :reject_if => :all_blank, :allow_destroy => true
has_many :moderators, class_name: "Moderator", foreign_key: "reviewee_id", dependent: :destroy
has_many :users, through: :moderators, source: :reviewer
validates_associated :users
end
class User < ActiveRecord::Base
belongs_to :organization, inverse_of: :users
has_many :moderators, class_name: "Moderator", foreign_key: "reviewer_id", dependent: :destroy
has_many :organizations, through: :moderators, source: :reviewee
end
class Moderator < ActiveRecord::Base
belongs_to :reviewee, class_name: "Organization"
belongs_to :reviewer, class_name: "User"
end
I intentionally used the reviewer
and reviewee
names. If I would just use user_id
and organization_id
in the Moderator model, I though this could mess things up. Because if you would then refer to @user.organization
then it wouldn't be defined which relationship to use. Would it use the 1:many relationship between user and organization, or the many:many through relationship...? By using different names for the many:many through relationship, @user.organization
should refer to the 1:many relationship, while @user.reviewee
for example should refer to the many:many through relationship.
Nevertheless, after this implementation, suddenly all sorts of tests fail. For example: I have a form that signs up an additional user for an organization. Clicing a button passes the organization_id to the form for which an additional user is to be created. Now suddenly this id doesn't get passed on to the form and I get all nil
error because the organization isn't defined (even though the link still is e.g. url/member?organization_id=43
). And I could give many more examples.
So there seems to be some kind of conflict because of the new relationship. Perhaps it fails to understand when to use the many:many through relationship and when the 1:many relationship, even though I used the differend reviewer and reviewee names... Have I modelled it incorrectly or is it impossible to have two different relationships between 2 of the same models?
If I remove the second has_many :users
line from the Organization model all tests pass again. So the problem seems to be that I have this relationship twice.
Upvotes: 1
Views: 107
Reputation: 102433
A good and common pattern for dealing with this is called resource scoped roles.
A User can have many roles (father, mother, moderator, hula-dancer etc) in some cases the Role is scoped to a particular resource. Like father/mother is scoped to a User (the child) or a moderator can be scoped to a Forum.
Having "system-level" roles like super-admin
which are not scoped to a resource is also common.
class User < ActiveRecord::Base
has_many :roles
scope :moderators, ->{ joins(:roles).where( roles: { name: 'moderator' } ) }
belongs_to :organization
end
# columns: name:string, resource_id:int, resource_type:string, user_id:int
class Role < ActiveRecord::Base
belongs_to :user
belongs_to :resource, polymorphic: true
end
class Organization < ActiveRecord::Base
has_many :roles, as: :resource
has_many :users
# This is just a relationship to users with a scope
has_many :moderators, -> { moderators }, class_name: 'User'
end
So to add a moderator we would do:
organization = Organization.find(1)
organization.roles.create(user: organization.users.find(1), name: 'moderator')
To get all moderators for a organization:
moderators = Organization.find(1).moderators
The awesome thing here is that we can use our Role class on any resource - not just an organization. Even better is that there are great gems to provide this functionality such as Rolify.
Upvotes: 1