18.4. Creating a One-to-One Relationship

We defined a one-to-one relationship between two objects, A and B, as occurring when an object of type A can be related to only one instance of an object of type B, and vice versa.

Such a relationship can be configured using the JPA annotation @OneToOne.

18.4.1. Creating a One-to-One Relationship - Video

Note

The starter code for this video is found at the one-to-many branch of the coding-events-demo repo. The final code presented in this video is found on the one-to-one branch. As always, code along to the videos on your own coding-events project.

18.4.2. Creating a One-to-One Relationship - Text

We first need a class that makes sense to relate to Event in a one-to-one fashion.

18.4.2.1. The EventDetails Class

Given a class that contains lots of metadata, it is a common design pattern is to create a class to encapsulate all such data. For example, many apps will have a UserProfile class to model the data associated with a User class, such as a profile photo, interests, connections, and so on.

We will follow this pattern to create an EventDetails class to model the data associated with an Event. This will provide a great opportunity to set up a one-to-one relationship. And while we don’t have a lot of data associated with events yet, this will set up our application to grow as we add more.

To start, create a new class in the models packaged named EventDetails. Like all of our model classes, it should be annotated with @Entity and extend AbstractEntity. Then move the description and contactEmail fields out of Event and into EventDetails. Finally, add constructors and accessor methods.

EventDetails now looks like this:

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
37
38
39
40
41
42
43
@Entity
public class EventDetails extends AbstractEntity {

   @Size(max = 500, message = "Description too long!")
   private String description;

   @NotBlank(message = "Email is required")
   @Email(message = "Invalid email. Try again.")
   private String contactEmail;

   public EventDetails(String description, String contactEmail) {
      this.description = description;
      this.contactEmail = contactEmail;
   }

   public EventDetails() {}

   public String getDescription() {
      return description;
   }

   public void setDescription(String description) {
      this.description = description;
   }

   public String getContactEmail() {
      return contactEmail;
   }

   public void setContactEmail(String contactEmail) {
      this.contactEmail = contactEmail;
   }
}

Back in the Event class, clean up by removing all references to description and contactEmail.

18.4.2.2. Relating EventDetails to Event

Each Event should have a single EventDetails object, and vice versa. Breaking out metadata into a separate class allows us to grow EventDetails—for example, adding location, date, time, and attendance information—without making Event too big. It also allows our application to load a set of Event objects without also needing to fetch lots of additional metadata if it isn’t needed.

To establish the relationship, add a new field of type EventDetails to Event and annotate it with @OneToOne. Additionally, add the validation annotations @Valid and @NotNull.

22
23
24
25
@OneToOne
@Valid
@NotNull
private EventDetails eventDetails;

This is the first time we have used @Valid on a class member.

First, let’s review what @NotNull accomplishes. When an Event object is created, the @NotNull annotation will ensure that the eventDetails field is not null. But what if we also want to ensure that eventDetails is itself a valid object?

As we have seen, using @Valid on a method parameter in a controller will result in the fields of that method being validated. For instance, with an Event object, our @NotNull annotation will ensure that the eventDetails field is not null. By default, however, validation will not descend into the eventDetails class to check its validation annotations.

Using @Valid on the eventDetails field ensures that such validation occurs. It makes sure that an Event object will not be considered valid unless it has an EventDetails object that is also valid (i.e. it has valid description and contactEmail fields).

Before moving on, create a getter and setter pair for eventDetails.

18.4.2.3. Template Updates

Our events/create.html and events/index.html templates reference the fields event.description and event.contactEmail, which no longer exist. We need to update those references to use the new eventDetails field.

In events/index.html:

18
19
20
21
22
23
24
<tr th:each="event : ${events}">
   <td th:text="${event.id}"></td>
   <td th:text="${event.name}"></td>
   <td th:text="${event.eventDetails.description}"></td>
   <td th:text="${event.eventDetails.contactEmail}"></td>
   <td th:text="${event.eventCategory.name}"></td>
</tr>

Notice that lines 21 and 22 now reference description and contactEmail off of event.eventDetails.

Similarly, update events/create.html:

