JPA hashCode()/ equals()困境


311

此处已经进行了一些 有关JPA实体的讨论,以及JPA实体类应使用哪种hashCode()/ equals()实现。它们中的大多数(如果不是全部)都依赖于Hibernate,但是我想中立地讨论它们(顺便说一下,我正在使用EclipseLink)。

在以下方面,所有可能的实现都有各自的优点缺点

  • hashCode()/equals()合同一致性(不变性)为List/ Set操作
  • 是否可以检测到相同的对象(例如,来自不同的会话,来自延迟加载的数据结构的动态代理)
  • 实体在分离(或非持久)状态下是否行为正确

据我所知,有三种选择

  1. 不要覆盖它们;依靠Object.equals()Object.hashCode()
    • hashCode()/ equals()工作
    • 无法识别相同的对象,动态代理问题
    • 独立实体没有问题
  2. 根据主键覆盖它们
    • hashCode()/ equals()坏了
    • 正确的身份(对于所有管理实体)
    • 独立实体的问题
  3. 根据Business-Id(非主键字段;外键如何?) 覆盖它们
    • hashCode()/ equals()坏了
    • 正确的身份(对于所有管理实体)
    • 独立实体没有问题

我的问题是:

  1. 我错过了一个选项和/或优点/缺点吗?
  2. 您选择了什么选项,为什么?



更新1:

通过“ hashCode()/ equals()被破坏”,我的意思是连续hashCode()调用可能会返回不同的值,也就是(当正确实施)不在的感觉打破ObjectAPI文档,但是当试图从检索改变实体引起的问题MapSet或其他基于哈希Collection。因此,在某些情况下,JPA实现(至少是EclipseLink)将无法正常工作。

更新2:

谢谢您的回答-大多数都具有卓越的质量。
不幸的是,我仍然不确定哪种方法对实际应用程序将是最好的,或者不确定如何为我的应用程序确定最佳方法。因此,我将保持开放的态度,并希望有更多的讨论和/或意见。


4
我不明白您所说的“ hashCode()/ equals()损坏”是什么意思
nanda

4
这样,它们就不会被“破坏”,就像在选项2和3中一样,您将使用相同的策略来实现equals()和hashCode()。
马特b

11
选项3并非如此。hashCode()和equals()应该使用相同的条件,因此,如果您的某个字段发生更改,则hashcode()方法将针对同一实例返回与之前不同的值,但是equals()也是如此。您已经从hashcode()javadoc中删除了该语句的第二部分:在Java应用程序执行期间,只要在同一对象上多次调用
马特b

1
实际上,句子的这一部分表示相反的含义- hashcode()除非在equals()实现中使用的任何字段发生更改,否则在同一对象实例上的调用应返回相同的值。换句话说,如果您的类中有三个字段,并且您的equals()方法仅使用其中两个字段来确定实例的相等性,那么hashcode()如果您更改了这些字段的值之一,则可以期望返回值发生变化-这在您考虑时是有意义的该对象实例不再“等于”旧实例表示的值。
马特b

2
“尝试从Map,Set或其他基于哈希的集合中检索更改的实体时出现的问题”……这应该是“尝试从HashMap,HashSet或其他基于哈希的集合中检索更改后的实体时的问题”
nanda

Answers:


122

阅读有关该主题的一篇非常不错的文章:不要让休眠模式窃取您的身份

本文的结论是这样的:

当将对象持久化到数据库时,很难正确实现对象标识。但是,问题完全出在允许对象在保存之前没有id的情况下。我们可以通过从对象关系映射框架(例如Hibernate)中分配对象ID的责任来解决这些问题。而是可以在实例化对象后立即分配对象ID。这使对象身份变得简单且无错误,并减少了域模型中所需的代码量。


21
不,那不是一篇好文章。那是关于该主题的精彩文章,每个JPA程序员都需要阅读它!+1!
汤姆·安德森

