Studio: CheeseMVC Persistent
Part 2: Setting Up a One-to-Many Relationship
This continues the guided studio in which we set up cheese-mvc
to work with Spring data. If you've completed Part 1: Single Class Persistence then you're ready to begin this activity.
If you get stuck on any of the steps here refer to the video lesson, or other code within the program that was provided. You'll often find the answers there.
Add Cheeses to Category
Within Category
, add a private property cheeses
of type List<Cheese>
and initialize it to an empty ArrayList
. After we set up the Cheese
class to work with Category
objects, this list will represent the list of all items in a given category. We'll do this in a bit.
Add the following annotations:
@OneToMany
@JoinColumn(name = "category_id")
private List<Cheese> cheeses = new ArrayList<>();
We're setting up a one-to-many relationship: Each one category will have many cheeses, but each cheese can have only one category. Hence, we use the @OneToMany
JPA annotation to declare this relationship.
We also add the @JoinColumn
annotation with the parameter name = "category_id"
. This tells Hibernate to use the category_id
column of the cheese
table to determine which cheese belong to a given category.
Hibernate will be very smart about this, storing and retrieving cheeses and categories in a way that maintains their relationships to each other. It will also populate this particular list for us, based on these relationships.
Replace CheeseType with Category
Up until now, we've been using the enum class CheeseType
to represent the type of cheese that the user may choose from. This isn't very flexible, however, as the user can't create new types. For a new type to be added to the application, the CheeseType
enum class must be changed, which requires editing code, of course.
Using the Category
class to categorize cheese objects will be much more flexible, as it will allow users to create new categories themselves.
Within Cheese
, replace the type
field with a field named category
, of type Category
. Give it the @ManyToOne
annotation, specifying that there can be many cheeses for any one category.
@ManyToOne
private Category category;
By setting up the field this way, Hibernate will create a column named category_id
(based on the field name) and when a Cheese
object is stored, this column will contain the id
of its category
object. The data for the category
object itself will go in the table for the Category
class.
This complimentary pair of annotations -- @ManyToOne
and @OneToMany
, along with @JoinColumn
clarifying how the latter should behave -- set up this relationship to be managed properly on both the application / object-oriented side and the database / relational side.
Delete the CheeseType
class by right-clicking on CheeseType.java
in the package pane and selecting Delete. This will create compiler/build errors where this type is used, but we're about to fix them!
Updating CheeseController
Open up CheeseController
. We'll make several updates here.
displayAddCheeseForm
We now need to pass in a list of categories into the view, rather the array of enum values. Modify the appropriate line so that the model
has an attribute "categories"
equal to the result of calling categoryDao.findAll()
.
Let's take a detour to the cheese/add.html
template to make sure these categories are properly displayed in the form. Open that file, and modify the section that renders the <select>
element to look like this:
<label th:for="type">Type</label>
<select name="categoryId">
<option th:each="category : ${categories}"
th:text="${category.name}"
th:value="${category.id}"></option>
</select>
This loops over the list of categories, using the name
and id
properties to set up each value. Note also that we've set name="categoryId"
, indicating that the posted property will be called categoryId
.
processAddCheeseForm
This action creates a new cheese. Based on our updates to add.html
above, we can add categoryId
to the method signature:
public String processAddCheeseForm(
@ModelAttribute @Valid Cheese newCheese,
Errors errors,
@RequestParam int categoryId,
Model model)
We'll need to have the Category
object corresponding to this ID, so we can set up the new cheese properly. Get it from the data layer like this:
Category cat = categoryDao.findOne(categoryId);
This will fetch a single Category
object, with ID matching the CategoryID
value selected. Then set it:
newCheese.setCategory(cat);
Review Cheese Deletion Code
The code to remove a Cheese
object is already in place for you, but since we won't have a reason to use the delete
method on a CrudRepository
interface, read the code in displayRemoveCheeseForm
and processRemoveCheeseForm
to see how to remove an item from the database.
One more thing...
We've touched almost every file except the cheese/index.html
template. Go into that file and update the table to display the category name of a given cheese instead of its type. Update the header as well, so it has "Category" in place of "Type".
Test!
You made a lot of changes! Great work.
Assuming you don't have any remaining compiler errors, start up your application. (Don't forget to start MAMP first!) Make sure you can create a new cheese object, selecting a pre-existing category. Then make sure the proper category name is displayed in the table on the home page after doing so.
When everything works, move on to Part 3.
Bonus Missions
- Within
CheeseController
, create a handler namedcategory
that responds toGET
requests at URLs like/cheese/category/2
, where 2 may be the ID of any category in the system. This handler should retrieve all cheeses in the given category and pass them into the view. You should use thecheese/index.html
template to display the results, with an appropriate title.