ChatGPT解决这个技术问题 Extra ChatGPT

JPA hashCode() / equals() 困境

这里有 some discussions 关于 JPA 实体以及应该为 JPA 实体类使用哪个 hashCode()/equals() 实现。它们中的大多数(如果不是全部)都依赖于 Hibernate,但我想讨论它们 JPA-implementation-neutrally(顺便说一下,我正在使用 EclipseLink)。

所有可能的实现在以下方面都有自己的优点和缺点:

列表/集合操作的 hashCode()/equals() 合约一致性(不变性)

是否可以检测到相同的对象(例如来自不同会话、来自延迟加载的数据结构的动态代理)

实体在分离(或非持久)状态下的行为是否正确

据我所知,有三个选项:

不要覆盖它们;依赖 Object.equals() 和 Object.hashCode() hashCode()/equals() 工作不能识别相同的对象,动态代理的问题 分离实体没有问题 覆盖它们,基于主键 hashCode()/equals()被破坏的正确身份(对于所有托管实体) 分离实体的问题 根据业务 ID 覆盖它们(非主键字段;外键呢?) hashCode()/equals() 正确身份被破坏(对于所有托管实体)分离实体没有问题

我的问题是:

我错过了一个选项和/或赞成/反对点吗?你选择了什么选项,为什么?

更新 1:

hashCode()/equals() 已损坏”是指连续的 hashCode() 调用可能会返回不同的值,这在 Object API 文档的意义上(正确实施时)不会损坏,但会导致问题尝试从 MapSet 或其他基于散列的 Collection 检索更改的实体时。因此,JPA 实现(至少是 EclipseLink)在某些情况下将无法正常工作。

更新 2:

谢谢你的回答——他们中的大多数都具有非凡的品质。不幸的是,我仍然不确定哪种方法最适合实际应用程序,或者如何确定最适合我的应用程序的方法。所以,我会保持这个问题的开放性,并希望有更多的讨论和/或意见。

我不明白“hashCode()/equals() 已损坏”是什么意思
从这个意义上说,它们不会被“破坏”,就像在选项 2 和 3 中一样,您将使用相同的策略同时实现 equals() 和 hashCode()。
选项 3 并非如此。 hashCode() 和 equals() 应该使用相同的标准,因此如果您的某个字段发生更改,是的 hashcode() 方法将为同一实例返回与以前不同的值,但equals()也是如此。您已经从 hashcode() javadoc 中省略了句子的第二部分:每当在 Java 应用程序执行期间多次在同一个对象上调用它时,hashCode 方法必须始终返回相同的整数,不提供任何信息对象上的 equals 比较中使用的被修改。
实际上,这句话的意思是相反的——在同一个对象实例上调用 hashcode() 应该返回相同的值,除非 equals() 实现中使用的任何字段发生变化。换句话说,如果您的类中有三个字段,并且您的 equals() 方法仅使用其中两个来确定实例的相等性,那么如果您更改其中一个字段的值,您可以预期 hashcode() 返回值会发生变化 -当您认为此对象实例不再“等于”旧实例表示的值时,这是有道理的。
“尝试从 Map、Set 或其他基于散列的集合中检索已更改实体时出现问题”...这应该是“尝试从 HashMap、HashSet 或其他基于散列的集合中检索已更改实体时出现问题”

a
andref

阅读有关该主题的这篇非常好的文章:Don't Let Hibernate Steal Your Identity

文章的结论是这样的:

当对象被持久化到数据库时,对象标识很难正确实现。然而,问题完全源于允许对象在保存之前没有 id 存在。我们可以通过从对象关系映射框架(如 Hibernate)中分配对象 ID 来解决这些问题。相反,只要实例化对象,就可以分配对象 ID。这使得对象标识简单且无错误,并减少了域模型中所需的代码量。


