覆盖持久化实体的hashCode()和equals()方法的正确方法是什么?

时间:2009-12-18 13:27:13

标签: java hibernate jpa java-ee

我有一个简单的类角色:

@Entity
@Table (name = "ROLE")
public class Role implements Serializable {

    @Id
    @GeneratedValue
    private Integer id;
    @Column
    private String roleName;

    public Role () { }

    public Role (String roleName) {
        this.roleName = roleName;
    }

    public void setId (Integer id) {
        this.id = id;
    }

    public Integer getId () {
        return id;
    }

    public void setRoleName (String roleName) {
        this.roleName = roleName;
    }

    public String getRoleName () {
        return roleName;
    }
}

现在我想覆盖它的方法equals和hashCode。我的第一个建议是:

public boolean equals (Object obj) {
    if (obj instanceof Role) {
        return ((Role)obj).getRoleName ().equals (roleName);
    }
    return false;
}

public int hashCode () {
    return id; 
}

但是当我创建新的Role对象时,它的id为null。这就是我在hashCode方法实现上遇到问题的原因。现在我可以简单地返回roleName.hashCode ()但是如果roleName不是必需的字段呢?我几乎可以肯定,通过返回其中一个字段的hashCode来构建更复杂的例子并不困难。

所以我希望看到相关讨论的一些链接或听取您解决此问题的经验。谢谢!

7 个答案:

答案 0 :(得分:13)

Bauer和King的书 Java Persistence with Hibernate 建议不要使用equals和hashCode的关键字段。他们建议您应该选择对象的业务关键字段(如果没有人工密钥)并使用它们来测试相等性。因此,在这种情况下,如果角色名称不是必填字段,您将找到必要的字段并将它们组合使用。如果您发布的代码除了id之外还有rolename,那么rolename就是我的目标。

以下是第398页的引用:

  

我们认为基本上每个实体类都应该有某些业务键,即使它包含了类的所有属性(这适用​​于某些不可变类)。业务键是用户的唯一标识特定记录的东西,而代理键是应用程序和数据库使用的。

     

业务键平等表示equals()方法仅比较构成业务键的属性。这是一个完美的解决方案,可以避免前面提到的所有问题。唯一的缺点是,首先需要额外考虑识别正确的业务密钥。无论如何都需要这种努力;如果数据库必须通过约束检查确保数据完整性,则识别任何唯一键很重要。

我用来构造equals和hashcode方法的一种简单方法是创建一个toString方法,该方法返回“业务键”字段的值,然后在equals()和hashCode()方法中使用它。澄清:这是一种懒惰的方法,当我不关心性能时(例如,在rinky-dink内部webapps中),如果预期性能是一个问题,那么自己编写方法或使用IDE的代码生成工具。

答案 1 :(得分:7)

我很抱歉因批评而迟到,但没有其他人提及它,这里有一个严重的缺陷。实际上可能是两个。

首先,其他人已经提到了如何处理null的可能性,但良好的hashcode()equals()方法对的一个关键要素是它们必须遵守合同,并且上面的代码不是这样做。

合同是 equals()返回true的对象必须返回相等的哈希码值,但在上面的类中,字段id和roleName是独立的。

这是一个致命缺陷的做法:你可以很容易地拥有两个具有相同roleName值但具有不同id值的对象。

实践是使用相同的字段来生成equals()方法使用的哈希码值,并且顺序相同。下面是我的哈希码方法的替代品:


public int hashCode () {
    return ((roleName==null) ? 0 : roleName.hashcode()); 
}

注意:我不知道你使用id字段作为hashcode的意图,或者你想用id字段做什么。我从注释中看到它是生成的,但它是外部生成的,因此所写的类无法履行合同。

如果由于某种原因你发现自己处于这样一种情况,即这个类完全由另一个忠实地生成了履行合同的roleNames的“id”值,那么你就没有功能问题,但它仍然是不好的做法,或者至少有人称之为“代码味道”。除了类定义中没有任何东西可以保证类只能以这种方式使用之外,哈希码不是ids,所以ids不是哈希码

这并不意味着您不能使用保证等于 - 等于 - 角色名称 - 值的标识符作为哈希码,但它们在概念上并不相同,所以非常至少,你应该有一个评论块来解释你偏离预期的做法。

作为一个很好的一般规则,如果你发现自己必须这样做,你可能会犯一个设计错误。并非总是,但可能。一个原因是什么?人们并不总是阅读评论,所以即使你创建一个功能完善的系统,随着时间的推移,有人会“滥用”你的课程并导致问题。

让类本身管理哈希码值的生成可以避免这种情况。无论出于何种目的,你仍然可以保存并提供外部生成的id。