2
是的,我正在使用相同的解决方案。不让数据库生成ID也具有其他优点,例如能够创建一个对象,并且已经创建了在持久化之前引用它的其他对象。这可以消除客户端服务器应用程序中的延迟和多个请求/响应周期。如果您需要灵感来解决这种问题,请查看我的项目:suid.jssuid-server-java。基本上suid.js获取ID块,suid-server-java然后您可以从中获取并使用客户端。
Stijn de Witt

2
这简直太疯狂了。我是刚进入休眠状态的新手,正在编写单元测试,发现修改后无法从集合中删除对象,得出的结论是这是由于哈希码更改而引起的,但无法理解解决。文章简单华丽!
XMight '16

这是一篇很棒的文章。但是,对于那些第一次看到该链接的人,我建议对于大多数应用程序来说,这可能是一个过高的选择。此页面上列出的其他3个选项应或多或少以多种方式解决此问题。
HopeKing

1
Hibernate / JPA是否使用实体的equals和hashcode方法检查记录是否已存在于数据库中?
Tushar Banne

64

我总是重写equals / hashcode并根据业务ID实施它。对我来说似乎是最合理的解决方案。请参阅以下链接

总结一下所有这些内容,下面列出了处理equals / hashCode的不同方式将起作用或不起作用的清单: 在此处输入图片说明

编辑

解释为什么这对我有用:

  1. 我通常在JPA应用程序中不使用基于散列的集合(HashMap / HashSet)。如果需要的话,我更喜欢创建UniqueList解决方案。
  2. 我认为在任何运行时更改业务ID都不是任何数据库应用程序的最佳做法。在极少数情况下,没有其他解决方案,我会进行特殊处理,例如删除该元素并将其放回基于散列的集合中。
  3. 对于我的模型,我在构造函数上设置了业务ID,但没有为其提供设置器。我让JPA实现更改字段而不是属性。
  4. UUID解决方案似乎有些过分。如果您具有自然的业务ID,为什么要选择UUID?毕竟,我将在数据库中设置业务ID的唯一性。为什么对数据库中的每个表都有三个索引呢?

1
但是此表缺少第五行“可与列表/集合一起使用”(如果您考虑从OneToMany映射中删除作为集合的一部分的实体),在最后两个选项上会被回答为“否”,因为其hashCode( )违反合同的更改。
MRalwasser 2011年

请参阅该问题的评论。您似乎误会了equals / hashcode合约
nanda

1
@MRalwasser:我认为您的意思是对的,不是等于equals / hashCode()合同本身。但是可变的equals / hashCode确实会给Set合同带来问题。
克里斯·勒彻

3
@MRalwasser:仅当业务ID更改时,哈希码才能更改,而要点是,业务ID 不会更改。因此,哈希码不会改变,这与哈希集合完美配合。
汤姆·安德森

1
如果您没有自然的业务密钥怎么办?例如,在图形绘制应用程序中有二维点Point(X,Y)的情况?您将如何将该点存储为实体?
jhegedus 2014年

35

通常,我们的实体中有两个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

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


2
我相信,这是一个正确和好的解决方案。它也可能具有一点性能优势,因为整数通常在数据库索引中的性能要优于uuid。但是除此之外,您可能可以消除当前的整数id属性,并用(分配的应用程序)uuid替换它?
克里斯·勒彻

4
这与将默认hashCode/ equals方法用于JVM平等和id持久性平等有何不同?这对我完全没有意义。
Behrang Saeedzadeh 2011年

2
当您有多个实体对象指向数据库中的同一行时,它可以工作。Objectequals()将返回false在这种情况下。基于UUID的equals()return true
安德鲁·АндрейЛисточкин2011年

4
-1-我看不到有两个ID以及两种身份的任何理由。这似乎毫无意义,并且可能对我有害。
汤姆·安德森

1
很抱歉批评您的解决方案而没有指出我想要的解决方案。简而言之,我将为对象提供一个ID字段,并基于该字段实现equals和hashCode,并且在创建对象时(而不是将其保存到数据库时)生成其值。这样,所有形式的对象都以相同的方式工作:非持久,持久和分离。Hibernate代理(或类似的代理)也应该正常工作,我认为甚至不需要为处理equals和hashCode调用而充饥。
汤姆·安德森

