We often have to represent many-to-many relationships between models in our applications. Rails provides a method in its migrations to generate a table in your database to support this. You can see the documentation in the Rails guide for ActiveRecord migrations.

However, these basic join tables often obscure a useful concept in your application that might be better represented as a named model.

Instead of…

…using a join table:

Migration

create_join_table :user , :organisation

app/models/user.rb

class User < ApplicationRecord has_and_belongs_to_many :organisations end

app/models/organisation.rb

class Organisation < ApplicationRecord has_and_belongs_to_many :users end

Use

…a real model and name the concept that the join table is hiding.

Migration

create_table :memberships do | t | t . references :user t . references :organisation end

app/models/membership.rb

class Membership < ApplicationRecord belongs_to :user belongs_to :organisation end

app/models/user.rb

class User < ApplicationRecord has_many :memberships has_many :organisations , through: :memberships end

app/models/organisation.rb

class Organisation < ApplicationRecord has_many :memberships has_many :users , through: :memberships end

Why?

Almost without fail, whenever a join model sits for any length of time in an application it begins to acquire behaviour. It’s nearly always worth having a first pass at naming the concept that the join model represents.

With a “basic” join table there is no way to add functionality to this unnamed concept. The lack of a place to put this extension means you might have to attach functionality to one of the joined models.

In the “join table” example above you might be forced to put a role attribute on the User , where a User ’s role is likely different for each organisation of which they’re a member. This need—for role information that belongs on the join table—demonstrates the requirement for a real Membership model.

Delaying the creation of the Membership concept will make for more refactoring later.

Why not?

There’s extra manual work, if you aren’t using the built-in functionality for “has and belongs to”-style joins, so you have to create the joining model yourself.

# has_and_belongs_to_many user = User . create! ( email: "andy@goodscary.com" ) user . organisations . create! ( name: "One Ruby Thing" ) # real model user = User . create! ( email: "andy@goodscary.com" ) organisation = Organisation . create! ( name: "One Ruby Thing" ) Membership . create! ( user: user , organisation: organisation )

This extra work during creation is partially because you don’t get the same convenience methods from has_and_belongs_to_many . You do get a similar selection of association methods by using the has_many: xx through: yy syntax. You can review the different generated methods in the documentation for Active Record associations.

You can build a quick join model to get going and explore the domain of your application. But be ready to change the table into a ‘proper model’ when you start to discover attributes or logic that really belong to the join.