不,那不是一篇好文章。这是一篇关于该主题的非常棒的文章,每个 JPA 程序员都应该阅读它! +1!
是的,我正在使用相同的解决方案。不让数据库生成 ID 还有其他优点,例如能够创建对象并在持久化之前创建引用它的其他对象。这可以消除客户端-服务器应用程序中的延迟和多个请求/响应周期。如果您需要此类解决方案的灵感,请查看我的项目:suid.jssuid-server-java。基本上 suid.jssuid-server-java 获取 ID 块,然后您可以获取并使用客户端。
这简直是疯了。我是 hibernate 底层工作的新手,正在编写单元测试,发现修改后我无法从集合中删除对象,得出结论是因为哈希码更改,但无法理解如何解决。文章简单华丽!
这是一篇很棒的文章。但是,对于第一次看到该链接的人,我建议这对于大多数应用程序来说可能是一种矫枉过正。此页面上列出的其他 3 个选项应该或多或少地以多种方式解决问题。
Hibernate/JPA 是否使用实体的 equals 和 hashcode 方法来检查记录是否已存在于数据库中?
n
nanda

我总是覆盖 equals/hashcode 并根据业务 ID 实现它。对我来说似乎是最合理的解决方案。请参阅以下 link

总结所有这些东西,这里列出了处理 equals/hashCode 的不同方法可以工作或不工作的列表:

编辑:

解释为什么这对我有用:

我通常不在我的 JPA 应用程序中使用基于散列的集合 (HashMap/HashSet)。如果必须,我更喜欢创建 UniqueList 解决方案。我认为在运行时更改业务 ID 不是任何数据库应用程序的最佳实践。在没有其他解决方案的极少数情况下,我会进行特殊处理,例如删除元素并将其放回基于散列的集合中。对于我的模型,我在构造函数上设置了业务 ID,并且没有为其提供设置器。我让 JPA 实现更改字段而不是属性。 UUID 解决方案似乎有点矫枉过正。如果您有自然业务 ID,为什么要使用 UUID?毕竟我会在数据库中设置业务 ID 的唯一性。那么为什么要为数据库中的每个表设置三个索引呢?


但是该表缺少第五行“与列表/集合一起使用”(如果您考虑从 OneToMany 映射中删除属于集合的一部分的实体),在最后两个选项中将回答“否”,因为它的 hashCode( ) 违反其合约的更改。
请参阅对问题的评论。您似乎误解了 equals/hashcode 合约
@MRalwasser:我认为您的意思是正确的,只是违反了 equals/hashCode() 合同本身。但是可变的 equals/hashCode 确实会给 Set 合约带来问题。
@MRalwasser:只有业务ID发生变化,哈希码才能改变,关键是业务ID不会改变。所以哈希码不会改变,这与哈希集合完美配合。
如果您没有天然的业务密钥怎么办?例如,在绘图应用程序中,如果是二维点 Point(X,Y)?您将如何将该点存储为实体?
B
Balder

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

但是还有其他选择(也有它们的优缺点):

a) hashCode/equals 基于一组不可变的,非空的,构造函数分配的,字段

(+) 保证所有三个标准

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

(-) 如果您必须更改其中之一,则处理复杂化

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

(+) 保证所有三个标准

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

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

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

(+) 保证所有三个标准

(-) UUID 生成的开销

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


我也是选项 1 和方法 C 的粉丝。在您绝对需要之前什么都不做,这是更敏捷的方法。
+1 选项 (b)。恕我直言,如果一个实体有一个自然的业务 ID,那么这也应该是它的数据库主键。这是简单、直接、良好的数据库设计。如果它没有这样的 ID,则需要代理键。如果您在创建对象时设置它,那么其他一切都很简单。当人们不使用自然密钥并且不及早生成代理密钥时,他们就会遇到麻烦。至于实现的复杂性——是的,有一些。但实际上并不多,并且可以通过一种非常通用的方式为所有实体解决一次。
我也更喜欢选项1,但是如何编写单元测试来断言完全相等是一个大问题,因为我们必须为Collection实现equals方法。
UUID 生成的开销是负数?这与将数据实际存储在数据库中相比如何?
C
Chris Lercher

如果您想为您的 Set 使用 equals()/hashCode(),即 同一个实体 只能在其中出现一次,那么只有一个选项:选项 2。那是因为 primary根据定义,实体的键永远不会改变(如果有人确实更新了它,它就不再是同一个实体了)

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

现在,理论上也可以使用选项 3 来实现这一点,即使所谓的“业务密钥”具有它们可以更改的讨厌的缺点:“您所要做的就是从集合中删除已经插入的实体( s),然后重新插入它们。”这是真的 - 但这也意味着,在分布式系统中,您必须确保在插入数据的任何地方都执行此操作(并且您必须确保执行更新, 在其他事情发生之前)。您将需要一个复杂的更新机制,特别是如果当前无法访问某些远程系统...

