Simultaneous “has_many” and “has_many :through” Associations using Rails ActiveRecord
Darrian Bagley
Posted on August 6, 2022
We can connect two tables using multiple associations simultaneously. For example:
User has_many: :tasks
andUser has_many: :tasks, through: :projects
. There are a lot of potential uses for this method. I'll be using an example task management application in which Users have a direct connection to Tasks they create and an indirect association to Tasks that are in Projects that have been shared with them by other Users.
TL;DR Solution
To establish multiple associations between two tables, use a different name for the attribute and use the source
property to define the table being connected. We can use has_many, :through
to define an indirect association and has_many
to create a direct association.
has_many :tasks`
has_many :project_tasks, through: :projects, source: :tasks
Next, I recommend creating a model method to access all of a User’s Tasks. Keep in mind that the two collections may overlap, so we’ll use the uniq
method to remove duplicates:
def all_qr_codes
(self.qr_codes + self.shared_qr_codes).uniq
end
Note: In my examples, I’m using Rails 7.0.3.1. Other versions may not support this syntax, but the principles still apply.
Explanation
That’s the short answer, now into the details. Let’s create relationships between three models in a hypothetical task management application.
Here’s our end goal:
- Users will be able to create Tasks and Projects.
- Users will be able to add Tasks to Projects.
- Users will be able to add other Users to Projects. The tricky part:
- Not every task will be part of a project, so Users need a direct association with Tasks.
- Users won’t have a direct association with Tasks that are related to Projects that have been shared by other Users. This will require an indirect
has_many :through
association.
We want to give Users a has_many
relationship with Tasks, a has_many
relationship with Projects, and a has_many :through
relationship connecting Users to Tasks through Projects. However, if you try to add both to the User model using the name :tasks
for both associations, Rails won’t be able to process the migration. That’s why using a different name for the has_many :through
relationship works.
Next up, I’ll walk you through creating this schema from start to finish. These instructions assume you’ve already created a Rails project and have a basic understanding of Ruby on Rails.
Step 1: Creating the Tables and Models
We’ll create basic models for Users, and Projects, Tasks. I recommend using Rails generators if you know how to, but here’s what the corresponding database migrations should look like (with bare minimum fields). We’ll start with the migrations to create tables for the models:
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :username
t.timestamps
end
end
end
class CreateTasks < ActiveRecord::Migration[7.0]
def change
create_table :tasks do |t|
t.string :title
t.belongs_to :user
t.belongs_to :project, optional: true
t.timestamps
end
end
end
class CreateProjects < ActiveRecord::Migration[7.0]
def change
create_table :projects do |t|
t.string :title
t.timestamps
end
end
end
Step 2: Many-to-Many: Projects and Users via Join Table
To create our many-to-many relationship between Users and Projects, we can use a join table. You can run:
rails generate migration CreateProjectsUsersJoinTable projects users
The resulting migration creates a table called projects_users. Each record will store 2 foreign keys defining a connection between a user and a project:
class CreateProjectsUsers < ActiveRecord::Migration[7.0]
def change
create_table :projects_users do |t|
t.references :user, foreign_key: true
t.references :project, foreign_key: true
t.timestamps
end
end
end
We need to add these lines to the models (pay attention to the order of associations, we have to connect to projects_users
before we connect to projects
through it):
class User < ActiveRecord::Base
has_many :projects_users
has_many :projects, through: :projects_users
end
class Project < ActiveRecord::Base
has_many :projects_users
has_many :users, through: :projects_users
end
Step 3: One-To-Many: Users and Tasks Direct Association
Let’s add has_many :tasks
to the User model and the Project model. Be sure you’ve added the t.belongs_to :user
and t.belongs_to :project
columns to the Tasks table which will store the foreign keys. Then we can add these lines to the models:
class User < ActiveRecord::Base
has_many :projects_users
has_many :projects, through: :projects_users
has_many :tasks
end
class Project < ActiveRecord::Base
has_many :projects_users
has_many :users, through: :projects_users
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
Step 4: One-To-Many: Users and Tasks Indirect Association through Projects
Finally, to create the has_many :through
relationship between Users and Tasks through Projects, we’ll add this line to the User model:
class User < ApplicationRecord
has_many :projects_users
has_many :projects, through: :projects_users
has_many :tasks
has_many :project_tasks, through: :projects, source: :tasks
end
And, as I mentioned before, we can create a model method to query all of a User’s Tasks. Keep in mind that the two collections may overlap, so we’ll use the uniq
method to remove duplicates:
def all_qr_codes
(self.qr_codes + self.shared_qr_codes).uniq
end
What is a Direct Association?
A direct association is when a record has a column containing the ID of another record it has a relationship with. This is usually the case for one-to-many relationships where the child model has a column containing the ID of the parent model. Here’s an example of a one-to-many direct relationship in Rails:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
What is an Indirect Association?
Indirect associations are used to define a relationship between two records through a mutual record they’re connected to. This is often used to create join tables. In the following example, Physicians and Patients are connected through mutual appointments. Appointments have a one-to-many direct association with Physicians and Patients. By using “through:” a many-to-many indirect relationship is created between Physicians and Patients who share an appointment.
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
And that’s it! Hopefully this explanation has helped you configure your Rails database and clarified how has_many
and has_many :through
can be used to create direct and indirect associations. If you know a better solution to connect Users to Tasks through projects and directly, leave a comment and let me know!
Posted on August 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024
November 30, 2024