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

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


当前回答

如果你想对你的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。但是要小心。

其他回答

请考虑以下基于预定义类型标识符和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

期待您的评论。

如果你想对你的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。但是要小心。

下面是一个简单的(经过测试的)Scala解决方案。

请注意,此解决方案不属于这3类中的任何一类 在问题中给出。 我所有的实体都是UUIDEntity的子类,所以我遵循 不要重复自己(DRY)原则。 如果需要,可以使UUID生成更精确(通过使用更多 伪随机数)。

Scala代码:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}

在我看来,你有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方法。

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

我们什么也不做。

如果我们确实需要= /hashcode来处理集合,则使用uuid。 您只需在构造函数中创建UUID。我们使用http://wiki.fasterxml.com/JugHome作为UUID。UUID的CPU开销稍高,但与序列化和db访问相比便宜。