JPA中的hashCode() / equals()困境

395
在这里已经有关于JPA实体的一些讨论,包括应该为JPA实体类使用哪个hashCode()/equals()实现。大多数(如果不是全部)依赖于Hibernate,但我想讨论JPA实现中性的问题(顺便说一下,我正在使用EclipseLink)。
所有可能的实现都有自己的优点缺点,涉及到以下方面:
  • hashCode()/equals()合同符合性(不可变性)适用于List/Set操作
  • 是否可以检测到相同对象(例如来自不同会话、从惰性加载的数据结构中的动态代理)
  • 实体在分离(或未持久化)状态下是否表现正确
就我所看到的,有三种选择
  1. 不要覆盖它们;依赖于Object.equals()Object.hashCode()
    • hashCode()/equals()有效
    • 无法识别相同的对象,动态代理存在问题
    • 与分离实体没有问题
  2. 根据主键覆盖它们
    • hashCode()/equals()被破坏了
    • 正确的标识(对于所有受控实体)
    • 与分离实体存在问题
  3. 根据业务ID(非主键字段;外键怎么办?)覆盖它们
    • hashCode()/equals()被破坏了
    • 正确的标识(对于所有受控实体)
    • 与分离实体没有问题

我的问题是:

  1. 我是否错过了任何选项和/或优缺点?
  2. 您选择了哪个选项以及为什么?



更新1:

我所谓的“hashCode()/ equals()已损坏”,是指连续的hashCode()调用可能返回不同的值,这在正确实现时并非Object API文档中所述的“损坏”,但当尝试从Map、Set或其他基于哈希的Collection检索更改的实体时会导致问题。因此,JPA实现(至少是EclipseLink)在某些情况下将无法正常工作。
更新2:
感谢您的回答 - 其中大部分质量很高。不幸的是,我仍然不确定哪种方法对于实际应用程序最好,或者如何确定我的应用程序的最佳方法。因此,我将保持问题开放,并希望进行更多讨论和/或意见。

4
“hashCode()/equals() broken” 是什么意思,我不明白。 - nanda
5
那么,如果你在选项2和3中使用相同的策略来实现equals()和hashCode(),它们就不会以那种意义上被“破坏”。 - matt b
14
选项3并非如此。hashCode()和equals()应该使用相同的标准,因此,如果您的某个字段更改了,hashcode()方法将为相同实例返回一个不同的值,而equals()方法也将是如此。您已经省略了hashcode() javadoc中第二部分的内容:在Java应用程序的执行过程中多次调用同一对象的hashCode方法时,只要没有修改用于比较对象的equals信息,hashCode方法必须始终返回相同的整数。 - matt b
2
实际上,该句子的这一部分意思是相反的 - 在同一对象实例上调用hashcode()应该返回相同的值,除非在equals()实现中使用的任何字段发生更改。换句话说,如果您的类中有三个字段,并且您的equals()方法仅使用其中两个来确定实例的相等性,则可以期望hashcode()返回值在更改其中一个字段的值时发生更改 - 当您考虑到此对象实例不再“等于”旧实例所代表的值时,这是有意义的。 - matt b
2
尝试从Map、Set或其他基于哈希的集合中检索更改的实体时可能会出现问题,应该是"尝试从HashMap、HashSet或其他基于哈希的集合中检索更改的实体时可能会出现问题"。 - nanda
显示剩余16条评论
21个回答

158

请阅读关于此主题的非常好的文章:不要让 Hibernate 偷走你的身份

这篇文章的结论如下:

当对象被持久化到数据库时,对象身份的正确实现非常困难。然而,所有问题都源于允许对象在被保存之前不存在 ID。我们可以通过将赋予对象 ID 的责任从像 Hibernate 这样的对象关系映射框架中取出来来解决这些问题。相反,可以在实例化对象时立即分配对象 ID。这使得对象身份简单且无误,并减少了域模型中所需的代码量。


