Nick
Nick

Reputation: 3160

Modelling two different relationships between the same models

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

Answers (1)

max
max

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

Related Questions