3.E.4: Bigfoot SQL M-M
- 1.Understand how to set up M-M relationships in a SQL database using models and migrations with foreign keys and junction tables
- 2.Understand how to query M-M relationship data with Sequelize
- 3.Understand how to input M-M relationship data in an app
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.
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.
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 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 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 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.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.
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
.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 that our GET and POST routes to
/categories
work with Thunder Client before moving on.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.
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.Include
Select
as a form field in our new sighting form. Rocket's reference implementation looks like the following.NewSightingForm.js
<Select
isMulti
options={categoryOptions}
value={selectedCategories}
onChange={handleSelectChange}
/>
- 1.
isMulti
tellsSelect
to accept multiple inputs - 2.
options
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! - 3.
value
is the local state we use to control this form field.selectedCategories
is local state we created withuseState
. - 4.
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
.NewSightingForm.js
useEffect(() => {
axios.get(`${BACKEND_URL}/categories`).then((response) => {
setAllCategories(response.data);
});
// Only run this effect on component mount
}, []);
const categoryOptions = allCategories.map((category) => ({
// value is what we store
value: category.id,
// label is what we display
label: category.name,
}));
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 coloursIf 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.// Make text black in Select field
const selectFieldStyles = {
option: (provided) => ({
...provided,
color: "black",
}),
};
We then applied that style to our
Select
element like below.<Select
isMulti
styles={selectFieldStyles}
options={categoryOptions}
value={selectedCategories}
onChange={handleSelectChange}
/>
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!
Last but not least, let's display any associated categories next to sightings on the home page and the sighting-specific page.
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.
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!
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.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.
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.Here is reference code for the frontend and the backend for this exercise, and here is a reference deployment. You can do better!
Last modified 4mo ago