JPA hashCode() / equals()困境

这里已经有了关于JPA实体的一些 讨论,以及哪些hashCode()/equals()实现应该用于JPA实体类。它们中的大多数(如果不是全部)依赖于Hibernate,但是我想中立地讨论它们的jpa实现(顺便说一下,我使用的是EclipseLink)。

所有可能的实现都有自己的优势缺点,涉及:

  • __ABC0 / equals()契约整合(不可变性)用于List/Set操作
  • 是否可以检测相同的对象(例如来自不同会话,来自惰性加载数据结构的动态代理)
  • 实体是否在分离(或非持久)状态下正确运行

据我所知,有三个选项:

  1. 不要覆盖它们;依赖Object.equals()Object.hashCode()
    • hashCode() / equals()工作
    • 无法识别相同的对象,动态代理存在问题
    • 分离实体没有问题
    • 李< / ul > < / >
    • 基于主键重写它们
      • hashCode()/equals()被破坏
      • 正确的标识(适用于所有被管理实体)
      • 分离实体的问题
      • 李< / ul > < / >
      • 基于Business-Id重写它们(非主键字段;外键呢?)
        • hashCode()/equals()被破坏
        • 正确的标识(适用于所有被管理实体)
        • 分离实体没有问题
        • 李< / ul > < / >

我的问题是:

  1. 我是否错过了一个选择和/或赞成/反对的观点?< br >
  2. 你选择了什么,为什么?
< p > < br > < br > 更新1:< / >强

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

更新2:

谢谢你的回答——大部分问题都很有质量 不幸的是,我仍然不确定哪种方法最适合实际应用程序,或者如何确定最适合我的应用程序的方法。所以,我将保持这个问题的开放性,希望有更多的讨论和/或意见
125996 次浏览

我总是重写equals/hashcode,并基于业务id实现它。对我来说这是最合理的解决办法。参见下面的链接

总而言之,这里列出了处理equals/hashCode的不同方法的工作或不工作的列表:enter image description here

编辑:

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

  1. 在JPA应用程序中,我通常不使用基于散列的集合(HashMap/HashSet)。如果我必须,我更喜欢创建UniqueList解决方案。
  2. 我认为在运行时更改业务id对于任何数据库应用程序都不是最佳实践。在没有其他解决方案的罕见情况下,我会进行特殊处理,例如删除元素并将其放回到基于散列的集合中。
  3. 对于我的模型,我在构造函数上设置了业务id,并且没有为它提供setter。我让JPA实现改变而不是属性。
  4. UUID解决方案似乎是多余的。如果你有自然的业务id,为什么要使用UUID ?我将在数据库中设置业务id的唯一性。为什么数据库中的每个表都有三个索引呢?

我个人已经在不同的项目中使用了这三种策略。我必须说,选项1在我看来是现实应用中最可行的。以我的经验来看,打破hashCode()/equals()一致性会导致许多疯狂的错误,因为你每次都会遇到这样的情况:在一个实体被添加到一个集合后,相等的结果发生了变化。

但也有更多的选择(也有它们的优点和缺点):


a) hashCode/equals基于一组不可变的非空构造函数分配字段

(+)三个标准都有保证

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

(-)如果你必须改变其中一个,处理起来会很复杂


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

(+)三个标准都有保证

(-)您不能利用简单可靠的ID生成策略,如DB序列

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


c) hashCode/equals基于实体的构造函数赋值的UUID

(+)三个标准都有保证

(-)生成UUID的开销

(-)可能会有使用两次相同UUID的风险,这取决于所使用的算法(可能由DB上的唯一索引检测到)

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

你应该从字面上理解:因为你的equals()/hashCode()是基于主键的,所以在设置主键之前,你不能使用这些方法。所以你不应该把实体放到集合里,直到它们被赋主键。(是的,uuid和类似的概念可能有助于早期分配主键。)

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

只有当集合中的所有对象都来自同一个Hibernate会话时,才可以使用选项1。Hibernate文档在13.1.3. 考虑对象同一性章中非常清楚地说明了这一点:

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

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

它继续主张选择3:

这里有一个警告:永远不要使用数据库标识符来实现相等。使用由唯一的、通常是不可变的属性组合而成的业务键。如果将瞬态对象持久化,则数据库标识符将更改。如果瞬态实例(通常与分离实例一起)保存在Set中,更改hashcode将破坏Set的契约。