39
不,那篇文章不仅仅是好,它是一篇非常棒的关于这个主题的文章,每个JPA程序员都应该阅读!点赞! - Tom Anderson
2
是的,我正在使用相同的解决方案。不让数据库生成ID也有其他优点,例如能够创建一个对象并在持久化之前创建引用它的其他对象。这可以消除客户端-服务器应用程序中的延迟和多个请求/响应周期。如果您需要此类解决方案的灵感,请查看我的项目:suid.jssuid-server-java。基本上,suid.jssuid-server-java获取ID块,然后您可以在客户端获取并使用它们。 - Stijn de Witt
3
这简直是疯了。我对于Hibernate底层的运行机制还比较陌生,正在编写单元测试时发现,修改过对象后无法从set中删除该对象。我推测这是由于hashcode改变导致的,但却不知道该如何解决。这篇文章写得真是太好了! - XMight
2
这是一篇很棒的文章。不过,对于第一次看到链接的人来说,我建议这可能会在大多数应用中过度。此页面列出的其他3个选项多少可以以多种方式解决问题。 - HopeKing
2
Hibernate/JPA是否使用实体的equals和hashcode方法来检查记录是否已经存在于数据库中? - Tushar Banne
@TusharBanne 我不确定它具体用于什么,但我在与Hibernate相关的异常中看到了hashCode方法,所以可以肯定它确实使用了它。 - Archmede

70

我总是覆盖equals/hashcode并基于业务id实现它。对我来说,这似乎是最合理的解决方案。请参见以下链接

为了总结所有这些内容,以下是处理equals/hashCode的不同方式的工作或不工作的列表: enter image description here

编辑:

为了解释为什么这对我有用:

  1. 我通常不在我的JPA应用程序中使用基于哈希的集合(HashMap / HashSet)。如果必须,我更喜欢创建UniqueList解决方案。
  2. 我认为在运行时更改业务ID不是任何数据库应用程序的最佳实践。在极少数情况下,如果没有其他解决方案,我会像删除元素并将其放回到基于哈希的集合中那样进行特殊处理。
  3. 对于我的模型,我在构造函数中设置业务id并且不提供setter。我让JPA实现更改字段而不是属性。
  4. UUID解决方案似乎过度复杂化。如果你有自然的业务id,为什么还要UUID?毕竟,我会在数据库中设置业务id的唯一性。为什么在数据库中每个表都要有三个索引呢?

2
但是这张表缺少第五行“适用于List/Sets”(如果您考虑从OneToMany映射中删除作为Set的一部分的实体),因为其hashCode()更改会违反其合同,所以最后两个选项的答案都将是“否”。 - MRalwasser
1
请看问题的注释。您似乎误解了等式/哈希码合同。 - nanda
2
@MRalwasser:我认为你的意思是正确的,只是违反了equals/hashCode()合同本身。但是可变的equals/hashCode确实会对Set合同造成问题。 - Chris Lercher
4
哈希码只有在业务ID更改时才会改变,关键是业务ID不会更改。因此哈希码不会改变,并且这与哈希集合完美配合使用。 - Tom Anderson
1
如果您没有自然业务键怎么办?例如,在绘图应用程序中的二维点Point(X,Y)的情况下?您将如何将该点存储为实体? - jhegedus
显示剩余2条评论

44

我个人在不同的项目中已经使用了这三种策略。我必须说,在实际应用中,选项1是我认为最实用的。根据我的经验,打破hashCode()/equals()的一致性会导致许多疯狂的错误,因为每次在将实体添加到集合后,相等性的结果都会发生变化。

但是还有其他选项(也有其优缺点):


a) 基于一组不可变非空构造函数分配的字段的hashCode/equals

(+) 保证了所有三个标准

(-) 必须提供字段值以创建新实例

(-) 如果必须更改其中一个,则会使处理复杂化


b) 基于应用程序分配的主键(在构造函数中)而不是 JPA,使用 hashCode/equals 方法。
(+)所有三个标准都得到保证。
(-)您无法利用简单可靠的 ID 生成策略,如 DB 序列。
(-)如果在分布式环境(客户端/服务器)或应用服务器集群中创建新实体,则会变得复杂。

c) 基于实体构造函数分配的 UUIDhashCode/equals

(+) 保证了所有三个标准

(-) UUID 生成的开销较大

(-) 根据所使用的算法,可能存在两次使用相同 UUID 的风险(可以通过数据库上的唯一索引检测到)


