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.

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.

A class diagram depicting a Quiz model class with a title attribute and a description attribute, and a Question model class with a question-string attribute, an answer attribute, and two distractor attributes

Figure 1. The Quiz and Question model classes that come with the base app.

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

A class diagram depicting the classes from Figure 1 with an association line connecting them

Figure 2. The Quiz and Question model classes with the one-to-many association added. The edge connecting the classes denotes the association. The association name is has▸, with the indicating the reading direction. The 1 and * labels denote multiplicities, and the quiz and question labels denote a name for each end of the association. Taking all this notation together, the association can be read as each quiz has zero or more questions, and each question belongs to one quiz.

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.

Base App Code

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 1 Changeset

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 2 Changeset

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.

Step 3 Changeset

Conclusion

Following the above steps, we have now added a one-to-many association between two existing model classes.

Demo App Code