31

我个人已经在不同的项目中使用了这三种策略。我必须说,在我看来,选项1在现实生活中是最可行的。以我的经验,破坏hashCode()/ equals()一致性会导致许多疯狂的错误,因为每次将最终结果添加到集合中之后,相等性的结果都会改变。

但是,还有其他选择(也各有利弊):


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

(+)所有三个条件均得到保证

(-)字段值必须可用于创建新实例

(-)如果必须更改其中之一,会使处理变得复杂


b)基于由应用程序(在构造函数中)而不是JPA分配的主键的hashCode /等于

(+)所有三个条件均得到保证

(-)您不能利用简单可靠的ID生成状态(如DB序列)

(-)如果在分布式环境(客户端/服务器)或应用程序服务器群集中创建新实体,则会很复杂


c)基于实体构造函数分配的UUID的 hashCode /等于

(+)所有三个条件均得到保证

(-)UUID生成的开销

(-)使用两次相同的UUID可能会有一点风险,具体取决于所使用的算法(可由数据库上的唯一索引检测到)


我也是选项1方法C的粉丝。直到您绝对需要它之前,什么都不做是更敏捷的方法。
亚当·根特

2
+1(b)。恕我直言,如果实体具有自然的业务ID,则该实体也应为其数据库主键。那是简单,直接,良好的数据库设计。如果没有这样的ID,则需要一个替代密钥。如果将其设置为对象创建,那么其他一切都很简单。当人们不使用自然密钥,并且不及早生成代理密钥时,他们就会陷入麻烦。至于实施的复杂性-是的,有一些。但实际上并没有很多,它可以通过非常通用的方式来解决,它可以为所有实体一次解决。
Tom Anderson

我也更喜欢选项1,但是随后如何编写单元测试来断言完全相等是一个大问题,因为我们必须实现的equals方法Collection
OOD水球

只是不要这样做。参见“ 不要与冬眠混在一起”
alonana

29

如果要equals()/hashCode()用于集合,就意味着同一实体只能在其中一次,则只有一个选项:选项2。这是因为根据定义,实体的主键永远不会更改(如果有人确实在更新它,它不再是同一实体了)

您应该从字面上理解:由于您equals()/hashCode()基于主键,因此在设置主键之前,不得使用这些方法。因此,在为实体分配主键之前,不要将其放入集合中。(是的,UUID和类似概念可能有助于尽早分配主键。)

现在,从理论上讲,也可以通过选项3实现这一目标,即使所谓的“业务密钥”具有可以更改的讨厌的缺点:“您要做的就是从集合中删除已插入的实体( s),然后重新插入它们。” 没错-但这也意味着,在分布式系统中,您必须确保在插入数据的所有地方都绝对可以做到这一点(并且必须确保执行更新,在其他事情发生之前)。您将需要复杂的更新机制,尤其是在某些当前无法访问远程系统的情况下。

仅当集合中的所有对象都来自同一Hibernate会话时,才可以使用选项1。Hibernate文档在第13.1.3章中非常清楚地说明了这一点考虑对象身份

在会话中,应用程序可以安全地使用==比较对象。

但是,在会话之外使用==的应用程序可能会产生意外结果。即使在某些意外的地方,也可能发生这种情况。例如,如果将两个分离的实例放入同一Set中,则它们可能具有相同的数据库标识(即,它们表示同一行)。但是,根据定义,JVM身份不能保证处于分离状态的实例。开发人员必须在持久性类中重写equals()和hashCode()方法,并实现其自己的对象相等性概念。

它将继续主张备选方案3:

有一个警告:不要使用数据库标识符来实现相等性。使用由独特的(通常是不可变的)属性组成的业务密钥。如果使临时对象持久化,则数据库标识符将更改。如果临时实例(通常与分离的实例一起)保存在Set中,则更改哈希码会破坏Set的协定。

