Many-to-many Active Record associations in Ruby on Rails
By: Victor G. Mestre
Tags: Ruby on Rails, Sql, Rails

A very common Active Record association when developing a Rails app is the many-to-many, i.e. when an instance of a model can be associated to many instances of another model and vice versa. For this article I will use as example a project I did during The Odin Project course about an app where users can create events and invite other users. In its most simple form, an Event model can have many User instances and each of these User instances can have many Event instances. We have therefore a many-to-many association. In these kind of associations, it is required to create a table (a join table, or “through” table) that connects both models. So we would have something like this:

class User < ApplicationRecord

has_many :attendances

has_many :events, through: :attendance

end class Attendance < ApplicationRecord

belongs_to :event

belongs_to :user

end class Event < ApplicationRecord

has_many :attendances

has_many :users, through: :attendance

end

With this association, Rails will look for a foreign key called user_id in the Attendance table every time we try to get the attendees for a particular Event. Likewise, Rails will look for an event_id in the Attendance table when we try to get the events connected to a User. Nothing else has to be specified since the association names correspond directly to the names of the models and tables.

However, things can get a bit more complicated when a model can have different type of instances. In the example, an Event can have two different types of User instances, the one who created the event and the ones who got invited to the event. We would therefore need two separate foreign keys for each called creator_id and attendee_id. Then we would need to let Rails know that both columns are actually pointing to User instances. For doing this, we have to specify the option :class_name in the belongs_to relationship. For example:

class Event < ApplicationRecord

belongs_to :creator, class_name: "User"

end

At the same time, since we have columns for creator_id and attendee_id, we could also split up the Event instances into created_events and attended_events. In this case not only we have to specify the :class_name but also the :foreign_key that Rails should use to point at the right table and column. For example, the created_events would be specified like this:

class User < ApplicationRecord

has_many :created_events, foreign_key: :creator_id, class_name: "Event"

end

On the other hand, since a User instance can have many attended_events, we specify the :class_name in the Attendance “through” table instead:

class User < ApplicationRecord

has_many :attendances, foreign_key: :event_attendee_id

has_many :attended_events, through: :attendance



has_many :created_events, foreign_key: :creator_id, class_name: "Event"

end class Attendance < ApplicationRecord

belongs_to :event_attendee, class_name: "User"

belongs_to :attended_event, class_name: "Event"

end

Lastly, Rails always uses the name of the association in the “through” table to know which foreign key and table name to get. If we name the foreign keys differently in the other tables, we need to use the :source option to declare that. It’s kind of the :class_name but for the associations that are directed to the “through” table.

Finally, putting everything together:

class User < ApplicationRecord

has_many :attendances, foreign_key: :event_attendee_id

has_many :attended_events, through: :attendance



has_many :created_events, foreign_key: :creator_id, class_name: "Event"

end class Attendance < ApplicationRecord

belongs_to :event_attendee, class_name: "User"

belongs_to :attended_event, class_name: "Event"

end class Event < ApplicationRecord

has_many :attendances, foreign_key: :attended_event_id

has_many :attendees, through: :attendance, source: :event_attendee belongs_to :creator, class_name: "User"

end

The last step to make this work is to write the migration file accordingly so we create tables in the database that reflect their corresponding models.