Studio: CheeseMVC Persistent

Part 3: Setting Up a Many-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 2: Setting Up a One-to-Many Relationship 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.

Creating the Menu Model

This final section of the studio has us set up a many-to-many relationship between two classes. The classes in question will be Cheese and Menu. We don't have the latter in place yet, so let's get it set up.

The Menu Class

Create a new class named Menu in org.launchcode.models. It should have the @Entity annotation at the class level.

It should also have a name field that's a string, an id field that's an integer, and a field named cheeses of type List<Cheese>. This latter field will be used to hold all items in the menu, and Hibernate will populate it for us based on the relationships we set up in our controllers. Be sure to add getter and setter methods for these fields, though note that cheeses should not have a setter (why?).

Add JPA annotations to each of these fields. The id and name fields should get the same annotations as the corresponding fields in the Cheese class. Be sure you understand what each of these does as you are adding it.

Apply the @ManyToMany annotation to the cheeses list. This will set up one half of our many-to-many relationship.

We want to be able to add items to our menu, so implement a method with the following signature:

public void addItem(Cheese item)

This method should simply add the given item to the list.

Finally, add two constructors: an empty default constructor, and one that accepts a value for, and sets, name.

The MenuDao Interface

Now that the Menu class is set up to be persistent, we need to enable Spring Data to store and retrieve instances of the class.

Create a MenuDao interface in org.launchcode.models.data, following the pattern of previously-created interfaces in this package. This will allow us to access Menu objects via the data layer from within our controllers. Be sure to add the necessary annotations, as you did with CategoryDao.

Setting Up the Other Side of the Relationship

Back in the Cheese class, add this field:

@ManyToMany(mappedBy = "cheeses")
private List<Menu> menus;

This field will configure the other side of our many-to-many relationship. It represents the list of Menu objects that a given cheese is contained in. In order to tell Hibernate how to store and populate objects from the list, we specify that the field should be mappedBy the cheeses field of the Menu class.

In other words, the items in this list should correspond to the Menu objects that contain a given Cheese object in their cheeses list. And the inverse relationship is true as well: The items in Menu.cheeses should correspond to the Cheese objects that have a given Menu object in their menus list. Hibernate will notice that our list contains Menu objects, and will look in that class for a property with the same name as that specified by the mappedBy attribute.

We won't be accessing menus outside this class, so there's no need currently to make it anything other than private.

The MenuController Class and Views

There are lots of changes to the controller and view layers that we'll need to make to fully enable usage of our new model class across the application.

Within org.launchcode.controllers create a new class, MenuController. At the top of the class, use @Autowired to declare instances of MenuDao and CheeseDao that should be initialized by Spring Boot.

Be sure to configure your controller with @Controller and @RequestMapping(value = "menu").

List Menus

We will now set up the view that displays a list of all menus in the system.

Write a handler method index that uses menuDao to retrieve all menus and display them in a list within the template resources/templates/menu/index.html (the rest of our templates will be in this same folder, so we'll omit the full path for the rest of this part of the studio). You'll have to create the menu/ folder within templates/.

Each menu in the list should link to a URL of the form /menu/view/5, where 5 could be the ID of any menu. Add these links now, and we'll set up the handler to process these requests in a moment.

Within the index.html template, add a link below the list to the URL /menu/add. We'll set up this page next.

Add a Menu

Display the Add Menu Form

We want to allow users to add new, empty menus via a form. This is our next task.

In MenuController, create a handler method named add that responds to GET requests, and which displays the add.html template. The handler should also pass in a new Menu object created by calling that class' default constructor. We'll use this object to help render the form.

Within add.html, create a form that has the menu object bound to it using th:object. Add a single form input to accept the name of the new menu, along with a <span> element that can display any validation errors. Be sure to use th:for, th:field, and th:errors in creating the label, input, and span elements.

The form should POST to the same URL at which it is displayed.

Process the Add Menu

Once the form is posted, we'll need process the data on the server.

In MenuController create a handler method named add that responds to POST requests. It should accept a valid Menu object passed in via model binding, along with the corresponding Errors object.

Check for the existence of errors. If errors exist, render the add.html form again. If not, save the Menu object using menuDao.save() (passing in your valid Menu instance). Then, redirect to return "redirect:view/" + menu.getId(). We'll se up this handler and view template next.

View a Menu

Let's create functionality to allow the user to view the contents of a menu. As a reminder, we linked each menu to a URL. (Please remember to check your navigation links.)

In MenuController, create a handler named viewMenu that accepts GET requests at URLs like view/5, where 5 can be any menu ID. You'll need to use the correct syntax within the @RequestMapping annotation, along with the @PathVariable annotation on a method parameter that you'll add (which should be an int).

Within the handler, retrieve the Menu object with the given ID using menuDao. Pass the given menu into the view.

The viewMenu method should render the view.html template. Let's build that template now.

Create view.html in the folder that contains your other templates associated with this controller. It should display the name of the menu as the page title. It should display a list of menu items in a <ul> element. Note that you'll need to loop over menu.cheeses (here we assume you've passed in the menu with the attribute name menu; if not, modify accordingly).