2
我喜欢选项1方法C。在绝对需要之前不要做任何事情是更敏捷的方法。 - Adam Gent
2
如果一个实体有一个自然的业务 ID,那么它也应该是数据库的主键。这是简单、直接、良好的数据库设计。如果没有这样的 ID,则需要使用代理键。如果在对象创建时设置了它,那么其他所有事情都很简单。问题出现在人们不使用自然键,并且不及早生成代理键时。至于实现上的复杂性——是的,有一些。但真的不多,而且可以用非常通用的方式解决所有实体的问题。 - Tom Anderson
我也更喜欢选项1,但是如何编写单元测试来断言完全相等是一个大问题,因为我们必须为“Collection”实现equals方法。 - OOD Waterball
1
UUID生成的开销是负面的吗?与实际将数据存储在数据库中相比如何? - john16384

37
如果您想要在集合中使用equals()/hashCode(),以便同一实体只出现一次,则只有一种选择:选项2。这是因为实体的主键根据定义永远不会更改(如果确实有人更新它,则不再是同一实体)。
您应该字面理解:由于您的equals()/hashCode()基于主键,因此在设置主键之前,您不能使用这些方法。因此,直到分配主键之前,您不应将实体放入集合中。(是的,UUID和类似概念可能有助于尽早分配主键。)
现在,理论上也可以通过选项3来实现,即使所谓的“业务键”具有可更改的恶性缺陷:“您需要做的就是从集合中删除已插入的实体,并重新插入它们。”这是正确的-但这也意味着,在分布式系统中,您必须确保已在数据插入到的每个地方都执行了此操作(并且您必须确保在发生其他事情之前执行更新)。特别是如果某些远程系统当前无法访问,您将需要复杂的更新机制...
如果您的集合中的所有对象都来自同一个Hibernate会话,则只能使用选项1。Hibernate文档在第13.1.3. Considering object identity章节中非常清楚地说明了这一点。
在Session中,应用程序可以安全地使用“==”来比较对象。然而,在Session之外使用“==”的应用程序可能会产生意外的结果。这可能发生在一些意想不到的地方。例如,如果将两个分离的实例放入同一个Set中,则两者都可能具有相同的数据库标识(即它们表示同一行)。然而,JVM身份在分离状态下的实例中是不能保证的。开发人员必须覆盖持久类中的equals()和hashCode()方法,并实现自己的对象相等概念。
它继续支持选项3:
有一个警告:永远不要使用数据库标识符来实现相等性。使用业务键,该键是唯一的、通常是不可变的属性组合。如果将瞬态对象(通常与分离的实例一起)保存在Set中,更改哈希码会破坏Set的协议。
这是真的,如果你不能早期分配id(例如使用UUID),但你绝对想在瞬态状态下将你的对象放入集合中,那么你可以选择选项2。
然后它提到了相对稳定性的需要:
业务键的属性不必像数据库主键那样稳定;只需在对象在同一个Set中时保证稳定性即可。

这是正确的。我看到的实际问题是:如果你不能保证绝对稳定性,那么如何能够保证"只要对象在同一个Set中就保持稳定性"呢?我可以想象一些特殊情况(比如仅在对话中使用集合然后将其丢弃),但我会质疑这种做法的普适性。


简短版:

  • 选项1只能用于单个会话中的对象。
  • 如果可以,请使用选项2。(尽早分配主键,因为在分配主键之前无法将对象用于集合。)
  • 如果您可以保证相对稳定性,则可以使用选项3。但是要小心。

1
你的假设主键永远不会改变是错误的。例如,Hibernate仅在会话保存时分配主键。因此,如果您将主键用作hashCode,则在首次保存对象之前和之后的hashCode()结果将不同。更糟糕的是,在保存会话之前,两个新创建的对象将具有相同的hashCode,并且在添加到集合时可能会互相覆盖。您可能需要立即强制保存/刷新对象创建以使用该方法。 - William Billingsley
4
@William:实体的主键不会改变,映射对象的id属性可能会改变,尤其是当一个瞬态对象变成持久化对象时。请仔细阅读我的答案,我提到了equals/hashCode方法的部分:“在主键设置之前,你不应该使用这些方法。” - Chris Lercher
完全同意。选择方案2,你还可以在超类中提取出等于/哈希码,并让所有实体重复使用它。 - Theo
+1 我是 JPA 的新手,但这里的一些评论和答案暗示人们不理解“主键”一词的含义。 - Raedwald

