【问题标题】:Unidirectional @OneToMany association fails equality test in JPA单向 @OneToMany 关联未通过 JPA 中的相等性测试
【发布时间】:2014-09-12 17:00:59
【问题描述】:

我已经建立了单向 OneToMany 关系,如 JPA 2.1 规范第 2.10.5.1 节中的示例:

@Entity
public class Client implements Serializable {

...

    @OneToMany
    private List<ServiceOrder> activeServiceOrders;

    public void setActiveServiceOrders( List<ServiceOrder> activeServiceOrders ) {

        this.activeServiceOrders = activeServiceOrders;
    }

    public List<ServiceOrder> getActiveServiceOrders() {

        return activeServiceOrders;
    }
}

ServiceOrder 类使用其自动生成的长 id 实现 hashCode 和 equals。它们是由 Eclipse 实现的。

public class ServiceOrder implements Serializable {

    @TableGenerator( name = "generator_serviceOrder", table = "SEQUENCE_TABLE", pkColumnName = "SEQ_NAME", valueColumnName = "LAST_VALUE_GEN", pkColumnValue = "SERVICE_ORDER_SEQ", allocationSize = 1, initialValue = 0 )
    @Id
    @GeneratedValue( strategy = GenerationType.TABLE, generator = "generator_serviceOrder" )
    private long id;
...
    @Override
    public boolean equals( Object obj ) {

        if ( this == obj )
            return true;
        if ( obj == null )
            return false;
        if ( getClass() != obj.getClass() )
            return false;
        ServiceOrder other = (ServiceOrder ) obj;
        if ( id != other.id )
            return false;
        return true;
    }
...
}

表格都是按预期自动生成的。然后,当我想建立关系时,我会这样做:

...
Client client = entityManager.find(...);
ServiceOrder so = entityManager.find(...);
client.getActiveServiceOrders().add( so );
...

到目前为止一切都很好,事务提交成功。当我尝试删除关系时问题就开始了(在另一个事务中,另一个时刻):

...
Client sameClient = entityManager.find(...);
ServiceOrder sameSo = entityManager.find(...);
log.info(sameClient.getActiveServiceOrders().size()); // "1", OK
log.info(sameClient.getActiveServiceOrders().contains(so)); // "false". Why?
sameClient.getActiveServiceOrders().remove(so); // does nothing, returns false
...

我调试并发现ServiceOrder.equals()中出现以下错误:

...
if ( getClass() != obj.getClass() ) // different probably because JPA (Hibernate) proxies one of the objects
    return false; // returns
...

我找到了两个临时解决方案:

  1. 删除 ServiceOrder equals() 和 hashCode(); 或
  2. 使关系双向(当然,每次添加/删除都会更新双方);

我不明白这种行为。如果关系是单向的或双向的,为什么会有不同的待遇?另外,如果我在同一事务的上下文中获取这些实体,第一个等于测试将如何失败:

if ( this == obj )
    return true;

我正在使用 JPA 2.1 (Wildfly 8.1.0)。

最好的问候,并提前感谢您。 仁南

【问题讨论】:

  • 以下是getClass() != obj.getClass() 不起作用的解释:Hibernate equals and proxy
  • 谢谢,我会尝试解决方案。但我想更好地理解为什么这会发生在单向关系上,但如果我让它成为双向关系就不会发生。这是特定于 Hibernate 还是在所有 JPA 实现中都相同?如果两个引用绑定到同一个 EntityManager(同一个事务),if ( this == obj ) return true; 将如何失败?

标签: java hibernate jpa orm wildfly


【解决方案1】:

您应该覆盖 equals 和 hashCode,但绝不应该将 ID 用于哈希码,除非您使 hashCode 不可变并且仅使用 ID when it's not null for equality。

否则,在保存 ID 为 null 的实体之前,该实体将在您将瞬态实体添加到集合时在刷新期间分配,当它被持久化并生成 ID 时,equals/hashCode 合同是快坏了。

Hibernate 最佳实践建议 using a business key 用于对象相等/hashCode。

所以引用参考文档:

一般的约定是:如果你想在 List 中存储一个对象,Map 或 Set 则要求 equals 和 hashCode 是 执行,因此他们遵守标准中规定的合同 文档。

为避免此问题,我们建议使用“半”唯一属性 你的持久类实现equals()(和hashCode())。 基本上你应该认为你的数据库标识符没有 商业意义(记住,代理标识符属性和 无论如何,建议使用自动生成的值)。数据库 标识符属性应该只是一个对象标识符,基本上 应该只被 Hibernate 使用。当然,你也可以使用 数据库标识符作为方便的只读句柄,例如建造 Web 应用程序中的链接。