如果您的集合中的所有对象都来自同一个 Hibernate 会话,则只能使用选项 1。 Hibernate 文档在第 13.1.3. Considering object identity 章中非常清楚地说明了这一点:

在 Session 中,应用程序可以安全地使用 == 来比较对象。但是,在 Session 之外使用 == 的应用程序可能会产生意想不到的结果。即使在一些意想不到的地方也可能发生这种情况。例如,如果您将两个分离的实例放入同一个 Set 中,则它们可能具有相同的数据库标识(即,它们代表同一行)。但是,根据定义,对于处于分离状态的实例,不能保证 JVM 身份。开发人员必须覆盖持久类中的 equals() 和 hashCode() 方法并实现他们自己的对象相等概念。

它继续支持选项 3:

有一个警告:永远不要使用数据库标识符来实现相等。使用由独特的(通常是不可变的)属性组合而成的业务密钥。如果一个瞬态对象被持久化,数据库标识符将会改变。如果瞬态实例(通常与分离的实例一起)保存在 Set 中,则更改哈希码会破坏 Set 的合同。

这是真的,如果你

不能提前分配 id(例如通过使用 UUID)

但是您绝对希望在对象处于瞬态时将它们放入集合中。

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

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

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

这是对的。我看到的实际问题是:如果你不能保证绝对的稳定性,那么“只要对象在同一个 Set 中”,你怎么能保证稳定性。我可以想象一些特殊情况(例如仅将集合用于对话然后将其丢弃),但我会质疑这种做法的一般实用性。

精简版:

选项 1 只能用于单个会话中的对象。

如果可以,请使用选项 2。(尽早分配 PK,因为在分配 PK 之前您不能使用集合中的对象。)

如果你能保证相对稳定,你可以使用选项3。但要小心。


您认为主键永远不会改变的假设是错误的。例如,Hibernate 仅在保存会话时分配主键。所以,如果你使用主键作为你的 hashCode,那么在你第一次保存对象之前 hashCode() 的结果和你第一次保存对象之后的结果会是不同的。更糟糕的是,在保存会话之前,两个新创建的对象将具有相同的 hashCode,并且在添加到集合时会相互覆盖。您可能会发现自己必须在创建对象时立即强制保存/刷新才能使用该方法。
@William:实体的主键不会改变。映射对象的 id 属性可能会改变。正如您所解释的,这种情况会发生,尤其是当瞬态对象被持久化时。请仔细阅读我的答案部分,其中我谈到了 equals/hashCode 方法:“在设置主键之前,您不得使用这些方法。”
完全同意。使用选项 2,您还可以在超类中提取 equals/hashcode 并让所有实体重新使用它。
+1 我是 JPA 的新手,但这里的一些评论和答案暗示人们不理解术语“主键”的含义。
C
Christian Conti-Vock

我们的实体中通常有两个 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,但您可能会发现两个构造函数方法更适合您。


我相信,这是一个正确而好的解决方案。它也可能有一点性能优势,因为整数在数据库索引中的性能通常比 uuid 更好。但除此之外,您可能会消除当前的整数 id 属性,并将其替换为(应用程序分配的)uuid?
这与使用默认的 hashCode/equals 方法实现 JVM 相等和使用 id 实现持久性相等有何不同?这对我来说根本没有意义。
它适用于您有多个实体对象指向数据库中的同一行的情况。在这种情况下,Objectequals() 将返回 false。基于 UUID 的 equals() 返回 true
-1 - 我看不出有任何理由拥有两个 ID,因此有两种身份。这似乎完全没有意义,可能对我有害。
很抱歉批评您的解决方案而没有指出我更喜欢的解决方案。简而言之,我会给对象一个单一的 ID 字段,我会基于它实现 equals 和 hashCode,我会在对象创建时生成它的值,而不是在保存到数据库时。这样,对象的所有形式都以相同的方式工作:非持久、持久和分离。 Hibernate 代理(或类似的)也应该正常工作,我认为甚至不需要水合来处理 equals 和 hashCode 调用。
V
Vlad Mihalcea