这是真的,如果

  • 不能提前分配id(例如使用uuid)
  • 当对象处于瞬态时,你肯定想把它们放到集合中。

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

然后它提到了相对稳定性的需求:

业务键的属性不必像数据库主键那样稳定;只要对象在同一集合中,你就必须保证稳定性。

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


短版:

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

显然,这里已经有了非常有用的答案,但我将告诉你我们是怎么做的。

我们什么也不做。

如果我们确实需要= /hashcode来为集合工作,我们使用uuid。 您只需在构造函数中创建UUID。我们使用http://wiki.fasterxml.com/JugHome作为UUID。UUID的CPU开销略高,但与序列化和db访问相比便宜

我们通常在实体中有两个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, 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,但您可能会发现双构造函数方法更适合您。

我同意Andrew的回答。我们在应用程序中做同样的事情,但不是将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();
}


}

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

文章的结论是这样的:

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

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

例如:

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

通过这种方式,我们可以从持久化提供程序获得实体的默认主键,并且我们的hashCode()和equals()函数可以依赖于它。

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

来说,如何?

Jakarta Persistence 3.0,第4.12节写道:

相同抽象模式类型的两个实体当且仅当它们具有相同的主键值时相等。

我看不出为什么Java代码的行为应该有所不同。

如果实体类处于所谓的“瞬态”;状态,即它还没有被持久化并且没有标识符,那么hashCode/equals方法不能返回值,它们应该爆炸,理想情况下,当方法试图遍历ID时,隐式地使用NullPointerException。无论哪种方式,这都将有效地阻止应用程序代码将非托管实体放入基于哈希的数据结构中。事实上,为什么不更进一步,如果类和标识符相等,但其他重要属性如version不相等(IllegalStateException)!以确定的方式快速失败总是首选的选择。

警告:也要记录下爆发行为。文档本身很重要,但它也希望能够阻止初级开发人员在未来对您的代码做一些愚蠢的事情(他们倾向于压制发生NullPointerException的地方,他们最不关心的是副作用,lol)。

哦,总是使用getClass()而不是instanceof。equals方法要求对称性。如果b等于a,那么a必须等于b。对于子类,instanceof打破了这种关系(ab实例)。

虽然我个人总是使用getClass(),即使在实现非实体类(类型状态,因此子类添加状态,即使子类是空的或只包含行为),但只有当类是最后时,instanceof才会很好。但是实体类必须不是final (§2.1),所以我们在这里真的没有选择。

有些人可能不喜欢getClass(),因为持久化提供程序的代理包装了对象。这在过去可能是一个问题,但它真的不应该是。提供商不为不同的实体返回不同的代理类,好吧,我会说这不是一个非常聪明的提供商,哈哈。一般来说,我们不应该在问题出现之前解决问题。而且,似乎Hibernate自己的文档甚至不认为它值得一提。事实上,他们在自己的例子中优雅地使用了getClass() (看到这个)。

最后,如果一个实体子类是实体,并且所使用的继承映射策略不是默认的(“单一表”),而是配置为“联接子类型”,则该子类表都是一样的中的主键作为超类表。如果映射策略是“每个具体类的表”,则主键可能是一样的与超类中的一样。实体子类很可能是添加状态,因此在逻辑上可能是一个不同的东西。但是使用instanceof的等号实现不一定只能依赖于ID,就像我们看到的,对于不同的实体可能是相同的。

在我看来,instanceof在非final Java类中根本没有位置。对于持久实体来说尤其如此。

我过去一直使用选项1,因为我知道这些讨论,并认为在我知道正确的事情之前最好什么都不做。这些系统仍在成功运行。

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

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

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

人们对这种方法有什么看法?

虽然使用业务键(选项3)是最常推荐的方法(Hibernate社区wiki,“Java Persistence with Hibernate”第398页),而且这是我们最常用的方法,但Hibernate有一个错误,它会为急于获取的集破坏此方法:终极战士- 3799。在这种情况下,Hibernate可以在字段初始化之前将一个实体添加到集合中。我不确定为什么这个错误没有得到更多的关注,因为它确实使推荐的业务键方法出现了问题。

我认为问题的核心是equals和hashCode应该基于不可变状态(引用Odersky等人。),而具有Hibernate管理的主键的Hibernate实体具有没有这样的不可变状态。当一个瞬态对象变成持久对象时,Hibernate会修改主键。当Hibernate在初始化过程中为对象补水时,业务键也会被Hibernate修改。