这是真的,如果

  • 无法及早分配ID(例如,通过使用UUID)
  • 但是,您绝对希望在对象处于过渡状态时将它们放在集合中。

否则,您可以自由选择选项2。

然后提到了相对稳定性的需求:

业务键的属性不必像数据库主键一样稳定。只要对象在同一Set中,您只需要保证稳定性即可。

这是对的。我看到的一个实际问题是:如果您不能保证绝对稳定性,那么“只要对象在同一个Set中”就如何保证稳定性。我可以想象一些特殊情况(例如仅将集合用于对话,然后将其丢弃),但是我会质疑这种做法的普遍实用性。


简洁版本:

  • 选项1只能与单个会话中的对象一起使用。
  • 如果可以,请使用选项2。(请尽早分配PK,因为在分配PK之前不能使用集中的对象。)
  • 如果可以保证相对的稳定性,则可以使用选项3。但是请谨慎使用。

您对主键永不更改的假设是错误的。例如,Hibernate仅在保存会话时分配主键。因此,如果将主键用作hashCode,则在第一次保存对象之前和之后第一次保存对象时,hashCode()的结果将有所不同。更糟糕的是,在保存会话之前,两个新创建的对象将具有相同的hashCode,并且在添加到集合时会相互覆盖。您可能会发现自己必须在对象创建时立即强制保存/刷新才能使用该方法。
William Billingsley

2
@威廉:实体的主键不变。映射对象的id属性可能会更改。如您所解释的,这会发生,特别是在使瞬态对象成为持久对象时。请仔细阅读我的回答部分,其中我谈到过equals / hashCode方法:“在设置主键之前,您不得使用这些方法。”
克里斯·勒彻

完全同意。使用选项2,您还可以在超类中排除equals / hashcode,并让所有实体重新使用它。
Theo

+1我是JPA的新手,但是这里的一些评论和答案暗示人们不理解“主键”一词的含义。
拉德瓦尔德

16
  1. 如果您有业务密钥,则应将其用于equals/ hashCode
  2. 如果您没有业务密钥,则不应将其保留为默认的Objectequals和hashCode实现,因为这在您merge和实体之后不起作用。
  3. 您可以按照本文中的建议使用实体标识符。唯一的问题是您需要使用hashCode始终返回相同值的实现,如下所示:

    @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 getId() != null && Objects.equals(getId(), book.getId());
        }
    
        @Override
        public int hashCode() {
            return 31;
        }
    
        //Getters and setters omitted for brevity
    }

哪个更好:(1)onjava.com/pub/a/onjava/2006/09/13/…或(2)vladmihalcea.com/…?解决方案(2)比(1)容易。那么,为什么要使用(1)。两者的效果是否相同?两者都保证相同的解决方案吗?
nimo23 '18

使用您的解决方案:在相同实例之间“ hashCode值不变”。这具有与被比较的“相同” uuid(来自解决方案(1))相同的行为。我对吗?
nimo23 '18

1
并将UUID存储在数据库中并增加记录和缓冲池的占用量吗?我认为,从长远来看,这可能会导致比唯一的hashCode更多的性能问题。至于其他解决方案,您可以签出以查看它是否在所有实体状态转换之间提供一致性。您可以在GitHub上找到检查它测试
Vlad Mihalcea

1
如果您有一个不可变的业务密钥,则hashCode可以使用它,它将从多个存储桶中受益,因此,如果有一个,则值得使用。否则,请按照我的文章中的说明使用实体标识符。
Vlad Mihalcea

1
我很高兴你喜欢它。我还有数百篇有关JPA和Hibernate的文章。
Vlad Mihalcea

10

尽管最常用的方法是使用业务密钥(选项3)(Hibernate社区Wiki,“ Java持久性与Hibernate”第398页),这是我们最常用的方法,但还是有一个Hibernate错误,使它难以为继。套:HHH-3799。在这种情况下,Hibernate可以在初始化其字段之前将实体添加到集合中。我不确定为什么这个错误没有得到更多的关注,因为它确实使建议的业务关键方法产生了问题。

