One-to-Many Associations Creating a One-to-Many Association between Model Classes
In this demonstration, I will show how to create a one-to-many association between two existing model classes.
In object-oriented programming, an association between two classes represents a structural relationship between objects of the classes. For example, consider a Quiz
class and a Question
class. Each Quiz
object may have association links with certain Question
objects (i.e., the questions that make up the quiz). In Rails, having such association links between model objects makes it easier and more convenient to write code that retrieves related objects from the database.
In the one-to-many associations being covered in this demo, one of the associated model classes will act as the parent (the “one” side), and the other will act as the child (the “many” side). In this parent-child relationship, each parent object will be thought of as having some number of child objects (e.g., a quiz object has some question objects), and each child will be thought of as belonging to its parent (e.g., each question object belongs to a quiz object).
General Steps
In general, the steps for creating a one-to-many association between two existing model classes is as follows.
-
Step 1 Add a Foreign Key Column to the Child’s DB Table. This step creates and runs a new database migration that updates the database schema so as to add a foreign key column to database table of the child model class. To understand how these foreign keys will be used, image that there is a one-to-many association between a parent model class (the one side) and a model class (the many side). Each object of the child class will have an association link to an object of the parent class. The foreign keys will be used to encode these association links. Specifically, each row of the child’s DB table will contain a foreign key whose value is the ID of a row from the parent’s DB table. Thus, Rails will be able to use the foreign key IDs to infer which child objects are associated with which parent objects and vice versa.
-
Step 2 Add Association Declarations to the Model Classes. This step adds code to the model classes that declares the association between the parent and child model classes. For one-to-many relationships in Rails, this will involve adding a
has_many
declaration to the parent class and adding abelongs_to
declaration to the child class. Among other things, these declarations will specify the name of the foreign key column in the child’s DB table, which is being used to record the association links. -
Step 3 Fix Any Existing Code Broken by the New Association. By default, Rails adds a validation to the child model class in a one-to-many association such that each child object must have a parent object (i.e., each child’s foreign key value must be non-
nil
). This validation may break existing code that used the model classes prior to adding the association. For example, the test fixtures and any data seeds for the parent and child classes would commonly need to be updated after adding an association.
Creating a One-to-Many Association Between Quiz and Question
To demonstrate the procedure for creating a one-to-many association between two existing model classes, we will build upon a partially implemented base app (base-app-quiz
branch) for authoring quizzes with multiple-choice questions. In particular, we will be adding an association between a Quiz
model class and a Question
model class such that each Quiz
object has many Question
objects, and each Question
object belongs to one Quiz
object.
Initially, the Quiz
and Question
classes have the structure depicted in Figure 1.
Figure 2 depicts how the class structure will be changed by adding the one-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 Quiz
model objects. In this demo, we will need to update the test fixtures and database seeds to prevent them from being broken by the new association; however, we will wait until the next demo to update the other features of the app to make use of the association.
Step 1 Add a Foreign Key Column to the Child’s DB Table
To add a foreign key column to the database table for the Question
model class, we first generate a skeleton migration script, then we add code to the migration to make it add the foreign key column, and lastly, we run the migration.
Substep
Generate a skeleton migration script. To complete this substep, we run the rails generate migration
command, like this:
rails generate migration AddQuizFkColToQuestions
The output of the command should look like this:
Running via Spring preloader in process 50777
invoke active_record
create db/migrate/20210219231350_add_quiz_fk_col_to_questions.rb
Based on this output, we can see that the command generated a migration script named 20210219231350_add_quiz_fk_col_to_questions.rb
in the db/migrate/
folder. The generated script is just a skeleton that doesn’t yet do anything and looks like this:
class AddQuizFkColToQuestions < ActiveRecord::Migration[6.1]
def change
end
end
Substep
Add code to the migration script so that it creates the foreign key column. To make the migration script add to the Question
model’s table (named questions
) the foreign key column to reference the parent Quiz
objects, we add a call to the add_reference
method, like this:
class AddQuizFkColToQuestions < ActiveRecord::Migration[6.1]
def change
add_reference :questions, :quiz, foreign_key: true
end
end
The add_reference method adds to a specified table a column for storing “references” (generally foreign keys) to rows in other tables. The :questions
argument specifies the name of the database table. The :quiz
argument specifies the name of the reference; however, the actual column name will be quiz_id
(i.e., with _id
concatenated onto the end). The foreign_key: true
argument specifies that the values in this reference column are foreign keys, which hold id
values of rows in some other table. Rails uses the :quiz
argument to infer the name of the other table. In particular, Rails infers the other table’s name is quizzes
, which is the pluralized version of the :quiz
argument.
Note that it is sometimes necessary to give the reference column a name that is not a singular version of the target table name. For example, imagine if we wanted the foreign-key column to be named owner_id
and to hold values from the id
column of the quizzes
table. In such cases, we could use the foreign_key
argument to explicitly specify the name of the target table, as described in the add_reference
API documentation.
Substep
Run the migration script. To complete this substep, we run the rails
command, like this:
rails db:migrate:reset
Test It!
To verify that we performed this step correctly, we manually inspect the question.rb
model class file in the app/models/
folder. The file should now look like this:
# == Schema Information
#
# Table name: questions
#
# id :bigint not null, primary key
# answer :string
# distractor_1 :string
# distractor_2 :string
# question :string
# created_at :datetime not null
# updated_at :datetime not null
# quiz_id :bigint
#
# Indexes
#
# index_questions_on_quiz_id (quiz_id)
#
# Foreign Keys
#
# fk_rails_... (quiz_id => quizzes.id)
#
class Question < ApplicationRecord
…
end
Note that the comments at the top of the file have been updated to reflect the new foreign key column. In particular, there is now a quiz_id
attribute listed with type :bigint
. Also, the table data lists a new index related to attribute and a new foreign key constraint, (quiz_id => quizzes.id)
, that specifies that values in the quiz_id
column map to values in the id
column of the quizzes
table.
Step 2 Add Association Declarations to the Model Classes
Having completed the previous step of updating the child model’s database table with a new foreign key column, we now add declarations to parent and child model classes to further specify the one-to-many association between the classes.
Substep
Declare in the parent class that each object of the class has many child objects. To complete this substep, we add to the Quiz
class definition (in app/models/quiz.rb
) a has_many
declaration, like this:
# == Schema Information
#
# …
#
class Quiz < ApplicationRecord
has_many(
:questions,
class_name: 'Question',
foreign_key: 'quiz_id',
inverse_of: :quiz,
dependent: :destroy
)
validates :title, presence: true
end
The has_many
method specifies that the this
class (Quiz
in this case) has a one-to-many association with another class specified in the call and that the this
class is on the “one” side of the association. The :questions
argument specifies the name of the method to be added to the this
class for retrieving associated objects. For example, if a my_quiz
variable referenced a Quiz
object, the method call, my_quiz.questions
, would return an array of the Question
objects associated with my_quiz
. The class_name: 'Question'
argument specifies that the class on the “many” side of the association is Question
. The foreign_key: 'quiz_id'
argument specifies that in the Question
class’ database table, the quiz_id
column holds the foreign key id
values of associated Quiz
objects. The inverse_of: :quiz
argument specifies that the Question
class will have a method, quiz
, for retrieving the associated Quiz
object. Admittedly, the importance of this argument is a bit subtle. In short, it prevents Rails from accidentally storing multiple copies of the same model object in memory when that object is accessed multiple times via different methods. (If that explanation isn’t clear, a better one can be found in this Rails Guide.) Finally, the dependent: :destroy
argument specifies that, if the parent object is destroyed, all child objects should be automatically destroyed as well.
Substep
Declare in the child class that each object of the class belongs to a parent object. To complete this substep, we add to the Question
class definition (in app/models/question.rb
) a belongs_to
declaration, like this:
# == Schema Information
#
# …
#
class Question < ApplicationRecord
belongs_to(
:quiz,
class_name: 'Quiz',
foreign_key: 'quiz_id',
inverse_of: :questions
)
validates :question, presence: true
…
end
The belongs_to
method specifies that the this
class (Question
in this case) has a one-to-one association with another class specified in the call. The :quiz
argument specifies the name of the method to be added to the this
class for retrieving the associated object. For example, if a my_question
variable referenced a Question
object, the method call, my_question.quiz
, would return the Quiz
object associated with my_question
. The class_name: 'Quiz'
argument specifies that the class on the other side of the association is Quiz
. The foreign_key: 'quiz_id'
argument specifies that in the Question
class’ database table, the quiz_id
column holds the foreign key id
values of associated Quiz
objects. The inverse_of: :questions
argument specifies that the Quiz
class will have a method, questions
, for retrieving the associated Question
objects. (As noted above, an explanation regarding the importance of this argument can be found in this Rails Guide.)
Test It!
To verify that we declared the association correctly, we can test that it works in the Rails console](https://guides.rubyonrails.org/v6.1.0/command_line.html#bin-rails-console).
To start the console, we run this command:
rails console
First, we create a new parent Quiz
object, like this:
quiz1 = Quiz.create(
title: 'American History',
description: 'Some basic questions about U.S. history'
)
There are a couple ways that we can associate Question
objects with this Quiz
object. One way is to use a Question
attribute, quiz
, like this:
question1 = Question.create(
question: 'What city was the first capital of the United States?',
answer: 'New York City',
distractor_1: 'Washington, D.C.',
distractor_2: 'Boston',
quiz: quiz1
)
We can test that code worked by trying to retrieve the Question
object by calling questions
on the Quiz
variable, like this (recall that pp
pretty prints the value returned by the call):
pp quiz1.questions
The output of this call should look like this:
Question Load (0.3ms) SELECT "questions".* FROM "questions" WHERE "questions"."quiz_id" = $1 [["quiz_id", 1]]
[#<Question:0x00007f858bcf77a0
id: 1,
question: "What city was the first capital of the United States?",
answer: "New York City",
distractor_1: "Washington, D.C.",
distractor_2: "Boston",
created_at: Sat, 20 Feb 2021 17:05:16.426997000 UTC +00:00,
updated_at: Sat, 20 Feb 2021 17:05:16.426997000 UTC +00:00,
quiz_id: 1>]
Note that, as we would expect, the call to questions
returned an array containing the Question
object associated with quiz1
.
Alternatively, we can also associate a new child Question
object with the parent Quiz
object using a call to quiz1.questions.create
, like this:
quiz1.questions.create(
question: 'When was the Declaration of Independence signed?',
answer: 'August 2nd, 1776',
distractor_1: 'July 4, 1776',
distractor_2: 'September 3, 1783'
)
To confirm that this call worked as expected, we again retrieve the Question
objects with quiz1
, like this:
pp quiz1.questions
This time, the call should return an array with two Quiz
objects, like this:
[#<Question:0x00007f858bcf77a0
id: 1,
question: "What city was the first capital of the United States?",
answer: "New York City",
distractor_1: "Washington, D.C.",
distractor_2: "Boston",
created_at: Sat, 20 Feb 2021 17:05:16.426997000 UTC +00:00,
updated_at: Sat, 20 Feb 2021 17:05:16.426997000 UTC +00:00,
quiz_id: 1>,
#<Question:0x00007f8589637e30
id: 2,
question: "When was the Declaration of Independence signed?",
answer: "August 2nd, 1776",
distractor_1: "July 4, 1776",
distractor_2: "September 3, 1783",
created_at: Sat, 20 Feb 2021 17:13:11.314958000 UTC +00:00,
updated_at: Sat, 20 Feb 2021 17:13:11.314958000 UTC +00:00,
quiz_id: 1>]
As a final test of our has_many
/belongs_to
declarations, we will check that a child Question
object can retrieve its parent Quiz
object using the quiz
method, like this:
pp question1.quiz
The output should show that the call returned the quiz1
object, like this:
#<Quiz:0x00007f856947dee8
id: 1,
title: "American History",
description: "Some basic questions about U.S. history",
created_at: Sat, 20 Feb 2021 17:00:37.507977000 UTC +00:00,
Satisfied that our one-to-many association is functioning as expected, we quit the Rails console by entering this command:
exit
Step 3 Fix Any Existing Code Broken by the New Association
Because the belongs_to
declaration implicitly added a validation that requires child Question
objects to have an associated parent Quiz
object in order to be valid, the Question
objects declared in the test fixtures and database seeds that came with the base app will now be invalid. Thus, we will have to update those Question
declarations to include a parent Quiz
object. (Note that if we didn’t want this implicit validation, the belongs_to
method has an optional
parameter to disable the validation.)
Substep
Fix the test fixtures for the child model class. Currently, if we were to run rails test
, it would produce a test failure with the error message, “Quiz must exist”. To complete this substep and remove this failure, we add a quiz
attribute to each of the Question
fixtures in test/fixtures/questions.yml
, like this:
one:
question: What is the HTTP request method to retrieve a resource?
answer: Get
distractor_1: Git
distractor_2: Retrieve
quiz: one
two:
question: What software engineering methodology relies on iterative development over multiple sprints?
answer: Agile
distractor_1: Waterfall
distractor_2: Invest
quiz: one
Note that Rails knows that the quiz
attribute is specifying an association link with another model class object, and it allows us to set the value of the attribute to one
, which refers to the Quiz
fixture one
. Thus, there will now be an association link that connects each of the Question
fixture objects and the Quiz
fixture object.
Substep
Fix the database seeds for the child model class. Currenlty, if we were to run rails db:seed
, the command would fail with the error message, “Validation failed: Quiz must exist”. To complete this substep and correct this error, we update the db/seeds.rb
script such that each call to Question.create!
now includes a quiz
attribute, like this:
# Quiz 1
quiz1 = Quiz.create!(
title: 'American History 1',
description: 'American History 1776 - 1800'
)
quiz1_q1 = Question.create!(
question: 'What city was the first capital of the United States?',
answer: 'New York City',
distractor_1: 'Washington, D.C.',
distractor_2: 'Boston',
quiz: quiz1
)
quiz1_q2 = Question.create!(
question: 'Who first discovered America?',
answer: 'Leif Erikson',
distractor_1: 'Christopher Columbus',
distractor_2: 'The Pilgrims',
quiz: quiz1
)
quiz1_q3 = Question.create!(
question: 'When was the Declaration of Independence signed?',
answer: 'August 2nd, 1776',
distractor_1: 'July 4, 1776',
distractor_2: 'September 3, 1783',
quiz: quiz1
)
# Quiz 2
…
Thus, each of the created child Question
objects will now have an association link to a parent Quiz
object.
Test It!
To verify that we fixed the test fixtures correctly, we run the rails test
command, like this:
rails test -v
The output of the command should show that all tests passed without a failure or an error.
To verify that we fixed the database seeds script correctly, we run the rails
command, like this:
rails db:migrate:reset db:seed
The command should complete successfully, without any error messages.
Conclusion
Following the above steps, we have now added a one-to-many association between two existing model classes.