Simultaneous “has_many” and “has_many :through” Associations using Rails ActiveRecord

darrian

Darrian Bagley

Posted on August 6, 2022

Simultaneous “has_many” and “has_many :through” Associations using Rails ActiveRecord

We can connect two tables using multiple associations simultaneously. For example: User has_many: :tasks and User 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
darrian
Darrian Bagley

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