JPA hashCode()/ equals()两难

MRa*_*ser 296 java identity hibernate jpa eclipselink

这里有一些 关于JPA实体的讨论,以及哪些hashCode()/ equals()实现应该用于JPA实体类.大多数(如果不是全部)它们依赖于Hibernate,但我想讨论它们JPA实现中性(顺便说一下,我使用的是EclipseLink).

所有可能的实现都有各自的优点缺点:

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

据我所知,有三种选择:

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

我的问题是:

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



更新1:

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

更新2:

感谢您的回答 - 其中大多数都具有卓越的品质.
不幸的是,我仍然不确定哪种方法对于现实应用程序最好,或者如何确定应用程序的最佳方法.所以,我会保持这个问题的开放性,希望能有更多的讨论和/或意见.

Sti*_*ens 116

阅读这篇关于这个主题的非常好的文章:不要让Hibernate窃取你的身份.

文章的结论是这样的:

当对象持久化到数据库时,对象标识很难正确实现.但是,问题完全源于允许对象在保存之前没有id存在.我们可以通过从对象关系映射框架(如Hibernate)中分配对象ID来解决这些问题.相反,只要实例化对象,就可以分配对象ID.这使对象标识简单且无错误,并减少了域模型中所需的代码量.

  • 不,这不是一篇好文章.这是一篇关于这个主题的**非常棒的文章,每个JPA程序员都需要阅读!+1! (20认同)
  • 是的我使用相同的解决方案.不让DB生成ID也具有其他优点,例如能够创建对象并且在持久化之前已经创建了引用它的其他对象.这可以消除客户端 - 服务器应用程序中的延迟和多个请求/响应周期.如果您需要这种解决方案的灵感,请查看我的项目:[suid.js](http://download.github.io/suid/)和[suid-server-java](http://download.github. IO/SUID-服务器的Java /).基本上`suid.js`从`suid-server-java`中获取ID块,然后你可以获取并使用客户端. (2认同)
  • 这简直太疯狂了。我是刚进入休眠状态的新手,正在编写单元测试,发现修改后无法从集合中删除对象,得出的结论是这是由于哈希码更改而引起的,但无法理解解决。文章简单华丽! (2认同)
  • 这是一篇很棒的文章。然而,对于第一次看到该链接的人来说,我建议这对于大多数应用程序来说可能有点过大了。此页面上列出的其他 3 个选项应该或多或少以多种方式解决该问题。 (2认同)
  • Hibernate/JPA是否使用实体的equals和hashcode方法来检查记录是否已存在于数据库中? (2认同)

nan*_*nda 64

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

总结所有这些内容,这里列出了处理equals/hashCode的不同方法将起作用或不起作用的列表: 在此输入图像描述

编辑:

解释为什么这对我有用:

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

  • @MRalwasser:哈希码只能在业务ID发生变化时更改,重点是业务ID*不会发生变化.因此哈希码不会改变,这与散列集合完美配合. (3认同)
  • 但是该表缺少第五行“与列表/集合一起使用”(如果您考虑从 OneToMany 映射中删除属于集合的一部分的实体),在最后两个选项中会回答“否”,因为它的 hashCode( ) 违反合同的变更。 (2认同)
  • @MRalwasser:我认为您的意思是正确的,只是违反了 equals/hashCode() 合同本身。但是可变的 equals/hashCode 确实会在 *Set* 合同中产生问题。 (2认同)

And*_*кин 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
}
Run Code Online (Sandbox Code Playgroud)

编辑:澄清我对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
Run Code Online (Sandbox Code Playgroud)

当我运行我的测试并看到日志输出我解决了问题:

User user = new User();
user.setUuid(UUID.randomUUID());
Run Code Online (Sandbox Code Playgroud)

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

@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.
}
Run Code Online (Sandbox Code Playgroud)

所以我的例子看起来像这样:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output
Run Code Online (Sandbox Code Playgroud)

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

  • 使用JVM相等的默认`hashCode` /`equals`方法和持久性相等的`id`有什么不同?这对我来说根本没有意义. (4认同)
  • -1 - 我没有看到任何理由有两个ID,所以有两种身份.这似乎完全没有意义,可能对我有害. (4认同)
  • 我相信,这是一个正确而好的解决方案.它也可能具有一点性能优势,因为整数通常在数据库索引中比uuids表现更好.但除此之外,您可能会消除当前的整数id属性,并将其替换为(应用程序分配的)uuid? (2认同)
  • 它适用于有多个实体对象指向数据库中的同一行的情况.在这种情况下,`Object`的`equals()`将返回`false`.基于UUID的`equals()`返回`true`. (2认同)

Chr*_*her 29

如果你想用equals()/hashCode()你的集合,在同一个实体只能在那里一次,那么只有一个选项:选项2.那是因为按定义实体的主键永远不会改变(如果有人确实更新了)它,它不再是同一个实体了)

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

现在,理论上也可以使用选项3实现这一点,即使所谓的"业务键"具有可以改变的令人讨厌的缺点:"所有你需要做的就是从集合中删除已插入的实体( s),并重新插入它们." 这是事实 - 但它也意味着,在分布式系统中,您必须确保在数据插入的任何地方都完成了(并且您必须确保执行更新) ,在其他事情发生之前).您需要一个复杂的更新机制,特别是如果某些远程系统当前无法访问...

如果集合中的所有对象来自同一个Hibernate会话,则只能使用选项1.Hibernate文档在第13.1.3节中非常清楚.考虑对象身份:

