在使用JPA和Hibernate时,如何实现equals和hashcode?

111

在Hibernate中,Model类的equals和hashcode应该如何实现?有哪些常见陷阱?默认实现对于大多数情况是否足够好?使用业务键有任何意义吗?

在考虑到延迟获取、ID生成、代理等因素时,似乎很难在每个情况下都正确地工作。


请参见https://dev59.com/Fm445IYBdhLWcg3wGmo6#39827962(spring-data-jpa实现) - Grigory Kislin
8个回答

83
Hibernate在文档中对覆盖equals() / hashCode()的时机和方式有很好的描述。简而言之,只有在你的实体将成为Set的一部分或者你将会附加 / 分离其实例时才需要考虑它。后者不太常见。前者通常最好是基于业务键 - 即属性的唯一组合,在对象(或至少是会话)生命周期内不会改变。如果以上不可能,则应根据主键进行equals() / hashCode(),如果设置了主键,否则基于对象标识 / System.identityHashCode()。这里的重要部分是,在新实体被添加到Set并持久化后,需要重新加载它,否则可能会出现奇怪的行为(最终导致错误和/或数据损坏),因为你的实体可能被分配到与其当前hashCode()不匹配的桶中。

1
当@ChssPly76说"reload"时,你的意思是执行refresh()吗?假设您具有足够好的hashcode实现,那么遵守“Set”合同的实体如何最终进入错误的桶中。 - non sequitor
4
刷新集合或重新加载整个(所有者)实体,是的。关于错误的存储桶:a)您向集合中添加新实体,其ID尚未设置,因此您使用identityHashCode将实体放置在Bucket#1中。b)您的实体(在集合中)已持久化,现在它确实具有一个ID,因此您使用基于该ID的hashCode()。它与上述情况不同,并且本来会将您的实体放置在Bucket#2中。现在,假设您在其他地方持有对此实体的引用,请尝试调用Set.contains(entity),您将得到false。对于get() / put() /等操作也是如此。 - ChssPly76
1
当使用Hibernate时,您也可能遇到此问题,至今我仍未找到解决方法。 - Giovanni Botta
@ChssPly76 由于业务规则决定了两个对象是否相等,我需要基于可能在对象生命周期内发生变化的属性来构建我的equals/hashcode方法。这真的很重要吗?如果是,那么我该如何解决这个问题? - ubiquibacon
我没有看到任何证据表明在将实体重新附加到会话时需要equals/hashCode。我认为文档已经过时了。除非有人能够证明相反的观点.. - Stanislav Bashkyrtsev
显示剩余2条评论

43

我认为接受的答案不准确。

回答原问题:

默认实现对大多数情况是否足够好?

大多数情况下,是的。

只有当实体将用于 Set (这非常普遍)并且该实体将从Hibernate会话中分离,然后重新连接到Hibernate会话时(这是Hibernate的一种不常见的用法),您才需要覆盖 equals() hashcode()

接受的答案表明,如果任一条件成立,则需要覆盖方法。


这与我的观察相符,是时候找出原因了:为什么 - Vlastimil Ovčáčík
只有在实体将被用于Set时,你才需要重写equals()和hashcode()方法。如果某些字段可以标识一个对象,那么你不希望依赖Object.equals()来标识对象,这时候重写equals()和hashcode()方法就足够了。 - davidxxx

22

当您使用唯一的业务键或自然标识符时,最好的equalshashCode实现如下:

@Entity
public class Company {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(unique = true, updatable = false)
    private String name;
 
    @Override
    public int hashCode() {
        HashCodeBuilder hcb = new HashCodeBuilder();
        hcb.append(name);
        return hcb.toHashCode();
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Company)) {
            return false;
        }
        Company that = (Company) obj;
        EqualsBuilder eb = new EqualsBuilder();
        eb.append(name, that.name);
        return eb.isEquals();
    }
}

业务关键字应在所有实体状态转换(瞬态、附加、分离、删除)中保持一致,这就是为什么不能仅依赖于 ID 进行相等性比较的原因。

