这里有一些关于JPA实体的讨论,以及应该为JPA实体类使用哪些hashCode()/equals()实现。它们中的大多数(如果不是全部)依赖于Hibernate,但是我想中立地讨论它们的jpa实现(顺便说一下,我使用的是EclipseLink)。
所有可能的实现都有其自身的优点和缺点:
hashCode()/equals()契约一致性(不可变性)用于列表/集操作
是否可以检测到相同的对象(例如来自不同会话的对象,来自惰性加载数据结构的动态代理)
实体在分离(或非持久化)状态下是否正确运行
在我看来,有三种选择:
Do not override them; rely on Object.equals() and Object.hashCode()
hashCode()/equals() work
cannot identify identical objects, problems with dynamic proxies
no problems with detached entities
Override them, based on the primary key
hashCode()/equals() are broken
correct identity (for all managed entities)
problems with detached entities
Override them, based on the Business-Id (non-primary key fields; what about foreign keys?)
hashCode()/equals() are broken
correct identity (for all managed entities)
no problems with detached entities
我的问题是:
我是否错过了一个选择和/或赞成/反对的观点?
你选择了什么,为什么?
更新1:
通过“hashCode()/equals()是坏的”,我的意思是连续的hashCode()调用可能返回不同的值,这(当正确实现时)在对象API文档的意义上不是坏的,但是当试图从Map、Set或其他基于哈希的集合中检索更改的实体时,会导致问题。因此,JPA实现(至少是EclipseLink)在某些情况下不能正确工作。
更新2:
谢谢你的回答——大部分问题都很有质量。
不幸的是,我仍然不确定哪种方法最适合实际应用程序,或者如何确定最适合我的应用程序的方法。所以,我将保持这个问题的开放性,希望有更多的讨论和/或意见。
请考虑以下基于预定义类型标识符和ID的方法。
JPA的具体假设:
具有相同“类型”和相同非空ID的实体被认为是相等的
非持久化实体(假设没有ID)永远不等于其他实体
抽象实体:
@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {
@Id @GeneratedValue
private K id;
@Transient
private final String kind;
public AbstractPersistable(final String kind) {
this.kind = requireNonNull(kind, "Entity kind cannot be null");
}
@Override
public final boolean equals(final Object obj) {
if (this == obj) return true;
if (!(obj instanceof AbstractPersistable)) return false;
final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
return null != this.id
&& Objects.equals(this.id, that.id)
&& Objects.equals(this.kind, that.kind);
}
@Override
public final int hashCode() {
return Objects.hash(kind, id);
}
public K getId() {
return id;
}
protected void setId(final K id) {
this.id = id;
}
}
具体实体示例:
static class Foo extends AbstractPersistable<Long> {
public Foo() {
super("Foo");
}
}
测试的例子:
@Test
public void test_EqualsAndHashcode_GivenSubclass() {
// Check contract
EqualsVerifier.forClass(Foo.class)
.suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
.withOnlyTheseFields("id", "kind")
.withNonnullFields("id", "kind")
.verify();
// Ensure new objects are not equal
assertNotEquals(new Foo(), new Foo());
}
主要优势:
简单
确保子类提供类型标识
使用代理类预测行为
缺点:
要求每个实体调用super()
注:
使用继承时需要注意。例如,类A和类B扩展A的实例相等性可能取决于应用程序的具体细节。
理想情况下,使用业务密钥作为ID
期待您的评论。
虽然使用业务键(选项3)是最常推荐的方法(Hibernate社区wiki,“Java Persistence with Hibernate”第398页),而且这是我们最常用的方法,但Hibernate有一个错误会破坏急于获取的集:HHH-3799。在这种情况下,Hibernate可以在字段初始化之前将一个实体添加到集合中。我不确定为什么这个错误没有得到更多的关注,因为它确实使推荐的业务键方法出现了问题。
我认为问题的核心是equals和hashCode应该基于不可变状态(参考Odersky等人),而具有Hibernate管理的主键的Hibernate实体没有这样的不可变状态。当一个瞬态对象变成持久对象时,Hibernate会修改主键。当Hibernate在初始化过程中为对象补水时,业务键也会被Hibernate修改。
这就只剩下选项1了,基于对象身份继承java.lang.Object实现,或者使用James Brundege在“不要让Hibernate窃取你的身份”(Stijn Geukens的回答已经引用了)和Lance Arlaus在“对象生成:Hibernate集成的更好方法”中建议的应用程序管理的主键。
The biggest problem with option 1 is that detached instances can't be compared with persistent instances using .equals(). But that's OK; the contract of equals and hashCode leaves it up to the developer to decide what equality means for each class. So just let equals and hashCode inherit from Object. If you need to compare a detached instance to a persistent instance, you can create a new method explicitly for that purpose, perhaps boolean sameEntity or boolean dbEquivalent or boolean businessEquals.
我个人已经在不同的项目中使用了这三种策略。我必须说,选项1在我看来是现实应用中最可行的。以我的经验来看,打破hashCode()/equals()一致性会导致许多疯狂的错误,因为你每次都会遇到这样的情况:在一个实体被添加到一个集合后,相等的结果发生了变化。
但也有更多的选择(也有它们的优点和缺点):
a) hashCode/equals基于一组不可变的、非空的、构造函数赋值的字段
(+)三个标准都有保证
(-)字段值必须可用以创建新实例
(-)如果你必须改变其中一个,处理起来会很复杂
b) hashCode/equals基于应用程序(在构造函数中)分配的主键,而不是JPA
(+)三个标准都有保证
(-)您不能利用简单可靠的ID生成策略,如DB序列
(-)如果在分布式环境(客户端/服务器)或应用服务器集群中创建新实体会很复杂
c) hashCode/equals基于实体的构造函数分配的UUID
(+)三个标准都有保证
(-)生成UUID的开销
(-)可能会有使用两次相同UUID的风险,这取决于所使用的算法(可能由DB上的唯一索引检测到)