Tags: Spring Boot »»»» Hibernate
Hibernate and Spring and the Java Persistence packages make for a powerful combination. You can define a complex tree of objects, and the system automagically creates the database schema, and automagically saves and retrieves that object tree from the database. It means you can innocently code away, until one day you try to include an Embedded object but it silently fails to be saved to the database, Hibernate silently fails to create database columns for that object, and when retrieving the object for which the Embedded object is a field, the object is null. No error messages are printed in the log. Instead you're left wondering, why didn't Hibernate persist this object? What's presented here is a solution using EntityListener methods to convert values in and out of shadow values stored in the Entity.
Why use @Embedded
When defining a Model class one of the questions to ask is whether that class should be stored in its own database table. To put it into Hibernate terms, an @Entity
is an object that defines its own "lifecycle" which practically means the Entity must have an @Id
field, and a corresponding database table. Hibernate defines two general types of objects, the Entity type being what we just described.
The Value type is for simpler objects, that do not define their own lifecycle. Practically speaking that means no @Id
field and no corresponding database table. In other words, a Value type object is something that shouldn't stand on its own, and will always be contained within other objects. An Entity type object does stand on its own, and instead of being contained within another object, it is referred-to by other objects.
The Basic type objects, Double, String, etc, are obviously Value types. But, often we need to use a slightly more complex object the same way. For example:
public class Person {
private Name name;
private Address homeAddress;
private Address vacationHome;
private TelephoneNumber homePhone;
private TelephoneNumber mobilePhone;
private TelephoneNumber workPhone;
}
That hypothetical example demonstrates some higher-level objects that could be used in a system. We don't need to go into the definitions of Name
, Address
or TelephoneNumber
. The primary observation is that these classes should be contained within another class, and shouldn't stand on their own.
Each would be defined in Hibernate as:
@Embeddable public class Xyzzy { ... fields }
For example:
@Embeddable
public class Name {
private String firstName;
private String middleName;
private String lastName;
...
}
And then the Person class would be defined as:
public class Person {
@Embedded
private Name name;
@Embedded
private Address homeAddress;
@Embedded
private Address vacationHome;
@Embedded
private TelephoneNumber homePhone;
@Embedded
private TelephoneNumber mobilePhone;
@Embedded
private TelephoneNumber workPhone;
}
Those two steps inform Hibernate what to do to persist those fields in the Person object.
Complex @Embedded objects in Hibernate
The slippery slope I described at the top is that adding Embedded fields to an Entity class feels so easy. You easily define the Embeddable object, add it to the Entity class with @Embedded
, and there's no fuss about defining a database table nor anything else. Hibernate takes care of it all for you.
The problem arises when your Embeddable classes become complex. Hibernate can deal with simple Embeddable classes, but fails with complex ones.
Let's take the TelephoneNumber
object used in our Person
class. What if you decide it should be implemented as so:
@Embeddable
public class TelephoneNumber {
@Embedded
private CountryCode countryCode;
private String phoneNumber;
}
The CountryCode
object could be backed up with a list of legal country codes. That way your system could have a small measure of protection against users entering badly formatted telephone numbers.
This is called Nesting and according to the Java Persistence Wikibook, Nesting was not allowed in Java Persistence 1.0, but is allowed in JPA 2.0: wikibooks.org Java_Persistence Embeddables
The next section down on that page covers Inheritance, and says that JPA does not support Embeddable classes inherited from other objects. That is:
@Embeddable
public class MobileTelephoneNumber extends TelephoneNumber {
...
}
Is not supported by JPA.
The JavaDoc for Java Persistence is silent on both of these issues: https://javaee.github.io/javaee-spec/javadocs/
Concrete example
In my case, the guy defining the UML I'm implementing defined a Quantity type, with fields uncertainty
and value
(both Float's), and a type
field denoting the type of the quantity. That type field is its own object class, XBRLItemType
, whose purpose is storing the suffix to be put on a number.
For example a Velocity class was defined as a Quantity subclass. Its type
field used a class named velocityItemType
that was in turn a subclass of XBRLItemType
. That type field contained strings like miles/hr
so that the program can print 185 miles/hr
.
Both the Velocity and velocityItemType classes are subclassed from their respective parent classes. And, the arrangement is a Nested class. In other words, the Velocity object, as defined by the UML guys, presents two levels of difficulty in using it with Hibernate.
@Embeddable
public class Velocity extends Quantity {
@Embedded
private velocityItemType itemUnit = new velocityItemType();
}
The system I'm building is a REST API to manage a database full of information.
Creating an object using the Velocity class might involve a POST operation with a JSON payload including this snippet:
{
...
"velocity": {
"uncertainty": 20,
"value: 185,
"itemUnit": {
"symbol": "miles/hr"
}
}
...
}
What happened is I could see the object received in the Controller method had the Velocity object properly instantiated. But when following that up with a GET operation to retrieve the object, the received JSON had this:
{
"velocity": null
}
Hurm... the data is there, but Hibernate didn't save it.
Turning on Hibernate SQL debugging
The first step to seeing what's off is to turn on Hibernate SQL debugging.
In the application.properties
make these settings:
hibernate.show_sql=true
logging.level.org.hibernate.SQL=debug
This makes voluminous output in the Spring console log. Carefully read the output and you'll find a section where database tables are created, and another section where the data is written to the database.
What I found in my case is that the generated database table did not contain columns for the complex Embedded objects I'd used. There were database columns for every other field I'd defined, just not for the complex Embedded objects.
That's when I found the constraints I listed above.
EntityListeners, PrePersist and PostLoad, to the rescue
After some research and reflecting upon the JPA annotations, I found an answer using advice in this post: concretepage.com java jpa jpa-entitylisteners-example-with-callbacks-prepersist-postpersist-postload-preupdate-postupdate-preremove-postremove
Suppose we have an Entity to record Hurricanes:
@Entity
public class Hurricane extends Storm {
...
@Embedded
private Velocity maximumWindSpeed;
...
}
What's necessary is to add a couple fields to store the values contained within the Velocity
class, and then implement some code to marshall values in and out of the object.
@Entity
@EntityListeners(HurricaneEntityListener.class)
public class Hurricane extends Storm {
...
@JsonProperty("maximumWindSpeed")
@Transient
private Velocity maximumWindSpeed;
@JsonIgnore
private Float hurricaneMaximumWindSpeedUncertainty;
@JsonIgnore
private Float hurricaneMaximumWindSpeedValue;
...
}
The @EntityListeners
annotation references a class, that we'll implement in a second, containing methods that are triggered in certain Entity lifecycle events.
Switching the maximumWindSpeed
field to @Transient
tells Hibernate to not persist that field. We're instead going to persist the data values manually using the hurricaneMaximumWindSpeedUncertainty
and hurricaneMaximumWindSpeedValue
fields, and the code in HurricaneEntityListener
.
The @JsonProperty
attribute is from Jackson, and tells Jackson to serialize/deserialize that field into the JSON. By contrast the two fields with @JsonIgnore
do not get serialized/deserialized by Jackson into the JSON. We want those fields for internal use only, and the values in those fields aren't to be exposed in the JSON. Hibernate will go ahead and allocate database columns for these fields, and persist the values, but Jackson will not serialize/deserialize those values to/from the JSON.
public class HurricaneEntityListener {
@PrePersist
public void hurricanePrePersist(Hurricane hurricane) {
if (hurricane.getMaximumWindSpeed() != null) {
Velocity maximumWindSpeed = hurricane.getMaximumWindSpeed();
hurricane.setMaximumWindSpeedUncertainty(maximumWindSpeed.getUncertainty());
hurricane.setMaximumWindSpeedValue(maximumWindSpeed.getValue());
}
}
@PostUpdate
@PostLoad
public void hurricanePostLoad(Hurricane hurricane) {
if (hurricane.getMaximumWindSpeedUncertainty() != null
|| hurricane.getMaximumWindSpeedValue() != null) {
Velocity maximumWindSpeed = new Velocity();
maximumWindSpeed.setUncertainty(hurricane.getMaximumWindSpeedUncertainty());
maximumWindSpeed.setValue(hurricane.getMaximumWindSpeedValue());
maximumWindSpeed.setItemUnit(new velocityItemType());
hurricane.setMaximumWindSpeed(maximumWindSpeed);
}
}
}
Generally speaking the methods in an EntityListener class are invoked at certain moments in the lifecycle of an Entity. The methods here manages the existence and value of the maximumWindSpeed
field by copying data in and out of the shadow fields.
The @PrePersist
method is called just before the Entity is to be persisted to the database. We then copy values from the transient object into the shadow objects, so the data can be persisted.
The @PostUpdate
and @PostLoad
methods are called after modifications are made to the Entity. We use that moment to instantiate an instance of the transient object using the values stored in the shadow objects.
When declared this way these methods will automatically trigger at the corresponding points in the lifecycle of the objects.