【问题标题】:Hibernate: When is it necessary to implement equals() and hashCode(), and if so, how?Hibernate:什么时候需要实现equals()和hashCode(),如果需要,如何实现?
【发布时间】:2012-02-23 17:54:24
【问题描述】:

基于various bad experiences,我作为Java 程序员的经验法则是只在不可变对象上实现equals()hashCode(),其中对象的两个实例确实可以互换。

基本上,我想避免该链接中出现HashMap 密钥问题之类的情况,或者如下所示:

  1. 获取具有特定身份的东西。
  2. 修改它。
  3. 将其添加到集合中。
  4. (稍后)获取另一个具有相同身份的东西。
  5. 修改它。
  6. 将其添加到同一个集合中。
  7. 没有注意到这个添加实际上并没有发生,因为集合认为这个东西已经在那里了。
  8. 用集合中的东西做点什么。
  9. 没有注意到步骤 (5) 中的更改被忽略,我们仍然拥有步骤 (2) 中的状态。

总的来说,在我的 Java 职业生涯中,我 haven't found a lot of use 代表 equals() 除了 代表 (1) 值对象和 (2) 将事物放入集合中。我还发现不变性 + 复制和修改构造函数/构建器通常比 setter 更快乐。两个对象可能具有相同的 ID 并且可能代表相同的逻辑实体,但如果它们具有不同的数据——如果它们代表概念实体在不同时间的快照——那么它们就不是 equal()

无论如何,我现在在一家 Hibernate 商店,我的更多精通 Hibernate 的同事告诉我这种方法行不通。具体来说,声称似乎是在以下情况下--

  1. Hibernate 从数据库中加载一个东西——我们称之为实例h1
  2. 这个东西被编组并通过网络服务发送到某个地方。
  3. Web 服务客户端对其进行修改并将修改后的版本发回。
  4. 修改后的版本在服务器上解组——我们将其称为实例h4
  5. 我们希望 Hibernate 使用修改来更新数据库。

-- 除非h1.equals(h4)(或者h4.equals(h1),我不清楚,但我希望它是可传递的,无论如何),Hibernate 将无法判断这些是同一件事,而且是坏事会发生的。

那么,我想知道的:

  • 这是真的吗?
  • 如果是这样,为什么? Hibernate 使用 equals() 做什么?
  • 如果 Hibernate 需要 h1h4 相等,它如何(以及我们如何)跟踪哪个是修改后的版本?

注意:我已经阅读了 Hibernate 文档中的Implementing equals() and hashCode(),但它并没有解决我担心的情况,至少是直接的,也没有详细解释什么是Hibernate 确实需要equals()hashCode()equals and hashcode in Hibernate 的答案也没有,否则我不会费心发布这个。

【问题讨论】:

  • “一个比二传手更幸福的世界”:同意。在我真正需要它之前,我从不实现 setter,并争取不可变的类。
  • 如果两个对象具有相同的身份和不同的数据(如您在 9 列表之后的段落中),您可能有一个错误。如果它们在不同的时间点代表同一个物体,那么它们确实是equal(),就像10年前的我一样,即使我有不同的属性。如果在我之后将他添加到集合中,我希望旧的我取代新的我......
  • @glowcoder 如果他被添加到你之后的集合中,你会期望旧的你替换新的你,但如果你是他的equal(),他不会。这不是哲学,这是 Java。
  • @DonRoby 不,不是。该问题的答案列出了官方的最佳实践,但没有解释幕后发生的事情或为什么这些实践是必要的。 (我还阅读了答案中链接的文档,您将在我的问题顶部看到。)

标签: hibernate equals hashcode


【解决方案1】:

首先,您最初的想法是,您应该只在不可变对象上实现 equals() 和 hashCode(),当然可行,但它比需要的更严格。您只需要这两种方法来依赖不可变字段。任何值可能发生变化的字段都不适合在这两种方法中使用,但其他字段不必是不可变的。

话虽如此,Hibernate 通过比较它们的主键知道它们是同一个对象。这导致很多人写这两种方法都依赖主键。 Hibernate 文档建议您不要这样做,但许多人会毫不费力地忽略此建议。这意味着您不能将实体添加到 Set 直到它们被持久化,这是一个不太难忍受的限制。

Hibernate 文档建议使用业务密钥。但是业务键应该依赖于唯一标识一个对象的字段。 Hibernate 文档说“使用由独特的、通常不可变的属性组合而成的业务密钥”。我在数据库中使用对它们具有唯一约束的字段。因此,如果您的 Sql CREATE TABLE 语句将约束指定为

CONSTRAINT uc_order_num_item UNIQUE (order_num, order_item)

那么这两个字段可以成为您的业务关键。这样,如果您更改其中一个,Hibernate 和 Java 都会将修改后的对象视为不同的对象。当然,如果您确实更改了这些“不可变”字段之一,那么您会弄乱它们所属的任何 Set。因此,我想您需要清楚地记录哪些字段构成业务键,并在编写应用程序时理解业务键中的字段永远不应更改为持久对象。我明白为什么人们会忽略建议而只使用主键。但是你可以像这样定义主键:

CONSTRAINT pk_order_num_item PRIMARY KEY (order_num, order_item)

你仍然会遇到同样的问题。

就个人而言,我希望看到一个注释,它指定业务键中的每个字段,并进行 IDE 检查,检查我是否为持久对象修改它。也许这要求太多了。

解决所有这些问题的另一种方法是使用 UUID 作为主键,当您第一次构造非持久实体时在客户端上生成该主键。由于您永远不需要向用户显示它,因此您的代码一旦设置它就不可能更改它的值。这使您可以编写始终有效且彼此保持一致的 hashCode() 和 equals() 方法。

还有一件事:如果您想避免将对象添加到已经包含不同(修改)版本的 Set 的问题,唯一的方法是在添加之前始终询问该集合是否已经存在。然后你可以编写代码来处理这种特殊情况。

【讨论】:

    【解决方案2】:

    JPA/Hibernate 强加了哪些语义?

    JPA 规范说明如下。

    2.4 主键和实体标识

    每个实体都必须有一个主键。 ... 其主键的值唯一标识了持久化上下文中的实体实例,并用于EntityManager 操作

    我解释说,JPA 实体的等价语义是主键的等价。这表明equals() 方法应该比较主键的等价性,仅此而已。

    但是您引用的Hibernate advice(以及我看过的另一篇文章)说不要这样做,而是使用“业务键”而不是主键。其原因似乎是因为我们无法保证实体对象has a value for a generated primary key 在实体已同步(使用EntityManager.flush())到数据库之前。

    【讨论】:

    • spec 说“主键类必须定义 equalshashCode 方法”,但它没有说明实体类,至少在该部分中 - 它并不是说EntityManager 或“持久性上下文”依赖于实体equals()。 (事实上​​,我希望他们不会,而是依赖Id 注释。但我不知道是不是这样,这就是我问的原因。)
    • 同意@DavidMoles。 JPA 规范说 Hibernate 应该使用实体的主键来确定相等性。换句话说,Hibernate 使用 ID(或复合 ID,如果有的话)。如果存在 Hibernate 在实体本身(而不是主键字段)上调用 .equals() 的任何情况,那么我想知道它。
    猜你喜欢
    • 2014-04-06
    • 2012-10-19
    • 1970-01-01
    • 1970-01-01
    • 2010-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多