这里有一些关于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:
谢谢你的回答——大部分问题都很有质量。
不幸的是,我仍然不确定哪种方法最适合实际应用程序,或者如何确定最适合我的应用程序的方法。所以,我将保持这个问题的开放性,希望有更多的讨论和/或意见。
实际上,似乎Option 2(主键)是最常用的。
自然的和不可变的业务密钥是很少的事情,创建和支持合成密钥对于解决情况来说太沉重了,这可能从来没有发生过。
看一下spring-data-jpa AbstractPersistable实现(唯一需要注意的是:对于Hibernate实现使用Hibernate. getclass)。
public boolean equals(Object obj) {
if (null == obj) {
return false;
}
if (this == obj) {
return true;
}
if (!getClass().equals(ClassUtils.getUserClass(obj))) {
return false;
}
AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
return null == this.getId() ? false : this.getId().equals(that.getId());
}
@Override
public int hashCode() {
int hashCode = 17;
hashCode += null == getId() ? 0 : getId().hashCode() * 31;
return hashCode;
}
注意在HashSet/HashMap中操作新对象。
相反,选项1(保留对象实现)在合并后被破坏,这是非常常见的情况。
如果你没有业务键,并且需要在哈希结构中操作新实体,则将hashCode重写为常量,如下所示Vlad Mihalcea的建议。
我个人已经在不同的项目中使用了这三种策略。我必须说,选项1在我看来是现实应用中最可行的。以我的经验来看,打破hashCode()/equals()一致性会导致许多疯狂的错误,因为你每次都会遇到这样的情况:在一个实体被添加到一个集合后,相等的结果发生了变化。
但也有更多的选择(也有它们的优点和缺点):
a) hashCode/equals基于一组不可变的、非空的、构造函数赋值的字段
(+)三个标准都有保证
(-)字段值必须可用以创建新实例
(-)如果你必须改变其中一个,处理起来会很复杂
b) hashCode/equals基于应用程序(在构造函数中)分配的主键,而不是JPA
(+)三个标准都有保证
(-)您不能利用简单可靠的ID生成策略,如DB序列
(-)如果在分布式环境(客户端/服务器)或应用服务器集群中创建新实体会很复杂
c) hashCode/equals基于实体的构造函数分配的UUID
(+)三个标准都有保证
(-)生成UUID的开销
(-)可能会有使用两次相同UUID的风险,这取决于所使用的算法(可能由DB上的唯一索引检测到)
请考虑以下基于预定义类型标识符和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
期待您的评论。
我试着自己回答这个问题,直到我读了这篇文章,尤其是画了一个,我才完全满意找到的解决方案。我喜欢他懒创建UUID和最佳存储它的方式。
但我想增加更多的灵活性,即惰性创建UUID仅当hashCode()/equals()被访问时,第一次持久化实体与每个解决方案的优点:
Equals()表示“对象指向相同的逻辑实体”
尽可能使用数据库ID,因为为什么我要做两次工作(性能问题)
防止在尚未持久的实体上访问hashCode()/equals()时出现问题,并在它确实被持久后保持相同的行为
我真的很感激对我的混合解决方案的反馈如下
public class MyEntity {
@Id()
@Column(name = "ID", length = 20, nullable = false, unique = true)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
@Transient private UUID uuid = null;
@Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
private Long uuidMostSignificantBits = null;
@Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
private Long uuidLeastSignificantBits = null;
@Override
public final int hashCode() {
return this.getUuid().hashCode();
}
@Override
public final boolean equals(Object toBeCompared) {
if(this == toBeCompared) {
return true;
}
if(toBeCompared == null) {
return false;
}
if(!this.getClass().isInstance(toBeCompared)) {
return false;
}
return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
}
public final UUID getUuid() {
// UUID already accessed on this physical object
if(this.uuid != null) {
return this.uuid;
}
// UUID one day generated on this entity before it was persisted
if(this.uuidMostSignificantBits != null) {
this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
// UUID never generated on this entity before it was persisted
} else if(this.getId() != null) {
this.uuid = new UUID(this.getId(), this.getId());
// UUID never accessed on this not yet persisted entity
} else {
this.setUuid(UUID.randomUUID());
}
return this.uuid;
}
private void setUuid(UUID uuid) {
if(uuid == null) {
return;
}
// For the one hypothetical case where generated UUID could colude with UUID build from IDs
if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
}
this.uuidMostSignificantBits = uuid.getMostSignificantBits();
this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
this.uuid = uuid;
}
业务密钥方法不适合我们。我们使用DB生成的ID、临时临时tempId和重写equal()/hashcode()来解决这个困境。所有实体都是Entity的后代。优点:
DB中没有额外字段
在后代实体中没有额外的编码,一种方法适用于所有的实体
没有性能问题(如UUID), DB Id生成
使用hashmap没有问题(不需要记住equal & etc的使用)。
新实体的Hashcode即使在持久化后也不会及时更改
缺点:
序列化和反序列化非持久化实体可能会出现问题
从DB重新加载后,保存的实体的Hashcode可能会改变
非持久化对象被认为总是不同的(也许这是对的?)
还有什么?
看看我们的代码:
@MappedSuperclass
abstract public class Entity implements Serializable {
@Id
@GeneratedValue
@Column(nullable = false, updatable = false)
protected Long id;
@Transient
private Long tempId;
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
private void setTempId(Long tempId) {
this.tempId = tempId;
}
// Fix Id on first call from equal() or hashCode()
private Long getTempId() {
if (tempId == null)
// if we have id already, use it, else use 0
setTempId(getId() == null ? 0 : getId());
return tempId;
}
@Override
public boolean equals(Object obj) {
if (super.equals(obj))
return true;
// take proxied object into account
if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
return false;
Entity o = (Entity) obj;
return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
}
// hash doesn't change in time
@Override
public int hashCode() {
return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
}
}