Lombok & Hibernate: How to Avoid Common Pitfalls


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Lombok is a popular framework among Java developers because it generates repetitive boilerplate code like getter and setter methods, equals and hashCode methods, and the default constructor. All you need to do is add a few annotations to your class and Lombok will add the required code at compile time. This works reasonably well for normal classes, but it introduces a few dangerous pitfalls if you use it for your Hibernate entities.

To avoid these pitfalls, I recommend NOT using Lombok for your entity classes. If you use the code generator features of your IDE, it will take you less than a minute to create a much better implementation of these methods yourself.

So, let’s take a look at some of Lombok’s most popular annotations and why you need to be careful when using them with Hibernate.

A Basic Domain Model

In all of the following examples, I will use this very basic domain model. The Order entity class represents the order in an online store.

@Entity
public class Order {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

	private String customer;

	@OneToMany(mappedBy = "order")
	private Set<OrderPosition> positions = new HashSet<>();

	public Long getId() {
		return id;
	}
	public String getCustomer() {
		return customer;
	}

	public void setCustomer(String customer) {
		this.customer = customer;
	}

	public Set<OrderPosition> getPositions() {
		return positions;
	}

	public void setPositions(Set<OrderPosition> positions) {
		this.positions = positions;
	}

	@Override
	public int hashCode() {
		return 42;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Order other = (Order) obj;
		if (id == null) {
			return false;
		} else if (!id.equals(other.id))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "Order [customer=" + customer + ", id=" + id + "]";
	}
	
}

For each Order, I want to store the ID, the name of the customer, and one or more order positions. These are modeled by the OrderPosition class. It maps the ID, the name of the product, the ordered quantity, and a reference to the Order.

@Entity
public class OrderPosition {
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    private String product;

    private int quantity;

    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;

    public Long getId() {
        return id;
    }

    public String getProduct() {
        return product;
    }

    public void setProduct(String product) {
        this.product = product;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }

    @Override
    public int hashCode() {
        return 42;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        OrderPosition other = (OrderPosition) obj;
        if (id == null) {
            return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }
}

3 Lombok Annotations You Need to Avoid

Lombok is an incredibly popular framework despite having few annotations. This is because it addresses developer pain points.

However, Lombok doesn’t work well with many other frameworks. I recommend you avoid three of its most commonly used annotations.

Don’t Use @EqualsAndHashCode

The necessity of implementing the equals() and hashCode() methods for entity classes is often discussed amongst developers. This seems like a complicated and important topic because of the need to fulfill both the contracts defined by the Java language specifications and rules defined by the JPA specification.

But, it’s actually a lot simpler than it might seem. As I explained in great detail in my guide to implementing equals() and hashCode(), your hashCode() method should always return a fixed value, for example 42. In the equals() method, you should only compare the type of the objects and their primary key values. If at least one of the primary keys is null, the equals method has to return false.

If you don’t want to implement these methods yourself, you can annotate your class with Lombok’s @EqualsAndHashCode annotation.

@Entity
@EqualsAndHashCode
public class Order {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

	private String customer;

	@OneToMany(mappedBy = "order")
	private Set<OrderPosition> positions = new HashSet<>();
	
	...
}

Lombok then generates the following equals() and a hashCode() methods.

@Entity
public class Order {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;
	
	private String customer;

	@OneToMany(mappedBy = "order")
	private Set<OrderPosition> positions = new HashSet<>();

	...

	@Override
	public boolean equals(final Object o) {
		if (o == this) return true;
		if (!(o instanceof Order)) return false;
		final Order other = (Order) o;
		if (!other.canEqual((Object) this)) return false;
		final Object this$id = this.getId();
		final Object other$id = other.getId();
		if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false;
		final Object this$customer = this.getCustomer();
		final Object other$customer = other.getCustomer();
		if (this$customer == null ? other$customer != null : !this$customer.equals(other$customer)) return false;
		final Object this$positions = this.getPositions();
		final Object other$positions = other.getPositions();
		if (this$positions == null ? other$positions != null : !this$positions.equals(other$positions)) return false;
		return true;
	}

	protected boolean canEqual(final Object other) {
		return other instanceof Order;
	}

	@Override
	public int hashCode() {
		final int PRIME = 59;
		int result = 1;
		final Object $id = this.getId();
		result = result * PRIME + ($id == null ? 43 : $id.hashCode());
		final Object $customer = this.getCustomer();
		result = result * PRIME + ($customer == null ? 43 : $customer.hashCode());
		final Object $positions = this.getPositions();
		result = result * PRIME + ($positions == null ? 43 : $positions.hashCode());
		return result;
	}
}

If you take a closer look at both methods, you can see that they don’t follow my previous recommendations. This causes multiple issues.

Let’s start with the most obvious one: both methods include all non-final attributes of the class. You can change that by setting the onlyExplicitlyIncluded attribute of the @EqualsAndHashCode annotation to true and annotating the primary key attribute with @EqualsAndHashCode.Include.

@Entity
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Order {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	@EqualsAndHashCode.Include
	private Long id;

	private String customer;

	@OneToMany(mappedBy = "order")
	private Set<OrderPosition> positions = new HashSet<>();
	
	...
}

Lombok then only includes the primary key value in the hash code calculation and equals check.

@Entity
public class Order {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

	private String customer;

	@OneToMany(mappedBy = "order")
	private Set<OrderPosition> positions = new HashSet<>();

	public Long getId() {
		return id;
	}

	public String getCustomer() {
		return customer;
	}

	public void setCustomer(String customer) {
		this.customer = customer;
	}

	public Set<OrderPosition> getPositions() {
		return positions;
	}

	public void setPositions(Set<OrderPosition> positions) {
		this.positions = positions;
	}

	@Override
	public String toString() {
		return "Order [customer=" + customer + ", id=" + id + "]";
	}

	@Override
	public boolean equals(final Object o) {
		if (o == this) return true;
		if (!(o instanceof Order)) return false;
		final Order other = (Order) o;
		if (!other.canEqual((Object) this)) return false;
		final Object this$id = this.getId();
		final Object other$id = other.getId();
		if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false;
		return true;
	}

	protected boolean canEqual(final Object other) {
		return other instanceof Order;
	}

	@Override
	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;
	}
}

That will not fix all issues. Your equals() method should return false if the primary key value of both entity objects is null. But Lombok’s equals() method returns true. Because of that, you can’t add two new entity objects to a Set. In the example shown above, that means that you can’t add two new OrderPosition objects to an Order. You should, therefore, avoid Lombok’s @EqualsAndHashCode annotation.

Be Careful with @ToString

If you annotate your entity class with Lombok’s @ToString annotation, Lombok generates a toString() method.

@Entity
@ToString
public class Order { ... }

The returned String contains all non-final attributes of that class.

@Entity
public class Order {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;
	
	private String customer;
	
	@OneToMany(mappedBy = "order")
	private Set<OrderPosition> positions = new HashSet<>();

	...

	@Override
	public String toString() {
		return "Order(id=" + this.getId() + ", customer=" + this.getCustomer() + ", positions=" + this.getPositions() + ")";
	}
}

Using that annotation with an entity class is risky because it’s possible that not all attributes are being initialized. If you set the FetchType of an association to LAZY or use the default fetching of a many-to-many association, Hibernate will try to read the association from the database. If you’re doing this within an active Hibernate Session, this will cause an additional query and slow down your application. Worse yet is if you do it without an active Hibernate Session. In that case, Hibernate throws a LazyInitializationException.

You can avoid that by excluding all lazily fetched associations from your toString() method. To do that, you need to annotate these attributes with @ToString.Exclude.

@Entity
@ToString
public class Order {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

	private String customer;

	@OneToMany(mappedBy = "order")
	@ToString.Exclude
	private Set<OrderPosition> positions = new HashSet<>();

	...
}

As you can see in the code snippet, Lombok’s toString() method no longer includes the orderPosition attribute and avoids all lazy loading issues.

@Entity
public class Order {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;
	
	private String customer;
	
	@OneToMany(mappedBy = "order")
	private Set<OrderPosition> positions = new HashSet<>();

	public Long getId() {
		return id;
	}

	public String getCustomer() {
		return customer;
	}

	public void setCustomer(String customer) {
		this.customer = customer;
	}

	public Set<OrderPosition> getPositions() {
		return positions;
	}

	public void setPositions(Set<OrderPosition> positions) {
		this.positions = positions;
	}

	@Override
	public String toString() {
		return "Order(id=" + this.getId() + ", customer=" + this.getCustomer() + ")";
	}
}

But for most entities, this approach:

  • Adds several @ToString.Exclude annotations to your class, which makes it harder to read;
  • Introduces the risk that every new lazily fetched association might break your application; and
  • Requires more effort than using your IDE to generate the toString() method.

Avoid @Data

Lombok’s @Data annotation acts as a shortcut for @ToString, @EqualsAndHashCode, and @RequiredArgsConstructor annotations on your class; @Getter annotations on all fields; and @Setter annotations on all non-final fields.

@Entity
@Data
public class Order { ... }

So, if you build the Order class in the previous code snippet, Lombok generates getter and setter methods for all attributes and the methods equals(), hashCode(), and toString().

As I explained earlier in this article, Lombok’s equals() method isn’t suitable for entity classes and you need to be careful when using the @ToString annotation. Because of this, you shouldn’t use Lombok’s @Data annotation on your entity classes. On the other hand, you could use it for your DTO classes.

Conclusion

Entity classes have different requirements than plain Java classes. That makes Lombok’s generated equals() and hashCode() methods unusable and its toString() method risky to use.

You could, of course, use other Lombok annotations, like @Getter, @Setter, @Builder. I don’t think that these annotations provide a lot of value on an entity class. Your IDE can easily generate getter and setter methods for your attributes, and a good implementation of the builder pattern requires too much domain knowledge.

The bottom line is that you can use the @Getter, @Setter, and @Builder annotation without breaking your application. The only Lombok annotations you need to avoid are @Data, @ToString, and @EqualsAndHashCode.

8 Comments

  1. I’m a big fan of Lombok and cannot fully agree with what is written. Despite the JPA issues I see it as a great helper especially when you add some variables to your Entity or change a String to a Integer. Well I know you can easily rebuild the code but honestly I don#t want to touch the code since I can hardly distinguish which part is handmade or which automatically generated.

    About the functions of hash or equals I’m not clear how much Hibernate bypass by methods proxied from PersistentBags – either for regular Entities or cached. The PersistentBags are as well an issue for themselves since if I want to compare the current instance `Order` with the current object it might lead to false results with PersistentBags.

    Anyway the problems with the @ToString apply as well to automatically generated code. One needs to pay attention what is included and what not.

    1. Avatar photo Thorben Janssen says:

      Hi Roland,

      Changing an entity always requires a change to your table model and to your business logic. So, taking the extra step to update your entity and add a getter/setter method for it shouldn’t be a big deal. That’s especially the case because entity classes usually have no business logic. So, everything besides the attributes themselves and your equals() and hashCode() methods are generated.

      I don’t understand your worries about the equals() and hashCode() methods. Do you know this article: https://thorben-janssen.com/ultimate-guide-to-implementing-equals-and-hashcode-with-hibernate/ ?
      Comparing entities is not an issue as long as you follow the recommendations in that article. If you don’t use detached entities, you don’t need to implement equals and hashCode. If you might need to merge a detached entity, your hashCode() method should return a fixed value, and equals() should only compare the type and the primary key attributes. If the primary key of one of the entities is null, these two are not equal.

      Regards,
      Thorben

  2. Is it correct to assume that these restrictions apply to kotlin’s data classes also? Do you recommend not using kotlin data classes for entities?

    1. Avatar photo Thorben Janssen says:

      Hi Stefan,
      Unfortunately, I have never looked into Kotlin’s data classes. So, I can’t make any recommendations for them.
      Regards,
      Thorben

  3. Very good advice. Instead of avoid I would recommend to be more strict and only allow for explicitly included properties when using @EqualsAndHashCode (and only when appropriate!) and @ToString. Also, NEVER use @Data in entities. It can and will otherwise surprise you with a stack overflow at the most inconvenient times, like during a debugging session (because of tooling that evaluates these generated methods with possible circular references)

    1. Avatar photo Thorben Janssen says:

      Yes, you could explicitly define which attributes shall be included in your equals(), hashCode(), and toString methods. But there is still a risk that you miss it somewhere and you could easily generate these methods using your IDE. I prefer to avoid these annotations.

      Regards,
      Thorben

      1. Avatar photo Ktoś Obcy says:

        I’m sorry but let me get this straight – you argue agains annotation with explicit list of fields (because you may miss something) and at the same time recommend… generated code from IDE – in which you can also miss something? don’t you see contradiction here?

        Not to mention that small list of fields is more easily comprehensible that full blow method…

        1. Avatar photo Thorben Janssen says:

          Hi Ktoś,
          Yes, I recommend generated code from IDE for 4 reasons:
          1. You see the code that will be executed. In my experience, that makes it less likely that something gets missed.
          2. When generating your equals and hashCode methods, you only include the primary key attributes (see: https://thorben-janssen.com/ultimate-guide-to-implementing-equals-and-hashcode-with-hibernate/). Because of that, you will not need to update them when changing your entity class.
          3. You add or remove entity attributes for a reason. This will also require some changes to your business code. That makes it very unlikely that you will miss to add or remove its getter or setter methods. So, no risk here. But if you want, you could let Lombok handle that.
          4. Missing a new attribute in the toString() method isn’t a critical bug. But unintentionally including an attribute that maps an association can have a severe performance impact. I, therefore, prefer to update a generated toString() method whenever I add a new attribute to an entity.

          Regards,
          Thorben

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.