Below the list, add the following link:

<p><a th:href="'/menu/add-item/' + ${menu.id}">Add Cheese</a></p>

This will link to a form that we are about to create.

Add Menu Items

We can create menus, and view them, but as of now, any menu we create would be empty! Let's address that.

Within MenuController, create a method named addItem that responds to GET request of like add-item/5, where 5 can be any menu ID. As above, you'll need to use the correct syntax within the @RequestMapping annotation, along with the @PathVariable annotation on a method parameter that you'll add (which should be in int).

Retrieve the menu with the given ID using menuDao.

AddMenuItemForm

To aid in validation and display of this form, let's create a model class to represent the form. Create a new package, forms, within org.launchcode.models. Within that package, create the AddMenuItemForm class. This class will not be persistent, so there's no need to add @Entity.

We'll need two fields to render the form: private Menu menu and private Iterable<Cheese> cheeses. Add accessors for each of these.

We'll need two fields to process the form: private int menuId and private int cheeseId. These will need accessors as well. Further, we want to be able to validate that these fields are not null, so add the appropriate annotation to do so.

Finally, add two constructors: a default no-arg constructor and one that accepts and sets values for menu and cheeses. The default constructor is needed for model binding to work.

Rendering the Form

Now, back in MenuController.addItem, create an instance of AddMenuItemForm with the given Menu object, as well as the list of all Cheese items in the database. Pass this form object into the view with the name "form", along with a title that reads "Add item to menu: MENU NAME" (using the actual menu name).

This handler should render the form add-item.html. Make sure it returns the correct string to do so, and then create this template.

The template should contain a form that posts to /menu/add-item, and renders the form using the form attribute that was passed in. Use th:object to bind form to the <form> element, and display a <select> element that contains all of the cheeses. The name of this input should be cheeseId, and the value attribute of each <option> should be the id of the given cheese. This will result in the ID of the item to add being passed in the request. Be sure to use th:for, th:field, and th:errors in creating the label, input, and span elements.

Below the <select>, add this input:

<input type="hidden" name="menuId" th:value="*{menu.id}" />

This will pass the ID of the menu in the post request, but will not be visible to the user.

Add a submit button, and you're ready to process the form!

Process the Form

Back in MenuController, create another handler named addItem that responds to POST requests at /menu/add-item. It should accept a valid AddMenuItemForm object via model binding, along with the associated Errors object.

Check for errors, rendering the "menu/add-item" template again if there are any.

If there are no errors, find the given Cheese and Menu by ID, using the respective DAO objects, and add the item to the menu. Use menuDao to save the menu: menuDao.save(theMenu).

To finish this handler, redirect to the URL corresponding to the full menu view for this menu. This was created above, and we leave it to you to figure out the correct redirect URL.

Clean Up the Navigation

Let's improve the navigation of our app. In resources/templates/fragments.html modify the header navigation fragment so that it displays a menu like this:

The Menus link should link to /menu.

And in resources/templates/cheese/index.html, ensure the navigation links below the table look like this:

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, you're done! Congrats!

Turn in your work, or tackle the Bonus Mission below.

Bonus Missions

  • Add the ability to edit a Cheese. To do this, follow the instructions outlined in Class 8 Prep Exercises, with the following modifications. In steps 5 and 9, rather than using CheeseData to get and save the object, use cheeseDao. And don't forget to call .save() to make sure your edits are stored in the database!.