我可以在equals/hashCode中使用实体的ID,并回退到实例相等吗?

4

我正在努力弄清楚这种方法有什么问题,考虑到我的特定使用模式:

@Entity
public class DomainObject {
  @Id // + sequence generator
  private Long id;

  @Override
  public boolean equals(Object o) {
    // bunch of other checks omitted for clarity
    if (id != null) { 
      return id.equals(o.getId());
     }
     return super.equals(o);
  }

  @Override
  public int hashCode() { 
    if (id != null) {
      return id.hashCode();
    } 
    return super.hashCode();
}

我已经阅读了几篇关于这个主题的文章,听起来你不想在equals/hashCode中使用数据库生成的序列值,因为它们直到对象被持久化后才会设置,你不希望不同的瞬态实例都相等,否则持久化层本身可能会出问题。
但是,对于瞬态对象,回退到默认的Object equals/hashCode(实例相等)是否有问题,并在有时使用生成的@Id?
我能想到的最糟糕的事情是,瞬态对象永远无法等于持久对象,在我的用例中没问题 - 我唯一需要将对象放入集合并希望contains工作的时间,所有对象都已经持久化并且都有ID。
然而,我觉得还有其他一些微妙、不明显的问题存在于持久化层中,但我无法完全理解是什么。
其他选项似乎也不那么吸引人:
  • 什么也不做,使用实例相等性(默认Object.equals):对于我的大多数实体来说效果很好,但在我想要一个包含脱离的实体(例如会话范围)和当前事务中的“活动”实体混合的集合时,需要解决一些问题。

  • 使用业务键:我有清晰的自然键,但它们是可变的,这将具有与上述相同的问题(如果对象更改,则hashCode稳定性)

  • 使用UUID-我知道这样做可以解决问题,但感觉在污染DB以支持java.util集合方面不太对。

另请参阅:

3个回答

2

可以的!但是你必须小心,hashCode 的实现始终返回相同的常量值如本文所述

@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 Objects.equals(getId(), book.getId());
    }
 
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
 
    //Getters and setters omitted for brevity
}

这是唯一的方法,可以确保在所有实体状态转换中equals和hashCode保持一致。


1
虽然从hashCode返回常量值会破坏HashMapHashSet等的性能特征,把所有对象放在同一个哈希桶中,但这是个好观点。 - wrschneider
一般来说,这是正确的。但是你不应该在一个集合中有太多的实体,无论是一对多还是多对多,因为这意味着你获取了大量的数据,这会对性能产生更大的影响。如果你保持集合足够小,那么由于单桶限制,就不会有明显的性能影响。 - Vlad Mihalcea

2
The Map的javadoc中写道:

注意:如果使用可变对象作为map的键,则必须非常小心。如果在对象作为map的键时以影响相等比较的方式更改对象的值,则map的行为未指定。

每当一个对象被持久化时,你的实现都会改变equals的含义。因此,包含该对象的任何集合都不再起作用。特别是,在HashMap中用作键的对象(或包含在HashSet中)的哈希码发生更改可能导致将来在该Map(Set)上查找该对象失败,并且即使在普通情况下,一个Map最多只能包含一个给定键的映射,一个Set最多只能包含每个对象一次,添加该对象到Map(Set)中也可能成功。

由于通常在集合中存储实体(以表示ToMany关联),因此该缺陷可能导致实际难以找到的错误。

因此,我强烈建议不要基于数据库生成的标识符实现哈希码。


0

如果您确定您永远不需要将未持久化的实体添加到Set或Map键中,您可以使用ID来测试相等性和哈希码。但是,如果您这样做,您可以通过为未持久化的对象抛出异常来强制执行:

@Entity
public class DomainObject {
  @Id // + sequence generator
  private Long id;

  @Override
  public boolean equals(Object that) {
    // bunch of other checks omitted for clarity
    if (id != null) { 
      throw new IllegalStateException("equals() before persisting");
    }
    if (this == that) {
      return true;
    }
    if (that instanceof DomainObject) {
      return id.equals(((DomainObject)that).id);
    }

  }

  @Override
  public int hashCode() { 
    if (id != null) {
      throw new IllegalStateException("hashCode() before persisting");
    } 
    return id;
  }
}

如果你这样做,你可能会看到意外的异常,因为你没有意识到你依赖于这些未持久化对象的方法。在调试时,你可能会发现这很有帮助。你也可能发现它使你现有的代码无法使用。无论哪种方式,你都会更清楚你的代码是如何工作的。
有一件事情你永远不应该做,那就是为哈希码返回一个常量。
public int hashCode() { return 5; } // Don't ever do this!

从技术上讲,它满足了合约,但实现得很糟糕。只需阅读Object.hashCode()的javadocs即可明白:“…为不相等的对象生成不同的整数结果可能会提高哈希表的性能。”(这里的“可能”是严重低估了。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接