这里有一些关于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:
仅用于持久化层(以便持久化提供程序和数据库能够找出对象之间的关系)。
是为了我们的应用程序需要(特别是equals()和hashCode())
来看看:
@Entity
public class User {
@Id
private int id; // Persistence ID
private UUID uuid; // Business ID
// assuming all fields are subject to change
// If we forbid users change their email or screenName we can use these
// fields for business ID instead, but generally that's not the case
private String screenName;
private String email;
// I don't put UUID generation in constructor for performance reasons.
// I call setUuid() when I create a new entity
public User() {
}
// This method is only called when a brand new entity is added to
// persistence context - I add it as a safety net only but it might work
// for you. In some cases (say, when I add this entity to some set before
// calling em.persist()) setting a UUID might be too late. If I get a log
// output it means that I forgot to call setUuid() somewhere.
@PrePersist
public void ensureUuid() {
if (getUuid() == null) {
log.warn(format("User's UUID wasn't set on time. "
+ "uuid: %s, name: %s, email: %s",
getUuid(), getScreenName(), getEmail()));
setUuid(UUID.randomUUID());
}
}
// equals() and hashCode() rely on non-changing data only. Thus we
// guarantee that no matter how field values are changed we won't
// lose our entity in hash-based Sets.
@Override
public int hashCode() {
return getUuid().hashCode();
}
// Note that I don't use direct field access inside my entity classes and
// call getters instead. That's because Persistence provider (PP) might
// want to load entity data lazily. And I don't use
// this.getClass() == other.getClass()
// for the same reason. In order to support laziness PP might need to wrap
// my entity object in some kind of proxy, i.e. subclassing it.
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (!(obj instanceof User))
return false;
return getUuid().equals(((User) obj).getUuid());
}
// Getters and setters follow
}
编辑:澄清我关于调用setUuid()方法的观点。下面是一个典型的场景:
User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("yoda@jedicouncil.org");
jediSet.add(user); // here's bug - we forgot to set UUID and
//we won't find Yoda in Jedi set
em.persist(user); // ensureUuid() was called and printed the log for me.
jediCouncilSet.add(user); // Ok, we got a UUID now
当我运行测试并看到日志输出时,我解决了这个问题:
User user = new User();
user.setUuid(UUID.randomUUID());
或者,也可以提供一个单独的构造函数:
@Entity
public class User {
@Id
private int id; // Persistence ID
private UUID uuid; // Business ID
... // fields
// Constructor for Persistence provider to use
public User() {
}
// Constructor I use when creating new entities
public User(UUID uuid) {
setUuid(uuid);
}
... // rest of the entity.
}
我的例子是这样的:
User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time
em.persist(user); // and no log output
我使用默认构造函数和setter,但您可能会发现双构造函数方法更适合您。
我总是重写equals/hashcode,并基于业务id实现它。对我来说这是最合理的解决办法。请看下面的链接。
总而言之,这里列出了处理equals/hashCode的不同方法中哪些是有效的,哪些是无效的:
编辑:
为了解释为什么这对我有用:
I don't usually use hashed-based collection (HashMap/HashSet) in my JPA application. If I must, I prefer to create UniqueList solution.
I think changing business id on runtime is not a best practice for any database application. On rare cases where there is no other solution, I'd do special treatment like remove the element and put it back to the hashed-based collection.
For my model, I set the business id on constructor and doesn't provide setters for it. I let JPA implementation to change the field instead of the property.
UUID solution seems to be overkill. Why UUID if you have natural business id? I would after all set the uniqueness of the business id in the database. Why having THREE indexes for each table in the database then?
我试着自己回答这个问题,直到我读了这篇文章,尤其是画了一个,我才完全满意找到的解决方案。我喜欢他懒创建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;
}
我个人已经在不同的项目中使用了这三种策略。我必须说,选项1在我看来是现实应用中最可行的。以我的经验来看,打破hashCode()/equals()一致性会导致许多疯狂的错误,因为你每次都会遇到这样的情况:在一个实体被添加到一个集合后,相等的结果发生了变化。
但也有更多的选择(也有它们的优点和缺点):
a) hashCode/equals基于一组不可变的、非空的、构造函数赋值的字段
(+)三个标准都有保证
(-)字段值必须可用以创建新实例
(-)如果你必须改变其中一个,处理起来会很复杂
b) hashCode/equals基于应用程序(在构造函数中)分配的主键,而不是JPA
(+)三个标准都有保证
(-)您不能利用简单可靠的ID生成策略,如DB序列
(-)如果在分布式环境(客户端/服务器)或应用服务器集群中创建新实体会很复杂
c) hashCode/equals基于实体的构造函数分配的UUID
(+)三个标准都有保证
(-)生成UUID的开销
(-)可能会有使用两次相同UUID的风险,这取决于所使用的算法(可能由DB上的唯一索引检测到)