JPA hashCode()/ equals()两难

时间:2011-02-17 16:22:13

标签: java hibernate jpa identity eclipselink

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

所有可能的实施都有自己的优势缺点关于:

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

据我所知,有三个选项

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

    1. 我是否错过了选项和/或赞成/赞成点?
    2. 您选择了什么选项?为什么?


    3. 更新1:

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

      更新2:

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

20 个答案:

答案 0 :(得分:107)

阅读这篇关于这个主题的非常好的文章:Don't Let Hibernate Steal Your Identity

文章的结论是这样的:

  

在使用时,对象标识很难正确实现   对象被持久化到数据库。然而,问题仍然存在   完全来自允许对象存在之前没有id   保存。我们可以通过承担责任来解决这些问题   从对象关系映射框架中分配对象ID   比如Hibernate。相反,可以尽快分配对象ID   对象被实例化。这使对象标识变得简单   没有错误,并减少了域模型中所需的代码量。

答案 1 :(得分:62)

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

  

总结所有这些内容,下面列出了处理equals / hashCode的不同方法将起作用或不起作用的列表:enter image description here

修改

解释为什么这对我有用:

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

答案 2 :(得分:32)

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

  1. 仅用于持久层(以便持久性提供程序和数据库可以找出对象之间的关系)。
  2. 是否满足我们的应用需求(特别是equals()hashCode()
  3. 看看:

    @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,但您可能会发现更适合您的双构造函数。

答案 3 :(得分:28)

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

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

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

如果集合中的所有对象来自同一个Hibernate会话,则只能使用选项1。 Hibernate文档在第13.1.3. Considering object identity章中非常清楚:

  

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

     

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

它继续支持备选方案3:

  

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

这是真的,如果

  • 无法提前分配ID(例如,通过使用UUID)
  • 然而你绝对想要在瞬态处置你的物品。

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

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

  

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

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


简短版本:

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

答案 4 :(得分:27)

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

但是还有其他选择(也有它们的优点和缺点):


a)hashCode / equals基于一组不可变非空指定构造函数,字段

(+)保证所有三个标准

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

( - )如果必须更改其中一个

,则会使处理变得复杂

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

分配的主键

(+)保证所有三个标准

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

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

( - )会很复杂


c)hashCode / equals基于实体

的构造函数分配的UUID

(+)保证所有三个标准

( - )UUID生成的开销

( - )可能有一点风险,使用两倍相同的UUID,具体取决于使用的算法(可能通过DB上的唯一索引检测到)

答案 5 :(得分:12)

  1. 如果您有business key,则应将其用于equals / hashCode
  2. 如果您没有业务密钥,则不应将其保留为默认的Object equals和hashCode实现,因为在merge和实体之后这不起作用。
  3. 您可以use the entity identifier as suggested in this post。唯一的问题是您需要使用始终返回相同值的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
    }
    

答案 6 :(得分:10)

虽然使用业务键(选项3)是最常推荐的方法(Hibernate community wiki,“Java Persistence with Hibernate”,第398页),这是我们最常用的,有一个Hibernate错误打破了这适用于渴望获取的集:HHH-3799。在这种情况下,Hibernate可以在其字段初始化之前向集合添加实体。我不确定为什么这个bug没有得到更多的关注,因为它确实使推荐的业务密钥方法成为问题。

我认为问题的核心是equals和hashCode应该基于不可变状态(引用Odersky et al.),并且具有Hibernate管理主键的Hibernate实体具有 no 等不可改变的国家。当瞬态对象变得持久时,主键由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 :(得分: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();
    }

}

答案 8 :(得分:3)

正如其他比我聪明的人已经指出的那样,那里有很多策略。虽然大多数应用的设计模式试图破解成功的方式,但似乎是这种情况。如果不使用专门的构造函数和工厂方法完全阻碍构造函数调用,它们会限制构造函数访问。事实上,使用明确的API总是令人愉快。但如果唯一的原因是使equals-和hashcode覆盖与应用程序兼容,那么我想知道这些策略是否符合KISS(Keep It Simple Stupid)。

对我来说,我喜欢通过检查id来覆盖equals和hashcode。在这些方法中,我要求id不为null并很好地记录此行为。因此,在将新实体存储到其他地方之前,将成为开发者合同。不遵守本合同的申请将在一分钟内失败(希望如此)。

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

答案 9 :(得分:2)

这里显然已有非常丰富的答案,但我会告诉你我们做了什么。

我们什么都不做(即不要覆盖)。

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

答案 10 :(得分:1)

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

  1. DB中没有额外的字段
  2. 后代实体没有额外的编码,一种方法适用于所有
  3. 没有性能问题(如UUID),DB Id生成
  4. Hashmaps没问题(不需要记住使用等号等)。
  5. 即使在坚持
  6. 之后,新实体的Hashcode也没有及时更改

    缺点:

    1. 序列化和反序列化非持久化实体可能存在问题
    2. 从DB
    3. 重新加载后,保存的实体的哈希码可能会发生变化
    4. 不持久的对象被认为总是不同(也许这是对的?)
    5. 还有什么?
    6. 查看我们的代码:

      @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();
          }
      }
      

答案 11 :(得分:1)

请根据预定义的类型标识符和ID考虑以下方法。

JPA的具体假设:

  • 相同的实体&#34;类型&#34;并且相同的非空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

期待您的意见。

答案 12 :(得分:0)

我过去总是使用选项1,因为我知道这些讨论并且认为在我知道正确的事情之前什么也不做。这些系统仍然运行成功。

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

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

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

人们怎么看待这种方法?

答案 13 :(得分:0)

这是使用Java和JPA的每个IT系统中的常见问题。痛点不仅仅是实现equals()和hashCode(),还会影响组织引用实体的方式以及客户端如何引用同一个实体。我已经看到了没有业务关键的痛苦,我写了my own blog来表达我的观点。

简而言之:使用一个简短的,人类可读的顺序ID,将有意义的前缀作为生成的业务密钥,而不依赖于除RAM之外的任何存储。 Twitter的Snowflake就是一个非常好的例子。

答案 14 :(得分:0)

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

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

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

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

如果您可以处理这些缺点,只需使用此方法即可。

要克服连接问题,可以使用UUID作为自然键,并使用序列值作为主键,但是您可能仍会遇到具有嵌入式ID的合成子实体中的equals / hashCode实现问题,因为您想要根据主键加入。在子实体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),而无需暴露系统内部。

如果可以期望从用户那里得到一个不错的主意,则可以根据业务密钥实施该操作,但是也存在一些缺点

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

  • 连接速度较慢,因为基于可变长度文本的连接速度很慢。如果键超过一定长度,某些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方法。

答案 15 :(得分:-1)

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

例如:

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

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

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

怎么样?

答案 16 :(得分:-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;
    }

答案 17 :(得分:-1)

在实践中,似乎最常使用选项2(主键)。 自然和IMMUTABLE业务密钥很少,创建和支持合成密钥太重,无法解决可能永远不会发生的情况。 看一下spring-data-jpa AbstractPersistable实现(唯一的事情:for Hibernate implementation use 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之后被破坏,这是非常常见的情况。

如果您没有业务密钥并且需要在哈希结构中操作新实体,请将hashCode覆盖为常量,如下面的Vlad Mihalcea所建议。

答案 18 :(得分:-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()
}

答案 19 :(得分:-2)

使用Java 14中的instanceof新样式,您可以在一行中实现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;
}