JPA的hashCode()/ equals()两难

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

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

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

  • 不要重写它们; 依赖于Object.equals()Object.hashCode()
  • hashCode() / equals()工作
  • 无法识别相同的对象,动态代理的问题
  • 没有问题与分离的实体
  • 根据主键覆盖它们
  • hashCode() / equals()被破坏
  • 正确的身份(对于所有管理实体)
  • 与分离的实体有关的问题
  • 根据Business-Id覆盖它们(非主键字段;外键怎么办?)
  • hashCode() / equals()被破坏
  • 正确的身份(对于所有管理实体)
  • 没有问题与分离的实体
  • 我的问题是:

  • 我错过了一个选项和/或亲/点?
  • 你选择了什么选项,为什么?


  • 更新1:

    通过“ hashCode() / equals()被破坏”,我的意思是连续的hashCode()调用可能会返回不同的值,这是(正确实现时)不会在Object API文档意义上破坏,但是在尝试时会导致问题从MapSet或其他基于散列的Collection检索已更改的实体。 因此,JPA实现(至少EclipseLink)在某些情况下无法正常工作。

    更新2:

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


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

    文章的结论如下:

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


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

    为了总结所有这些东西,下面列出了使用不同方法处理equals / hashCode的工作或不工作:

    编辑

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

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

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

  • 仅用于持久层(以便持久性提供程序和数据库可以找出对象之间的关系)。
  • 对于我们的应用程序需求(特别是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()方法的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,但是您可能会发现两个构造函数更适合您。

    链接地址: http://www.djcxy.com/p/36979.html

    上一篇: The JPA hashCode() / equals() dilemma

    下一篇: Order By