When you have different models sharing the same join table in Ruby on Rails, you can create a polymorphic has_many through relationship without a hassle.

The Problem

Let’s suppose that you have Task, Project and Group models and all have them have multiple members, i.e. Users. One solution is to define a seperate membership model for all models :

class User
  has_many :group_memberships
  has_many :groups, through: :group_memberships
  has_many :project_memberships
  has_many :projects, through: :project_memberships
  # etc...
end

class Project
  has_many :project_memberships
  has_many :users, through: :project_memberships
end

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

Yet in this solution, you’re clearly violating DRY principle, repeating common code in different models.

The Solution

A better solution might be having a polymorphic membership model for all models which have a membership :

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :memberable, polymorphic: true
end

class Project < ActiveRecord::Base
  has_many :memberships, as: :memberable, dependent: :destroy
  has_many :users, through: :memberships
end

class Group < ActiveRecord::Base
  has_many :memberships, as: :memberable, dependent: :destroy
  has_many :users, through: :memberships
end

class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, through: :memberships, source: :memberable, source_type: 'Group'
  has_many :projects, through: :memberships, source: :memberable, source_type: 'Project'
end

The reason we’re using source and source_type on User model for polymorphic has_many through is Rails doesn’t know which association to look for on the join model without knowing the type of source.

Yet we can further DRY our code by writing a concern instead of repeating same code on models :

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :memberable, polymorphic: true
end

module Memberable
  extend ActiveSupport::Concern
  included do
    has_many :memberships, as: :memberable, dependent: :destroy
    has_many :users, through: :memberships
    after_create :create_membership
  end

  def create_membership
    Membership.create(memberable_type: self.class.name, memberable_id: self.id, user_id: self.creator_id)
  end
end

class Project < ActiveRecord::Base
  include Memberable
end

class Group < ActiveRecord::Base
  include Memberable
end

class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, through: :memberships, source: :memberable, source_type: 'Group'
  has_many :projects, through: :memberships, source: :memberable, source_type: 'Project'
end
             

Note that since Membership is not created automatically on new Group or Project creation, we’re using an `after_create` callback for the person who creates a group or project, thus making them members.

This way, you can call `@user.first.groups` or `user.first.projects` conveniently from the same membership model, without repeating yourself.

Happy coding.