在实现equals/hashCode方面,您有三种选择:
- 使用应用程序生成的标识符,例如UUID
- 基于业务键实现它
- 基于主键实现它
使用应用程序生成的标识符是最简单的方法,但也有一些缺点
- 如果将其用作PK,则使用128位比32位或64位更慢
- "调试更难",因为检查某些数据是否正确非常困难
如果您可以使用这些缺点,则只需使用此方法。
为了克服连接问题,可以将UUID用作自然键,将序列值用作主键,但是在具有嵌入式ID的组合子实体中仍可能遇到equals/hashCode实现问题,因为您将希望基于主键进行连接。在子实体ID中使用自然键,使用主键来引用父级是一个很好的折衷方案。
@Entity class Parent {
@Id @GeneratedValue Long id;
@NaturalId UUID uuid;
@OneToMany(mappedBy = "parent") Set<Child> children;
}
@Entity class Child {
@EmbeddedId ChildId id;
@ManyToOne Parent parent;
@Embeddable class ChildId {
UUID parentUuid;
UUID childUuid;
}
}
我认为这是最干净的方法,因为它将避免所有缺点,并同时为您提供一个值(UUID),您可以与外部系统共享,而不会暴露系统内部。
如果您期望用户提供业务密钥,则基于业务密钥实现是一个不错的想法,但也有一些缺点
大多数情况下,此业务密钥将是用户提供的某种代码,较少情况下是多个属性的组合。
- 连接速度较慢,因为基于可变长度文本的连接速度很慢。如果关键字超过一定长度,某些DBMS甚至可能无法创建索引。
- 根据我的经验,业务密钥往往会发生变化,这将需要对引用它的对象进行级联更新。如果外部系统引用它,则这是不可能的。
我认为您不应该仅基于业务密钥实现或操作。这是一个不错的附加功能,即用户可以快速通过业务密钥搜索,但系统不应依赖它来运行。
基于主键实现也有问题,但可能并不是那么重要
如果您需要向外部系统公开ID,请使用我建议的UUID方法。如果不需要,您仍然可以使用UUID方法,但不必这样做。
使用DBMS生成的ID在equals/hashCode中的问题源于对象可能已经被添加到基于哈希的集合中,而此时尚未分配ID。
解决这个问题的明显方法是在分配ID之前不要将对象添加到基于哈希的集合中。我知道这并非总是可能的,因为您可能希望在分配ID之前进行去重。为了仍然能够使用基于哈希的集合,您只需在分配ID后重新构建集合即可。
您可以像这样做:
@Entity class Parent {
@Id @GeneratedValue Long id;
@OneToMany(mappedBy = "parent") Set<Child> children;
}
@Entity class Child {
@EmbeddedId ChildId id;
@ManyToOne Parent parent;
@PrePersist void postPersist() {
parent.children.remove(this);
}
@PostPersist void postPersist() {
parent.children.add(this);
}
@Embeddable class ChildId {
Long parentId;
@GeneratedValue Long childId;
}
}
我自己没有测试过确切的方法,所以我不确定在 pre- 和 post-persist 事件中更改集合的方式是如何工作的,但思路是:
- 暂时从基于哈希的集合中删除对象
- 持久化它
- 将对象重新添加到基于哈希的集合中
另一种解决方法是在更新/持久化后简单地重建所有基于哈希的模型。
最终决定权在你手中。我个人大多数情况下使用序列号为基础的方法,只有在需要向外部系统公开标识符时才使用 UUID 方法。
hashcode()
应该返回相同的值,除非在equals()
实现中使用的任何字段发生更改。换句话说,如果您的类中有三个字段,并且您的equals()
方法仅使用其中两个来确定实例的相等性,则可以期望hashcode()
返回值在更改其中一个字段的值时发生更改 - 当您考虑到此对象实例不再“等于”旧实例所代表的值时,这是有意义的。 - matt b