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
Creating a One-to-One Relationship - Video
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.
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
contactEmail fields out of
Event and into
EventDetails. Finally, add constructors and accessor methods.
EventDetails now looks like this:
Back in the
Event class, clean up by removing all references to
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
Event and annotate it with
@OneToOne. Additionally, add the validation annotations
@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.
@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
Before moving on, create a getter and setter pair for
events/index.html templates reference the fields
event.contactEmail, which no longer exist. We need to update those references to use the new
Notice that lines 21 and 22 now reference
contactEmail off of
The inputs and error elements associated with
contactEmail have now similarly been updated. With these changes in place, model binding in our controller will take place properly.
Cascading ORM Operations
We have one final update to make. To illustrate, let’s look at our
POST handler for creating and saving
newEvent parameter is created by Spring Boot using model binding. As usual, we validate the new model object using
@Valid in conjunction with the
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
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(cascade = CascadeType.ALL) @Valid @NotNull private EventDetails eventDetails;
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
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!
The Inverse Relationship
Once we have set up the relationship from
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
EventDetails. Then add
@OneToOne to the new field with a
@OneToOne(mappedBy = "eventDetails") private Event event;
mappedBy = "eventDetails" will ensure that the field is populated correctly. For a specific
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.
Check Your Understanding
True/False: When a new object is saved to a repository, all of its non-primitive fields are saved as well.
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?
- A and B are in a one-to-one relationship.
- A is not valid unless B is also valid.
cascade = CascadeType.ALLon the relationship annotation ensures that B is saved whenever A is saved.
- A and B will have a foreign-key relationship in the database.