36

我们的实体通常有两个ID:

  1. 仅用于持久层(以便持久性提供程序和数据库可以确定对象之间的关系)。
  2. 用于我们应用程序的需要(特别是equals()hashCode())。

看一下:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    // assuming all fields are subject to change
    // If we forbid users change their email or screenName we can use these
    // fields for business ID instead, but generally that's not the case
    private String screenName;
    private String email;

    // I don't put UUID generation in constructor for performance reasons. 
    // I call setUuid() when I create a new entity
    public User() {
    }

    // This method is only called when a brand new entity is added to 
    // persistence context - I add it as a safety net only but it might work 
    // for you. In some cases (say, when I add this entity to some set before 
    // calling em.persist()) setting a UUID might be too late. If I get a log 
    // output it means that I forgot to call setUuid() somewhere.
    @PrePersist
    public void ensureUuid() {
        if (getUuid() == null) {
            log.warn(format("User's UUID wasn't set on time. " 
                + "uuid: %s, name: %s, email: %s",
                getUuid(), getScreenName(), getEmail()));
            setUuid(UUID.randomUUID());
        }
    }

    // equals() and hashCode() rely on non-changing data only. Thus we 
    // guarantee that no matter how field values are changed we won't 
    // lose our entity in hash-based Sets.
    @Override
    public int hashCode() {
        return getUuid().hashCode();
    }

    // Note that I don't use direct field access inside my entity classes and
    // call getters instead. That's because Persistence provider (PP) might
    // want to load entity data lazily. And I don't use 
    //    this.getClass() == other.getClass() 
    // for the same reason. In order to support laziness PP might need to wrap
    // my entity object in some kind of proxy, i.e. subclassing it.
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        return getUuid().equals(((User) obj).getUuid());
    }

    // Getters and setters follow
}

编辑:为了阐明我关于调用setUuid()方法的观点。以下是一个典型的场景:

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("yoda@jedicouncil.org");

jediSet.add(user); // here's bug - we forgot to set UUID and 
                   //we won't find Yoda in Jedi set

em.persist(user); // ensureUuid() was called and printed the log for me.

jediCouncilSet.add(user); // Ok, we got a UUID now

当我运行我的测试并查看日志输出时,我会修复问题:
User user = new User();
user.setUuid(UUID.randomUUID());

或者,也可以提供一个单独的构造函数:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    ... // fields

    // Constructor for Persistence provider to use
    public User() {
    }

    // Constructor I use when creating new entities
    public User(UUID uuid) {
        setUuid(uuid);
    }

    ... // rest of the entity.
}

所以我的示例将如下所示:
User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

我使用默认构造函数和setter方法,但您可能会发现使用两个构造函数的方法更适合您。


2
我相信这是一个正确且好的解决方案。它可能还具有一点性能优势,因为整数通常在数据库索引中比uuids表现更好。但除此之外,您可能可以消除当前的整数ID属性,并将其替换为(应用程序分配的)UUID? - Chris Lercher
4
使用此方法与使用JVM默认的hashCode/equals方法进行相等性判断和使用ID进行持久性相等性判断有何不同?我完全不理解这个。 - Behrang
2
在您有多个实体对象指向数据库中同一行的情况下,它可以正常工作。在这种情况下,Objectequals()方法将返回false。基于UUID的equals()方法将返回true - Andrei Андрей Листочкин
4
我认为没有理由需要两个身份证件和两种身份,这似乎完全没有意义,而且可能会有害。 - Tom Anderson
1
如果您可以使用UUID作为键,那么这是一个合理的选择,并且使生成变得容易。如果您不能或不想使用UUID,则建议从持久计数器(可能是混淆的)生成ID。这很简单,但可能值得解释一下;我将尝试在接下来的几天内写一篇文章来解释它。 - Tom Anderson
显示剩余13条评论

