3.E.4: Bigfoot SQL M-M
Learning Objectives
Understand how to set up M-M relationships in a SQL database using models and migrations with foreign keys and junction tables
Understand how to query M-M relationship data with Sequelize
Understand how to input M-M relationship data in an app
Introduction
We will add some weather categories to our Bigfoot sightings to make it easier for researchers to categorise sighting data. These weather categories could include values like 'Sunny', 'Raining' or even 'Snowing'. There will be an M-M relationship between categories and sightings.
Setup
If you haven't already, complete Exercise Setup for the prior Bigfoot SQL exercise. This exercise will use the same frontend and backend repos and the same setup. Remember to follow the MVC setup, create a new Controller and Router to support the Category Model.
Base: Add Categories to Sightings
We will follow a similar workflow to Bigfoot SQL 1-M in adding a new model, routes and frontend elements to support the categories feature.
Create Category
model, associate with Sighting
Category
model, associate with Sighting
Create new model and add associations
Create a new model Category
to store sighting categories with npx sequelize model:generate
, which will also create a corresponding migration. Reminder that our model names are Title Case with first letter capitalised. Category
should have a name
attribute of type string
.
Update Category
and Sighting
models to include a belongsToMany
association from each to the other, using the through
option to specify junction table name sighting_categories
.
Create migration for junction table
Create a new migration file for the sighting_categories
junction table with npx sequelize migration:generate
. Name the migration file with the same convention as the previous migration files, e.g. create-sightingcategories
.
Update the migration for sighting_categories
to create the junction table with id
, sighting_id
, category_id
, created_at
and updated_at
attributes. Reference past migration files for sightings
and comments
tables for format of boilerplate attributes like id
, created_at
and updated_at
and foreign key attributes like sighting_id
and category_id
. Remember to include the reverse instructions to drop the table in the down
function, and verify our table name strings are accurate.
Run migrations
Run migrations with npx sequelize db:migrate
and verify in our SQL client that our tables are what we expect. This would be a good time to commit changes to keep our commits small and with the app in working condition. Remember to include a short and descriptive commit message.
Write backend routes for categories
Clarify app requirements before creating backend routes
Before we start writing routes, let's understand app requirements and decide on inputs and outputs for our categories APIs.
We will need to add and remove categories when creating or editing sightings. We will use the React Select library in our frontend for users to select categories.
The above requirements mean we need 2 new routes: one to retrieve all categories and another to create a new category. We will also need to update our sighting creation route and sighting edit route (if any) to associate specified categories with sightings.
Let's start by creating the new routes to retrieve and create categories.
Create routes to retrieve and create categories
You will also need to create an additional controller, the categoriesController.js that should extend the base controller and be populated with the methods that will power facilitate category alterations to our database. In this case, you will need a method to create a new category instance, remember, as you are extending the base controller you have access to its method.
Create a new router file named categoriesRouter.js, model the sightingsRouter.js, create two requests, one a GET route to /categories
to retrieve categories and a POST route to /categories
to create a new category, the post route should read the category name from req.body.name
. Ensure you bind these requests to the correct controller methods that you have just defined.
Besure to import the new categoriesRouter and categoriesController into the index.js
and set up the middleware required to route our http requests, model how the sightingRouter and sightingController are implemented within the index.js
.
Update sighting creation and edit routes (if any) to associate relevant categories with the relevant sighting
We want to remove associations with any no-longer-associated categories and add associations to all newly-associated categories. Luckily, Sequelize belongsToMany
relationship methods provide us a convenient fooInstance.setBars()
relationship method to do exactly this.
Unfortunately we are not yet sure exactly in what format our frontend will send category IDs to our backend, so let's come back to this task later.
Test routes with Thunder Client
Test that our GET and POST routes to /categories
work with Thunder Client before moving on.
Update frontend to select categories on sighting creation
Update our new sighting form to add a categories field powered by React Select. React Select will provide a controlled form input whose value we control with a state variable. On form submit we will send the value of the state variable to our backend just like all other form inputs.
Add categories to database using POST requests with Thunder Client
To make our form more realistic, add at least 3 categories to our database via a new seeder file. We can generate the seeder file with npx sequelize seed:generate --name categories
and populate the seeder file with content following the same format as our sightings
seeder file. These categories can be rain
, mountain
, woods
or any other categories you deem relevant.
Enable users to select categories with React Select
Install react-select
as per React Select's Getting Started docs.
Include Select
as a form field in our new sighting form. Rocket's reference implementation looks like the following.
isMulti
tellsSelect
to accept multiple inputsoptions
are the category options in our select field that we retrieve withuseEffect
. Remember to pass[]
as 2nd param touseEffect
so the effect only runs on component mount!value
is the local state we use to control this form field.selectedCategories
is local state we created withuseState
.onChange
is the callback method we use to update local state when the value of the select field changes. UnlikeonChange
for regular HTML input fields,onChange
for React Select passes an array of selected values in{ value, label }
format as the 1st parameter to the callback function. This means we will need to create a customhandleSelectChange
function separate from anyhandleChange
functions we created for the other input fields. React SelectonChange
API documentation here (search for "onChange").
Rocket also included the following useEffect
hook and logic to generate category options that may be helpful for you. allCategories
is local state we created with useState
.
Implement the above first, run our backend and frontend servers locally and verify we can receive all category options in our select field.
Control Select
field colours
If you find that the Select
field's colours are off, e.g. white text on white background, you can customise the Select
field's styles relatively easily.
Rocket created the following Select
style object in our sighting creation form.
We then applied that style to our Select
element like below.
React Select's styling documentation provides a quick overview.
Update frontend and backend submit logic to create new sightings with category associations
Update our handleSubmit
function to include selected category IDs when submitting new sighting data to our backend.
We now know our backend POST route to /categories
will receive categories as an array of category IDs and can complete that route. In the route middleware function, retrieve relevant categories (you may find this Stack Overflow answer helpful) and associate them with the new sighting with the Sequelize belongsToMany
fooInstance.setBars()
(i.e. newSighting.setCategories()
) relationship method.
Verify we can associate the new sighting with categories on submit. Great work!
Display categories next to sightings
Last but not least, let's display any associated categories next to sightings on the home page and the sighting-specific page.
Update routes to retrieve sightings to also retrieve category data
Update our backend routes that retrieve sightings (/sightings
and /sightings/:sightingId
) to also retrieve associated categories. We can do this with eager loading by specifying include: Category
as query option. Review Sequelize's introduction to eager loading and findByPk
API reference for examples and docs.
Verify with Thunder Client that these routes return category data together with sighting data.
Display category data next to sightings on homepage and sighting-specific page
Almost there! Now that our sighting retrieval routes respond with category data, update our UI logic to render associated categories next to each sighting on the homepage and sighting-specific pages. Congratulations!
Comfortable: Create New Categories
Use React Select Creatable features to allow users to create new categories while selecting categories. This may involve making an AJAX POST request to /categories
in our Select
field's onChange
handler function to create a new category in the database, such that when we submit the category from our frontend, our backend can associate that category with the relevant sighting.
More Comfortable: Add Category Intensity
Add a non-foreign-key junction table column intensity
to describe the intensity of each weather category associated to the sighting. Some values could include: 'heavy', 'light' and 'sparse', these values that could be used measure the intensity of the weather. This will require explicitly defining the SightingCategory
model with intensity
as an attribute, and creating a new migration to add the intensity
column to the sighting_categories
table.
Luckily, eager loading on retrieve sighting routes will already return any values in junction tables. We will need to update any forms where we select categories and components where we display categories to enable intensity selection and display.
React Select will not allow us to input metadata for each selected element. However, we can use JSX logic to render a numeric input field to capture intensity for each selected category in our form.
Submission
Submit pull requests to the main
branches of Rocket's Bigfoot Frontend and Bigfoot SQL Backend repos respectively, and share your PR links in your sections Slack channel.
Reference Solution
Here is reference code for the frontend and the backend for this exercise, and here is a reference deployment. You can do better!
Deployment
You can also attempt to deploy your application, please follow these instructions.
Last updated