答案 2 :(得分:2)

对象的业务键可能需要其父级(或另一种一对一或多对一)关系。在这些情况下,调用equals()或hashcode()可能会导致数据库命中。除了性能之外,如果会话关闭将导致错误。我大多放弃尝试使用业务键;我使用主id并避免在地图和集合中使用未保存的实体。到目前为止一直运作良好,但它可能取决于应用程序(注意通过父级联保存多个孩子)。偶尔,我会使用一个单独的无意义的关键字段,它是在构造函数中或由对象创建者自动生成的。

答案 3 :(得分:1)

知道何时覆盖hashCode和equals不是一件容易的事, 还有另一个讨论,你在这里有示例和文档链接What issues should be considered when overriding equals and hashCode in Java?

答案 4 :(得分:1)

如前所述,您必须使用业务键来实现equal和hashCode。此外,您必须使您的equals和hashCode实现 null-safe 或添加 not null contraints (以及对代码进行不变检查)以确保业务密钥永远不为null。

我认为添加约束是解决问题的正确方法。否则,将允许没有名称的角色实例,并且所有这些物理实例都将被视为相等。

答案 5 :(得分:1)

请在下面找到如何使用Apache commons构建器创建hashCode,equals和toString方法的简单说明。

的hashCode

  1. 如果两个对象根据equals()方法相等,则它们必须具有相同的hashCode()值
  2. 两个不同的对象可能具有相同的hashCode()。
  3. 请使用唯一的业务ID进行hashCode创建(这意味着您应该使用一些代表业务实体的唯一属性,例如名称)
  4. Hibernate实体:请不要使用Hibernate id来创建hashCode
  5. 如果您的类是子类,您可以调用.appendSuper(super.hashCode())

    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .append(getName())
                .toHashCode();
    }
    
  6. 等于

    1. 请比较商家ID(这意味着您应该使用代表商业实体的某些独特属性,例如名称)
    2. Hibernate实体:请不要比较Hibernate id
    3. Hibernate Entity:访问其他对象字段时使用getter让Hibernate加载属性
    4. 如果您的类是子类,您可以调用.appendSuper(super.equals(other))

      @Override
      public boolean equals(final Object other) {
          if (this == other)
              return true;
          if (!(other instanceof TreeNode))
              return false;
          TreeNode castOther = (TreeNode) other;
          return new EqualsBuilder()
                  .append(getName(), castOther.getName())
                  .isEquals();
      }
      
    5. 的toString

      1. 请确保toString不会抛出NullPointerException。
      2. 如果您的类是子类,您可以调用.appendSuper(super.toString())

        @Override
        public String toString() {
            return new ToStringBuilder(this)
                    .append("Name", getName())
                    .toString();
        }
        

答案 6 :(得分:0)

我在项目中使用了Lombok,这是适用于JPA实体的规则的汇总,我将equals()hashCode()的特定实体移到了顶部

建立实体类的规则

创建时,请确保以下内容

  • EqualsHashCode仅应在“业务键”上完成

  • 复合密钥通常是业务密钥。因此它们不得为EqualsHashCode.Exclude

  • EqualsHashCode。排除“代理密钥” /“技术ID”,例如生成的ID,版本,审核字段,由@PrePersist或@PreUpdate更新的值

  • ManyToOne必须具有EqualsHashCode.Exclude和ToString.Exclude以防止堆栈溢出

  • ManyToOne必须具有对应的EqualsHashCode.Include和ToString.Include才能获取引用对象的ID字段。该方法必须带有ID后缀,并且不能以get

    开头

  • @Entity类名必须为单数。 @Repository类名将是复数。
  • @Entity必须具有@Data
  • @Entity必须为Serializable
  • 如果提供了构造函数,
  • @Entity必须具有@NoArgsConstructor
  • 复合键必须具有@Data
  • @OneToMany必须具有cascadeType = Cascade.ALL
  • @OneToMany必须具有mappingBy
  • @OneToMany必须具有orphanRemoval = true
  • @OneToMany必须是集合
  • ManyToOne必须具有optional = false
  • ManyToOne必须具有@JoinColumn

什么是业务密钥?

使用两个等效的纸牌的类比,它们被弄脏并混在一起。收集将是套牌,其中应包含具有唯一可识别值的卡。

  • 公司的钥匙是西服,甚至是卡号。这些唯一地标识卡的价值。

  • 仅在卡片表面上的污垢图案不相关。因此,它不是业务密钥的一部分。

  • 卡本身的物理存在也不是业务密钥的一部分。这类似于为卡生成的私钥。这也称为代理密钥或技术ID。