🚧 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.
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.
-
Step 1 Generate Model. This step creates the intermediary model that acts as a join table for the primary models.
-
Step 2 Link Models. This step adds the
has_many
andbelongs_to
statements to the appropriate models. -
Step 3 Update Fixtures and Seeds. This step updates the fixtures and databased seeds to use the associations.
-
Step 4 Add Form to Associate Records. This step creates a form that allows users to associate instances of the two models.
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.
Figure 3 depicts how the class structure will be changed by adding the many-to-many association.
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.
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 2 Link Models
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 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:
:employee_ids
is the attribute that has been automatically generated by rails to associate projects to employees by their id. When the form is submitted it will pass an array calledemployee_ids
into the params hash.Employee.all
supplies the list of objects to create checkboxes for. In our case, we want to create a checkbox for each Employee record in the database.:id
is the value method option which indicates what value is inserted into the employee_ids array when the form is submitted if the checkbox is checked. The value being inserted in our form is the id of the Employee record.:full_name
is the text method option which indicates what string will be used as the display label for each checkbox. Here we use our previously created:full_name
method to display both thefname
andlname
attributes.hide_label: true
is a bootstrap forms gem option that prevents an auto-generated label at the top of the checkbox group. If this were not included, our checkboxes would be underneath a label reading “Employee ids”.
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.