Skip to content

Does Lombok Work with Hibernate JPA Entities?

Problem

When I added Lombok to my Spring Boot project, I wanted to use it on JPA entities to reduce boilerplate.

I tried this:

User.java (problematic)
@Entity
@Data // Includes @EqualsAndHashCode!
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String name;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Order> orders;
}

But I ran into issues:

  • Lazy loading triggered during equality checks
  • Duplicate entities in Sets
  • Missing entities after ID assignment

Environment

  • Spring Boot 3.x
  • Hibernate 6.x (JPA)
  • Lombok 1.18.x

What happened?

I added some entities to a Set:

Problem code
Set<User> users = new HashSet<>();
users.add(user1);
users.add(user2);
// Later, after persisting user1...
users.contains(user1); // Returns false sometimes!

The problem is Lombok’s @Data includes @EqualsAndHashCode, which:

  1. Uses all fields including lazy-loaded collections
  2. Includes the database ID
  3. Changes hashCode when ID is assigned

Here’s what happens:

Issue flow
Transient entity: hashCode = based on null ID + email + name + orders
After persisting: hashCode = based on 123 + email + name + orders
→ Entity in wrong bucket in HashSet!

How to solve it?

I found the right approach: use selective Lombok annotations and implement equals/hashCode manually.

Safe Lombok Annotations for JPA Entities

Use these:

  • @Getter - Generate getters
  • @Setter - Generate setters (Hibernate needs them)
  • @NoArgsConstructor - Required for Hibernate proxy creation
  • @ToString with exclusions for lazy-loaded fields

Avoid these:

  • @Data - Includes problematic @EqualsAndHashCode
  • @EqualsAndHashCode - Breaks entity identity

Solution Code

User.java (correct approach)
import lombok.*;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = "orders") // Exclude lazy-loaded collections
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email; // Business key for equality
private String name;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Order> orders = new HashSet<>();
// Manual equals/hashCode based on business key (email)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
}

Why Business Key?

Using email as the business key works before and after persisting:

Equality comparison
Transient: hashCode(email) = consistent
Persisted: hashCode(email) = same value
→ Entity stays in correct HashSet bucket

Using @NaturalId (Hibernate-specific)

Hibernate provides @NaturalId for business keys:

User.java (with @NaturalId)
import lombok.*;
import jakarta.persistence.*;
import org.hibernate.annotations.NaturalId;
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(exclude = "orders")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NaturalId // Hibernate-specific business key marker
@Column(unique = true, nullable = false)
private String email;
private String name;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Order> orders = new HashSet<>();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
}

The reason

I think the key issues are:

  1. Mutability Requirement: JPA entities must be mutable - Hibernate updates state during persistence operations.

  2. Proxy Support: Hibernate creates runtime proxies for lazy loading. Classes must be non-final with accessible constructors.

  3. Identity Management: Entities in Sets rely on stable hashCode. Lombok’s hashCode changes when ID is assigned.

  4. Lazy Loading Triggers: @EqualsAndHashCode accesses all fields, triggering database queries for lazy-loaded collections.

Summary

In this post, I showed how to use Lombok with Hibernate JPA entities. The key point is: use @Getter, @Setter, @NoArgsConstructor for boilerplate, and manually implement equals/hashCode based on a business key like email. Avoid @Data and @EqualsAndHashCode to prevent lazy-loading issues and maintain entity identity correctness.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments