16

Lombok & Hibernate: How to Avoid Common Pitfalls

 3 years ago
source link: https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Lombok & Hibernate: How to Avoid Common Pitfalls

By Thorben Janssen



Mapping

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.

Java8supportinHibernate5-3dcover.png
NativeQuerieswithHibernate-3dcover1.png

2 Free Ebooks 

to boost your Hibernate skills

Sign up below to join the Thoughts on Java Newsletter and to get your free ebooks:

  • Java 8 Support in Hibernate 5
  • Native Queries with Hibernate

I will collect, use and protect your data in accordance with my Privacy policy.

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK