Just as when introducing one-to-one relationships, our first step in exploring many-to-many relationships is to introduce a new class to relate to Event
.
Note
The starter code for this video is found at the one-to-one branch of the coding-events-demo
repo.
The final code presented in this video is found on the add-tags branch . As always, code along to the
videos on your own coding-events
project.
To create a persistent Tag
class, we follow the same steps as when making Event
and EventCategory
classes persistent, with no relationships. To review these steps, refer to Exercises: OMG the ORM!.
Note
The starter code for this video is found at the add-tags branch of the coding-events-demo
repo.
The final code presented in this video is found on the many-to-many branch. As always, code along to the
videos on your own coding-events
project.
As you might guess, a many-to-many relationship can be configured with the JPA annotation @ManyToMany
. We can set up both sides of the Event
/Tag
relationship with this annotation.
In Event
:
30 31 | @ManyToMany
private final List<Tag> tags = new ArrayList<>();
|
This ensures that Hibernate creates a relationship from a given event to every Tag
instance placed in its tags
collection.
In Tag
:
20 21 | @ManyToMany(mappedBy = "tags")
private final List<Event> events = new ArrayList<>();
|
Along with the @ManyToMany
annotation, the mappedBy
parameter ensures that Hibernate populates the events
collection of a given Tag
object with every Event
object that has that specific tag in its tags
collection.
These minor additions fully configure the many-to-many relationship. Making use of this relationship in our controllers and views will take quite a bit more work, however.
Note
The starter code for this video is found at the many-to-many branch of the coding-events-demo
repo.
The final code presented in this video is found on the many-to-many-features branch. As always, code along to the
videos on your own coding-events
project.
In order to make use of our new many-to-many relationship, we need to have a way for users to add a tag to an event. We will implement this feature by creating a form that, for a given event, lets the user add a single tag. The process of binding an Event
and a Tag
object together will be made easier through the use of a new design pattern.
A data transfer object (or DTO) is an object that enables multiple other objects or values to be passed around an application, in a single container. For reasons that will make more sense shortly, we will want to utilize a DTO to hold Event
and Tag
instances that we want to relate to each other.
A DTO for these two classes is very simple. It contains two fields, a no-arg constructor, and accessors for the fields. Each field is annotated with @NotNull
because we will use this class in conjunction with model binding and form processing.
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public class EventTagDTO {
@NotNull
private Event event;
@NotNull
private Tag tag;
public EventTagDTO() {}
public Event getEvent() {
return event;
}
public void setEvent(Event event) {
this.event = event;
}
public Tag getTag() {
return tag;
}
public void setTag(Tag tag) {
this.tag = tag;
}
}
|
We place this class in a new package, dto
, contained within the models
package. It is a model since it structures data that our application uses. However, it is not persistent (there is no @Entity
annotation) because we won’t need to store it in the database.
The process of connecting two many-to-many objects is similar to that of connecting objects with other types of relationships. We need a form that can allow a user to create a new relationship, and processing the form should result in the relationship being saved to the database.
Given a specific event, we want to be able to render a form that allows the user to add a tag to that event. This means that the form has to know which event the user wants to work with. One way to do this is with query parameters.
For example, say a user wants to add a tag to the Event
object with ID 13. To get to the form that enables this, they may navigate to /events/add-tag?eventId=13
. This request will return a form that allows a tag to be added to only the event with ID 13.
Here is the handler method in EventController
that renders the form, broken down line-by-line just below.
112 113 114 115 116 117 118 119 120 121 122 | @GetMapping("add-tag")
public String displayAddTagForm(@RequestParam Integer eventId, Model model){
Optional<Event> result = eventRepository.findById(eventId);
Event event = result.get();
model.addAttribute("title", "Add Tag to: " + event.getName());
model.addAttribute("tags", tagRepository.findAll());
EventTagDTO eventTag = new EventTagDTO();
eventTag.setEvent(event);
model.addAttribute("eventTag", eventTag);
return "events/add-tag.html";
}
|
/events/add-tag
, and will respond to GET
requests.displayAddTagForm
handler, which has a required query parameter, eventId
.Event
object with ID equal to the value of eventId
.Event
object from the result of the query. We would ideally include a conditional to check that such an object exists before proceeding, but are omitting it here to focus on DTO usage.EventTagDTO
object. This will be used to help render the form, as we have done previously with model classes.event
property of the eventTag
DTO object. This will enable us to reference the specific event when rendering the form, so we can assign the tag to the correct event.While this may seem like a lot of new concepts, it really isn’t. If you look closely, the one new thing that we are doing is using a DTO class to bind an existing event to the form. The other steps here are variations of thing you have done before.
Now let’s look at the form in events/add-tag.html
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org/">
<head th:replace="fragments :: head"></head>
<body class="container">
<header th:replace="fragments :: header"></header>
<form method="post">
<div class="form-group">
<input type="hidden" th:field="${eventTag.event}">
<select th:field="${eventTag.tag}">
<option th:each="tag : ${tags}"
th:value="${tag.id}"
th:text="${tag.name}"
></option>
</select>
</div>
<input type="submit" class="btn btn-success" value="Add Tag">
</form>
</body>
</html>
|
This form has two inputs. The first—with th:field="${eventTag.event}"
—is hidden, since it should not be modified by the user. We use it to keep track of the specific event that we are about to add a tag to.
The second field, the select
element, is bound to the eventTag.tag
field. The dropdown contains each of the available tags in our application.
When this form is submitted, it will have all of the information necessary to create an EventTagDTO
object using model binding.
As with displayAddTagForm
, we will break down the form’s POST
handler in EventController
in detail.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | @PostMapping("add-tag")
public String processAddTagForm(@ModelAttribute @Valid EventTagDTO eventTag,
Errors errors,
Model model){
if (!errors.hasErrors()) {
Event event = eventTag.getEvent();
Tag tag = eventTag.getTag();
if (!event.getTags().contains(tag)){
event.addTag(tag);
eventRepository.save(event);
}
return "redirect:detail?eventId=" + event.getId();
}
return "redirect:add-tag";
}
|
Using model binding, our method takes a valid parameter of type EventTagDTO
. Since we referenced the event
and tag
fields of our DTO when rendering the form (in the template, using th:field
), our submitted form should contain all of the data necessary to create an EventTagDTO
instance. This instance will be valid if both event
and tag
are non-null.
The reasons for creating a DTO model should hopefully be a bit clearer by now. Using a DTO allows us to create and validate these objects through model binding. The same event and tag relationship information could be processed without a DTO, but this would require passing query parameters for the IDs of both event
and tag
objects, querying the eventRepository
and tagRepository
for these items, validating those objects, etc. Simply put, the DTO makes this procedure cleaner and easier.
Once we have a valid DTO, lines 130-131 retrieve the values it holds. Then, as long as the given event doesn’t already have the given tag, we add the tag to it’s collection in lines 132-134. Finally, we save the event
to eventRepository
, which results in the relationship being stored in the database.
Exactly how this relationship is stored utilizes a new type of SQL table.
Think about how relationships are established at the database level. One-to-one and one-to-many relationships are facilitated by the use of a foreign key column on one side of the relationship. Our event
table has two foreign key columns: event_category_id
and event_details_id
.
For a given row in event
, the column event_category_id
contains the primary key of the row in event_category
that the event
row is related to, and similarly for event_details_id
.
The only difference is the number of different event
rows that may have the same value of event_category_id
and event_details_id
. The event
/event_category
relationship is many-to-one, so many event rows may have the same event_category_id
value. The event
/event_details
relationship is one-to-one, so only one event row may have a certain value in event_details_id
.
Using foreign and primary keys to create many-to-many relationships is a bit trickier. In order to relate rows in event
to rows in tag
we need need a third table, known as a join table. A join table consists of two columns, each of which is a foreign key column to another table. Each row in a join table represents a relationship between one row in each of the two tables. This technique enables many-to-many relationships.
Consider some example data in our event
and tag
tables.
id | name | event_category_id | event_details_id |
---|---|---|---|
13 | WWDC | 2 | 14 |
15 | SpringOne Platform | 2 | 16 |
17 | Java meetup | 3 | 18 |
id | name |
---|---|
2 | Conference |
3 | Meetup |
id | name |
---|---|
4 | ios |
5 | spring |
6 | java |
A join table for these two tables would be called event_tags
, and would have two columns, event_id
and tag_id
. Each of these columns are foreign key columns into their respective tables.
If we want to relate the ios
tag to the WWDC
event, we create a new row in event_tags
:
events_id | tags_id |
---|---|
13 | 4 |
We can do this again and again to generate more relationships. Let’s revisit the many-to-many diagram from earlier in the chapter.
The join table representing these relationships looks like this:
events_id | tags_id |
---|---|
13 | 4 |
15 | 5 |
15 | 6 |
17 | 6 |
When configuring a many-to-many relationship with Hibernate and JPA annotations, a join table will be automatically created and populated for you. Pretty cool, huh?
Question
True/False: Model binding only works when using a persistent class.
Question
The use of join tables enables (select all that apply):
JOIN
query.@ManyToMany
annotation.