这里有 some discussions 关于 JPA 实体以及应该为 JPA 实体类使用哪个 hashCode()
/equals()
实现。它们中的大多数(如果不是全部)都依赖于 Hibernate,但我想讨论它们 JPA-implementation-neutrally(顺便说一下,我正在使用 EclipseLink)。
所有可能的实现在以下方面都有自己的优点和缺点:
列表/集合操作的 hashCode()/equals() 合约一致性(不变性)
是否可以检测到相同的对象(例如来自不同会话、来自延迟加载的数据结构的动态代理)
实体在分离(或非持久)状态下的行为是否正确
据我所知,有三个选项:
不要覆盖它们;依赖 Object.equals() 和 Object.hashCode() hashCode()/equals() 工作不能识别相同的对象,动态代理的问题 分离实体没有问题 覆盖它们,基于主键 hashCode()/equals()被破坏的正确身份(对于所有托管实体) 分离实体的问题 根据业务 ID 覆盖它们(非主键字段;外键呢?) hashCode()/equals() 正确身份被破坏(对于所有托管实体)分离实体没有问题
我的问题是:
我错过了一个选项和/或赞成/反对点吗?你选择了什么选项,为什么?
更新 1:
“hashCode()
/equals()
已损坏”是指连续的 hashCode()
调用可能会返回不同的值,这在 Object
API 文档的意义上(正确实施时)不会损坏,但会导致问题尝试从 Map
、Set
或其他基于散列的 Collection
检索更改的实体时。因此,JPA 实现(至少是 EclipseLink)在某些情况下将无法正常工作。
更新 2:
谢谢你的回答——他们中的大多数都具有非凡的品质。不幸的是,我仍然不确定哪种方法最适合实际应用程序,或者如何确定最适合我的应用程序的方法。所以,我会保持这个问题的开放性,并希望有更多的讨论和/或意见。
hashcode()
应该返回相同的值,除非 equals()
实现中使用的任何字段发生变化。换句话说,如果您的类中有三个字段,并且您的 equals()
方法仅使用其中两个来确定实例的相等性,那么如果您更改其中一个字段的值,您可以预期 hashcode()
返回值会发生变化 -当您认为此对象实例不再“等于”旧实例表示的值时,这是有道理的。
阅读有关该主题的这篇非常好的文章:Don't Let Hibernate Steal Your Identity。
文章的结论是这样的:
当对象被持久化到数据库时,对象标识很难正确实现。然而,问题完全源于允许对象在保存之前没有 id 存在。我们可以通过从对象关系映射框架(如 Hibernate)中分配对象 ID 来解决这些问题。相反,只要实例化对象,就可以分配对象 ID。这使得对象标识简单且无错误,并减少了域模型中所需的代码量。
我总是覆盖 equals/hashcode 并根据业务 ID 实现它。对我来说似乎是最合理的解决方案。请参阅以下 link。
总结所有这些东西,这里列出了处理 equals/hashCode 的不同方法可以工作或不工作的列表:
编辑:
解释为什么这对我有用:
我通常不在我的 JPA 应用程序中使用基于散列的集合 (HashMap/HashSet)。如果必须,我更喜欢创建 UniqueList 解决方案。我认为在运行时更改业务 ID 不是任何数据库应用程序的最佳实践。在没有其他解决方案的极少数情况下,我会进行特殊处理,例如删除元素并将其放回基于散列的集合中。对于我的模型,我在构造函数上设置了业务 ID,并且没有为其提供设置器。我让 JPA 实现更改字段而不是属性。 UUID 解决方案似乎有点矫枉过正。如果您有自然业务 ID,为什么要使用 UUID?毕竟我会在数据库中设置业务 ID 的唯一性。那么为什么要为数据库中的每个表设置三个索引呢?
我个人已经在不同的项目中使用了所有这三种策略。我必须说选项 1 在我看来是现实生活中最实用的应用程序。根据我的经验,打破 hashCode()/equals() 一致性会导致许多疯狂的错误,因为您每次都会遇到在将实体添加到集合后相等结果发生变化的情况。
但是还有其他选择(也有它们的优缺点):
a) hashCode/equals 基于一组不可变的,非空的,构造函数分配的,字段
(+) 保证所有三个标准
(-) 字段值必须可用于创建新实例
(-) 如果您必须更改其中之一,则处理复杂化
b) hashCode/equals 基于应用程序(在构造函数中)而不是 JPA 分配的主键
(+) 保证所有三个标准
(-) 您不能利用简单可靠的 ID 生成策略,如 DB 序列
(-) 如果在分布式环境(客户端/服务器)或应用服务器集群中创建新实体,则复杂
c) hashCode/equals 基于由实体的构造函数分配的 UUID
(+) 保证所有三个标准
(-) UUID 生成的开销
(-) 使用两次相同的 UUID 可能会有一点风险,具体取决于所使用的算法(可能由 DB 上的唯一索引检测到)
Collection
实现equals方法。
如果您想为您的 Set 使用 equals()/hashCode()
,即 同一个实体 只能在其中出现一次,那么只有一个选项:选项 2。那是因为 primary根据定义,实体的键永远不会改变(如果有人确实更新了它,它就不再是同一个实体了)
您应该从字面上理解:由于您的 equals()/hashCode()
基于主键,因此在设置主键之前,您不得使用这些方法。所以你不应该把实体放在集合中,直到它们被分配一个主键。 (是的,UUID 和类似概念可能有助于尽早分配主键。)
现在,理论上也可以使用选项 3 来实现这一点,即使所谓的“业务密钥”具有它们可以更改的讨厌的缺点:“您所要做的就是从集合中删除已经插入的实体( s),然后重新插入它们。”这是真的 - 但这也意味着,在分布式系统中,您必须确保在插入数据的任何地方都执行此操作(并且您必须确保执行更新, 在其他事情发生之前)。您将需要一个复杂的更新机制,特别是如果当前无法访问某些远程系统...
如果您的集合中的所有对象都来自同一个 Hibernate 会话,则只能使用选项 1。 Hibernate 文档在第 13.1.3. Considering object identity 章中非常清楚地说明了这一点:
在 Session 中,应用程序可以安全地使用 == 来比较对象。但是,在 Session 之外使用 == 的应用程序可能会产生意想不到的结果。即使在一些意想不到的地方也可能发生这种情况。例如,如果您将两个分离的实例放入同一个 Set 中,则它们可能具有相同的数据库标识(即,它们代表同一行)。但是,根据定义,对于处于分离状态的实例,不能保证 JVM 身份。开发人员必须覆盖持久类中的 equals() 和 hashCode() 方法并实现他们自己的对象相等概念。
它继续支持选项 3:
有一个警告:永远不要使用数据库标识符来实现相等。使用由独特的(通常是不可变的)属性组合而成的业务密钥。如果一个瞬态对象被持久化,数据库标识符将会改变。如果瞬态实例(通常与分离的实例一起)保存在 Set 中,则更改哈希码会破坏 Set 的合同。
这是真的,如果你
不能提前分配 id(例如通过使用 UUID)
但是您绝对希望在对象处于瞬态时将它们放入集合中。
否则,您可以自由选择选项 2。
然后它提到了相对稳定性的需要:
业务键的属性不必像数据库主键那样稳定;只要对象在同一个集合中,您只需要保证稳定性。
这是对的。我看到的实际问题是:如果你不能保证绝对的稳定性,那么“只要对象在同一个 Set 中”,你怎么能保证稳定性。我可以想象一些特殊情况(例如仅将集合用于对话然后将其丢弃),但我会质疑这种做法的一般实用性。
精简版:
选项 1 只能用于单个会话中的对象。
如果可以,请使用选项 2。(尽早分配 PK,因为在分配 PK 之前您不能使用集合中的对象。)
如果你能保证相对稳定,你可以使用选项3。但要小心。
我们的实体中通常有两个 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()
方法的观点。这是一个典型的场景:
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,但您可能会发现两个构造函数方法更适合您。
hashCode
/equals
方法实现 JVM 相等和使用 id
实现持久性相等有何不同?这对我来说根本没有意义。
Object
的 equals()
将返回 false
。基于 UUID 的 equals()
返回 true
。
如果您有业务密钥,则应将其用于 equals 和 hashCode。如果您没有业务密钥,则不应将其保留为默认的 Object equals 和 hashCode 实现,因为在合并和实体后这不起作用。只有当 hashCode 实现返回一个常量值时,才能在 equals 方法中使用实体标识符,如下所示:@Entity public class Book implements Identifiable
看看这个 test case on GitHub,它证明了这个解决方案就像一个魅力。
尽管使用业务密钥(选项 3)是最常用的推荐方法(Hibernate community wiki,“Java Persistence with Hibernate”,第 398 页),而且这是我们最常使用的方法,但有一个 Hibernate 错误会破坏这种急切获取集:HHH-3799。在这种情况下,Hibernate 可以在其字段初始化之前将实体添加到集合中。我不确定为什么这个错误没有得到更多关注,因为它确实使推荐的业务关键方法存在问题。
我认为问题的核心是equals和hashCode应该基于不可变状态(参考Odersky et al.),而具有Hibernate管理的主键的Hibernate实体没有这样的不可变状态。当瞬态对象变为持久对象时,Hibernate 会修改主键。当 Hibernate 在初始化过程中对对象进行水合时,业务密钥也被 Hibernate 修改。
只剩下选项 1,基于对象标识继承 java.lang.Object 实现,或使用 James Brundege 在 "Don't Let Hibernate Steal Your Identity" 中建议的应用程序管理的主键(已由 Stijn Geukens 的答案引用)和 Lance Arlaus 在 { 2}。
选项 1 的最大问题是无法使用 .equals() 将分离实例与持久实例进行比较。但这没关系; equals 和 hashCode 的约定让开发人员决定相等对每个类的含义。所以只要让equals和hashCode从Object继承。如果您需要将分离实例与持久实例进行比较,您可以为此目的显式创建一个新方法,可能是 boolean sameEntity
或 boolean dbEquivalent
或 boolean businessEquals
。
我同意安德鲁的回答。我们在应用程序中做同样的事情,但不是将 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();
}
}
Jakarta Persistence 3.0, section 4.12 写道:
当且仅当它们具有相同的主键值时,相同抽象模式类型的两个实体才相等。
我看不出为什么 Java 代码的行为会有所不同。
如果实体类处于所谓的“瞬态”状态,即它还没有被持久化并且它没有标识符,那么 hashCode/equals 方法不能返回值,它们应该会爆炸,理想情况下是隐式地使用 NullPointerException
当方法尝试遍历 ID 时。无论哪种方式,这都会有效地阻止应用程序代码将非托管实体放入基于哈希的数据结构中。实际上,如果类和标识符相等,为什么不更进一步并炸毁,但其他重要的属性,例如 version
是不相等的(IllegalStateException
)!以确定性方式快速失败始终是首选选项。
注意事项:还要记录爆炸行为。文档本身很重要,但它也有望在未来阻止初级开发人员对您的代码做一些愚蠢的事情(他们倾向于在发生 NullPointerException 的地方抑制它,而他们最不想考虑的就是副作用,哈哈) .
哦,总是使用 getClass()
而不是 instanceof
。 equals 方法需要对称性。如果 b
等于 a
,则 a
必须等于 b
。对于子类,instanceof
打破了这种关系(a
不是 b
的实例)。
虽然我个人总是使用 getClass()
,即使在实现非实体类时(类型 is 状态,因此子类 adds state 即使子类为空或仅包含行为),instanceof
只有当类是 final 时才可以。但是实体类不能是最终的(§2.1),所以我们在这里真的没有选择。
有些人可能不喜欢 getClass()
,因为持久性提供者的代理包装了对象。这在过去可能是一个问题,但它真的不应该是。提供者不为不同的实体返回不同的代理类,好吧,我会说这不是一个非常聪明的提供者,哈哈。一般来说,我们不应该在出现问题之前解决问题。而且,似乎 Hibernate 自己的文档甚至不认为值得一提。事实上,他们在自己的示例 (see this) 中优雅地使用了 getClass()
。
最后,如果一个实体子类是实体,并且使用的继承映射策略不是默认的(“单表”),而是配置为“连接子类型”,那么该子类表中的主键 将与超类表相同。如果映射策略是“每个具体类的表”,那么主键可能与超类中的相同。实体子类很可能会添加状态,因此在逻辑上也可能是不同的东西。但是使用 instanceof
的 equals 实现不一定只能依赖 ID,因为我们看到对于不同的实体可能是相同的。
在我看来,instanceof
在非最终 Java 类中根本没有位置。对于持久实体尤其如此。
Object#getClass()
很糟糕。调用 Hibernate.getClass(o)
会有所帮助,但不同种类实体的平等问题仍然存在。有一个使用 canEqual 的解决方案,有点复杂,但可以使用。同意通常不需要。 +++ 在空 ID 上抛出 eq/hc 违反了合同,但它非常务实。
这里显然已经有非常丰富的答案,但我会告诉你我们做什么。
我们什么都不做(即不覆盖)。
如果我们确实需要 equals/hashcode 来处理集合,我们使用 UUID。您只需在构造函数中创建 UUID。我们使用 http://wiki.fasterxml.com/JugHome 作为 UUID。 UUID 在 CPU 方面稍微贵一点,但与序列化和数据库访问相比便宜。
请根据预定义的类型标识符和 ID 考虑以下方法。
JPA的具体假设:
相同“类型”和相同非空 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()
笔记:
使用继承时需要注意。例如,类 A 和类 B 扩展 A 的实例相等性可能取决于应用程序的具体细节。
理想情况下,使用业务密钥作为 ID
期待您的评论。
过去我一直使用选项 1,因为我知道这些讨论,并认为在我知道正确的事情之前什么都不做会更好。这些系统都仍在成功运行。
但是,下次我可能会尝试选项 2 - 使用数据库生成的 Id。
如果未设置 id,Hashcode 和 equals 将抛出 IllegalStateException。
这将防止涉及未保存实体的细微错误意外出现。
人们如何看待这种方法?
业务密钥方法不适合我们。我们使用 DB 生成的 ID、临时临时 tempId 和覆盖 equal()/hashcode() 来解决困境。所有实体都是实体的后代。优点:
DB 中没有额外的字段 后代实体中没有额外的编码,一种方法适用于所有 没有性能问题(如 UUID),DB Id 生成 Hashmaps 没有问题(不需要记住使用 equal & 等) Hashcode即使在坚持之后,新实体的数量也不会及时改变
缺点:
序列化和反序列化非持久化实体可能存在问题 保存实体的哈希码在从数据库重新加载后可能会发生变化 非持久化对象被认为总是不同的(也许这是对的?)还有什么?
看看我们的代码:
@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();
}
}
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),您可以与外部系统共享而不暴露系统内部。
如果您可以期望用户的想法是一个好主意,那么基于业务密钥实施它,但也有一些缺点
大多数情况下,此业务密钥将是用户提供的某种代码,而不是多个属性的组合。
连接速度较慢,因为基于可变长度文本的连接速度很慢。如果键超过一定长度,一些 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 方法。
我试图自己回答这个问题,但在我阅读这篇文章,尤其是 DREW 之前,我从未对找到的解决方案完全满意。我喜欢他懒惰地创建 UUID 并优化存储它的方式。
但是我想增加更多的灵活性,即仅当在实体的第一次持久性之前访问 hashCode()/equals() 时才延迟创建 UUID,具有每个解决方案的优点:
equals() 表示“对象引用相同的逻辑实体”
尽可能多地使用数据库 ID,因为我为什么要做两次工作(性能问题)
防止在尚未持久化的实体上访问 hashCode()/equals() 时出现问题,并在它确实持久化后保持相同的行为
我真的很感谢下面对我的混合解决方案的反馈
公共类 MyEntity { @Id() @Column(name = "ID", length = 20, nullable = false, unique = true) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; @Transient 私有 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;返回 this.getUuid().equals(((MyEntity)toBeCompared).getUuid()); } public final UUID getUuid() { // UUID 已经在这个物理对象上访问过 if(this.uuid != null) { return 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; } 私有 void setUuid(UUID uuid) { if(uuid == null) { return; } // 对于生成的 UUID 可能与从 ID 构建的 UUID 共存的一种假设情况 if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) { throw new Exception("UUID: " + this.getUuid() + "格式仅供内部使用"); } this.uuidMostSignificantBits = uuid.getMostSignificantBits(); this.uuidLeastSignificantBits = uuid.getLeastSignificantBits(); this.uuid = uuid; }
这是每个使用 Java 和 JPA 的 IT 系统中的常见问题。痛点超出了实现 equals() 和 hashCode() 的范围,它会影响组织如何引用实体以及其客户如何引用同一实体。我已经看到了没有业务密钥的痛苦,以至于我写了 my own blog 来表达我的观点。
简而言之:使用一个简短的、人类可读的、带有有意义前缀的顺序 ID 作为生成的业务密钥,而不依赖于 RAM 以外的任何存储。 Twitter 的 Snowflake 就是一个很好的例子。
我使用类 EntityBase 并继承到我的所有 JPA 实体,这对我来说非常有用。
/**
* @author marcos.oliveira
*/
@MappedSuperclass
public abstract class EntityBase<TId extends Serializable> implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
@Id
@Column(name = "id", unique = true, nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected TId id;
public TId getId() {
return this.id;
}
public void setId(TId id) {
this.id = id;
}
@Override
public int hashCode() {
return (super.hashCode() * 907) + Objects.hashCode(getId());//this.getId().hashCode();
}
@Override
public String toString() {
return super.toString() + " [Id=" + id + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
EntityBase entity = (EntityBase) obj;
if (entity.id == null || id == null) {
return false;
}
return Objects.equals(id, entity.id);
}
}
参考:https://thorben-janssen.com/ultimate-guide-to-implementing-equals-and-hashcode-with-hibernate/
如果 UUID 是很多人的答案,为什么我们不使用业务层的工厂方法来创建实体并在创建时分配主键?
例如:
@ManagedBean
public class MyCarFacade {
public Car createCar(){
Car car = new Car();
em.persist(car);
return car;
}
}
这样我们就可以从持久化提供者那里得到一个实体的默认主键,我们的 hashCode() 和 equals() 函数可以依赖它。
我们还可以将 Car 的构造函数声明为 protected,然后在我们的业务方法中使用反射来访问它们。这样开发人员就不会打算用 new 实例化 Car,而是通过工厂方法。
那怎么样?
在实践中,选项 2(主键)似乎是最常用的。自然和不可变的业务密钥很少发生,创建和支持合成密钥太重而无法解决可能从未发生过的情况。查看 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 被建议。
下面是 Scala 的一个简单(且经过测试)的解决方案。
请注意,此解决方案不适合问题中给出的 3 个类别中的任何一个。
我所有的实体都是 UUIDEntity 的子类,所以我遵循不重复(DRY)原则。
如果需要,可以更精确地生成 UUID(通过使用更多伪随机数)。
斯卡拉代码:
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()
}
不定期副业成功案例分享
suid.js
从suid-server-java
获取 ID 块,然后您可以获取并使用客户端。