【问题标题】:What is the correct way of overriding hashCode () and equals () methods of persistent entity?覆盖持久实体的 hashCode () 和 equals () 方法的正确方法是什么?
【发布时间】:2010-12-28 01:03:02
【问题描述】:

我有一个简单的类角色:

@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 为空。这就是为什么我对 hashCode 方法的实现有一些问题。现在我可以简单地返回 roleName.hashCode () 但如果角色名称不是必需字段怎么办?我几乎可以肯定,通过返回其中一个字段的 hashCode 无法解决的更复杂的示例并不难。

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

【问题讨论】:

    标签: java hibernate jpa jakarta-ee


    【解决方案1】:

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

    这是第 398 页的引述:

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

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

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

    【讨论】:

    • 对大量数据的经验不足的旁注:在 hashCode() 或 equals() 中使用字符串连接(或任何非原始计算)将导致严重的、几乎不可追踪的性能影响,尤其是。如果对象保存在大型集合中。不建议使用反射 EqualsBuilders/HashCodeBuilders 或使用 toString()。最好使用 IDE 生成的 hashCode() 方法,例如 Eclipse 用于业务领域。
    • 一个集合必须增长到多大才能成为一个真正的问题?
    • @Nathan,如果您缓存 toString 和 hashCode 的结果,您的方法不必表现得像您担心的那样糟糕。当然,如果你的类是不可变的,这会更容易,但即使类是可变的,它也是可管理的。
    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          【解决方案5】:

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

          首先,其他人已经提到如何处理 null 的可能性,但是好的 hashcode()equals() 方法对的一个关键要素是它们必须遵守约定,而您上面的代码没有这样做。

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

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

          实践是使用与 equals() 方法相同的字段以相同的顺序生成哈希码值。下面是我替换你的 hashcode 方法:

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

          注意:我不知道您将 id 字段用作哈希码的目的是什么,或者您打算对 id 字段做什么。我从注释中看到它是生成的,但它是外部生成的,因此编写的类无法履行合同。

          如果由于某种原因,您发现自己处于此类由另一个忠实地为满足合同的角色名称生成“id”值的排他性管理的情况下,您将没有功能问题,但这仍然是不好的做法,或者至少有人们所说的“代码味道”。除了在类定义中没有任何东西可以保证类只能以这种方式使用之外,哈希码不是 id,所以 id 不是哈希码

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

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

          让类自己管理哈希码值的生成可以避免这种情况。而且您仍然可以保存外部生成的 id 并使其可用,无论您出于何种目的使用它。

          【讨论】:

          • hashCode方法我已经改了,和你的差不多。
          • @Roman:所以id和hashCode现在是独立的,hashCode和equals()是基于同一个字段的?太棒了。
          【解决方案6】:

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

          哈希码

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

            @覆盖 公共 int hashCode() { 返回新的 HashCodeBuilder() .append(getName()) .toHashCode(); }

          等于

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

            @覆盖 公共布尔等于(最终对象其他){ 如果(这 == 其他) 返回真; if (!(其他树节点实例)) 返回假; TreeNode castOther = (TreeNode) 其他; 返回新的 EqualsBuilder() .append(getName(), castOther.getName()) .isEquals(); }

          toString

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

            @覆盖 公共字符串 toString() { 返回新的 ToStringBuilder(this) .append("姓名", getName()) .toString(); }

          【讨论】:

            【解决方案7】:

            我在我的项目中使用 Lombok 这是适用于 JPA 实体的规则汇总

            构建实体类的规则

            创建时,请确保以下内容

            • EqualsHashCode 只能在“业务密钥”上完成

            • 复合键通常是业务键。所以它们不能是 EqualsHashCode.Exclude

            • “代理键”/“技术 ID”上的 EqualsHashCode.Exclude,例如生成的 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 一定有 mappedBy
            • @OneToMany 必须有 orphanRemoval=true
            • @OneToMany 必须是一个集合
            • ManyToOne 必须有 optional=false
            • ManyToOne 必须有一个@JoinColumn

            什么是业务密钥?

            使用两张等量的纸牌进行类比,它们被弄脏并洗牌在一起。一个集合是一个包含具有唯一可识别价值的卡片的套牌。

            • 业务关键是花色,数字甚至是卡片背面。这些唯一标识卡的价值。

            • 仅在卡片正面提供的土壤图案不相关。因此,它不是业务密钥的一部分。

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-12-30
              • 2014-03-19
              • 2020-06-09
              • 1970-01-01
              • 2020-02-09
              • 1970-01-01
              相关资源
              最近更新 更多