这里有一些关于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的建议。

其他回答

这是每个使用Java和JPA的IT系统中的常见问题。痛点不仅仅是实现equals()和hashCode(),它还影响组织引用实体的方式以及其客户机引用同一实体的方式。我已经看够了没有商业钥匙的痛苦,以至于我写了自己的博客来表达我的观点。

简而言之:使用一个简短的、人类可读的、带有有意义前缀的顺序ID作为业务键,该ID生成时不依赖于RAM以外的任何存储。Twitter的雪花就是一个很好的例子。

显然,这里已经有了非常有用的答案,但我将告诉你我们是怎么做的。

我们什么也不做。

如果我们确实需要= /hashcode来处理集合,则使用uuid。 您只需在构造函数中创建UUID。我们使用http://wiki.fasterxml.com/JugHome作为UUID。UUID的CPU开销稍高,但与序列化和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

期待您的评论。

Jakarta Persistence 3.0,第4.12节写道:

相同抽象模式类型的两个实体当且仅当它们具有相同的主键值时相等。

我看不出为什么Java代码的行为应该有所不同。

If the entity class is in a so called "transient" state, i.e. it's not yet persisted and it has no identifier, then the hashCode/equals methods can not return a value, they ought to blow up, ideally implicitly with a NullPointerException when the method attempts to traverse the ID. Either way, this will effectively stop application code from putting a non-managed entity into a hash-based data structure. In fact, why not go one step further and blow up if the class and identifier are equal, but other important attributes such as the version are unequal (IllegalStateException)! Fail-fast in a deterministic way is always the preferred option.

警告:也要记录下爆发行为。文档本身很重要,但它也希望能够阻止初级开发人员在未来对您的代码做一些愚蠢的事情(他们倾向于压制发生NullPointerException的地方,他们最不关心的是副作用,lol)。

哦,总是使用getClass()而不是instanceof。equals方法要求对称性。如果b等于a,那么a必须等于b。对于子类,instanceof打破了这种关系(a不是b的实例)。

尽管我个人总是使用getClass(),即使在实现非实体类(类型是状态,所以子类添加状态,即使子类是空的或只包含行为),只有当类是final时,instanceof才会很好。但实体类必须不是最终的(§2.1),所以我们真的别无选择。

Some folks may not like getClass(), because of the persistence provider's proxy wrapping the object. This might have been a problem in the past, but it really shouldn't be. A provider not returning different proxy classes for different entities, well, I'd say that's not a very smart provider lol. Generally, we shouldn't solve a problem until there is a problem. And, it seems like Hibernate's own documentation doesn't even see it worthwhile mentioning. In fact, they elegantly use getClass() in their own examples (see this).

Lastly, if one has an entity subclass that is an entity, and the inheritance mapping strategy used is not the default ("single table"), but configured to be a "joined subtype", then the primary key in that subclass table will be the same as the superclass table. If the mapping strategy is "table per concrete class", then the primary key may be the same as in the superclass. An entity subclass is very likely to be adding state and therefore just as likely to be logically a different thing. But an equals implementation using instanceof can not necessarily and secondarily rely on the ID only, as we saw may be the same for different entities.

在我看来,instanceof在非final Java类中根本没有位置。对于持久实体来说尤其如此。

我过去一直使用选项1,因为我知道这些讨论,并认为在我知道正确的事情之前最好什么都不做。这些系统仍在成功运行。

但是,下次我可能会尝试选项2 -使用数据库生成的Id。

如果未设置id, Hashcode和equals将抛出IllegalStateException。

这将防止涉及未保存实体的细微错误意外出现。

人们对这种方法有什么看法?