29
  1. 如果您有业务键,则应将其用于equalshashCode。(equalshashCode的实现方法)

  2. 如果您没有业务键,则不应将其与默认的Object equals和hashCode实现一起使用,因为这在您合并实体后将不起作用。

  3. 只有在hashCode实现返回一个常量值时,您才能在equals方法中使用实体标识符。(如下所示:equalshashCode方法的实现方法)

@Entity
public class Book {

    @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 getId() != null && Objects.equals(getId(), book.getId());
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }

    //Getters and setters omitted for brevity
}

查看这个在GitHub上的测试案例,证明这个解决方案非常有效。


哪个更好:(1)http://www.onjava.com/pub/a/onjava/2006/09/13/dont-let-hibernate-steal-your-identity.html 还是(2)https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/? 解决方案(2)比(1)更容易。那么为什么我要使用(1)呢?两者的效果相同吗?两者都能保证相同的解决方案吗? - nimo23
而且使用您的解决方案:“hashCode值在相同实例之间不会改变”。这与比较“相同”的UUID(来自解决方案(1))具有相同的行为。我是对的吗? - nimo23
1
将UUID存储在数据库中并增加记录的占用空间和缓冲池大小,我认为这可能会导致长期性能问题比唯一的hashCode更多。至于另一个解决方案,您可以检查它是否提供了所有实体状态转换的一致性。您可以在GitHub上找到检查此项的测试 - Vlad Mihalcea
3
如果您有一个不变的业务键,hashCode可以使用它,并且会从多个桶中获益,因此如果您有一个不变的业务键,则值得使用。否则,请按照我文章中所述使用实体标识符。 - Vlad Mihalcea
2
如果有疑问,只需访问 Vlad Mihalcea Dot Com。 - Vlad Mihalcea
显示剩余23条评论

16
尽管使用业务键(选项3)是最常推荐的方法(Hibernate community wiki,“Java Persistence with Hibernate” p. 398),并且这也是我们大多数人使用的方法,但存在一个Hibernate bug会破坏急切获取集合的业务键:HHH-3799。在这种情况下,Hibernate可能会在实体字段初始化之前向集合中添加实体。我不确定为什么这个bug没有得到更多关注,因为它确实使推荐的业务键方法变得有问题。
我认为问题的核心在于equals和hashCode应该基于不可变状态(参考Odersky et al.),而具有Hibernate管理的主键的Hibernate实体没有这样的不可变状态。当一个瞬态对象变成持久对象时,Hibernate会修改主键。当Hibernate在初始化过程中填充对象时,业务键也会被修改。

这样就只剩下选项1了,根据对象标识继承java.lang.Object实现,或者像James Brundege在"Don't Let Hibernate Steal Your Identity"(已经被Stijn Geukens的回答引用)和Lance Arlaus在"Object Generation: A Better Approach to Hibernate Integration"中建议的那样使用应用程序管理的主键。

选项1的最大问题是无法使用.equals()比较脱离实例和持久实例。但这没关系; equals和hashCode的约定将决定每个类的相等意味着什么。所以让equals和hashCode从Object继承。如果你需要比较一个脱离实例和一个持久实例,你可以为此创建一个新的方法,例如boolean sameEntityboolean dbEquivalentboolean businessEquals


7

Jakarta Persistence 3.0, 第4.12节 写道:

如果两个抽象模式类型的实体具有相同的主键值,则它们是相等的。

我不明白为什么Java代码会有不同的行为。

如果实体类处于所谓的“瞬态”状态,即尚未持久化且没有标识符,则hashCode/equals方法不能返回值,它们应该崩溃,理想情况下在方法尝试遍历ID时隐式地引发NullPointerException。无论哪种方式,这都将有效地阻止应用程序代码将非托管实体放入基于哈希的数据结构中。实际上,为什么不再进一步,如果类和标识符相等但其他重要属性(例如version)不相等,则崩溃(IllegalStateException)!以确定性方式快速失败始终是首选选项。

警告:还需记录崩溃行为。文档本身非常重要,但它也将希望未来的初级开发人员不要在您的代码中做一些愚蠢的事情(他们有压制NullPointerException的倾向,而最后一件事是副作用lol)。

哦,并且始终使用getClass()而不是instanceof。equals方法需要对称性。如果b等于a,则a必须等于b。对于子类,instanceof会破坏此关系(a不是b的实例)。

