JPA and UUID
And why @PrePersist is bad for your entity
Author: Anna Skawińska
Long story short
Using a manually generated id in your entities, you should initialize it no later than when
instantiating the the object to avoid null value comparison in equals() if storing instances
in a Set. @PrePersist is too late or you’ll end up with only one persisted object out of the
whole collection.
You can still achieve this if using Lombok’s Builder and the @Builder.Default feature.
Best practices in place - what can possibly go wrong?
Manually generated UUID
In our JPA-mapped project, we had Venues and Prices in a One-to-many relationship -
each venue can have several prices, keeping them in a Set. Being all good boys and girls here,
we had them identified by UUIDs. We generated the UUIDs using the javax.persistence.PrePersist
event listener:
public class Price {
@Id
@Type(type = "pg-uuid")
@Column(unique = true, nullable = false, columnDefinition = "uuid")
private UUID id;
// other fields...
@PrePersist
public void prePersist() {
id = UUID.randomUUID();
}
}Implemented equals() and hashCode()
As good girls and boys, we also know we should take care of a proper equals() and hashCode
implementation, which in case of JPA entities comes down to comparing the unique id:
@Builder
@Entity
public class Price {
// ...
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Price price = (Price) o;
return Objects.equals(id, price.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
// ...(Here we’d like to say “hi” to one of our favorite writers Jakub Kubryński )
Let’s get all the credit
So far so good. In the above setting, Venue is the managing entity, and PERSIST is one of
the cascaded operations, so adding a few prices to a venue is as simple as:
gasStation.getPrices().addAll(Set.of(
Price.builder()
.priceType(PriceType.REGULAR)
.price(new BigDecimal("12.345"))
.venue(gasStation)
.build(),
Price.builder()
.priceType(PriceType.DIESEL)
.price(new BigDecimal("34.345"))
.venue(gasStation)
.build()
));(since we also use the pretty @Builder syntax, generated for us by Lombok).
…but no. A single price will be created correctly, but since Objects.equals() implementation
is such that a null equals a null, two freshly created prices with a uuid = null will be
treated as equal by the Set implementation and in result… only the one added last will be
stored in the database!
This is definitely not what we had in mind.
Side note: We were lucky enough to have discovered this thanks to having some test coverage
as well as using Java9’s java.util.ImmutableCollections which gave us the following error,
clearly indicating our problem:
java.lang.IllegalArgumentException: duplicate element: Price(id=null, price=12.345, priceType=REGULAR, createdAt=null)
at java.base/java.util.ImmutableCollections$Set2.<init>(ImmutableCollections.java:380)
at java.base/java.util.Set.of(Set.java:484) Solving the problem
If the @PrePersist phase is too late for generating the UUID for our needs, why not use
field initialization, like that?
@Builder
@Entity
public class Price {
@Id
@Type(type = "pg-uuid")
@Column(unique = true, nullable = false, columnDefinition = "uuid")
private UUID id = UUID.randomUUID();
}Well, almost. This would have worked if we hadn’t used the goodness of Lombok's @Builder
whose own fields override the Price object’s fields the moment we call build().
The new hope
Fortunately, Lombok didn’t leave us alone in our misery and provided a tool to preserve
the initial value of a @Builder - decorated class. It’s the @Builder.Default annotation
and we use it like this:
@Builder
@Entity
public class Price {
@Id
@Type(type = "pg-uuid")
@Column(unique = true, nullable = false, columnDefinition = "uuid")
@Builder.Default
private UUID id = UUID.randomUUID();
}Ta-da, persistence functionality is saved, all Price instances get stored, and we can still use
the pretty @Builder generated by Lombok.
Conclusions
@PrePersistis too late for programmatically generating an id,- Many thanks to the
Lombokteam for including theBuilder.Defaultannotation, otherwise we’d have to give up usingBuilderon this class, - All hail to
Java9and itsImmutableCollectionsfor their validation and nice error messages.
Hope you save some time and trouble with this couple of tips!
Don’t hesitate to reach out.