使用引用的ManyToOne实体实现equals/hashCode

4
假设我们有以下JPA实体:
class Parent {
    @Id
    private Long id;
}

class Child {
    @Id
    private Long id;

    @Column
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Parent parent;
}

假设我们可以通过父项上的名称(两者的组合)唯一地识别一个子项。因此,父项和名称可以被视为子项的业务键。
我现在不确定在类Child上实现equals(和hashCode)的最佳方法是什么。
引用应用程序的ID
由于应用程序代理将被加载并且其ID将设置在代理上,因此应用程序实体本身不会被初始化:
public boolean equals (Object o) {
    //null check, instanceof check ...
    return new EqualsBuilder().append(getName(), other.getName())
            .append(getParent().getId(), other.getParent().getId())
            .isEquals(); 

}

这样做可以解决问题,但我也看到了一些缺点。首先(较小的缺点),在父对象上增加一个额外的非null检查可能是明智的,这会使您的equals方法更简单。
其次(较小的缺点),这将需要Hibernate访问属性而不是字段;因此,我需要在getter上设置注释,而不是在字段上设置注释。对我个人来说,这是可以接受的,但当前项目的习惯是在字段级别上放置注释。 不要使用引用实体来评估相等性 好的,但那么我需要其他东西。我不想使用Child的ID(不良做法),这只留下了一个选择:使用单独的属性,如UUID。我并不反对使用UUID,但前提是没有其他可用的选项。
我的问题是:
  1. 我错过了其他的选项吗?
  2. 在您的意见中,建议采用什么方式?

我不明白为什么你必须在getter上放置注释而不是字段。如果父类正确实现了equals方法,你可以只比较父类而不是它们的ID,这样可以避免空检查。 - JB Nizet
为什么这个实现需要在getter上添加注释而不是字段? - Tom Anderson
@JB Nizet & Tom:如果注解放在字段(id)上,则调用getId()会导致代理初始化。这方面存在hibernate bug。请参见此说明 - Stijn Geukens
由于关联是急切获取的,因此没有代理(或始终初始化)。 - JB Nizet
@JB,抱歉,是我的错误,应该是LAZY,我已经更正了。 - Stijn Geukens
2个回答

4
另一种可能是添加另一个字段,其中包含对父项的外键,可以在不获取引用实体的情况下在equals和hashCode方法中使用该字段:
@Column(name="parent_id", insertable=false, updatable=false)
private String parentId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="parent_id")
private Parent parent;

有趣,没想到这是可能的。你以前做过这个吗? - Stijn Geukens
TheStijn:是的,至少在 TopLink Essentials 和 Hibernate 中可以工作。 - Jörn Horstmann
有趣。如果这是纯SQL,我会将子表的主键设置为由父表ID和名称组成的复合自然键。我原本认为在JDBC中没有任何方法可以实现这一点,但这样做确实可以 - 将parent_id映射到parent字段,并且也可以映射到复合键中的字符串字段。或许。 - Tom Anderson
花了一些时间才处理好,但它的功能正如所承诺的那样。谢谢。 - Stijn Geukens
有趣的解决方案,但对于未持久化的Parent对象,我们会遇到与使用id作为业务键相同的问题:当创建一个新的Parent对象并将其分配给setParent(p)时,parentId没有被设置,equals()和hashCode()方法不起作用。 - Sebien
你应该有以下公共方法:public void setParent(Parent p) { this.parent = p; this.parentId = p.getId(); } - Sebien

0
如果您可以获得EntityManagerFactory(并且如果您可以获得EntityManager,EntityManager.getEntityManagerFactory()是一种方法),另一种获取父ID的方法是:
emf.getPersistenceUnitUtil().getIdentifier(parent);

不过,你仍然需要进行空值检查。


这样操作真的能避免从数据库中提取引用实体吗? - Jörn Horstmann
我猜上面代码的 Hibernate 版本应该是 Serializable id = ((HibernateProxy) entity).getHibernateLazyInitializer().getIdentifier()。但我不喜欢这个方法,感觉不太对。此外,这里的实体甚至可能不是代理对象,这又是另一个需要检查的问题。 - Stijn Geukens
1
我认为在JPA中,它是否是代理并不重要。实际上,我不知道这是否会加载实体,但我的猜测是不会。我同意这更像是一个hack而不是解决方案。 - Tom Anderson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接