JPA的hashCode()/ equals()两难

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

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

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

据我所知,有三种select

  1. 不要超越他们; 依靠Object.equals()Object.hashCode()
    • hashCode() / equals()工作
    • 不能识别相同的对象,dynamic代理的问题
    • 没有问题与分离的实体
  2. 根据主键覆盖它们
    • hashCode() / equals()被破坏
    • 正确的身份(所有被pipe理的实体)
    • 与分离的实体有关的问题
  3. 根据Business-Id (非主键字段;外键?)覆盖它们
    • hashCode() / equals()被破坏
    • 正确的身份(所有被pipe理的实体)
    • 没有问题与分离的实体

我的问题是:

  1. 我错过了一个选项和/或亲/点?
  2. 你select了什么选项,为什么?

更新1:

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

更新2:

谢谢你的回答 – 他们中的大多数都有非凡的品质。
不幸的是,我仍然不确定哪种方法对于实际应用程序是最好的,或者如何确定我的应用程序的最佳方法。 所以,我会保持这个问题的公开,并希望有更多的讨论和/或意见。

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

文章的结论是这样的:

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

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

总结所有这些东西,这里是什么将工作或不会用不同的方式处理equals / hashCode的列表: 在这里输入图像描述

编辑

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

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

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

当我运行我的testing,看到日志输出我解决了这个问题:

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

如果你想为你的集合使用equals()/hashCode() ,那么同一个实体只能在那里存在一次,那么只有一个选项:选项2.这是因为一个实体的主键从来没有改变(如果有人确实更新它,它不再是同一个实体)

你应该这样做:因为你的equals()/hashCode()是基于主键,所以你不能使用这些方法,直到主键被设置。 所以你不应该把实体放在集合中,直到它们被分配一个主键。 (是的,UUID和类似的概念可能有助于早期分配主键。)

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

选项1只能使用,如果你的集合中的所有对象来自同一个Hibernate会话。 Hibernate文档在第13.1.3节中已经很清楚了。 考虑对象身份 :

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

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

它继续争论赞成备选scheme3:

有一个警告:从不使用数据库标识符来实现平等。 使用一个结合了唯一的,通常是不可变的属性的商业密钥。 如果瞬态对象是持久的,数据库标识符将会改变。 如果瞬时实例(通常与分离实例一起)保存在Set中,则更改散列码会中断Set的合约。

这是真的, 如果

  • 不能提前分配id(例如使用UUID)
  • 然而你绝对想在你处于暂态的时候把你的物体放进去。

否则,你可以自由select选项2。

然后它提到了一个相对稳定的需要:

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

这是对的。 我看到的实际问题是:如果不能保证绝对的稳定性,只要物体在同一个集合中,你将如何保证稳定性。 我可以想象一些特殊情况(比如只用于会话然后扔掉),但是我会质疑这个的一般实用性。


简短版本:

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

我个人已经在不同的项目中使用了这三种策略。 我必须说,在我看来,选项1是现实生活中最实用的应用程序。 一个破坏hashCode()/ equals()一致性的经验会导致许多疯狂的错误,因为每当最后一个实体被添加到一个集合之后,等式的结果发生变化的时候你就会结束。

但还有其他select(也有其优点和缺点):


a)hashCode / equals基于一组不可变的 赋值构造函数赋值的字段

(+)所有三个标准是有保证的

( – )字段值必须可用来创build一个新的实例

( – )使处理复杂化,如果你必须改变之一


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

(+)所有三个标准是有保证的

( – )你不能利用像DB序列这样简单可靠的ID生成状态

( – )在分布式环境(客户机/服务器)或应用程序服务器集群中创build新实体时会变得复杂


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

(+)所有三个标准是有保证的

( – )UUID生成开销

( – )可能有一点风险,即使用相同UUID的两倍,具体取决于使用的algorythm(可以通过DB上的唯一索引来检测)

尽pipe使用业务密钥(选项3)是最常用的方法( Hibernate社区wiki ,“Hibernate的Java持久性”,第398页),而这正是我们最常用的方法,这里有一个Hibernate的bug,套: HHH-3799 。 在这种情况下,Hibernate可以在实体初始化之前添加一个实体。 我不确定为什么这个bug没有得到更多的关注,因为它确实使得推荐的业务关键方法成为问题。

我认为问题的核心在于equals和hashCode应该基于不可变状态(参考Odersky等 ),而Hibernate实体和Hibernatepipe理的主键没有这种不可变的状态。 当临时对象变为持久性时,主键由Hibernate修改。 在初始化的过程中,当水合物体时,业务键也被Hibernate修改。

只留下选项1,inheritance基于对象标识的java.lang.Object实现,或者使用James Brundege在“不要让Hibernate窃取您的标识” (已经被Stijn Geukens的答案引用)中build议的应用程序pipe理的主键)和兰斯·阿劳斯(Lance Arlaus)在“对象生成:一种更好的hibernate集成方法”中提到。

