使用JPA持久化实体时,equals和hashcode的最佳实践

本文探讨了在使用JPA进行实体持久化时,equals和hashCode方法的实现最佳实践。根据JAVA规范,好的equals实现应满足自反性、对称性、传递性和一致性。标识符分为被赋值和数据库生成两种类型,数据库生成的标识符在持久化前后可能导致hashCode不一致。因此,推荐将hashCode固定为31以确保一致性。虽然这可能导致所有实体放入同一哈希桶,但在处理小规模数据集时,性能影响有限。

起因描述

项目使用的是JhipsterJhipster可以自动生成entity,dto,service等类。但是注意到,JHipster生成的entityhashcode()方法永远都返回31

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

于是我去github看了相关issue,作者说他是故意这样做的,并提供了一篇参考:
https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-with-jpa-and-hibernate/
我也找了另一个相关参考
https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/

以下最佳实践部分实际上是以上两篇参考的部分摘录,所以读者可以直接阅读参考而不进行下面的阅读

最佳实践

equals需要遵循的规范

根据JAVA规范一个好的equals实现必须满足以下条件

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.

即:
- 自反性
- 对称性
- 传递性
- 一致性

前三项都很直观且易于实现,只有一致性对于JPAHibernate来说有一定挑战

标识符分类

原文使用的是Identifier,我也不知道怎么翻译,就叫标识符好了,实际上指的就是实体的一个字段。标识符可以分为两类:
- 被赋值的标识符:也就是说在持久化之前,已经被赋值了
- 数据库生成的标识符:一般是id,由数据库根据generator生成

对于被赋值的标识符而言,在持久化前就已经知道它们的值了,因此执行equals是很安全的,对他们进行equals比较一般用如下方式:

@Entity(name = "Book")
@Table(name = "book")
public class Book
    implements Identifiable<Long> {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @NaturalId
    private String isbn;
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Book)) return false;
        Book book = (Book) o;
        return Objects.equals(getIsbn(), book.getIsbn());
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(getIsbn());
    }
 
    //Getters and setters omitted for brevity
}

数据库生成的标识符

但是对于数据库生成的标识符,却又完全不同了,我们稍微修改下上面的代码

@Entity
public class Book implements Identifiable<Long> {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Book)) return false;
        Book book = (Book) o;
        return Objects.equals(getId(), book.getId());
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(getId());
    }
 
    //Getters and setters omitted for brevity
}

想象我们先创建了一个entity,把它存入一个HashSet里,此时由于idnull,则计算出的hashCode0
然后,我们对entity进行持久化,entity被分配了一个id,也就是说再对它执行hashCode方法将返回一个完全不同的值
那么再去之前的HashSet里找这个entity,找得到吗?当然找不到

因此,最佳实践为

@Entity(name = "Post")
@Table(name = "post")
public class Post implements Identifiable<Long> {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    public Post() {}
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
 
        if (!(o instanceof Post))
            return false;
 
        Post other = (Post) o;
 
        return id != null &&
               id.equals(other.getId());
    }
 
    @Override
    public int hashCode() {
        return 31;
    }
  
    //Getters and setters omitted for brevity
}

通过把hashCode设置为永远返回31,使得entity在任何状态下都保持一致性

结论

hashcode固定为31看起来是个傻叉的设计,因为这会使得HashSetHashMap把所有实体都放在一个bucket
但是,从性能方面考虑,开发者要做的首要是限制数据集合的数量。也就是说,如果你从数据库读取的数据有几千上万条,那么你要考虑的是如何避免一次性读取这么多数据,而非HashSetHashMap的性能。正常情况下,几十或几百条数据,放在一个bucket,性能并不会太差

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值