我认为问题的核心是equals和hashCode应该基于不可变状态(参考Odersky等人),并且具有由Hibernate管理的主键的Hibernate实体没有这种不可变状态。当临时对象变为持久对象时,Hibernate会修改主键。当在初始化过程中将对象水合时,Hibernate还会修改业务密钥。

仅剩下选项1,基于对象身份继承java.lang.Object实现,或使用James Brundege在“不要让休眠模式窃取您的身份”中建议的应用程序管理的主键(已由Stijn Geukens的答案引用) )和Lance Arlaus在“对象生成:一种更好的休眠集成方法”中

选项1的最大问题是无法使用.equals()将分离的实例与持久性实例进行比较。但是没关系;equals和hashCode的约定由开发人员决定是否相等对于每个类意味着什么。因此,让equals和hashCode从Object继承。如果您需要一种超然的实例比较实例的持久化,你可以明确地创建一个新的方法用于该目的,或许boolean sameEntity还是boolean dbEquivalentboolean businessEquals


5

我同意安德鲁的回答。我们在应用程序中执行相同的操作,但没有将UUID存储为VARCHAR / CHAR,而是将其分为两个长值。请参阅UUID.getLeastSignificantBits()和UUID.getMostSignificantBits()。

需要考虑的另一件事是,对UUID.randomUUID()的调用非常慢,因此您可能希望研究仅在需要时(例如,在持久性期间或对equals()/ hashCode()的调用期间)延迟生成UUID。

@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。因此,我看不出这样做的意义。
安德鲁АндрейЛисточкин2011年

我投票赞成您的答案,因为我真的很喜欢您的其他想法:将UUID作为一对数字存储在数据库中,并且不转换为equals()方法中的特定类型-这真的很整洁!我以后肯定会使用这两个技巧。
2011年

1
感谢您的投票。延迟初始化UUID的原因是在我们的应用程序中,我们创建了很多实体,这些实体永远不会放入HashMap或保留下来。因此,当我们创建对象时(其中有100,000个对象),性能下降了100倍。因此,我们仅在需要时才初始化UUID。我只是希望MySql对128位数字有良好的支持,所以我们也可以将UUID用作ID,而不必关心auto_increment。
德鲁

哦,我明白了。就我而言,如果不将相应实体放入集合中,我们甚至不会声明UUID字段。缺点是有时必须添加它,因为后来发现我们实际上需要将它们放入集合中。有时在开发过程中会发生这种情况,但是幸运的是,在最初部署给客户之后,我们从未发生过这种情况,所以这没什么大不了的。如果在系统启动后发生这种情况,我们将需要进行数据库迁移。惰性UUID在这种情况下非常有用。
安德鲁АндрейЛисточкин2011年

如果性能在您的情况下是至关重要的问题,也许您还应该尝试更快的UUID生成器Adam建议的答案。
2011年

3

正如其他人比我更聪明地指出的那样,这里有许多策略。尽管大多数应用设计模式都试图破解成功之道,但情况似乎确实如此。它们会限制构造函数的访问,即使这并不妨碍使用专门的构造函数和工厂方法完全调用构造函数。确实,使用明确的API总是很令人愉快。但是,如果唯一的原因是使equals-和hashcode替代与应用程序兼容,那么我想知道这些策略是否符合KISS(保持简单愚蠢)。

对我来说,我喜欢通过检查id来覆盖equals和hashcode。在这些方法中,我要求id不能为null并很好地记录此行为。因此,将新实体保存在其他地方之前,将成为开发人员的合同。不遵守该合同的应用程序将在几分钟之内失败(希望如此)。

不过请注意:如果您的实体存储在不同的表中,并且您的提供者对主键使用自动生成策略,那么您将在实体类型之间获得重复的主键。在这种情况下,还要将运行时类型与对Object#getClass()的调用进行比较,这当然会使不可能将两种不同的类型视为相等。这在大多数情况下都适合我。


