这里有关于JPA实体的some discussions,其中 hashCode()
/ equals()
实现应该用于JPA实体类 . 大多数(如果不是全部)它们依赖于Hibernate,但我想讨论它们JPA实现中性(顺便说一下,我使用的是EclipseLink) .
所有可能的实现都有自己的 advantages 和 disadvantages 关于:
-
hashCode()/equals() Contract conformity (不变性)
List
/Set
操作 -
是否可以检测 identical 对象(例如来自不同会话,来自延迟加载的数据结构的动态代理)
-
实体是否在 detached (or non-persisted) state 中正确运行
据我所知,有 three options :
-
不要覆盖它们;依靠
Object.equals()
和Object.hashCode()
-
hashCode()
/equals()
工作 -
无法识别相同的对象,动态代理的问题
-
分离实体没有问题
-
根据 primary key 覆盖它们
-
hashCode()
/equals()
坏了 -
正确的身份(适用于所有托管实体)
-
分离实体的问题
-
根据 Business-Id (非主键字段;外键怎么办?)覆盖它们 .
-
hashCode()
/equals()
被打破了 -
正确的身份(适用于所有托管实体)
-
分离实体没有问题
My questions are:
-
我错过了选项和/或赞成/赞成点吗?
-
您选择了哪个选项?为什么?
UPDATE 1:
通过“ hashCode()
/ equals()
已被破坏”,我的意思是连续的 hashCode()
调用可能会返回不同的值,这些值(正确实现时)在 Object
API文档意义上没有被破坏,但在尝试从中检索已更改的实体时会导致问题a Map
, Set
或其他基于散列的 Collection
. 因此,在某些情况下,JPA实现(至少EclipseLink)将无法正常工作 .
UPDATE 2:
谢谢你的答案 - 大多数都有非凡的品质 .
不幸的是,我仍然不确定哪种方法对于现实应用程序最好,或者如何确定应用程序的最佳方法 . 所以,我会保持这个问题的开放性,希望能有更多的讨论和/或意见 .
19 回答
阅读这篇非常好的文章:Don't Let Hibernate Steal Your Identity .
文章的结论如下:
我总是重写equals / hashcode并根据业务ID实现它 . 对我来说似乎是最合理的解决方案 . 请参阅以下link .
EDIT :
解释为什么这对我有用:
我通常不在我的JPA应用程序中使用基于散列的集合(HashMap / HashSet) . 如果必须,我更喜欢创建UniqueList解决方案 .
我认为在运行时更改业务ID不是任何数据库应用程序的最佳实践 . 在没有其他解决方案的极少数情况下,我会做一些特殊处理,比如删除元素并将其放回基于散列的集合中 .
对于我的模型,我在构造函数上设置了业务ID,并没有为它提供setter . 我让JPA实现更改 field 而不是属性 .
UUID解决方案似乎有点矫枉过正 . 如果你有自然的商业ID,为什么选择UUID?毕竟我会在数据库中设置业务ID的唯一性 . 为什么数据库中的每个表都有 THREE 索引呢?
我们的实体通常有两个ID:
仅用于持久层(以便持久性提供程序和数据库可以找出对象之间的关系) .
是否满足我们的应用需求(特别是
equals()
和hashCode()
)看一看:
EDIT: 澄清了关于
setUuid()
方法调用的观点 . 这是一个典型的场景:当我运行我的测试并看到日志输出我解决了问题:
或者,可以提供单独的构造函数:
所以我的例子看起来像这样:
我使用默认构造函数和setter,但您可能会发现更适合您的双构造函数方法 .
如果你想使用
equals()/hashCode()
作为你的集合,在同一个实体只能在那里一次的意义上,那么只有一个选项:选项2.这是因为按实际定义,一个实体的 primary key 永远不会改变(如果有人确实更新了)它,它不是同一个实体了)您应该从字面上理解:由于
equals()/hashCode()
基于主键,因此在设置主键之前,不得使用这些方法 . 所以你不应该为主键分配一个主键 . (是的,UUID和类似的概念可能有助于尽早分配主键 . )现在,理论上也可以通过选项3实现这一点,即使所谓的“业务键”具有可以改变的令人讨厌的缺点:“所有你需要做的就是从集合中删除已插入的实体( s),并重新插入它们 . “这是事实 - 但它也意味着,在分布式系统中,您必须确保在数据插入的任何地方都完成(并且您必须确保执行更新) ,在其他事情发生之前) . 您需要一个复杂的更新机制,特别是如果某些远程系统当前无法访问...
如果集合中的所有对象来自同一个Hibernate会话,则只能使用选项1 . Hibernate文档在13.1.3. Considering object identity章节中非常清楚:
它继续支持备选方案3:
这是真的, if 你
无法提前分配ID(例如,通过使用UUID)
然而你绝对想要在瞬态处理你的对象 .
否则,您可以自由选择选项2 .
然后它提到了相对稳定性的必要性:
这是对的 . 我看到的实际问题是:如果你不能保证绝对的稳定性,只要对象在同一个Set中,你怎么能保证稳定性 . 我可以想象一些特殊情况(比如只使用集合进行对话,然后扔掉它),但我会质疑这种情况的一般实用性 .
精简版:
选项1只能与单个会话中的对象一起使用 .
如果可以,请使用选项2.(尽早分配PK,因为在分配PK之前不能使用集合中的对象 . )
如果可以保证相对稳定性,可以使用选项3.但请注意这一点 .
我个人已经在不同的项目中使用了所有这三种状态 . 我必须说选项1在我看来是真实应用中最实用的 . 使得体验破坏hashCode()/ equals()符合性会导致许多疯狂的错误,因为在将实体添加到集合之后,每次都会出现平等变化的结果 .
但还有其他选择(也有其优缺点):
a)hashCode / equals基于一组 immutable , not null , constructor assigned ,字段
()所有三个标准都得到保证
( - )字段值必须可用于创建新实例
( - )如果必须更改其中一个,则复杂处理
b)hashCode / equals基于由应用程序(在构造函数中)而不是JPA分配的主键
()所有三个标准都得到保证
( - )你不能利用简单可靠的ID生成状态,如DB序列
( - )如果在分布式环境(客户端/服务器)或应用服务器集群中创建新实体,则会很复杂
c)hashCode / equals基于由实体的构造函数分配的UUID
()所有三个标准都得到保证
( - )UUID生成的开销
( - )可能有一点风险,使用两倍相同的UUID,具体取决于使用的algorythm(可能由DB上的唯一索引检测到)
尽管使用业务键(选项3)是最常推荐的方法(Hibernate community wiki,"Java Persistence with Hibernate"第398页),这是我们最常使用的,但是有一个Hibernate错误可以为急切的集合打破这个:HHH-3799 . 在这种情况下,Hibernate可以在其字段初始化之前向集合添加实体 . 我得到了更多的关注,因为它确实使推荐的业务密钥方法成为问题 .
我觉得问题的核心是equals和hashCode应该基于不可变状态(引用Odersky et al.),而具有Hibernate管理主键的Hibernate实体没有这样的不可变状态 . 当瞬态对象变得持久时,主键由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的最大问题是分离的实例可以't be compared with persistent instances using .equals(). But that'确定; equals和hashCode的 Contract 让开发人员决定每个类的平等意味着什么 . 所以让equals和hashCode继承自Object . 如果需要将分离的实例与持久化实例进行比较,可以为此目的明确创建一个新方法,可能是
boolean sameEntity
或boolean dbEquivalent
或boolean businessEquals
.我过去总是使用选项1,因为我知道这些讨论并认为在我知道正确的事情之前什么也不做 . 这些系统仍然运行成功 .
但是,下次我可以尝试选项2 - 使用数据库生成的Id .
如果未设置id,Hashcode和equals将抛出IllegalStateException .
这将防止涉及未保存实体的细微错误意外出现 .
人们怎么看待这种方法?
如果您有一个business key,那么您应该将其用于
equals
/hashCode
.如果您没有业务密钥,则不应将其保留为默认
Object
equals和hashCode实现,因为在merge
和实体之后这不起作用 .你可以use the entity identifier as suggested in this post . 唯一的问题是您需要使用始终返回相同值的
hashCode
实现,如下所示:我同意安德鲁的回答 . 我们在应用程序中执行相同的操作,但不是将UUID存储为VARCHAR / CHAR,而是将其拆分为两个长值 . 请参阅UUID.getLeastSignificantBits()和UUID.getMostSignificantBits() .
还有一件事要考虑,对UUID.randomUUID()的调用非常慢,所以你可能只想在需要时懒洋洋地生成UUID,例如在持久性或调用equals()/ hashCode()期间
正如其他比我聪明的人已经指出的那样,那里有很多策略 . 虽然大多数应用的设计模式试图破解成功的方式,但似乎是这种情况 . 如果不使用专门的构造函数和工厂方法完全阻碍构造函数调用,它们会限制构造函数访问 . 事实上,使用明确的API总是令人愉快 . 但如果唯一的原因是使equals-和hashcode覆盖与应用程序兼容,那么我想知道这些策略是否符合KISS(Keep It Simple Stupid) .
对我来说,我喜欢通过检查id来覆盖equals和hashcode . 在这些方法中,我要求id不为null并且很好地记录这种行为 . 因此,在将新实体存储到其他地方之前,将成为开发者 Contract . 不遵守本 Contract 的申请将在一分钟内失败(希望如此) .
但请注意:如果您的实体存储在不同的表中,并且您的提供程序使用主键的自动生成策略,那么您将获得跨实体类型的重复主键 . 在这种情况下,还要将运行时类型与对Object#getClass()的调用进行比较,这当然会使两种不同类型被认为是不相等的 . 在大多数情况下,这对我来说很合适 .
这里显然已有非常丰富的答案,但我会告诉你我们做了什么 .
我们什么都不做(即不要覆盖) .
如果我们确实需要equals / hashcode来处理集合,我们使用UUID . 您只需在构造函数中创建UUID . 我们使用http://wiki.fasterxml.com/JugHome作为UUID . UUID是一个更加昂贵的CPU,但与序列化和数据库访问相比便宜 .
业务密钥方法不适合我们 . 我们使用DB生成的 ID ,临时瞬态 tempId 和 override equal()/ hashcode()来解决这个难题 . 所有实体都是实体的后代 . 优点:
DB中没有额外的字段
后代实体没有额外的编码,一种方法适用于所有人
没有性能问题(如UUID),DB Id生成
Hashmaps没问题(不需要记住使用等号等)
新实体的Hashcode即使在持久化后也不会及时更改
缺点:
序列化和反序列化非持久化实体可能存在问题
从DB重新加载后,保存的实体的哈希码可能会发生变化
不持久的对象被认为总是不同的(也许这是对的?)
还有什么?
看看我们的代码:
请根据预定义的类型标识符和ID考虑以下方法 .
JPA的具体假设:
相同"type"的实体和相同的非空ID被认为是相等的
非持久化实体(假设没有ID)永远不会等于其他实体
抽象实体:
具体实例示例:
测试示例:
这里的主要优点:
简单
确保子类提供类型标识
使用代理类预测行为
缺点:
super()
笔记:
使用继承时需要注意 . 例如 .
class A
和class B extends A
的实例相等可能取决于应用程序的具体细节 .理想情况下,使用业务密钥作为ID
期待您的评论 .
这是使用Java和JPA的每个IT系统中的常见问题 . 痛点不仅仅是实现equals()和hashCode(),还会影响组织引用实体的方式以及客户端如何引用同一个实体 . 我已经看到了没有业务关键的痛苦,我写了my own blog以表达我的观点 .
简而言之:使用一个简短的,人类可读的顺序ID,将有意义的前缀作为业务密钥,'s generated without any dependency on any storage other than RAM. Twitter' s Snowflake就是一个很好的例子 .
IMO你有3个实现equals / hashCode的选项
使用应用程序生成的标识,即UUID
基于业务密钥实施它
基于主键实现它
Using an application generated identity is the easiest approach, but comes with a few downsides
使用它作为PK时连接速度较慢,因为128位只是大于32位或64位
"Debugging is harder"因为用自己的眼睛检查一些数据是否正确是非常困难的
如果您可以解决这些问题,请使用此方法 .
为了克服连接问题,可以使用UUID作为自然键和序列值作为主键,但是您可能仍会遇到具有嵌入式ID的组合子实体中的equals / hashCode实现问题,因为您将要基于联接在主键上 . 使用子实体id中的自然键和引用父实体的主键是一个很好的折衷方案 .
IMO这是最干净的方法,因为它可以避免所有缺点,同时为您提供一个值(UUID),您可以与外部系统共享,而不会暴露系统内部 .
Implement it based on a business key if you can expect that from a user is a nice idea, but comes with a few downsides as well
大多数情况下,此业务键将是用户提供的某种代码,而不是多个属性的组合 .
连接速度较慢,因为基于可变长度文本的连接速度很慢 . 如果密钥超过一定长度,某些DBMS甚至可能在创建索引时遇到问题 .
根据我的经验,业务密钥往往会发生变化,这需要对引用它的对象进行级联更新 . 如果外部系统引用它,这是不可能的
IMO您不应该专门使用或使用业务密钥 . 这是一个很好的附加组件,即用户可以快速搜索该业务键,但系统不应该依赖它进行操作 .
Implement it based on the primary key has it's problems, but maybe it's not such a big deal
如果需要将ID公开给外部系统,请使用我建议的UUID方法 . 如果不这样做,您仍然可以使用UUID方法,但您不必这样做 . 在equals / hashCode中使用DBMS生成的id的问题源于在分配id之前可能已将对象添加到基于散列的集合的事实 .
解决这个问题的显而易见的方法是在分配id之前不要将对象添加到基于散列的集合 . 我知道这并不总是可行的,因为您可能在分配ID之前需要重复数据删除 . 要仍然能够使用基于散列的集合,您只需在分配ID后重建集合 .
你可以这样做:
我自己没有测试过确切的方法,所以我不确定在前后持续事件中如何更改集合,但这个想法是:
临时从基于散列的集合中删除对象
坚持下去
将对象重新添加到基于散列的集合
另一种解决方法是在更新/持久化之后简单地重建所有基于散列的模型 .
最后,这取决于你 . 我个人大多数时候都会使用基于序列的方法,只有在需要向外部系统公开标识符时才使用UUID方法 .
如果UUID是许多人的答案,为什么我们不只是使用业务层中的工厂方法来创建实体并在创建时分配主键?
例如:
这样我们就可以从持久性提供程序中获取实体的默认主键,而我们的hashCode()和equals()函数可以依赖它 .
我们还可以声明Car的构造函数受到保护,然后在我们的业务方法中使用反射来访问它们 . 这样开发人员就不会有意图使用new实例化Car,但是通过工厂方法 .
怎么样?
我试着自己回答这个问题,直到我读完这篇文章,特别是DREW之前,我一直对找到的解决方案都不满意 . 我喜欢他懒惰创建UUID的方式并以最佳方式存储它 .
但是我希望增加更多的灵活性,即只有在实体的第一次持久化之前访问hashCode()/ equals()并且具有每个解决方案的优点时,才会创建UUID:
equals()表示"object refers to the same logical entity"
尽可能多地使用数据库ID,因为为什么我要做两次工作(性能问题)
防止在尚未持久化的实体上访问hashCode()/ equals()时出现问题,并在确实持久化之后保持相同的行为
我真的会对下面的混合解决方案提出反馈意见
@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;
@覆盖
public final int hashCode(){
return this.getUuid() . hashCode();
}
@覆盖
public final boolean equals(Object toBeCompared){
if(this == toBeCompared){
返回true;
}
if(toBeCompared == null){
返回虚假;
}
if(!this.getClass() . isInstance(toBeCompared)){
返回虚假;
}
return this.getUuid() . equals(((MyEntity)toBeCompared).getUuid());
}
public final UUID getUuid(){
//已在此物理对象上访问过UUID
if(this.uuid!= null){
返回this.uuid;
}
//在该实体被持久化之前,有一天生成了UUID
if(this.uuidMostSignificantBits!= null){
this.uuid = new UUID(this.uuidMostSignificantBits,this.uuidLeastSignificantBits);
//在持久化之前,UUID从未在此实体上生成
} else if(this.getId()!= null){
this.uuid = new UUID(this.getId(),this.getId());
// UUID从未在此尚未持久化的实体上访问过
} else {
this.setUuid(UUID.randomUUID());
}
返回this.uuid;
}
private void setUuid(UUID uuid){
if(uuid == null){
返回;
}
//对于一个假设的情况,生成的UUID可以使用ID从UUID构建
if(uuid.getMostSignificantBits()== uuid.getLeastSignificantBits()){
抛出新的异常(“UUID:”this.getUuid()“格式仅供内部使用”);
}
this.uuidMostSignificantBits = uuid.getMostSignificantBits();
this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
this.uuid = uuid;
}
在实践中,似乎最常使用选项2(主键) . 自然和IMMUTABLE业务密钥很少,创建和支持合成密钥太重,无法解决可能永远不会发生的情况 . 看看spring-data-jpa AbstractPersistable实现(唯一的一件事:for Hibernate implementation use Hibernate.getClass) .
只知道在HashSet / HashMap中操作新对象 . 相反,选项1(保持
Object
实施)在merge
之后被破坏,这是非常常见的情况 .如果您没有业务密钥并且需要在哈希结构中操作新实体,则将
hashCode
覆盖为常量,如下所示,Vlad Mihalcea被建议 .下面是Scala的 simple (并经过测试)解决方案 .
请注意,此解决方案不适合问题中给出的任何3个类别 .
我的所有实体都是UUIDEntity的子类,因此我遵循不重复自己(DRY)原则 .
如果需要,可以使UUID生成更精确(通过使用更多的伪随机数) .
斯卡拉代码: