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

所有可能的实现都有其自身的优点和缺点:

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

在我看来,有三种选择:

Do not override them; rely on Object.equals() and Object.hashCode() hashCode()/equals() work cannot identify identical objects, problems with dynamic proxies no problems with detached entities Override them, based on the primary key hashCode()/equals() are broken correct identity (for all managed entities) problems with detached entities Override them, based on the Business-Id (non-primary key fields; what about foreign keys?) hashCode()/equals() are broken correct identity (for all managed entities) no problems with detached entities

我的问题是:

我是否错过了一个选择和/或赞成/反对的观点? 你选择了什么,为什么?

更新1:

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

更新2:

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


当前回答

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

例如:

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

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

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

来说,如何?

其他回答

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

例如:

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

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

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

来说,如何?

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

总而言之,这里列出了处理equals/hashCode的不同方法中哪些是有效的,哪些是无效的:

编辑:

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

I don't usually use hashed-based collection (HashMap/HashSet) in my JPA application. If I must, I prefer to create UniqueList solution. I think changing business id on runtime is not a best practice for any database application. On rare cases where there is no other solution, I'd do special treatment like remove the element and put it back to the hashed-based collection. For my model, I set the business id on constructor and doesn't provide setters for it. I let JPA implementation to change the field instead of the property. UUID solution seems to be overkill. Why UUID if you have natural business id? I would after all set the uniqueness of the business id in the database. Why having THREE indexes for each table in the database then?

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

在我看来,你有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方法。

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

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