即使数据库缺少序列(例如Mysql),也可以模拟它们(例如,表hibernate_sequence)。因此,您可能总是在表之间获得唯一的ID。+++但是您不需要它。Object#getClass() 由于H.代理人,通话不正确。调用有Hibernate.getClass(o)帮助,但是仍然存在不同种类的实体相等的问题。有一个使用canEqual的解决方案,有点复杂,但是可以使用。同意通常不需要。+++将eq / hc放在空ID上违反了合同,但这非常实用。
maaartinus

2

显然这里已经有非常有用的答案,但是我会告诉您我们所做的。

我们什么也不做(即不超越)。

如果确实需要equals / hashcode来处理集合,则可以使用UUID。您只需在构造函数中创建UUID。我们使用http://wiki.fasterxml.com/JugHome作为UUID。UUID比CPU更昂贵,但与序列化和数据库访问相比便宜。


1

我过去一直使用选项1,因为我知道这些讨论,并认为最好不要做任何事情,直到我知道正确的做法。这些系统仍在成功运行。

但是,下次我可以尝试选项2-使用数据库生成的ID。

如果未设置ID,则Hashcode和equals将抛出IllegalStateException。

这样可以防止涉及未保存实体的细微错误意外出现。

人们如何看待这种方法?


1

业务密钥方法不适合我们。我们使用数据库生成的ID,临时的临时tempId覆盖 equal()/ hashcode()来解决难题。所有实体都是Entity的后代。优点:

  1. 数据库中没有多余的字段
  2. 后代实体中无需额外编码,一种适用于所有人的方法
  3. 没有性能问题(如UUID),数据库ID生成
  4. Hashmaps没问题(不必记住使用equal&等。)
  5. 即使持续存在,新实体的哈希码也不会及时更改

缺点:

  1. 序列化和反序列化未持久化的实体可能存在问题
  2. 从数据库重新加载后,所保存实体的哈希码可能会更改
  3. 没有持久化的对象被认为总是不同的(也许这是对的吗?)
  4. 还有什么?

看一下我们的代码:

@MappedSuperclass
abstract public class Entity implements Serializable {

    @Id
    @GeneratedValue
    @Column(nullable = false, updatable = false)
    protected Long id;

    @Transient
    private Long tempId;

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    private void setTempId(Long tempId) {
        this.tempId = tempId;
    }

    // Fix Id on first call from equal() or hashCode()
    private Long getTempId() {
        if (tempId == null)
            // if we have id already, use it, else use 0
            setTempId(getId() == null ? 0 : getId());
        return tempId;
    }

    @Override
    public boolean equals(Object obj) {
        if (super.equals(obj))
            return true;
        // take proxied object into account
        if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
            return false;
        Entity o = (Entity) obj;
        return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
    }

    // hash doesn't change in time
    @Override
    public int hashCode() {
        return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
    }
}

1

请根据预定义的类型标识符和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()

笔记:

  • 使用继承时需要注意。EG的实例平等class Aclass B extends A可能取决于应用程序的具体细节。
  • 理想情况下,使用业务密钥作为ID

期待您的评论。


0

这是每个使用Java和JPA的IT系统中的常见问题。痛点超出了实现equals()和hashCode()的范围,它影响了组织如何引用实体以及其客户如何引用同一实体。我已经苦于没有业务密钥,以至于我写了自己的博客来表达自己的观点。

简而言之:使用简短的,易于阅读的,具有有意义前缀的顺序ID作为生成的业务密钥,该业务密钥不依赖于RAM以外的任何存储。Twitter的Snowflake是一个很好的例子。


0

IMO你有3个实现equals / hashCode的选项

  • 使用应用程序生成的身份(即UUID)
  • 根据业务密钥实施
  • 根据主键实施

使用应用程序生成的身份是最简单的方法,但存在一些缺点

  • 将其用作PK时,连接速度较慢,因为128位仅大于32位或64位
  • “调试更困难”,因为用肉眼检查一些数据是否正确非常困难