尽管我个人始终在实现非实体类时使用getClass()(类型是状态,因此子类即使为空或仅包含行为也会添加状态),但如果类是final,那么instanceof也可以。但实体类不能是final(§2.1),因此我们确实没有其他选择。

一些人可能不喜欢使用getClass(),因为持久性提供程序将对象包装在代理中。这在过去可能是一个问题,但现在真的不应该是。如果提供程序不为不同的实体返回不同的代理类,那么我会说这不是一个很聪明的提供程序。通常,在没有问题出现之前,我们不应该解决问题。而且,似乎Hibernate自己的文档甚至都不值得一提。事实上,他们在自己的示例中优雅地使用了getClass()参见此处)。

最后,如果一个实体子类也是一个实体,并且继承映射策略不是默认值(“单表格”),而是配置为“连接子类型”,那么该子类表中的主键将与超类表相同。如果映射策略是“具体类的每个表格”,那么主键可能与超类中的主键相同。实体子类很可能会添加状态,因此很可能逻辑上是不同的东西。但是使用instanceof的等于实现不能仅次要依赖于ID,因为我们看到对于不同的实体可能是相同的。

在我看来,instanceof在非final Java类中根本没有任何地方,特别是对于持久性实体而言更是如此。


即使缺少DB序列(如Mysql),也可以模拟它们(例如,表hibernate_sequence)。因此,您始终可以获得跨表唯一的ID。+++但是您不需要它。调用Object#getClass()很糟糕,因为存在H.代理。调用Hibernate.getClass(o)有所帮助,但是不同类型实体的相等性问题仍然存在。使用canEqual有一个解决方案,有点复杂,但可用。通常情况下,确实不需要它。+++在空ID上抛出eq/hc违反了合同,但这非常实用。 - maaartinus
谢谢您的评论。我更新了答案。我想在这里补充一点,即语句“在空ID上抛出eq/hc违反了合同”是错误的。它客观上是错误的,因为它根本不是合同的一部分。虽然这并不影响真实性,但我想补充一下,其他人也同意 - Martin Andersson
我认为在提供的身份属性的上下文中,你的getClass()超过了instanceof是不正确的。换句话说,根据你在回答中开始的第一个引用断言,类型/子类型应该基本上被忽略,就像提供的身份属性之外的其他字段一样。换句话说,整个理念都以“独立”值为锚定的身份为中心。 - undefined
即使你仍然想要getClasss()检查,我仍然建议使用更现代、流畅的函数式表达方式:https://stackoverflow.com/a/75402885/501113 - undefined
这是我对OP的问题选择选项2的答案/解决方案。在这种模式下,我使用instanceof(有效处理null检查)。getClass()行被注释掉,因为它在JPA/Hibernate代理中失败了。https://stackoverflow.com/a/77091598/501113 - undefined
getClass()是非最终类的唯一有效替代方法。真的没有什么“上下文”需要烦恼吗?如果您能详细说明您对Hibernate代理有什么问题,我会很高兴的。我自己从未遇到过任何问题,而且显然Hibernate也没有,在他们的文档中使用了getClass() - undefined

6

我同意安德鲁的回答。我们在应用程序中也是这样做的,但是我们将UUID拆分为两个长整型值,而不是将其存储为VARCHAR / CHAR。请参见UUID.getLeastSignificantBits()和UUID.getMostSignificantBits()。

还有一件事需要考虑,就是调用UUID.randomUUID()相当缓慢,因此您可能希望仅在需要时延迟生成UUID,例如在持久性或调用equals() / hashCode()期间。

