JPA实体的equals和hashcode应该包含什么?

3

在创建JPA实体时,equals和hashcode应该包含什么,不应该包含什么?以Address实体为例。

我读到过ID不应该包含在内,但不确定为什么。像我的情况中的State这样的嵌套对象呢?我没有包含locations,因为State是非拥有端,Location拥有关系。

以下类中应该包含什么,不应该包含什么?

@Entity
@Table(name = "T_ADDRESS")
@XmlRootElement
@EqualsAndHashCode(exclude = {"id", "locations"})
@ToString(exclude = {"location"})
public class Address implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "addressSeq")
    @SequenceGenerator(name = "addressSeq", sequenceName = "T_ADDRESS_SEQ", allocationSize = 1)
    @Column(name = "ID")
    private Long id;

    @Size(max = 255)
    @Column(name = "STREET_LINE_1")
    private String streetLine1;

    @Size(max = 255)
    @Column(name = "STREET_LINE_2")
    private String streetLine2;

    @NotBlank
    @Size(max = 255)
    @Column(name = "CITY")
    private String city;

    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = "STATE_ID", referencedColumnName = "ID")
    private State state;

    @NotBlank
    @Size(max = 10)
    @Column(name = "POSTAL_CODE")
    private String postalCode;

    // Referenced Properties
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "address")
    private List<Location> locations;

    public Address() {
    }

    public Address(String streetLine1, String streetLine2, String city, State state, String postalCode) {
        this.streetLine1 = streetLine1;
        this.streetLine2 = streetLine2;
        this.city = city;
        this.state = state;
        this.postalCode = postalCode;
    }

    public Long getId() {
        return id;
    }

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

    public String getStreetLine1() {
        return streetLine1;
    }

    public void setStreetLine1(String streetLine1) {
        this.streetLine1 = streetLine1;
    }

    public String getStreetLine2() {
        return streetLine2;
    }

    public void setStreetLine2(String streetLine2) {
        this.streetLine2 = streetLine2;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public String getPostalCode() {
        return postalCode;
    }

    public void setPostalCode(String postalCode) {
        this.postalCode = postalCode;
    }

    public List<Location> getLocations() {
        return locations;
    }

    public void setLocations(List<Location> locations) {
        this.locations = locations;
    }
}

1
我们无法为您回答这个问题。这高度取决于您的业务需求。在您的业务应用程序中,什么情况下您认为两个地址是相等的,而不是代码方面的,比如说同一个位置?或者可能是同一条街道线? - Tunaki
在这种情况下,如果streetLine1、streetLine2、city、state和postalCode匹配,则地址将是相同的,数据库“应该”不存储重复的地址,因此ID并不重要,但ID是表中的唯一标识符,因此可能应该包括它。这就是我有点困惑的地方。 - greyfox
那么你刚刚回答了自己的问题:在equals方法中应该测试streetLine1、streetLine2、city、state和postalCode。让数据库管理ID,你只需要关注你的业务需求。 - Tunaki
2个回答

2
我看到的最常见的建议是,实体的相等性应该反映您的业务逻辑的适当相等性,而不是持久性逻辑。如果您使用代理 ID 而不是自然 ID (这在 JPA 中很常见),那么这意味着您不能将实体的相等性基于它们的 ID。
考虑一下:假设您从数据库加载了一个 Address 实体,并且您还根据应用程序 UI 提供的数据构建了一个实体。如果您希望它们有任何可能相互测试为 equals(),则必须仅基于除 ID 之外的属性进行测试,因为后者对象尚未分配任何 ID。由您决定哪些属性应对此测试做出贡献。
当然,如果您替换了 equals(),那么您也应该重写 hashCode(),以确保测试为 equals() 的任何两个对象具有相同的哈希码不变量。为此,您可以在哈希码计算中仅使用对等测试有所贡献的属性,并且对于最具区别性的哈希码,您应该使用所有这些属性。
请注意,当实体与一个或多个其他实体关联时,其他实体的 ID 可以合理地纳入相等性测试。例如,您的 Address 实体可能会在其 equals() 和 hashCode() 决定中包含其关联的 State 实体的 ID。

这是一个非常好的答案和详尽的解释。 - greyfox
在我看来,equals方法应该反映主键,无论它是简单的还是复合的。如果不是这样,你应该考虑使用复合键而不是简单的ID键。如果你的equals方法和主键不一致,你可能会在实体内使用集合或映射时出现意想不到的行为,特别是如果另一个进程可能修改底层数据库,从而绕过实体中的相等性检查。然而,你必须考虑那些尚未持久化且没有分配ID的实例。对于这些实例,你可以回退到调用super.equals()。 - OndroMih
@OndrejM,当然可以提出基于ID的实体相等性的论点,但我个人不喜欢这种方法。然而,你绝不能做的最糟糕的事情是使equals()不一致或不对称,这是我能想到的任何实现特定ID值的回退行为所导致的结果。 - John Bollinger
@John Bollinger:我建议只有在尚未定义ID时才使用回退 - 实体尚未持久化 - 因此无法通过ID进行比较。它很可能是一个全新的实体,与任何其他实体都不同,并保持equals的约定。如果将equals用于比较实体的数据字段,则在哈希键或集合中使用它也不稳定,因为哈希应该是不可修改的。迄今为止最好的解决方案是确保在创建实体时正确分配id,但在某些情况下这可能是低效的。 - OndroMih
@OndrejM,我理解了你提出的回退条件。我再次重申,在任何情况下,这样的行为都是一个糟糕的想法。我也理解基于ID还是基于属性来确定平等的争论双方的论点。我承认双方都有优点,而且深思熟虑的人可能会在一般或特定情况下对哪一方更倾向产生分歧。我不打算在这里辩论这个问题,但当然你可以发表自己对这个问题的答案。 - John Bollinger

0

这取决于你的“业务”规则。如果以下条件成立,则两个Address实例是等价的:

  • address1.streetline = address2.streetline
  • 并且 address1.city = address2.city
  • 并且 address1.postalcode = address2.postalcode
  • 并且 address1.state = address2.state

(例如,也许你需要包括streetline2)

你还可以通过创建一个新的属性state_id来改变编写equals()和hashcode()方法的方式。它将包含状态标识符,而不必获取“State”实体:

@Column(name="STATE_ID", insertable=false, updatable=false)
private Long stateId;

@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "STATE_ID", referencedColumnName = "ID")
private State state;

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