只剩下选项1,基于对象标识继承java.lang.Object实现,或者使用James Brundege在《别让Hibernate偷走你的身份》中建议的应用程序管理的主键(已经被Stijn Geukens的回答引用)和Lance Arlaus在对象生成:Hibernate集成的更好方法中建议的。

选项1的最大问题是无法使用.equals()将分离实例与持久实例进行比较。但这没关系;equals和hashCode的契约让开发人员决定每个类的相等意味着什么。让equals和hashCode从Object继承。如果你需要比较一个分离实例和一个持久实例,你可以为这个目的显式地创建一个新方法,可能是boolean sameEntityboolean dbEquivalentboolean businessEquals

我试着自己回答这个问题,直到我读了这篇文章,尤其是画了一个,我才完全满意找到的解决方案。我喜欢他懒创建UUID和最佳存储它的方式。

但我想增加更多的灵活性,即惰性创建UUID仅当hashCode()/equals()被访问时,第一次持久化实体与每个解决方案的优点:

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

下面是Scala的简单的(已测试)解决方案。

  • 请注意,该解决方案不符合任何三类 在问题中给出。李< / p > < / >

  • 我所有的实体都是UUIDEntity的子类,所以我遵循 不要重复自己(DRY)原则

  • 如果需要,UUID生成可以更精确(通过使用更多 伪随机数)。< / p > < /李>

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

这是每个使用Java和JPA的IT系统中的常见问题。痛点不仅仅是实现equals()和hashCode(),它还影响组织引用实体的方式以及其客户机引用同一实体的方式。我已经看够了没有业务键的痛苦,以至于我写了我自己的博客来表达我的观点。

简而言之:使用一个简短的、人类可读的、带有有意义前缀的顺序ID作为业务键,该ID生成时不依赖于RAM以外的任何存储。Twitter的雪花就是一个很好的例子。

  1. 如果你有一个业务键,那么你应该将它用于equalshashCode

  2. 如果你没有业务键,你不应该将它保留为默认的Object equals和hashCode实现,因为在你merge和entity之后这就不起作用了。

  3. 只有当hashCode实现返回一个常量值时,你才能在equals方法中使用实体标识符,如下所示:

    @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 getClass().hashCode();
    }
    
    
    //Getters and setters omitted for brevity
    }
    

看看这个GitHub上的测试用例,它证明了这个解决方案的工作就像一个魅力。

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

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

缺点:

  1. 序列化和反序列化非持久化实体可能会出现问题
  2. 从DB重新加载后,保存的实体的Hashcode可能会改变
  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();
}
}
在实践中,似乎Option 2(主键)是最常用的。 自然的和不可变的业务密钥是很少的事情,创建和支持合成密钥对于解决情况来说太沉重了,这可能从来没有发生过。 看一下spring-data-jpa AbstractPersistable实现(唯一的东西:对于Hibernate实现,使用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之后被破坏,这是非常常见的情况。< / p >

如果你没有业务键,并且需要在哈希结构中操作新实体,则将hashCode重写为常量,如下所示Vlad Mihalcea的建议。

请考虑以下基于预定义类型标识符和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()

注:

  • 使用继承时需要注意。例如,class Aclass B extends A的实例相等性可能取决于应用程序的具体细节。
  • 理想情况下,使用业务密钥作为ID

期待您的评论。

在我看来,你有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
}

在我看来,这是最干净的方法,因为它将避免所有的缺点,同时为您提供一个值(UUID),您可以与外部系统共享,而不暴露系统内部。

基于业务键来实现它(如果你能从用户那里得到的话)是个好主意,但也有一些缺点

大多数情况下,这个业务键将是用户提供的某种代码类型,很少是多个属性的组合。

  • 连接速度较慢,因为基于可变长度文本的连接速度很慢。如果键超过一定长度,一些DBMS甚至可能在创建索引时遇到问题。
  • 根据我的经验,业务键往往会发生变化,这就需要对引用它的对象进行级联更新。如果外部系统引用它,这是不可能的

在我看来,你不应该专门实现或使用业务键。这是一个很好的附加功能,用户可以通过业务键快速搜索,但系统不应该依赖它来运行。

基于主键实现它有它的问题,但也许这不是什么大问题

如果您需要向外部系统公开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方法。

我使用类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/