起因描述
项目使用的是Jhipster,Jhipster可以自动生成entity,dto,service等类。但是注意到,JHipster生成的entity的hashcode()方法永远都返回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.
即:
- 自反性
- 对称性
- 传递性
- 一致性
前三项都很直观且易于实现,只有一致性对于JPA和Hibernate来说有一定挑战
标识符分类
原文使用的是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里,此时由于id为null,则计算出的hashCode为0
然后,我们对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看起来是个傻叉的设计,因为这会使得HashSet或HashMap把所有实体都放在一个bucket里
但是,从性能方面考虑,开发者要做的首要是限制数据集合的数量。也就是说,如果你从数据库读取的数据有几千上万条,那么你要考虑的是如何避免一次性读取这么多数据,而非HashSet或HashMap的性能。正常情况下,几十或几百条数据,放在一个bucket,性能并不会太差

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

3774

被折叠的 条评论
为什么被折叠?