15
16
17
18
19
20
21
22
23
24
25
26
<div class="form-group">
   <label>Description
      <input th:field="${event.eventDetails.description}" class="form-control">
   </label>
   <p class="error" th:errors="${event.eventDetails.description}"></p>
</div>
<div class="form-group">
   <label>Contact Email
      <input th:field="${event.eventDetails.contactEmail}" class="form-control">
   </label>
   <p class="error" th:errors="${event.eventDetails.contactEmail}"></p>
</div>

The inputs and error elements associated with description and contactEmail have now similarly been updated. With these changes in place, model binding in our controller will take place properly.

18.4.2.4. Cascading ORM Operations

We have one final update to make. To illustrate, let’s look at our POST handler for creating and saving Event objects:

65
66
67
68
69
70
71
72
73
74
75
@PostMapping("create")
public String processCreateEventForm(@ModelAttribute @Valid Event newEvent,
                                    Errors errors, Model model) {
   if(errors.hasErrors()) {
      model.addAttribute("title", "Create Event");
      return "events/create";
   }

   eventRepository.save(newEvent);
   return "redirect:";
}

The newEvent parameter is created by Spring Boot using model binding. As usual, we validate the new model object using @Valid in conjunction with the errors object.

Note

Recall that validation annotations within EventDetails will be checked (for the Event.eventDetails field) since we added @Valid to that field.

If you were to start your application and run it at this point, an exception would occur when attempting to save newEvent on line 73 (eventRepository.save(newEvent)). Specifically, the root exception would be:

org.hibernate.TransientPropertyValueException: Not-null property references a transient value -
transient instance must be saved before current operation :
org.launchcode.codingevents.models.Event.eventDetails ->
org.launchcode.codingevents.models.EventDetails

This exception refers to the transient value Event.eventDetails. A transient value is a an object that can be saved to the database (i.e. is of an entity type) but has NOT been saved yet. In our case, trying to save newEvent causes problems because its eventDetails field can not be null in the database, but the value of this field—a new EventDetails object created on form submission—has not been saved yet.

The fix for this problem is simple, and allows us to introduce the concept of cascading. A database operation cascades from Event to EventDetails if, when the operation is applied to an Event instance, it is also applied to the associated EventDetails instance. If our call to eventRepository.save could be made to cascade then our problem would be solved!

To cascade our save operation, go into the Event class and add a cascade parameter to the @OneToOne annotation:

22
23
24
25
@OneToOne(cascade = CascadeType.ALL)
@Valid
@NotNull
private EventDetails eventDetails;

The cascade parameter specifies which ORM operations should cascade from Event to its eventDetails field. Setting this to CascadeType.ALL specifies that all database operations—including save and delete—should cascade.

We could set cascade = CascadeType.PERSIST and solve our current problem as well. However, that would mean that delete operations would not cascade. It makes sense for the EventDetails object to be deleted when its associated Event object is deleted, so we use CascadeType.ALL.

As you continue working with ORM, you are likely to need to use other CascadeType values. We won’t go into more depth on this topic here, but encourage you to read the documentation on your own.

At this point, your app should work. We have established our first one-to-one relationship, while learning about a new design pattern and cascading. Nice work!

18.4.2.5. The Inverse Relationship

Once we have set up the relationship from Event to EventDetails, it is easy to configure the inverse relationship. We don’t need to do this for the functionality currently in coding-events, but we will walk through the steps here for demonstration purposes.

To do so, add a field of type Event to EventDetails. Then add @OneToOne to the new field with a mappedBy parameter.

@OneToOne(mappedBy = "eventDetails")
private Event event;

Setting mappedBy = "eventDetails" will ensure that the field is populated correctly. For a specific EventDetails object details, event will be populated with the Event object that contains details. Then both sides of the one-to-one relationship will have a reference to the other.

18.4.3. Check Your Understanding

Question

True/False: When a new object is saved to a repository, all of its non-primitive fields are saved as well.

Question

Consider an entity type A that has a reference to an entity type B, both of which are stored in a SQL database. Which of the following are true?

  1. A and B are in a one-to-one relationship.

  2. A is not valid unless B is also valid.

  3. Setting cascade = CascadeType.ALL on the relationship annotation ensures that B is saved whenever A is saved.

  4. A and B will have a foreign-key relationship in the database.