而不是使用数据库标识符进行相等 比较,您应该为 equals() 使用一组属性 识别您的单个对象。例如,如果您有一个“项目” 类,它有一个“名称”字符串和“创建”日期,我可以同时使用 实现一个好的 equals() 方法。无需使用持久化 标识符,所谓的“业务密钥”要好得多。它是 自然键,不过这次用起来也没什么问题!

【讨论】:

  • 我不会对“永远不应该使用 ID 进行相等/散列”声明如此武断。我正在从事一个成功使用 ID 作为哈希键多年的大型项目。 ID“非常符合这个标准”(PRO JPA 2.0 书)。关键是不要比较尚未持久化的实体。根据实体的业务属性维护哈希键可能是个好主意,但更具挑战性。
  • 实体持久化后,代理键可能会起作用,但它仍然为微妙的问题打开了大门,一些没有经验的开发人员可能会介绍。业务密钥更具弹性,因此我认为提出一个有效的唯一密钥组合是值得的。几乎总是,每个表格行都代表着与其他所有行不同的东西。
  • 您为什么不举一个例子来说明缺乏经验的开发人员可以引入的微妙问题?我还挑战您提供一种方法来将业务密钥引入实体。对于像货币这样的简单例子,它是显而易见的,但对于像地址这样更复杂的例子?您是否从地址的所有字段构建哈希?如果是这样,那么如果没有经验的开发人员来项目并在地址中添加一个新列而不更新 hashCode&equals 呢?
  • 一个用例,您将新的子实体添加到 OneToMany Set,您将父实体传递给合并并将其返回到视图,结果发现 Set 根本不迭代,即使大小大于零。我曾经修复过它。 Address 实体是一个相当简单的示例。每个地址都是唯一的,否则邮递员在投递邮件时会迷路。大部分时间是:邮政编码 + 街道号码 + 公寓号码。
  • 我的项目使用 ID 进行相等和散列,没有问题,直到出现这种单向关系。我知道相等/散列是一个很好的辩论,其中更好的情况通常取决于其他细节,但特别是对于这种情况,Hibernate 在单向和双向上有什么不同?是否有让我的代码在实现(Hibernate、EclipseLink 等)之间更具 JPA 可移植性的指南?
【解决方案2】:

不要覆盖equals 和hashCode。 Hibernate 有自己的实现来找出对象,这就是为什么你没有得到预期的结果。 这篇文章解释了更多: https://community.jboss.org/wiki/EqualsandHashCode?_sscc=t

【讨论】:

  • 不覆盖equals和hashCode是什么意思?你从官方文档那里得到的。恰恰相反。您应该覆盖 equals 和 hashCode,否则会使用默认的引用相等。对于 equals/hashCode,没有“Hibernate 自己的实现”之类的东西。它只是正在使用的标准 Java 平等合同。我唯一同意的是相等和 hashCode 起着重要的作用,尤其是在使用 Hash 集合时。
  • @VladMihalcea 嘿,男人下来喘口气。在实体上实现 equals 和 hashCode 不是一个好习惯。当实现足够健壮时,为什么需要它们?你为什么要冒险并实施它?你应该对此有充分的理由。你怎么知道你的 hashCode 会生成唯一值?
  • 另外,如果您查看文档,它会解释 hibernate 如何使用 hashCode 和 equals。
  • 业务键通常是一个列(或列的组合),由数据库强制为唯一,因此您的 equals 在所有 Hibernate 实体状态中都是一致的。引用参考文档:“一般合同是:如果您想将对象存储在 List、Map 或 Set 中,则要求实现 equals 和 hashCode,以便它们遵守文档中指定的标准合同。n 相反使用数据库标识符进行相等比较时,您应该为 equals() 使用一组属性来标识您的各个对象。"
  • equals() 和 hashCode() 关于 Hibernate 的争论非常激烈。但是JPA呢?我希望我的代码在其他 JPA 实现(EclipseLink、TopLink 等)中尽可能可移植。我不太了解实现细节,我尽量遵守 JPA 规范。你们会推荐什么最适合这种便携性要求?
猜你喜欢
  • 2011-02-04
  • 2021-10-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-15
  • 2014-10-12
  • 2022-01-23
相关资源
最近更新 更多