如果您可以克服这些缺点,请使用此方法。

为了克服连接问题,可以将UUID用作自然键,将序列值用作主键,但是由于您希望基于在主键上。在子实体id和主键中使用自然键来引用父键是一个不错的选择。

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @NaturalId UUID uuid;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on uuid
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @Embeddable class ChildId {
    UUID parentUuid;
    UUID childUuid;
    // equals/hashCode based on parentUuid and childUuid
  }
  // equals/hashCode based on id
}

IMO这是最干净的方法,因为它将避免所有不利因素,同时为您提供一个可以与外部系统共享的值(UUID),而无需暴露系统内部。

如果您可以期望从用户那里得到一个不错的主意,则可以基于业务密钥实施它,但是也有一些缺点

大多数时候,此业务密钥将是用户提供的某种代码,而很少是多种属性的组合。

  • 连接比较慢,因为基于可变长度文本的连接很慢。如果键超过一定长度,某些DBMS甚至可能无法创建索引。
  • 以我的经验,业务密钥往往会发生变化,这需要对引用它的对象进行级联更新。如果外部系统引用它是不可能的

IMO,您不应仅实现或使用业务密钥。这是一个很好的附加组件,即用户可以通过该业务密钥快速搜索,但是系统不应依赖它进行操作。

基于主键实施它有问题,但是也许没什么大不了的

如果您需要向外部系统公开ID,请使用我建议的UUID方法。如果没有,您仍然可以使用UUID方法,但不必这样做。在equals / hashCode中使用DBMS生成的ID的问题源于以下事实:在分配ID之前可能已将对象添加到基于哈希的集合中。

解决此问题的明显方法是在分配ID之前,不将对象添加到基于哈希的集合中。我知道这并非总是可能的,因为您可能已经希望在分配ID之前进行重复数据删除。为了仍然能够使用基于散列的集合,您只需要在分配ID后重新构建集合即可。

您可以执行以下操作:

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on id
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @PrePersist void postPersist() {
    parent.children.remove(this);
  }
  @PostPersist void postPersist() {
    parent.children.add(this);
  }

  @Embeddable class ChildId {
    Long parentId;
    @GeneratedValue Long childId;
    // equals/hashCode based on parentId and childId
  }
  // equals/hashCode based on id
}

我自己尚未测试确切的方法,因此我不确定在持续前和持续后事件中更改集合的工作方式,但想法是:

  • 从基于哈希的集合中临时删除对象
  • 坚持下去
  • 将对象重新添加到基于哈希的集合中

解决此问题的另一种方法是在更新/持久化之后简单地重建所有基于哈希的模型。

最后,取决于您。我大部分时间亲自使用基于序列的方法,并且仅在需要向外部系统公开标识符时才使用UUID方法。


0

使用instanceofJava 14中的新样式,您可以equals在一行中实现。

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

@Override
public int hashCode() {
    return 31;
}

-1

如果UUID是很多人的答案,为什么我们不只使用业务层的工厂方法来创建实体并在创建时分配主键?

例如:

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

这样,我们将从持久性提供程序获取实体的默认主键,而我们的hashCode()和equals()函数可以依靠该主键。

我们还可以声明Car的构造函数受保护,然后在我们的业务方法中使用反射来访问它们。这样,开发人员将不会通过新方法实例化Car,而是通过工厂方法实例化。

怎么样


如果您愿意在执行数据库查找时同时产生性能指标的性能受到影响,那么这种方法将非常有用。
Michael Wiles 2012年

1
单元测试车呢?在这种情况下,您需要数据库连接进行测试?此外,您的域对象不应依赖于持久性。
jhegedus 2014年

-1

我自己尝试回答这个问题,直到找到这篇文章,尤其是DREW为止,我对找到的解决方案都不满意。我喜欢他懒惰地创建UUID并以最佳方式存储它的方式。