如果您有业务密钥,则应将其用于 equals 和 hashCode。如果您没有业务密钥,则不应将其保留为默认的 Object equals 和 hashCode 实现,因为在合并和实体后这不起作用。只有当 hashCode 实现返回一个常量值时,才能在 equals 方法中使用实体标识符,如下所示:@Entity public class Book implements Identifiable { @Id @GeneratedValue private Long id;私有字符串标题; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) 返回 false;书书=(书)o; return getId() != null && Objects.equals(getId(), book.getId()); } @Override public int hashCode() { return getClass().hashCode(); } //为简洁起见省略了getter和setter }

看看这个 test case on GitHub,它证明了这个解决方案就像一个魅力。


哪个更好:(1) onjava.com/pub/a/onjava/2006/09/13/… 或 (2) vladmihalcea.com/…?解决方案(2)比(1)更容易。那么我为什么要使用(1)。两者的效果一样吗?两者都保证相同的解决方案吗?
并使用您的解决方案:相同实例之间的“hashCode 值不会改变”。这与被比较的“相同”uuid(来自解决方案(1))具有相同的行为。我对吗?
如果(2)在每个状态下都有效,我为什么还要为“业务密钥”而烦恼?
并将 UUID 存储在数据库中并增加记录和缓冲池中的占用空间?我认为从长远来看,这可能会导致比唯一的 hashCode 更多的性能问题。至于其他解决方案,您可以查看它是否提供跨所有实体状态转换的一致性。您可以找到 test that checks that on GitHub
如果您有一个不可变的业务密钥,则 hashCode 可以使用它,并且它将从多个存储桶中受益,因此如果您有一个,那么它是值得使用的。否则,只需按照我的文章中的说明使用实体标识符。
j
jbyler

尽管使用业务密钥(选项 3)是最常用的推荐方法(Hibernate community wiki,“Java Persistence with Hibernate”,第 398 页),而且这是我们最常使用的方法,但有一个 Hibernate 错误会破坏这种急切获取集:HHH-3799。在这种情况下,Hibernate 可以在其字段初始化之前将实体添加到集合中。我不确定为什么这个错误没有得到更多关注,因为它确实使推荐的业务关键方法存在问题。

我认为问题的核心是equals和hashCode应该基于不可变状态(参考Odersky et al.),而具有Hibernate管理的主键的Hibernate实体没有这样的不可变状态。当瞬态对象变为持久对象时,Hibernate 会修改主键。当 Hibernate 在初始化过程中对对象进行水合时,业务密钥也被 Hibernate 修改。

只剩下选项 1,基于对象标识继承 java.lang.Object 实现,或使用 James Brundege 在 "Don't Let Hibernate Steal Your Identity" 中建议的应用程序管理的主键(已由 Stijn Geukens 的答案引用)和 Lance Arlaus 在 { 2}。

选项 1 的最大问题是无法使用 .equals() 将分离实例与持久实例进行比较。但这没关系; equals 和 hashCode 的约定让开发人员决定相等对每个类的含义。所以只要让equals和hashCode从Object继承。如果您需要将分离实例与持久实例进行比较,您可以为此目的显式创建一个新方法,可能是 boolean sameEntityboolean dbEquivalentboolean businessEquals


D
Drew

我同意安德鲁的回答。我们在应用程序中做同样的事情,但不是将 UUID 存储为 VARCHAR/CHAR,而是将其拆分为两个长值。请参阅 UUID.getLeastSignificantBits() 和 UUID.getMostSignificantBits()。

要考虑的另一件事是,对 UUID.randomUUID() 的调用非常慢,因此您可能希望仅在需要时才延迟生成 UUID,例如在持久性或调用 equals()/hashCode() 期间