选项1的最大问题是分离的实例不能与使用.equals()的持久实例进行比较。 但是没关系 equals和hashCode的合约留给开发者决定每个类的平等意味着什么。 所以让equals和hashCodeinheritance自Object。 如果您需要将分离的实例与持久实例进行比较,则可以为此目的明确创build一个新方法,可能是boolean sameEntityboolean dbEquivalentboolean businessEquals

  1. 如果你有一个商业密钥 ,那么你应该使用equals / hashCode
  2. 如果您没有业务密钥,则不应将其与默认的Object equals和hashCode实现保留在一起,因为在merge和实体之后该操作不起作用。
  3. 您可以使用本文中build议的实体标识符 。 唯一的问题是你需要使用总是返回相同值的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 } 

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

还有一件事要考虑的是,对UUID.randomUUID()的调用是非常慢的,所以你可能只想在需要的时候懒惰的生成UUID,例如在persistence或者调用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(); } } 

像其他人比我更聪明的方式已经指出,有很多的策略在那里。 大多数的应用devise模式试图破解他们的成功之道,似乎是这样。 他们限制构造函数的访问,如果不妨碍构造函数调用完全与专门的构造函数和工厂方法。 事实上,一个清晰的API总是令人愉快的。 但是,如果唯一的原因是使等号和哈希码覆盖与应用程序兼容,那么我想知道这些策略是否符合KISS(Keep It Simple Stupid)。

对于我来说,我喜欢通过检查id来覆盖equals和hashcode。 在这些方法中,我要求id不能为空,并很好地logging这种行为。 因此,它将成为开发者的合同,坚持一个新的实体,然后将其存储在别的地方。 不遵守此合同的应用程序将在一分钟内失败(希望)。

谨慎的话,如果您的实体存储在不同的表中,并且您的提供者使用主键的自动生成策略,那么您将在实体types之间获得重复的主键。 在这种情况下,还可以将运行时types与对Object#getClass()的调用进行比较,这当然会使两种不同的types被认为是相等的。 这对我来说很适合。

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

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

如果我们确实需要equals / hashcode来为集合工作,我们使用UUID。 您只需在构造函数中创buildUUID。 我们使用http://wiki.fasterxml.com/JugHome作为UUID。; UUID是一个稍微昂贵的CPU明智,但比串行和数据库访问便宜。

商业钥匙方法不适合我们。 我们使用DB生成的ID ,暂时的临时tempIdoverride ()/ hashcode()来解决这个困境。 所有实体都是实体的后代。 优点:

  1. 数据库中没有额外的字段
  2. 后代实体没有额外的编码,所有的方法
  3. 性能问题(如使用UUID),数据库ID生成
  4. 没有问题的Hashmaps(不需要记住使用平等&等)
  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(); } } 

I have always used option 1 in the past because I was aware of these discussions and thought it was better to do nothing until I knew the right thing to do. Those systems are all still running successfully.

However, next time I may try option 2 – using the database generated Id.

Hashcode and equals will throw IllegalStateException if the id is not set.

This will prevent subtle errors involving unsaved entities from appearing unexpectedly.

What do people think of this approach?

This is a common problem in every IT system that uses Java and JPA. The pain point extends beyond implementing equals() and hashCode(), it affects how an organization refer to an entity and how its clients refer to the same entity. I've seen enough pain of not having a business key to the point that I wrote my own blog to express my view.

In short: use a short, human readable, sequential ID with meaningful prefixes as business key that's generated without any dependency on any storage other than RAM. Twitter's Snowflake is a very good example.

If UUID is the answer for many people, why don't we just use factory methods from business layer to create the entities and assign primary key at creation time?

例如:

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

this way we would get a default primary key for the entity from the persistence provider, and our hashCode() and equals() functions could rely on that.

We could also declare the Car's constructors protected and then use reflection in our business method to access them. This way developers would not be intent on instantiate Car with new, but through factory method.

How'bout that?

I tried to answer this question myself and was never totally happy with found solutions until i read this post and especially DREW one. I liked the way he lazy created UUID and optimally stored it.

But I wanted to add even more flexibility, ie lazy create UUID ONLY when hashCode()/equals() is accessed before first persistence of the entity with each solution's advantages :

  • equals() means "object refers to the same logical entity"
  • use database ID as much as possible because why would I do the work twice (performance concern)
  • prevent problem while accessing hashCode()/equals() on not yet persisted entity and keep the same behaviour after it is indeed persisted

I would really apreciate feedback on my mixed-solution below

 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; } 

In practice it seems, that Option 2 (Primary key) is most frequently used. Natural and IMMUTABLE business key is seldom thing, creating and supporting synthetic keys are too heavy to solve situations, which are probably never happened. Have a look at spring-data-jpa AbstractPersistable implementation (the only thing: 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; } 

Just aware of manipulating new objects in HashSet/HashMap. In opposite, the Option 1 (remain Object implementation) is broken just after merge , that is very common situation.

If you have no business key and have a REAL needs to manipulate new entity in hash structure, override hashCode to constant, as below Vlad Mihalcea was advised.

Below is a simple (and tested) solution for Scala.

  • Note that this solution does not fit into any of the 3 categories given in the question.

  • All my Entities are subclasses of the UUIDEntity so I follow the don't-repeat-yourself (DRY) principle.

  • If needed the UUID generation can be made more precise (by using more pseudo-random numbers).

Scala Code:

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