在Session中,应用程序可以安全地使用==来比较对象.

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

它继续支持备选方案3:

有一点需要注意:永远不要使用数据库标识符来实现相等性.使用业务键,该键是唯一的,通常不可变的属性的组合.如果瞬态对象是持久的,则数据库标识符将更改.如果瞬态实例(通常与分离的实例一起)保存在Set中,则更改哈希码会破坏Set的约定.

如果你这是真的

  • 无法提前分配id(例如,通过使用UUID)
  • 然而你绝对想要在瞬态处理你的物体.

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

然后它提到了相对稳定性的必要性:

业务键的属性不必像数据库主键一样稳定; 只要对象在同一个Set中,您就必须保证稳定性.

这是对的.我看到的实际问题是:如果你不能保证绝对的稳定性,只要对象在同一个Set中,你怎么能保证稳定性.我可以想象一些特殊情况(比如仅使用集合进行对话然后将其丢弃),但我会质疑这种情况的一般实用性.


精简版:

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

  • @William:**实体**的主键不会改变.映射的**对象**的id属性可能会更改.正如您所解释的那样,这种情况会发生,特别是当*transient*对象*持久*时.请仔细阅读我的答案部分,其中我说的是equals/hashCode方法:"在设置主键之前,不得使用这些方法." (2认同)

lwe*_*ler 28

我个人已经在不同的项目中使用了所有这三种状态.我必须说选项1在我看来是现实生活中最实用的应用程序.使得体验破坏hashCode()/ equals()符合性会导致许多疯狂的错误,因为在将实体添加到集合后,每次都会出现平等变化结果发生变化的情况.

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


A)的hashCode /基于一组等于一成不变,不为空,分配构造函数,字段

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

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

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


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

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

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

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


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

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

( - )UUID生成的开销

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

  • 我也是 **选项 1** 和 **方法 C** 的粉丝。在绝对需要之前什么都不做,这是更敏捷的方法。 (2认同)
  • 选项(b)为+1.恕我直言,如果一个实体有自然的业务ID,那么它也应该是它的数据库主键.这是简单,直接,良好的数据库设计.如果它没有这样的ID,则需要代理密钥.如果在创建对象时设置它,那么其他一切都很简单.当人们不使用自然键时,*和*在他们遇到麻烦的早期就不会产生代理键.至于实施的复杂性 - 是的,有一些.但实际上并不是很多,而且可以通过一种非常通用的方式来解决所有实体的问题. (2认同)

Vla*_*cea 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
    }
    
    Run Code Online (Sandbox Code Playgroud)

  • 如果您有一个不可变的业务密钥,则 hashCode 可以使用它,并且它将从多个存储桶中受益,因此如果您有一个存储桶,那么它值得使用。否则,只需使用我的文章中所解释的实体标识符。 (3认同)
  • 如有疑问,请访问 Vlad Mihalcea Dot Com (2认同)

jby*_*ler 10

虽然使用业务键(选项3)是最常推荐的方法(Hibernate社区wiki,"Java Persistence with Hibernate",第398页),这是我们最常使用的,但是有一个Hibernate错误可以解决这个问题.套装:HHH-3799.在这种情况下,Hibernate可以在其字段初始化之前向集合添加实体.我不确定为什么这个bug没有得到更多关注,因为它确实使推荐的业务密钥方法成为问题.

我认为问题的核心是equals和hashCode应该基于不可变状态(引用Odersky等人),并且具有Hibernate管理主键的Hibernate实体没有这样的不可变状态.当瞬态对象变得持久时,主键由Hibernate修改.当Hibernate在初始化过程中对对象进行水合时,业务键也会被修改.

这只留下选项1,继承基于对象标识的java.lang.Object实现,或者使用James Brundege在"不要让Hibernate窃取你的身份"中提出的应用程序管理的主键(Stijn Geukens的答案已经引用)和Lance Arlaus在"对象生成:更好的Hibernate集成方法"中的作用.

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


Mar*_*son 7

Jakarta Persistence 3.0,第 4.12 节写道:

\n
\n

相同抽象模式类型的两个实体当且仅当它们具有相同的主键值时才相等。

\n
\n

我认为 Java 代码没有理由表现得不同。

\n

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

\n

警告:还要记录爆炸行为。文档本身很重要,但它也有望阻止初级开发人员在未来对您的代码做一些愚蠢的事情(他们倾向于抑制 NullPointerException 发生的地方,而他们最不想看到的就是副作用,哈哈) 。

\n

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

\n

尽管我个人getClass()即使在实现非实体类时也总是使用(类型状态,因此即使子类为空或仅包含行为,子类也会添加状态),但只有当该类是Finalinstanceof时才可以。但实体类不能是最终的(\xc2\xa72.1),所以我们真的没有选择。

\n

有些人可能不喜欢getClass(),因为持久性提供者的代理包装了对象。这在过去可能是一个问题,但实际上不应该是。一个提供商不会为不同的实体返回不同的代理类,好吧,我想说这不是一个非常聪明的提供商哈哈。一般来说,在问题出现之前我们不应该解决问题。而且,Hibernate 自己的文档似乎根本不值得一提。事实上,他们getClass()在自己的示例中优雅地使用了(请参阅此)。

\n

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

\n

在我看来,instanceof在非最终 Java 类中根本没有地位。对于持久性实体尤其如此。

\n


Dre*_*rew 5

我同意安德鲁的回答.我们做同样的事情在我们的应用程序,但它不是用来存放的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();
    }

}
Run Code Online (Sandbox Code Playgroud)