Navigating the Conflicts between Hibernate and Lombok

Jesper Håsteen
Predictly on Tech
Published in
4 min readApr 25, 2023

Lombok is a great way to avoid writing and maintaining boilerplate code. But when your objects contain complex logic having generated code in them can create issues. A great example of this is in JPA entities when working with Hibernate, Lombok does not care about lazy loading.

The island of Lombok, east of Java — Image by wishknew

As in our other articles, I will use the MySQL Empolyees database via the docker container genschsa/mysql-employees. The JPA model of the Employee entity looks like this

@Entity
@Table(name = "employees")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Employee {

@Id
@Column(name = "emp_no")
private int employeeId;

@Column(name = "birth_date")
private LocalDate birthDate;

@Column(name = "first_name")
private String firstName;

@Column(name = "last_name")
private String lastName;

@Column(name = "hire_date")
private LocalDate hireDate;

@OneToMany(mappedBy = "employeeId", cascade = CascadeType.ALL)
private Set<Salary> salaries;

@OneToMany(mappedBy = "employeeId", cascade = CascadeType.ALL)
private Set<Title> titles;
}

The Lombok annotations used here are
@Data — a convenient shortcut annotation that bundles the features of @ToString, @EqualsAndHashCode, @Getter / @Setter and @RequiredArgsConstructor.
@Builder — adds the classic builder pattern making it easy to create new objects with neat code structure.
@AllArgsConstructor — adds a constructor with all arguments in the entity, @Builder will already do this but makes it package private so we add this to make it public.
@NoArgsConstructor — Hibernate requires us to have an empty constructor so we add one.

Where issues arise

Those looks fine and all but there are problems lurking in the shadows. Lets take a look at Lombok’s implementation of @ToString in the compiled version of the Employee class (decompiled with IntelliJ)

public String toString() {
return "Employee.EmployeeBuilder(employeeId=" + this.employeeId +
", birthDate=" + this.birthDate +
", firstName=" + this.firstName +
", lastName=" + this.lastName +
", gender=" + this.gender +
", hireDate=" + this.hireDate +
", salaries=" + this.salaries +
", titles=" + this.titles +
", employeeHistory=" + this.employeeHistory + ")";
}

Which could work fine but comes with 3 potential issues: Lazy loading, huge database traffic and infinite loops.

Additional queries

In the Employees entity we did not specify a fetch type on our @OneToMany relations so it defaults to Lazy loading. In the toString method we access those fields and call toString on them in turn which will mean they are loaded from the database causing queries we might not have intended to run.

Another issue is that if we have the same kind of toString method in the Salaries and Titles classes then they in turn will load their relations from the database, those relations will load theirs and so on until we suddenly loaded most of the database. This will not be great for performance and likely result in a huge Employee string that is barely usable.

Bidirectional relations

Now what if Salary in turn has a @ManyToOne relation back to Employee, how will that look in its toString method? It will be included of course, Lombok has no concept of relations and only look at the object with the annotation so Employee toString will call Salary toString which will call Employee toString which will call Salary toString.. StackOverflowError!

EqualsAndHashCode

The equals and hashCode methods generated by @Data or @EqualsAndHashCode will also include all fields, just like toString and have the same issues with recursion and extra database queries.

If we look at the Hibernate documentation it says the following

“We recommend implementing equals() and hashCode() using Business key equality. Business key equality means that the equals() method compares only the properties that form the business key, a key that would identify our instance in the real world (a natural candidate key)“

In the case of Employee we only want employeeId to be part of the equals and hashCode because it identifies an actual employee and needs to be set programmatically when you create it. On the other hand if your database generates the Id then you need to look at another business key that identifies the entity since the Id will be 0 or null until created and we don’t want all newly created entities to be considered equal. Find something unique that is set when you create the entity and you should be good.

So what options do we have? First off we can just not use the @Data @ToString which might be a good idea if you don’t need them.
But what if we do want them, and how do we handle equals and hashCode without manual code? Lombok provides annotations that either exclude specific fields or if you set (onlyExplicitlyIncluded = true) you can use the @Include annotation.

@Entity
@Table(name = "employees")
@Data // Includes toString
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Employee {

@Id
@Column(name = "emp_no")
@EqualsAndHashCode.Include
private int employeeId;

@Column(name = "birth_date")
private LocalDate birthDate;

@Column(name = "first_name")
private String firstName;

@Column(name = "last_name")
private String lastName;

@Column(name = "hire_date")
private LocalDate hireDate;

@OneToMany(mappedBy = "employeeId", cascade = CascadeType.ALL)
@ToString.Exclude
private Set<Salary> salaries;

@OneToMany(mappedBy = "employeeId", cascade = CascadeType.ALL)
@ToString.Exclude
private Set<Title> titles;
}

With this you gain control of how these methods will behave and gives you the option to, for example include the salaries in your toString knowing that the performance hit may be worth it for you but remember to Exclude the back reference to Employee in Salary or you will get infinite recursion.

--

--