@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {

    private static final long   serialVersionUID    = 1L;

    @Version
    @Column(name = "version", nullable = false)
    private int                 version             = 0;

    @Column(name = "uuid_least_sig_bits")
    private long                uuidLeastSigBits    = 0;

    @Column(name = "uuid_most_sig_bits")
    private long                uuidMostSigBits     = 0;

    private transient int       hashCode            = 0;

    public AbstractJpaEntity() {
        //
    }

    public abstract Integer getId();

    public abstract void setId(final Integer id);

    public boolean isPersisted() {
        return getId() != null;
    }

    public int getVersion() {
        return version;
    }

    //calling UUID.randomUUID() is pretty expensive, 
    //so this is to lazily initialize uuid bits.
    private void initUUID() {
        final UUID uuid = UUID.randomUUID();
        uuidLeastSigBits = uuid.getLeastSignificantBits();
        uuidMostSigBits = uuid.getMostSignificantBits();
    }

    public long getUuidLeastSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidLeastSigBits;
    }

    public long getUuidMostSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidMostSigBits;
    }

    public UUID getUuid() {
        return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
        }
        return hashCode;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractJpaEntity)) {
            return false;
        }
        //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
        //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
        //if they have different types) having the same UUID is astronomical
        final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
        return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
    }

    @PrePersist
    public void prePersist() {
        // make sure the uuid is set before persisting
        getUuidLeastSigBits();
    }

}

好吧,实际上如果你重写 equals()/hashCode() 那么你必须为每个实体生成 UUID(我假设你想保留你在代码中创建的每个实体)。您只需执行一次 - 在第一次将其存储到数据库之前。之后,UUID 仅由 Persistence Provider 加载。因此,我不认为懒惰地做这件事有什么意义。
我对您的答案投了赞成票,因为我真的很喜欢您的其他想法:将 UUID 作为一对数字存储在数据库中,而不是在 equals() 方法中强制转换为特定类型 - 那真的很整洁!我将来肯定会使用这两个技巧。
感谢您的投票。延迟初始化 UUID 的原因是在我们的应用程序中,我们创建了许多从未放入 HashMap 或持久化的实体。因此,当我们创建对象(其中有 100,000 个)时,我们看到性能下降了 100 倍。所以我们只在需要时才初始化 UUID。我只是希望 MySql 对 128 位数字有很好的支持,所以我们也可以将 UUID 用于 id,而不关心 auto_increment。
我懂了。在我的情况下,如果相应的实体不会被放入集合中,我们甚至不会声明 UUID 字段。缺点是有时我们必须添加它,因为后来事实证明我们实际上需要将它们放入集合中。这有时会在开发过程中发生,但幸运的是,在最初部署给客户后我们从未发生过,所以这没什么大不了的。如果在系统上线后发生这种情况,我们将需要进行数据库迁移。惰性 UUID 在这种情况下非常有用。
如果性能在您的情况下是一个关键问题,也许您还应该尝试 Adam 在他的回答中建议的更快的 UUID 生成器。
M
Martin Andersson

Jakarta Persistence 3.0, section 4.12 写道:

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

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

如果实体类处于所谓的“瞬态”状态,即它还没有被持久化并且它没有标识符,那么 hashCode/equals 方法不能返回值,它们应该会爆炸,理想情况下是隐式地使用 NullPointerException当方法尝试遍历 ID 时。无论哪种方式,这都会有效地阻止应用程序代码将非托管实体放入基于哈希的数据结构中。实际上,如果类和标识符相等,为什么不更进一步并炸毁,但其他重要的属性,例如 version 是不相等的(IllegalStateException)!以确定性方式快速失败始终是首选选项。

注意事项:还要记录爆炸行为。文档本身很重要,但它也有望在未来阻止初级开发人员对您的代码做一些愚蠢的事情(他们倾向于在发生 NullPointerException 的地方抑制它,而他们最不想考虑的就是副作用,哈哈) .

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

虽然我个人总是使用 getClass(),即使在实现非实体类时(类型 is 状态,因此子类 adds state 即使子类为空或仅包含行为),instanceof 只有当类是 final 时才可以。但是实体类不能是最终的(§2.1),所以我们在这里真的没有选择。

有些人可能不喜欢 getClass(),因为持久性提供者的代理包装了对象。这在过去可能是一个问题,但它真的不应该是。提供者不为不同的实体返回不同的代理类,好吧,我会说这不是一个非常聪明的提供者,哈哈。一般来说,我们不应该在出现问题之前解决问题。而且,似乎 Hibernate 自己的文档甚至不认为值得一提。事实上,他们在自己的示例 (see this) 中优雅地使用了 getClass()