另一种选择是切换到使用应用程序逻辑分配的 UUID 标识符。这种情况下,您可以使用 UUID 进行 equals/hashCode,因为 ID 在实体被刷新之前已分配。

您甚至可以使用实体标识符进行 equalshashCode,但这要求您始终返回相同的 hashCode 值,以确保实体 hashCode 值在所有实体状态转换中保持一致,如下所示:

@Entity(name = "Post")
@Table(name = "post")
public class Post implements Identifiable<Long> {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    public Post() {}
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
 
        if (!(o instanceof Post))
            return false;
 
        Post other = (Post) o;
 
        return id != null &&
               id.equals(other.getId());
    }
 
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
  
    //Getters and setters omitted for brevity
}

+1 for the uuid approach。将其放入“BaseEntity”中,永远不要再考虑这个问题。它在数据库方面需要一些空间,但为了舒适性,您最好支付这个价格 :) - Martin Frey

13

当一个实体通过延迟加载被加载时,它不是基础类型的实例,而是由javassist动态生成的子类型,因此对同一类类型进行检查会失败,所以不要使用:

if (getClass() != that.getClass()) return false;

改用:

if (!(otherObject instanceof Unit)) return false;

同样这也是一个好的习惯,正如在Java实践中实现equals方法中所解释的那样。

出于同样的原因,直接访问字段可能不起作用并返回null,而不是底层值,因此不要对属性进行比较,而应该使用获取器,因为它们可能会触发加载底层值。


1
如果您正在比较具体类的对象,则此方法有效,但在我的情况下不起作用。我正在比较超类的对象,在这种情况下,以下代码适用于我:obj1.getClass().isInstance(obj2)。 - Tad

6

是的,这很困难。在我的项目中,equals和hashCode都依赖于对象的id。这种解决方案的问题是,如果对象尚未被持久化,那么它们都不起作用,因为id是由数据库生成的。在我的情况下,这是可以容忍的,因为几乎所有情况下对象都会立即被持久化。除此之外,它运行良好且易于实现。


我认为我们所做的是在ID未生成的情况下使用对象标识。 - Kathy Van Stone
2
问题在于,如果您持久化对象,则其哈希码会更改。如果对象已经是哈希基础数据结构的一部分,这可能会产生严重的不利影响。因此,如果您最终使用对象标识符,请确保在对象完全释放之前继续使用对象标识符(或从任何基于哈希的结构中删除对象,进行持久化,然后重新添加)。个人认为最好不要使用标识符,并基于对象的不可变属性来生成哈希值。 - Kevin Day

1
在Hibernate 5.2文档中,它提到根据你的情况,你可能不想实现hashCode和equals。

https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode

通常情况下,如果两个从同一会话加载的对象在数据库中相等(没有实现hashCode和equals),它们将是相等的。

如果您使用两个或更多会话,则情况会变得复杂。在这种情况下,两个对象的相等性取决于您的equals方法实现。

此外,如果您的equals方法比较的是仅在第一次持久化对象时生成的ID,则可能会遇到问题。当调用equals时,它们可能还不存在。


0

如果您重写了equals,请确保满足以下契约:

  • 对称性
  • 自反性
  • 传递性
  • 一致性
  • 非空性

并重写hashCode,因为其契约依赖于equals的实现。

Joshua Bloch(集合框架的设计者)强烈建议遵循这些规则。

  • 第9条:在重写equals时总是要重写hashCode

如果不遵循这些契约,可能会带来严重的意外效果。例如,List#contains(Object o)可能会返回错误的boolean值,因为通用契约未得到满足。


-1

这里有一篇非常好的文章:https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/persistent-classes-equalshashcode.html

引用文章中的一句重要话:

我们建议使用业务键相等来实现equals()和hashCode()。业务键相等意味着equals()方法仅比较形成业务键的属性,这是一个在现实世界中可以识别我们实例的关键(自然候选键):

简单来说

public class Cat {

...
public boolean equals(Object other) {
    //Basic test / class cast
    return this.catId==other.catId;
}

public int hashCode() {
    int result;

    return 3*this.catId; //any primenumber 
}

}

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