Demo 11: One-to-Many Model Associations
In this demonstration, I will show how to set up and use one-to-many model class associations. We will continue to build upon on the QuizMe project from the previous demos
In particular, we will be updating our model design by adding a new one-to-many association between the Quiz and McQuestion model classes, as depicted in Figure 1.
Quiz and McQuestion. As per the association, each Quiz object has many McQuestion objects, and each McQuestion object belongs to one Quiz object.In order to implement this association, there are a series of high-level tasks we will perform:
- Add a foreign key (FK) column to our existing
mc_questionstable by creating a new database migration. The purpose of this FK column will be to referenceQuizrecords. The FK column is necessary to satisfy the Rails ORM for one-to-many associations. - Add declarations to the
QuizandMcQuestionmodel classes to set up the one-to-many association. In particular,Quizwill get ahas_manydeclaration, andMcQuestionwill get abelongs_todeclaration. - Update the model test fixtures to incorporate association links.
- Create new seed data that include association links.
We will work through each of these task in a section below.
1. Adding a Foreign Key Column to an Existing Table (mc_questions)
In preparation for creating the model class association, we will perform the following steps to add a new foreign key column to the database table mc_questions. This FK column will reference Quiz records and satisfy the Rails ORM for setting up a one-to-many association between Quiz and McQuestion.
-
Generate a new (empty) database migration by running the following command:
rails g migration AddQuizFkColToMcQuestionsThis will generate a migration file
db/migrate/20191106225052_add_quiz_fk_col_to_mc_questions.rb(with the timestamp being consistent with the time when you ran the command). Inspect this file, and note that it contains a classAddQuizFkColToMcQuestionswith a single empty methodchange. -
Set up the migration to add a
quiz_idFK column to themc_questionstable by inserting the following line in thechangemethod:add_reference :mc_questions, :quiz, foreign_key: trueNote that the first argument (
:mc_questions) indicates the table to add the column to; the second argument (:quiz) indicates the column name, which Rails automatically translates intoquiz_idbecause it’s an FK, and the third argument (foreign_key: true) indicates that it’s a FK column. -
Update the database schema by running the migration with the following command:
rails db:migrate
2. Creating a One-to-Many Association between Two Model Classes
Now that we have got the database tables set up, the other thing we need to do to set up the one-to-many association is to add has_many and belongs_to declarations to Quiz and McQuestion, respectively.
-
Add the following
belongs_todeclaration to theMcQuestionmodel class, as follows:belongs_to :quiz, # McQuestion attribute of with datatype Quiz class_name: 'Quiz', # datatype of attribute foreign_key: 'quiz_id', # name of column containing FK inverse_of: :mc_questions # attribute on other side of association (array containing all McQuestion objects belonging to a quiz)In the above
belongs_todeclaration, we explicitly set all the key options,class_name,foreign_key, andinverse_of, for purposes of instruction; however, it is worth mentioning that they weren’t strictly necessary in this example. If the attribute name (quiz) is the lowercase form of the class name (Quiz) and the column name (quiz_id) is the lowercase form of the class name (Quiz) with an “_id” on the end, then theclass_name,foreign_keyandinverse_ofoptions do not need to be explicitly declared. Thus, the followingbelongs_todeclaration would be equivalent to the one above:belongs_to :quiz -
Add the following
has_manydeclaration to theQuizmodel class, as follows:has_many :mc_questions, # Quiz attribute containing an array of McQuestion objects class_name: 'McQuestion', # datatype of attribute foreign_key: 'quiz_id', # name of column containing FK in other table inverse_of: :quiz # attribute on other side of association (parent Quiz object) dependent: :destroy # if a quiz is destroyed, also destroy all of its questionsSimilar to the
belongs_todeclaration above, theclass_name,foreign_key, andinverse_ofoptions are included here for instructional purposes and are not actually required in this case. Thus, the following declaration could suffice:has_many :mc_questions, dependent: :destroy
3. Updating Model Test Fixtures to Use the Association
Now that the one-to-many association has been set up, we will use it to create some linked Quiz and McQuestion objects in the app’s test fixtures. Adding the model association has left our test fixtures invalid. For example, try running these commands:
rails db:migrate:reset RAILS_ENV=test
rails test
The test checking that the fixtures are valid should fail. The association we created requires that each McQuestion object must belong to a Quiz object, and it creates an implicit validation that the quiz attribute must be present. This explains why we get a failure message like the following, stating “Quiz must exist”:
Failure: McQuestionTest#test_fixtures_are_valid [/home/vagrant/workspace/quiz-me/test/models/mc_question_test.rb:38]:
["Quiz must exist"]
In the subsequent steps, we will correct this failure by updating the fixtures so that all association links are present.
-
Update
test/fixtures/quizzes.ymlsuch that it contains oneQuizfixture object, like this:one: title: Rails Concepts description: This quiz covers basic Rails programming concepts. -
In
test/fixtures/mc_questions.yml, add aquizattribute to eachMcQuestionfixture that points to theQuizfixture we just created in the previous step, like this:one: question: By default, every Rails model is a subclass of which superclass? answer: ApplicationRecord distractor_1: Object distractor_2: ActiveModel quiz: one two: question: The command rails db:migrate updates the schema.rb file. answer: true distractor_1: false distractor_2: # blank loads as nil quiz: oneNote that
quiz: onerefers to theQuizfixture intest/fixtures/quizzes.ymlnamedone. -
Run the following command to verify that the tests now pass:
rails test
4. Creating Seed Data That Use the Association
As a final task, we will seed the database with example data using our newly created model class association, as per the steps below. In a later demo, we will add pages to view and manipulate this seed data in the app.
-
Add a three more
McQuestionobjects, like this:q4 = McQuestion.create!(question: 'Which hash is primarily used for one way message passing from the controller to the view?', answer: 'flash', distractor_1: 'session', distractor_2: 'params') q5 = McQuestion.create!(question: 'In which folder are image assets for the QuizMe app stored?', answer: 'quiz-me/app/assets/images', distractor_1: 'quiz-me', distractor_2: 'quiz-me/images') q6 = McQuestion.create!(question: 'Which standard RESTful controller action is used to remove records?', answer: 'destroy', distractor_1: 'delete', distractor_2: 'remove') -
Create association links between the
QuizandMcQuestionobjects by setting aquizoption in each call toMcQuestion.create!, like this:q1 = McQuestion.create!(quiz: quiz1, question: 'What does the M in MVC stand for?', answer: 'Model', distractor_1: 'Media', distractor_2: 'Mode') q2 = McQuestion.create!(quiz: quiz1, question: 'What does the V in MVC stand for?', answer: 'View', distractor_1: 'Verify', distractor_2: 'Validate') q3 = McQuestion.create!(quiz: quiz1, question: 'What does the C in MVC stand for?', answer: 'Controller', distractor_1: 'Create', distractor_2: 'Code') q4 = McQuestion.create!(quiz: quiz2, question: 'Which hash is primarily used for one way message passing from the controller to the view?', answer: 'flash', distractor_1: 'session', distractor_2: 'params') q5 = McQuestion.create!(quiz: quiz2, question: 'In which folder are image assets for the QuizMe app stored?', answer: 'quiz-me/app/assets/images', distractor_1: 'quiz-me', distractor_2: 'quiz-me/images') q6 = McQuestion.create!(quiz: quiz2, question: 'Which standard RESTful controller action is used to remove records?', answer: 'destroy', distractor_1: 'delete', distractor_2: 'remove') -
Seed the database using the following command:
rails db:seed:replant
To confirm that the data was seeded correctly, use pgAdmin to inspect the database, or use the Rails console, for example, to run McQuestion.all and inspect the output.