最后,如果一个实体子类是实体,并且使用的继承映射策略不是默认的(“单表”),而是配置为“连接子类型”,那么该子类表中的主键 将与超类表相同。如果映射策略是“每个具体类的表”,那么主键可能与超类中的相同。实体子类很可能会添加状态,因此在逻辑上也可能是不同的东西。但是使用 instanceof 的 equals 实现不一定只能依赖 ID,因为我们看到对于不同的实体可能是相同的。

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


即使数据库缺少序列(如 Mysql),也可以模拟它们(例如,表 hibernate_sequence)。因此,您可能总能获得跨表唯一的 ID。 +++ 但你不需要它。由于 H. 代理,调用 Object#getClass() 很糟糕。调用 Hibernate.getClass(o) 会有所帮助,但不同种类实体的平等问题仍然存在。有一个使用 canEqual 的解决方案,有点复杂,但可以使用。同意通常不需要。 +++ 在空 ID 上抛出 eq/hc 违反了合同,但它非常务实。
感谢您的评论。我更新了答案。我想在这里补充的一件事是“在空 ID 上抛出 eq/hc 违反合同”的说法是错误的。这在客观上是错误的,因为它根本不是合同的一部分。并不是说它对真实性很重要,但我想补充一下others agree
A
Adam Gent

这里显然已经有非常丰富的答案,但我会告诉你我们做什么。

我们什么都不做(即不覆盖)。

如果我们确实需要 equals/hashcode 来处理集合,我们使用 UUID。您只需在构造函数中创建 UUID。我们使用 http://wiki.fasterxml.com/JugHome 作为 UUID。 UUID 在 CPU 方面稍微贵一点,但与序列化和数据库访问相比便宜。


a
aux

请根据预定义的类型标识符和 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

期待您的评论。


N
Neil Stevens

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

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

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

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

人们如何看待这种方法?


D
Demel

业务密钥方法不适合我们。我们使用 DB 生成的 ID、临时临时 tempId 和覆盖 equal()/hashcode() 来解决困境。所有实体都是实体的后代。优点:

DB 中没有额外的字段 后代实体中没有额外的编码,一种方法适用于所有 没有性能问题(如 UUID),DB Id 生成 Hashmaps 没有问题(不需要记住使用 equal & 等) 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();
    }
}

C
Christian Beikov

IMO 你有 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
}

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

如果您可以期望用户的想法是一个好主意,那么基于业务密钥实施它,但也有一些缺点

大多数情况下,此业务密钥将是用户提供的某种代码,而不是多个属性的组合。

连接速度较慢,因为基于可变长度文本的连接速度很慢。如果键超过一定长度,一些 DBMS 甚至可能在创建索引时出现问题。

根据我的经验,业务密钥往往会发生变化,这需要对引用它的对象进行级联更新。如果外部系统引用它,这是不可能的

IMO,您不应该专门实施或使用业务密钥。这是一个很好的附加组件,即用户可以通过该业务密钥快速搜索,但系统不应该依赖它来操作。

基于主键实现有问题,但也许没什么大不了的

如果您需要向外部系统公开 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 方法。


u
user2083808

我试图自己回答这个问题,但在我阅读这篇文章,尤其是 DREW 之前,我从未对找到的解决方案完全满意。我喜欢他懒惰地创建 UUID 并优化存储它的方式。

但是我想增加更多的灵活性,即仅当在实体的第一次持久性之前访问 hashCode()/equals() 时才延迟创建 UUID,具有每个解决方案的优点:

equals() 表示“对象引用相同的逻辑实体”

尽可能多地使用数据库 ID,因为我为什么要做两次工作(性能问题)

防止在尚未持久化的实体上访问 hashCode()/equals() 时出现问题,并在它确实持久化后保持相同的行为

我真的很感谢下面对我的混合解决方案的反馈

