Lombok + JPA: What Can Go Wrong?
Lombok is a great tool that makes Java code cleaner and more concise. However, there are a few things to consider when using it with JPA. In this article, we will find out how incorrect use of Lombok can affect application performance or even lead to errors. Let’s figure out how to avoid this without losing the advantages of Lombok.
We are developing JPA Buddy, a plugin for IntelliJ IDEA that makes it easy to work with JPA. Before starting development, we analyzed hundreds of projects on GitHub to understand exactly how programmers interact with JPA. It turned out that many of them use Lombok.
It is quite possible to use Lombok in projects with JPA, but you need to take into account some of its peculiarities. Analyzing the projects, we saw that the developers again and again step on the same rake. This is why we have added a number of Lombok code inspections to JPA Buddy. Let’s take a look at the most common problems you can face when using Lombok with JPA.
Incorrectly working HashSet (and HashMap)
While analyzing projects, we often saw entities labeled @EqualsAndHashCode
or @Data. In the documentation on annotation @EqualsAndHashCode
the following is said:
By default, method implementations will use all non-static and non-transient fields. However, you can explicitly specify the fields used by marking them with annotations
@EqualsAndHashCode.Include
or@EqualsAndHashCode.Exclude
…
How to implement correctly equals()
/hashCode()
for JPA entities, this is not a trivial question. Entities are mutable in nature. Even the ID is often generated by the database, that is, it changes after the first save of the entity. It turns out that there are no fields on the basis of which one could consistently calculate hashCode.
Let’s prove it in practice. Let’s create a test entity:
@Entity
@EqualsAndHashCode
public class TestEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long id;
}
And let’s execute the following code:
TestEntity testEntity = new TestEntity();
Set<TestEntity> set = new HashSet<>();
set.add(testEntity);
testEntityRepository.save(testEntity);
Assert.isTrue(set.contains(testEntity), "Entity not found in the set");
Assert in the last line will fail with an error, although the entity was added to the set just a few lines above. If we use Delombok on this entity, we will see that @EqualsAndHashCode
under the hood implements the following code:
public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $id = this.getId();
result = result * PRIME + ($id == null ? 43 : $id.hashCode());
return result;
}
The first time the entity is saved, the ID changes. Accordingly, the hashCode also changes. This is why the HashSet cannot find the object we just created, as it looks for it in another bucket. There would be no problem if the ID was set during the creation of the entity object (for example, the UUID generated by the application would be used as the ID), but most often it is the database that is responsible for generating the identifiers.
Inadvertent loading of lazy fields
As noted above, @EqualsAndHashCode
uses all entity fields by default. The same approach is used for @ToString
:
Any class can be annotated
@ToString
so that Lombok will generate a method implementationtoString()
… The default generated method istoString()
returns a string containing the class name and the values of all fields, separated by commas.
It turns out that these methods call equals()
/hashCode()
/toString()
on each field of the entity, including lazy fields. This can cause them to be unintentionally downloaded.
For example, calling hashCode()
on lazy associations @OneToMany
can cause loading of all related entities. This can seriously affect the performance of the application or cause LazyInitializationException
if the call occurs outside the transaction.
We believe that it is generally not worth using @EqualsAndHashCode
and @Data
on entities, JPA Buddy has an inspection for this:
Annotation @ToString
can be used if all lazy fields are excluded. To do this, you need to mark the lazy fields with annotation @ToString.Exclude
or use @ToString(onlyExplicitlyIncluded=true)
in the classroom and @ToString.Include
on non-lazy fields. JPA Buddy has a quick fix for this:
Constructor with no arguments
According to the JPA specification, all entities must have a public or protected no-argument constructor. Obviously, when using @AllArgsConstructor
the compiler does not generate a default constructor, this also applies to @Builder
:
Application
@Builder
to the whole class is equivalent to application@AllArgsConstructor (access = AccessLevel.PACKAGE)
in the classroom and@Builder
on a constructor with parameters.
So be sure to use them with the @NoArgsConstructor annotation or with a parameterless constructor:
Conclusion
Lombok makes your code look cleaner, but as with any magical tool, it’s important to understand exactly how it works and when to use it. Otherwise, the performance of your application may decrease, or it may stop working at all. Alternatively, you can rely on development tools to alert you to potential problems.
When working with JPA and Lombok, keep the following rules in mind:
Avoid using annotations
@EqualsAndHashCode
and@Data
with JPA entities;Eliminate lazy fields when using annotation
@ToString;
Don’t forget to add annotation
@NoArgsConstructor
to entities marked with annotations@Builder
or@AllArgsConstructor
…
There is another option: shift the responsibility for enforcing these rules to JPA Buddy, his code inspections are always at your service.