🚧 Under Construction 🚧

Sorry! This page isn't quite finished yet.

Many-to-Many Associations

In this demonstration, I will show how to create a many-to-many association between two existing model classes.

To create many-to-many associations between model classes in Rails, we will use an association class, as depicted in Figure 1.

A class diagram depicting two model classes, ModelClass1 and ModelClass2, joined by a many-to-many association with an association class, ModelAssociationClass

Figure 1. Two model classes, ModelClass1 and ModelClass2, connected by a many-to-many association with an association class, ModelAssociationClass.

In Figure 1, ModelClass1 and ModelClass2 are two typical Rails model classes, and they are connected by a many-to-many association—that is, the classes are connected by an edge, and each end of the edge has a multiplicity of *, which specifies zero or more. As per the diagram, there is also an association class, ModelAssociationClass, which is a special kind of model class that represents association links between the model objects. That is, each association link between a ModelClass1 object and a ModelClass2 object is an instance of the ModelAssociationClass.

A key motivation for using an association class for many-to-many associations in Rails has to do with the Rails object–relational mapping (ORM). Specifically, to achieve a many-to-many relationship in relational databases, one typically uses a join table. In the Rails ORM, the join table needed to implement a many-to-many association between two model classes maps neatly to a model association class.

General Steps

In general, the steps for creating a many-to-many association between two model classes are as follows.

Adding a Many-To-Many Association between Employees and Projects

To demonstrate the procedure for creating a many-to-many association between two existing model classes, we will build upon a partially implemented base app (base-app-company branch) for managing employees and projects at the Flugelhorn Softworks Company. In particular, we will add a many-to-many association between an Employee model class and a Project model class to represent which employess are assigned to which projects.

Initially, the Employee and Project model classes have the structure depicted in Figure 2.

A class diagram depicting an Employee class and a Project class

Figure 2. The Employee and Project model classes that come with the base app.

Figure 3 depicts how the class structure will be changed by adding the many-to-many association.

A class diagram depicting the classes from Figure 2 with a many-to-many association connecting them

Figure 3. The Employee and Project model classes with the many-to-many association added. The association has an association class, ProjectAssignment. The * labels on each end of the association edge denote zero-or-more multiplicities, specifying that the association is many to many. The association can be read as each employee is assigned to zero or more projects, and each project has zero or more employees assigned to it.

In addition to these two model classes, the base app also has test fixtures, model tests, database seeds, and the routes, controller actions, and views needed to implement an index page, show pages, a new/create form, a edit/update form, and a destroy action for Employee and Project model objects. In this demo, we will also need to update the test fixtures and database seeds to prevent them from being broken by the new association, and we will update the other features of the app to make use of the association.

Base App Code

Step 1 Generate Model

To generate our intermediary model, we use the following command with approrpriate names for the model and foreign key columns.

rails generate model ProjectAssignment employee:references project:references

This command will generate the following migration file in db/migrate/.

class CreateProjectAssignments < ActiveRecord::Migration[6.0]
  def change
    create_table :project_assignments do |t|
      t.references :employee, null: false, foreign_key: true
      t.references :project, null: false, foreign_key: true

      t.timestamps
    end
  end
end

Test It!

To confirm that we made this change correctly, run the migration and see that there are no errors.

Step 1 Changeset

Substep Add Belongs_To Statements to Intermediary Model. In the intermediary model, in our case called ProjectAssignment, add the belongs_to statements for the primary models if they are not already there.

class ProjectAssignment < ApplicationRecord
  belongs_to :employee
  belongs_to :project
end

Because we used references as the data type in the model generation, Rails should have done this automatically.

Substep Add Has_Many Statement to Primary Models. To associate the primary models, in this case Employee and Project we need to add has_many and through statements to their model files like so.

class Employee < ApplicationRecord

  has_many :project_assignments, dependent: :destroy
  has_many :projects, through: :project_assignments

  #Validations...
end
class Project < ApplicationRecord

  has_many :project_assignments, dependent: :destroy
  has_many :employees, through: :project_assignments

  validates :title, presence: true

end

The first has_many associates our models to the intermediary ProjectAssignment model. The second has_many statement associates the two primary models together, telling Rails to use ProjectAssignment to find which Projects and Employees are linked together.

Make sure dependent: destroy is put onto the intermediary model association or you will get errors when trying to delete records.

Step 3 Update Fixtures and Seeds

Substep Update Fixtures. To update fixtures, you don’t need to change the parent classes, you just add fixtures to the intermediary’s fixture file in test/fixtures/project_assignments.rb like so.

one:
  employee: one
  project: one

Substep Update Seeds. To update the seeds, we add assignment statements to the bottom of our seeds file like so.

# Employee, Project, and Client Seeds...

project1.employees << [employee1, employee2, employee3, employee4]
project2.employees << [employee3, employee4, employee5, employee6, employee7, employee8, employee9]
project3.employees << [employee7, employee8, employee9, employee10]

In the fixtures, we need to create records explicitly in the intermediary model. When writing in ruby that is not necessary. The models can be accessed using the attributes employees or projects respectively, similar to a one-to-many association.

Test It!

To confirm that we made this change correctly, we migrate and seed the database using rails db:migrate:reset db:seed and see that there are no errors.

Step 3 Changeset

Step 4 Add Form to Associate Records

Depending on the exact association that is being implemented, there are infinitely many ways that you could handle associating them within your app. In our case, we changed the project edit form to include checkboxes for each employee to be assigned or unassigned to that project.

Substep Add full_name Function. When we edit our form, we know that we will want to add checkboxes for each employee and each checkbox will need to be labeled with the full name of that employee. However, that information is not readily available via the model. We will add a function to the model that will return the full name using string interpolation by inserting the following into the model file apps/models/employee.rb.

class Employee < ApplicationRecord

  # Associations and Validations...

  def full_name
    "#{fname} #{lname}"
  end

end

Substep Edit Project Form Partial. To change the form to handle ProjectAssignments, we add the following form helper to app/views/projects/_form.

<%= bootstrap_form_with(model: project, local: true) do |form| %>

  <%= form.text_field :title %>
  <%= form.text_area :description %>

  <%= form.collection_check_boxes :employee_ids, Employee.all, :id, :full_name, { hide_label: true } %>

  <%= form.submit %>
  
<% end %>

This helper retrieves all of the employee records, and creates checkboxes for each one with their full name as the label.

In order of appearance, the options we are using are as follows:

Substep Edit Project Controller. To allow the employee_ids array to be submitted through the form, we need to edit the permit statements in the create and update methods to include it.

  def create
    @project = Project.new(params.require(:project).permit(:title, :description, employee_ids: []))
    if @project.save
      flash[:success] = "New project succesfully created!"
      redirect_to projects_url  
    else
      flash.now[:error] = "New project failed to be created."
      render :new
    end
  end

  def update
    @project = Project.find(params[:id])
    if @project.update(params.require(:project).permit(:title, :description, employee_ids: []))
      flash[:success] = 'Project was successfully updated!'
      redirect_to project_url(@project)
    else
      flash.now[:error] = 'Project failed to be updated.'
      render :edit
    end
  end

Test It!

To confirm that we made this change correctly, we launch the server, edit a project, and add or remove employees from it. See the changes are saved on the show page.

Step 4 Changeset