@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {

    private static final long   serialVersionUID    = 1L;

    @Version
    @Column(name = "version", nullable = false)
    private int                 version             = 0;

    @Column(name = "uuid_least_sig_bits")
    private long                uuidLeastSigBits    = 0;

    @Column(name = "uuid_most_sig_bits")
    private long                uuidMostSigBits     = 0;

    private transient int       hashCode            = 0;

    public AbstractJpaEntity() {
        //
    }

    public abstract Integer getId();

    public abstract void setId(final Integer id);

    public boolean isPersisted() {
        return getId() != null;
    }

    public int getVersion() {
        return version;
    }

    //calling UUID.randomUUID() is pretty expensive, 
    //so this is to lazily initialize uuid bits.
    private void initUUID() {
        final UUID uuid = UUID.randomUUID();
        uuidLeastSigBits = uuid.getLeastSignificantBits();
        uuidMostSigBits = uuid.getMostSignificantBits();
    }

    public long getUuidLeastSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidLeastSigBits;
    }

    public long getUuidMostSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidMostSigBits;
    }

    public UUID getUuid() {
        return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
        }
        return hashCode;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractJpaEntity)) {
            return false;
        }
        //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
        //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
        //if they have different types) having the same UUID is astronomical
        final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
        return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
    }

    @PrePersist
    public void prePersist() {
        // make sure the uuid is set before persisting
        getUuidLeastSigBits();
    }

}

其实,如果您覆盖equals() / hashCode(),则必须为每个实体生成UUID(我假设您想在代码中持久化创建的每个实体)。 您只需要在第一次将其存储到数据库之前执行一次即可。之后,Persistence Provider只需加载UUID即可。因此,我不认为懒惰地这样做有什么意义。 - Andrei Андрей Листочкин
我点赞了你的答案,因为我特别喜欢你的其他想法:在数据库中将UUID存储为一对数字而不是在equals()方法中转换为特定类型-真的很巧妙! 我以后肯定会使用这两个技巧。 - Andrei Андрей Листочкин
1
感谢点赞。懒惰初始化UUID的原因是我们的应用程序中创建了许多从未放入HashMap或持久化的实体。因此,当我们创建对象时(成千上万个),性能下降了100倍。因此,我们只有在需要时才初始化UUID。我只希望MySql对128位数字有很好的支持,这样我们就可以将UUID用作ID,而不必关心自动增量。 - Drew
哦,我明白了。在我的情况下,如果相应的实体不会被放入集合中,我们甚至不声明UUID字段。缺点是有时候我们需要添加它,因为后来发现我们实际上需要将它们放入集合中。这种情况有时会在开发过程中发生,但幸运的是,在最初部署到客户端之后,从未发生过这种情况,所以这不是什么大问题。如果在系统上线后发生这种情况,我们将需要进行数据库迁移。在这种情况下,懒惰UUID非常有帮助。 - Andrei Андрей Листочкин
如果在您的情况下性能是一个关键问题,那么也许您应该尝试Adam在他的回答中建议的更快的UUID生成器。 - Andrei Андрей Листочкин
愚蠢的问题:拆分UUID有什么优势? - Bill Rosmus

2
请考虑基于预定义的类型标识符和ID的以下方法。
JPA的具体假设为:
- 相同“类型”和相同非空ID的实体被视为相等。 - 未持久化的实体(假设没有ID)永远不会等于其他实体。
抽象实体:
@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {

  @Id @GeneratedValue
  private K id;

  @Transient
  private final String kind;

  public AbstractPersistable(final String kind) {
    this.kind = requireNonNull(kind, "Entity kind cannot be null");
  }

  @Override
  public final boolean equals(final Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof AbstractPersistable)) return false;
    final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null != this.id
        && Objects.equals(this.id, that.id)
        && Objects.equals(this.kind, that.kind);
  }

  @Override
  public final int hashCode() {
    return Objects.hash(kind, id);
  }

  public K getId() {
    return id;
  }

  protected void setId(final K id) {
    this.id = id;
  }
}

具体实体示例:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

测试例子:

@Test
public void test_EqualsAndHashcode_GivenSubclass() {
  // Check contract
  EqualsVerifier.forClass(Foo.class)
    .suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
    .withOnlyTheseFields("id", "kind")
    .withNonnullFields("id", "kind")
    .verify();
  // Ensure new objects are not equal
  assertNotEquals(new Foo(), new Foo());
}

以下是主要优点:

  • 简单易懂
  • 确保子类提供类型标识
  • 对代理类的预测行为

缺点:

  • 每个实体都需要调用super()

注意事项:

  • 在使用继承时需要注意,例如class Aclass B extends A的实例相等性可能取决于应用程序的具体细节。
  • 最好使用业务键作为ID

期待您的评论。


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