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

时间:2012-11-12 02:20:19

标签: java jpa equals hashcode

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

@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中使用DB生成的序列值,因为在对象被持久化之前它们不会被设置而你不会希望不同的瞬态实例都相等,或者持久层本身可能会中断。

但是对于瞬态对象回退到默认的Object equals / hashCode(实例相等)然后在拥有它时使用生成的@Id会有什么问题吗?

我能想到的最糟糕的事情是,瞬态对象永远不能等同于持久对象,这在我的用例中很好 - 我唯一一次将对象放入集合中并希望{{1要工作,所有对象都是持久的,并且都有ID。

然而,我觉得在持久层深处有一个非常微妙,非显而易见的方式还有其他错误,但我无法弄清楚是什么。

其他选项似乎也没有吸引力:

  • 无所事事并且与实例相等(默认的Object.equals):对我的大多数实体都很有用,但是当我想要一个混合了分离实体的集合时,厌倦了少数情况下的变通方法(例如,会话范围)和当前交易中的“实时”

  • 使用业务键:我有明确的自然键,但它们是可变的,这就可以了 与上面相同的一些问题(如果对象发生变化,hashCode的稳定性)

  • 使用UUID - 我知道这样可行,但是使用工件污染数据库以支持java.util集合感觉不对。

另见:

3 个答案:

答案 0 :(得分:2)

javadoc of Map写道:

  

注意:如果将可变对象用作映射键,则必须非常小心。如果在对象是地图中的键时,以影响等于比较的方式更改对象的值,则不指定映射的行为。

只要持久化对象,您的实现就会改变equals的含义。因此,任何包含该对象的集合都不再需要正常工作。特别是,更改在HashMap(或包含在HashSet中)中用作键的对象的哈希码很可能导致将来在该Map(Set)上查找未找到该对象,并将该对象再次添加到Map(Set)很可能会成功,即使在一般情况下,一个Map最多可以包含每个给定键的一个映射,而Set最多只包含一个对象一次。

由于将实体存储在集合中(表达ToMany关联)很常见,因此该漏洞很可能导致实际难以发现的错误。

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

答案 1 :(得分:0)

是的,你可以!但是你必须要小心,hashCode实现总是返回相同的常量值as explained in this post

@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 31;
    }

    //Getters and setters omitted for brevity
}

这是确保equals和hashCode在所有entity state transitions内保持一致的唯一方法。

答案 2 :(得分:0)

如果确定不需要在Set或Map键中添加非持久性实体,则可以使用ID来测试是否相等,也可以将其用作哈希码。但是,如果执行此操作,则可以通过对非持久对象抛出Exception来强制执行此操作:

@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:…为不相等的对象生成不同的整数结果可能会提高哈希表的性能。(这里的“可能”一词是一种轻描淡写的说法。)