Mongoid Multiple Many-to-Many Relations (between two models)

Prerequisites

You know the basics of Ruby, MongoDB and Mongoid (and maybe a little bit Rails, but it's not important here, which framework you use).

You use a recent version of the Mongoid gem (~> 2.0.2), which has the has_and_belongs_to_many statement (earlier versions used an alternative syntax for references).

Notice Since version 2.1.x and newer this workaround isn't working around!

You have a lot of fun reading blog posts which mostly contain code snippets.

Case

We have two models: User and Group.

A group can have multiple members and admins.

A user can belong to multiple groups and also can administrate multiple groups.

How these relationships can be managed with Mongoid and its reference logic?

Solution

Using mongoid 2.2.x/2.3.x? Jump directly to new solution!

User model

class User
  include Mongoid::Document

  field :name
  field :email

  has_and_belongs_to_many :admin_of,  class_name: "Group"
  has_and_belongs_to_many :member_of, class_name: "Group"

end

We don't use the group model directly here, instead we define pseudonyms (:admin_of and :member_of), so defining a class_name is mandantory!

The User model is more on the belongs to side, so we must not use the :as statement here.

Group model

class Group
  include Mongoid::Document

  field :name

  has_and_belongs_to_many :admins,  as: :admin_of,  class_name: "User"
  has_and_belongs_to_many :members, as: :member_of, class_name: "User"

end

We define the pseudonyms :admins and :members, map them to the corresponding references of the User model. The :as statement will build a polymorphic structure internally, but we don't really use this. Of course, don't forget the class_name statement here, too.

Now we have a mixture of many-to-many references, custom relation names and polymorphism.

Seed

u1 = User.new(name: "peter foo", email: "peter@example.com")
u1.save!

u2 = User.new(name: "bjorn bar", email: "bjorn@example.com")
u2.save!

u3 = User.new(name: "john baz",  email: "john@example.com")
u3.save!

g1 = Group.new(name: "pbj common")
g1.admins << u1
g1.members.concat [u1,u2,u3]
g1.save!

g2 = Group.new(name: "pbj music")
g2.admins.concat [u1,u2]
g2.members.concat [u1,u2]
g2.save!

Check

u = User.first(conditions: {name: "peter foo"})
puts u.admin_of.map(&:name)
# => ["pbj masters", "pbj slaves"]

We also can build a admin_of? method to check against a single group.

class User
  # ...

  def admin_of? group
    self.admin_of.include?(group)
    # also works: group.admins.include?(self)
  end

end

Furthermore we could write an admin_of method for a single group assignment and rename the reference to admin_of_groups to avoid mind fucks. ;o)

Conclusion

With these models and their relationship levels it's possible to put multiple users in many groups as members and also as admins.

And we haven't to use a complex role system for this!

Conclusion 2 (2011-09-02)

I know, this example is very hacky, and since 2.1.x of mongoid not functional anymore. And furthermore I also prefer another solution to handle User<->Group relations.

So let's take this as an experiment and hack, but nothing to work with.

New solution (2011-10-30)

New example code working with mongoid 2.2.x / 2.3.x:

class User
  include Mongoid::Document

  field :name, type: String

  has_and_belongs_to_many :admin_of, inverse_of: :admins, class_name: 'Group'
  has_and_belongs_to_many :member_of, inverse_of: :members, class_name: 'Group'

end

class Group
  include Mongoid::Document

  field :name, type: String

  has_and_belongs_to_many :admins,  inverse_of: :admin_of, class_name: 'User'
  has_and_belongs_to_many :members, inverse_of: :member_of, class_name: 'User'

end

As you can see, the as statement isn't used anymore here (seems that it's only used for polymorphic relations now). Instead the inverse_of in combination with class_name has to be used to have a proper working relationship with better wording.

The origin answer could be found here: "mongoid self to self relationship?" (StackOverflow) - Thanks to SO-User Steve!


Found a mistake?

Have a better idea? A cooler solution?

Then: Please tell me! Write a comment here! Thanks!