我应该在JPA实体中编写equals()和hashCode()方法吗?

72

我想检查一个实体是否在另一个实体的集合成员(@OneToMany@ManyToMany)中:

if (entity2.getEntities1().contains(entity1)) { }

为了阐述这个问题的动机:Collection.contains(JpaEntity someObject) 需要一个合理的 JpaEntity.equals(...) 方法。 - Abdull
请参见https://dev59.com/Fm445IYBdhLWcg3wGmo6#39827962(spring-data-jpa实现) - Grigory Kislin
6个回答

129
不一定。有三个选项:
1. 不覆盖 - 这样你将使用实例进行操作。在只使用与会话相关联的实体集合时,这是可以的(因此可以保证是相同的实例)。在许多情况下,这是我首选的方式,因为它需要更少的代码和更少的考虑来进行覆盖。
2. 使用业务键覆盖 hashCode() 和 equals()。业务键可以是标识实体的一部分属性。例如,对于一个用户(User),一个好的业务键可能是用户名(username)或电子邮件(email)。这被认为是良好的实践。
3. 仅使用 ID 字段覆盖 hashCode() 和 equals()。在某些情况下,这是可以的,特别是如果你有一个手动分配的标识符(如 UUID)。如果你的实体永远不会进入集合,这也是可以的。但对于没有标识符的临时实体进入集合时,会引发问题,所以要小心使用这个选项。正如 seanizer 所指出的 - 你应该避免使用它。通常情况下,除非你真正了解自己在做什么(并可能对其进行文档化),否则请避免使用这个选项。

查看这篇文章以获取更多详细信息。还请注意,equals()hashCode()是相互关联的,应该使用完全相同的字段来实现它们。


好的,我已经将以下代码添加到Entity1中,现在它可以正常工作了: public boolean equals(Object other){ if (this.getClass().isInstance(other)){ return this.id == ((UserEntity)other).id; } else { return false; } } public int hashCode() { return id; } - j2j
1
所以你选择了最不可取的选项 ;) 要注意副作用。 - Bozho
1
每次我开始一个JPA项目时,我总是陷入这个问题,感谢您一次又一次地帮我澄清思路 :) - eliocs
1
我建议使用Lombok,它有一个很好的@EqualsAndHashCode注解,可以为您创建这些方法。 - Elias Dorneles
对我来说,removeAll() 对于实体的 ArrayList 不需要 equals()。 - Prabhat Gaur

24

是的,你应该这样做!

如果你不重写默认的Java.lang.Object中的equalshashCode实现:

@Entity(name = "Book")
public class Book implements Identifiable<Long> {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    //Getters and setters omitted for brevity
}

merge操作将返回一个不同的对象实例,等式合约将被打破。

最好的方法是使用业务键,像这样:

@Entity
public class Book implements Identifiable<Long> {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @NaturalId
    private String isbn;
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Book)) return false;
        Book book = (Book) o;
        return Objects.equals(getIsbn(), book.getIsbn());
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(getIsbn());
    }
 
    //Getters and setters omitted for brevity
}

你也可以使用标识符来进行比较,但请注意hashCode实现应始终返回相同的值。对于实体来说,这不是真正的问题,因为您每个数据库事务中不会获取太多实体,否则,获取数据的成本将比使用固定hashCode带来的单桶HashMap惩罚大得多:

@Entity
public class Book implements Identifiable<Long> {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Book)) return false;
        Book book = (Book) o;
        return Objects.equals(getId(), book.getId());
    }
 
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
 
    //Getters and setters omitted for brevity
}

13

是的,你应该定义相应的equals()hashcode()方法,但你绝不能让id成为它们的一部分。(请参见我在类似问题中的最近回答


我曾经使用过仅基于id覆盖equals和hashcode方法,效果还不错。当然,我考虑了使用情况以及我不会处理瞬态实体这一事实。所以我不能完全说是“从不”。 - Bozho
我本来会说绝不吧,因为不能比较短暂和附加实体对我来说是无法接受的,但我理解你方法的简单性(只要你知道缺点)。 - Sean Patrick Floyd
这取决于您何时分配您的ID。如果有业务键,我更喜欢根本没有其他ID。这样就没有争议了。 - Joeri Hendrickx
@Joeri 我通常不会分配ID,而是由JPA提供程序完成。 - Sean Patrick Floyd
@Joeri 但我同意,将业务键作为ID是一个不错的概念。 - Sean Patrick Floyd

10

1
https://developer.jboss.org/docs/DOC-13933 - Tyagi Akhilesh

7

我们倾向于让IDE为我们生成hashCode()equals()方法。但要小心。当您为JPA实体生成这些方法时,某些版本的equals()方法会检查类标识。

// ... inside equals() - wrong approach for Entities (cause of generate proxies)
if (o == null || this.getClass() != o.getClass()) {
        return false;
}
// ...

如果你使用一些JPA库,例如Hibernate,这将会破坏你的集合,因为这些库会为你的实体(子类)创建代理,比如MyGreatEntity_$$_javassist_7

在实体中,始终允许子类使用equals()方法。


2

这是唯一的方法。您可以尝试使用Pojomatic库,它可以为您完成繁琐的工作。


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