编程

Lombok & Hibernate: 如何避免常见的陷阱

481 2024-06-18 19:46:00

Lombok 是 Java 开发人员中流行的框架,因为它生成重复的样板代码,如 gettersetter 方法、equalshashCode 方法以及默认构造函数。你所需要做的就是向类中添加一些注释,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