Lombok & Hibernate: 如何避免常见的陷阱
Lombok 是 Java 开发人员中流行的框架,因为它生成重复的样板代码,如 getter
和 setter
方法、equals
和 hashCode
方法以及默认构造函数。你所需要做的就是向类中添加一些注释,Lombok 将在编译时添加所需的代码。这对于普通类来说效果相当好,但如果将其用于 Hibernate 实体,则会引入一些危险的陷阱。
为了避免这些陷阱,我建议不要将 Lombok 用于实体类。如果使用 IDE 的代码生成器功能,你将用不到一分钟的时间自己创建这些方法的更好实现。
因此,让我们来看看 Lombok 最流行的一些注释,以及为什么在 Hibernate 中使用它们时需要小心。
基本域模型
在下面的所有示例中,我将使用这个非常基本的域模型。Order 实体类表示在线商店中的订单。
@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 + "]";
}
}
对于每个订单,需要保存 ID,客户名称以及一个或多个订单位置。这些是由 OrderPosition
类建模的。它映射 ID、产品名称、订购数量以及对订单的引用。
@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 注释
Lombok 是一个非常流行的框架,尽管它几乎没有注释。这是因为它解决了开发人员的痛点。
然而,Lombok 不能很好地与许多其他框架配合使用。我建议你避免使用它最常用的三种注释。
不要使用 @EqualsAndHashCode
开发人员经常讨论为实体类实现 equals()
和 hashCode()
方法的必要性。这似乎是一个复杂而重要的主题,因为需要同时满足 Java 语言规范定义的契约和 JPA 规范定义的规则。
但是,它实际上比看起来简单得多。正如我在实现 equals()
和 hashCode()
的指南中详细解释的那样,你的 hashCode()
方法应该始终返回一个固定值,例如 42。在 equals()
方法中,应该只比较对象的类型及其主键值。如果有一个主键为 null
,那么 equals
方法必须返回false
。
如果你不想自己实现这些方法,可以用 Lombok 的 @EqualsAndHashCode
注释来注释你的类。
@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 生成以下 equals()
和 hashCode()
方法。
@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;
}
}
如果你仔细观察这两种方法,你会发现它们没有遵循我之前的建议。这会导致多个问题。
让我们从最明显的一个开始:这两个方法都包括类的所有非 final 属性。你可以通过将 @EqualAndHashCode
注释的 onlyExplicitlyIncluded
属性设置为 true
并使用 @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 只在哈希代码计算和等值检查时包含主键值。
@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;
}
}
这并不能解决所有问题。如果两个实体对象的主键值都为 null
,则 equals()
方法应返回 false
。但是 Lombok 的 equals()
方法返回 true
。因此,不能将两个新实体对象添加到一个集中。在上面显示的示例中,这意味着不能向 Order
添加两个新的 OrderPosition
对象。因此,应该避免使用 Lombok 的 @EqualsAndHashCode
注释。
小心使用 @ToString
如果你使用 Lombok 的 @ToString
注释你的实体类,Lombok 会生成一个 toString()
方法。
@Entity
@ToString
public class Order { ... }
其返回的字符串包含所有该类的非 final 属性。
@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() + ")";
}
}
将该注释与实体类一起使用是有风险的,因为可能不是所有属性都被初始化过。如果将关联的 FetchType
设置为 LAZY
或使用多对多关联的默认查询,Hibernate 将尝试从数据库中读取关联。如果在活动的 Hibernate 会话中执行此操作,这将导致额外的查询并降低应用的速度。更糟糕的是,如果你在不活跃的 Hibernate 会话的情况下进行此操作。在这种情况下,Hibernate 抛出一个 LazyInitializationException
。
可以通过再 toString()
方法中排除所有延迟获取的关联可以避免这种情况。为此,你需要使用 @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<>();
...
}
如上述代码所示,Lambok 的 toString()
方法不再包含 orderPosition
属性并避免了所有懒加载问题。
@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() + ")";
}
}
但对于大部分实体,该方法:
- 将多个 @ToString.Exclude 注释添加到你类中,使之更难阅读;
- 新的懒加载关联可能带来风险,破坏应用,并且
- 比起使用 IDE 来生成
toString()
方法,需要更多的付出。
避免 @Data
Lombok 的 @Data 注释扮演着在类上进行 @ToString
, @EqualsAndHashCode
和 @RequiredArgsConstructor
注释;在所有字段上进行 @Getter
注释; 以及在所有非 final 字段上进行 @Setter
注释的角色。
@Entity
@Data
public class Order { ... }
因此,如果你使用前面的代码片段中构建 Order
类,Lombok 将为所有属性生成 getter 和 setter 方法,以及 equals()
、hashCode()
和toString()
方法。
正如我在本文前面所解释的,Lombok 的 equals()
方法不适合实体类,在使用 @ToString
注释时需要小心。因此,不应该在实体类上使用Lombok 的 @Data
注释。另一方面,你可以将它用于 DTO 类。
结论
实体类与普通 Java 类有不同的要求。这使得 Lombok 生成的 equals()
和 hashCode()
方法不可用,其 toString()
方法使用起来有风险。
当然,你可以使用其他 Lombok 注释,如 @Getter
、@Setter
、@Builder
。我不认为这些注释为实体类提供了太多价值。IDE 可以很容易地为属性生成 getter 和 setter 方法,而构建器模式的良好实现需要太多的域知识。
底线是,你可以在不破坏应用的情况下使用 @Getter
、@Setter
和 @Builder
注释。唯一需要避免的 Lombok 注释是 @Data
、@ToString
和 @EqualAndHashCode
。