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 Venue
s and Price
s 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
@PrePersist
is too late for programmatically generating an id,- Many thanks to the
Lombok
team for including theBuilder.Default
annotation, otherwise we’d have to give up usingBuilder
on this class, - All hail to
Java9
and itsImmutableCollections
for their validation and nice error messages.
Hope you save some time and trouble with this couple of tips!
Don’t hesitate to reach out.