公共类 MyEntity { @Id() @Column(name = "ID", length = 20, nullable = false, unique = true) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; @Transient 私有 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;返回 this.getUuid().equals(((MyEntity)toBeCompared).getUuid()); } public final UUID getUuid() { // UUID 已经在这个物理对象上访问过 if(this.uuid != null) { return this.uuid; } // UUID 在该实体被持久化前一天生成 if(this.uuidMostSignificantBits != null) { this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits); // UUID 在持久化之前从未在该实体上生成 } else if(this.getId() != null) { this.uuid = new UUID(this.getId(), this.getId()); // 在这个尚未持久化的实体上从未访问过 UUID } else { this.setUuid(UUID.randomUUID()); } 返回 this.uuid; } 私有 void setUuid(UUID uuid) { if(uuid == null) { return; } // 对于生成的 UUID 可能与从 ID 构建的 UUID 共存的一种假设情况 if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) { throw new Exception("UUID: " + this.getUuid() + "格式仅供内部使用"); } this.uuidMostSignificantBits = uuid.getMostSignificantBits(); this.uuidLeastSignificantBits = uuid.getLeastSignificantBits(); this.uuid = uuid; }


“在我被坚持之前一天在这个实体上生成的 UUID”是什么意思?你能举个例子吗?
你可以使用指定的世代类型吗?为什么需要身份生成类型?它比分配有优势吗?
如果您 1)创建一个新的 MyEntity,2)将其放入列表中,3)然后将其保存到数据库,然后 4)您从数据库中加载该实体,并且 5)尝试查看加载的实例是否在列表中,会发生什么情况.我的猜测是它不会,即使它应该是。
感谢您的第一条评论,这些评论告诉我我并没有像我应该的那样清楚。首先,“在我被持久化之前一天在这个实体上生成的 UUID”是一个错字……“在它被持久化之前”应该改为阅读。对于其他评论,我将很快编辑我的帖子以尝试更好地解释我的解决方案。
C
Christopher Yang

这是每个使用 Java 和 JPA 的 IT 系统中的常见问题。痛点超出了实现 equals() 和 hashCode() 的范围,它会影响组织如何引用实体以及其客户如何引用同一实体。我已经看到了没有业务密钥的痛苦,以至于我写了 my own blog 来表达我的观点。

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


M
Marcos Oliveira

我使用类 EntityBase 并继承到我的所有 JPA 实体,这对我来说非常有用。

/**
 * @author marcos.oliveira
 */
@MappedSuperclass
public abstract class EntityBase<TId extends Serializable> implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected TId id;



    public TId getId() {
        return this.id;
    }

    public void setId(TId id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return (super.hashCode() * 907) + Objects.hashCode(getId());//this.getId().hashCode();
    }

    @Override
    public String toString() {
        return super.toString() + " [Id=" + id + "]";
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        EntityBase entity = (EntityBase) obj;
        if (entity.id == null || id == null) {
            return false;
        }
        return Objects.equals(id, entity.id);
    }
}

参考:https://thorben-janssen.com/ultimate-guide-to-implementing-equals-and-hashcode-with-hibernate/


i
illEatYourPuppies

如果 UUID 是很多人的答案,为什么我们不使用业务层的工厂方法来创建实体并在创建时分配主键?

例如:

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

这样我们就可以从持久化提供者那里得到一个实体的默认主键,我们的 hashCode() 和 equals() 函数可以依赖它。

我们还可以将 Car 的构造函数声明为 protected,然后在我们的业务方法中使用反射来访问它们。这样开发人员就不会打算用 new 实例化 Car,而是通过工厂方法。

那怎么样?


如果您愿意在进行数据库查找时生成 guid 对性能造成影响,那么这种方法效果很好。
那么单元测试 Car 呢?在这种情况下,您需要一个数据库连接进行测试?此外,您的域对象不应依赖于持久性。
G
Grigory Kislin

在实践中,选项 2(主键)似乎是最常用的。自然和不可变的业务密钥很少发生,创建和支持合成密钥太重而无法解决可能从未发生过的情况。查看 spring-data-jpa AbstractPersistable 实现(唯一的:for Hibernate implementation use 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(保留 Object 实现)在 merge 之后被破坏,这是非常常见的情况。

如果您没有业务密钥并且需要在哈希结构中操作新实体,请将 hashCode 覆盖为常量,如下所示 Vlad Mihalcea 被建议。


j
jhegedus

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

请注意,此解决方案不适合问题中给出的 3 个类别中的任何一个。

我所有的实体都是 UUIDEntity 的子类,所以我遵循不重复(DRY)原则。

如果需要,可以更精确地生成 UUID(通过使用更多伪随机数)。

斯卡拉代码:

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()
}