这里有一些关于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:

谢谢你的回答——大部分问题都很有质量。 不幸的是,我仍然不确定哪种方法最适合实际应用程序,或者如何确定最适合我的应用程序的方法。所以,我将保持这个问题的开放性,希望有更多的讨论和/或意见。


当前回答

我试着自己回答这个问题,直到我读了这篇文章,尤其是画了一个,我才完全满意找到的解决方案。我喜欢他懒创建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; }

其他回答

实际上,似乎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的建议。

在我看来,你有3个实现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;
  // equals/hashCode based on uuid
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @Embeddable class ChildId {
    UUID parentUuid;
    UUID childUuid;
    // equals/hashCode based on parentUuid and childUuid
  }
  // equals/hashCode based on id
}

在我看来,这是最干净的方法,因为它将避免所有的缺点,同时为您提供一个值(UUID),您可以与外部系统共享,而不暴露系统内部。

基于业务键来实现它(如果你能从用户那里得到的话)是个好主意,但也有一些缺点

大多数情况下,这个业务键是用户提供的某种代码,很少是多个属性的组合。

连接速度较慢,因为基于可变长度文本的连接速度很慢。如果键超过一定长度,一些DBMS甚至可能在创建索引时遇到问题。 根据我的经验,业务键往往会发生变化,这就需要对引用它的对象进行级联更新。如果外部系统引用它,这是不可能的

在我看来,你不应该专门实现或使用业务键。这是一个很好的附加功能,用户可以通过业务键快速搜索,但系统不应该依赖它来运行。

基于主键实现它有它的问题,但也许这不是什么大问题

如果需要向外部系统公开id,请使用我建议的UUID方法。如果您不这样做,您仍然可以使用UUID方法,但不必这样做。 在equals/hashCode中使用DBMS生成的id的问题源于这样一个事实,即对象可能在分配id之前已被添加到基于哈希的集合中。

解决这个问题的明显方法是在分配id之前不将对象添加到基于哈希的集合中。我知道这并不总是可行的,因为您可能需要在分配id之前进行重复数据删除。要仍然能够使用基于散列的集合,您只需在分配id后重新构建集合。

你可以这样做:

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on id
}

@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;
    // equals/hashCode based on parentId and childId
  }
  // equals/hashCode based on id
}

我自己还没有测试过确切的方法,所以我不确定在持久化事件之前和之后更改集合是如何工作的,但这个想法是:

临时从基于散列的集合中移除对象 坚持它 将对象重新添加到基于散列的集合中

解决这个问题的另一种方法是在更新/持久化之后重新构建所有基于哈希的模型。

最后,决定权在你。我个人大部分时间使用基于序列的方法,只有在需要向外部系统公开标识符时才使用UUID方法。

虽然使用业务键(选项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.

如果你想对你的set使用equals()/hashCode(),也就是说同一个实体只能出现一次,那么只有一个选项:选项2。这是因为根据定义,实体的主键永远不会改变(如果有人确实更新了它,它就不再是同一个实体了)

您应该从字面上理解:因为equals()/hashCode()是基于主键的,所以在设置主键之前,您不能使用这些方法。所以你不应该把实体放到集合里,直到它们被赋主键。(是的,uuid和类似的概念可能有助于早期分配主键。)

Now, it's theoretically also possible to achieve that with Option 3, even though so-called "business-keys" have the nasty drawback that they can change: "All you'll have to do is delete the already inserted entities from the set(s), and re-insert them." That is true - but it also means, that in a distributed system, you'll have to make sure, that this is done absolutely everywhere the data has been inserted to (and you'll have to make sure, that the update is performed, before other things occur). You'll need a sophisticated update mechanism, especially if some remote systems aren't currently reachable...

只有当集合中的所有对象都来自同一个Hibernate会话时,才可以使用选项1。Hibernate文档在13.1.3章中非常清楚地说明了这一点。考虑对象同一性:

Within a Session the application can safely use == to compare objects. However, an application that uses == outside of a Session might produce unexpected results. This might occur even in some unexpected places. For example, if you put two detached instances into the same Set, both might have the same database identity (i.e., they represent the same row). JVM identity, however, is by definition not guaranteed for instances in a detached state. The developer has to override the equals() and hashCode() methods in persistent classes and implement their own notion of object equality.

它继续主张选择3:

这里有一个警告:永远不要使用数据库标识符来实现相等。使用由唯一的、通常是不可变的属性组合而成的业务键。如果将瞬态对象持久化,则数据库标识符将更改。如果瞬态实例(通常与分离实例一起)保存在Set中,更改hashcode将破坏Set的契约。

这是真的,如果你

不能提前分配id(例如使用uuid) 当对象处于瞬态时,你肯定想把它们放到集合中。

否则,您可以自由选择选项2。

然后它提到了相对稳定性的需求:

业务键的属性不必像数据库主键那样稳定;只要对象在同一集合中,你就必须保证稳定性。

这是正确的。我所看到的实际问题是:如果你不能保证绝对的稳定性,你如何能够保证“只要对象在同一个集合中”的稳定性。我可以想象一些特殊的情况(比如只在对话中使用集合,然后将其丢弃),但我会质疑这种方法的一般实用性。


短版:

选项1只能用于单个会话中的对象。 如果可以,使用选项2。(尽早分配PK,因为在分配PK之前你不能在集合中使用对象。) 如果你能保证相对的稳定性,你可以使用选项3。但是要小心。

我个人已经在不同的项目中使用了这三种策略。我必须说,选项1在我看来是现实应用中最可行的。以我的经验来看,打破hashCode()/equals()一致性会导致许多疯狂的错误,因为你每次都会遇到这样的情况:在一个实体被添加到一个集合后,相等的结果发生了变化。

但也有更多的选择(也有它们的优点和缺点):


a) hashCode/equals基于一组不可变的、非空的、构造函数赋值的字段

(+)三个标准都有保证

(-)字段值必须可用以创建新实例

(-)如果你必须改变其中一个,处理起来会很复杂


b) hashCode/equals基于应用程序(在构造函数中)分配的主键,而不是JPA

(+)三个标准都有保证

(-)您不能利用简单可靠的ID生成策略,如DB序列

(-)如果在分布式环境(客户端/服务器)或应用服务器集群中创建新实体会很复杂


c) hashCode/equals基于实体的构造函数分配的UUID

(+)三个标准都有保证

(-)生成UUID的开销

(-)可能会有使用两次相同UUID的风险,这取决于所使用的算法(可能由DB上的唯一索引检测到)