这里有一些关于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方法。

来说,如何?

其他回答

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

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


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

(+)三个标准都有保证

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

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


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

(+)三个标准都有保证

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

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


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

(+)三个标准都有保证

(-)生成UUID的开销

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

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

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

例如:

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

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

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

来说,如何?

实际上,似乎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(保留对象实现)在合并后被破坏,这是非常常见的情况。

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