但是我想增加更大的灵活性,即仅在首次持久化具有每种解决方案优点的实体之前访问hashCode()/ equals()时才懒惰地创建UUID:

  • equals()表示“对象引用相同的逻辑实体”
  • 尽可能使用数据库ID,因为为什么我要做两次工作(性能方面的关注)
  • 防止在尚未持久化的实体上访问hashCode()/ equals()时出现问题,并在确实持久化后保持相同的行为

我真的很喜欢下面的混合解决方案反馈

public class MyEntity { 

    @Id()
    @Column(name = "ID", length = 20, nullable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Transient private UUID uuid = null;

    @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
    private Long uuidMostSignificantBits = null;
    @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
    private Long uuidLeastSignificantBits = null;

    @Override
    public final int hashCode() {
        return this.getUuid().hashCode();
    }

    @Override
    public final boolean equals(Object toBeCompared) {
        if(this == toBeCompared) {
            return true;
        }
        if(toBeCompared == null) {
            return false;
        }
        if(!this.getClass().isInstance(toBeCompared)) {
            return false;
        }
        return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
    }

    public final UUID getUuid() {
        // UUID already accessed on this physical object
        if(this.uuid != null) {
            return this.uuid;
        }
        // UUID one day generated on this entity before it was persisted
        if(this.uuidMostSignificantBits != null) {
            this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
        // UUID never generated on this entity before it was persisted
        } else if(this.getId() != null) {
            this.uuid = new UUID(this.getId(), this.getId());
        // UUID never accessed on this not yet persisted entity
        } else {
            this.setUuid(UUID.randomUUID());
        }
        return this.uuid; 
    }

    private void setUuid(UUID uuid) {
        if(uuid == null) {
            return;
        }
        // For the one hypothetical case where generated UUID could colude with UUID build from IDs
        if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
            throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
        }
        this.uuidMostSignificantBits = uuid.getMostSignificantBits();
        this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
        this.uuid = uuid;
    }

“我被坚持之前在此实体上产生的UUID一天”是什么意思?你能给这个例子举个例子吗?
jhegedus 2014年

你可以使用分配的GenerationType吗?为什么需要身份生成类型?与分配相比有什么优势吗?
jhegedus 2014年

如果您1)创建一个新的MyEntity,2)将其放入列表中,3)然后将其保存到数据库中,然后4)从DB加载该实体,5)尝试查看加载的实例是否在列表中,会发生什么? 。我的猜测是,即使应该也不会。
jhegedus 2014年

感谢您的第一句话,这表明我不太清楚。首先,“我坚持之前在这个实体上产生的一天的UUID”是一个错字……“ IT坚持之前”应该改为阅读。对于其他评论,我将尽快编辑我的帖子,以尝试更好地解释我的解决方案。
user2083808 2014年

-1

实际上,似乎最常使用选项2(主键)。自然和IMMUTABLE业务密钥很少涉及,创建和支持合成密钥对于解决无法解决的情况而言过于繁重。看一下spring-data-jpa AbstractPersistable实现(唯一的事情:供Hibernate实现使用Hibernate.getClass)。

public boolean equals(Object obj) {
    if (null == obj) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (!getClass().equals(ClassUtils.getUserClass(obj))) {
        return false;
    }
    AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null == this.getId() ? false : this.getId().equals(that.getId());
}

@Override
public int hashCode() {
    int hashCode = 17;
    hashCode += null == getId() ? 0 : getId().hashCode() * 31;
    return hashCode;
}

只要知道在HashSet / HashMap中操作新对象即可。相反,选项1(其余Object实现)仅在之后中断merge,这是非常常见的情况。

如果您没有业务密钥,并且有REAL需求来操纵哈希结构中的新实体,hashCode则将其覆盖为常量,如下所示,建议使用Vlad Mihalcea。


-2

以下是Scala 的简单(经过测试)解决方案。

  • 请注意,此解决方案不适合问题中给出的3个类别。

  • 我所有的实体都是UUIDEntity的子类,因此我遵循“不要重复自己”(DRY)原则。

  • 如果需要,可以使UUID生成更加精确(通过使用更多的伪随机数)。

Scala代码:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.