在Hibernate中,Model类的equals和hashcode应该如何实现?有哪些常见陷阱?默认实现对于大多数情况是否足够好?使用业务键有任何意义吗?
在考虑到延迟获取、ID生成、代理等因素时,似乎很难在每个情况下都正确地工作。
equals()
/ hashCode()
的时机和方式有很好的描述。简而言之,只有在你的实体将成为Set
的一部分或者你将会附加 / 分离其实例时才需要考虑它。后者不太常见。前者通常最好是基于业务键 - 即属性的唯一组合,在对象(或至少是会话)生命周期内不会改变。如果以上不可能,则应根据主键进行equals()
/ hashCode()
,如果设置了主键,否则基于对象标识 / System.identityHashCode()
。这里的重要部分是,在新实体被添加到Set
并持久化后,需要重新加载它,否则可能会出现奇怪的行为(最终导致错误和/或数据损坏),因为你的实体可能被分配到与其当前hashCode()
不匹配的桶中。refresh()
吗?假设您具有足够好的hashcode实现,那么遵守“Set”合同的实体如何最终进入错误的桶中。 - non sequitorSet.contains(entity)
,您将得到false
。对于get() / put() /等操作也是如此。 - ChssPly76我认为接受的答案不准确。
回答原问题:
默认实现对大多数情况是否足够好?
大多数情况下,是的。
只有当实体将用于 Set
(这非常普遍)并且该实体将从Hibernate会话中分离,然后重新连接到Hibernate会话时(这是Hibernate的一种不常见的用法),您才需要覆盖 equals()
和 hashcode()
。
接受的答案表明,如果任一条件成立,则需要覆盖方法。
当您使用唯一的业务键或自然标识符时,最好的equals
和hashCode
实现如下:
@Entity
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(unique = true, updatable = false)
private String name;
@Override
public int hashCode() {
HashCodeBuilder hcb = new HashCodeBuilder();
hcb.append(name);
return hcb.toHashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Company)) {
return false;
}
Company that = (Company) obj;
EqualsBuilder eb = new EqualsBuilder();
eb.append(name, that.name);
return eb.isEquals();
}
}
业务关键字应在所有实体状态转换(瞬态、附加、分离、删除)中保持一致,这就是为什么不能仅依赖于 ID 进行相等性比较的原因。
另一种选择是切换到使用应用程序逻辑分配的 UUID 标识符。这种情况下,您可以使用 UUID 进行 equals
/hashCode
,因为 ID 在实体被刷新之前已分配。
您甚至可以使用实体标识符进行 equals
和 hashCode
,但这要求您始终返回相同的 hashCode
值,以确保实体 hashCode 值在所有实体状态转换中保持一致,如下所示:
@Entity(name = "Post")
@Table(name = "post")
public class Post implements Identifiable<Long> {
@Id
@GeneratedValue
private Long id;
private String title;
public Post() {}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Post))
return false;
Post other = (Post) o;
return id != null &&
id.equals(other.getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
//Getters and setters omitted for brevity
}
当一个实体通过延迟加载被加载时,它不是基础类型的实例,而是由javassist动态生成的子类型,因此对同一类类型进行检查会失败,所以不要使用:
if (getClass() != that.getClass()) return false;
改用:
if (!(otherObject instanceof Unit)) return false;
同样这也是一个好的习惯,正如在Java实践中实现equals方法中所解释的那样。
出于同样的原因,直接访问字段可能不起作用并返回null,而不是底层值,因此不要对属性进行比较,而应该使用获取器,因为它们可能会触发加载底层值。
是的,这很困难。在我的项目中,equals和hashCode都依赖于对象的id。这种解决方案的问题是,如果对象尚未被持久化,那么它们都不起作用,因为id是由数据库生成的。在我的情况下,这是可以容忍的,因为几乎所有情况下对象都会立即被持久化。除此之外,它运行良好且易于实现。
通常情况下,如果两个从同一会话加载的对象在数据库中相等(没有实现hashCode和equals),它们将是相等的。
如果您使用两个或更多会话,则情况会变得复杂。在这种情况下,两个对象的相等性取决于您的equals方法实现。
此外,如果您的equals方法比较的是仅在第一次持久化对象时生成的ID,则可能会遇到问题。当调用equals时,它们可能还不存在。
如果您重写了equals
,请确保满足以下契约:
并重写hashCode
,因为其契约依赖于equals
的实现。
Joshua Bloch(集合框架的设计者)强烈建议遵循这些规则。
如果不遵循这些契约,可能会带来严重的意外效果。例如,List#contains(Object o)
可能会返回错误的boolean
值,因为通用契约未得到满足。
这里有一篇非常好的文章:https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/persistent-classes-equalshashcode.html
引用文章中的一句重要话:
我们建议使用业务键相等来实现equals()和hashCode()。业务键相等意味着equals()方法仅比较形成业务键的属性,这是一个在现实世界中可以识别我们实例的关键(自然候选键):
简单来说
public class Cat {
...
public boolean equals(Object other) {
//Basic test / class cast
return this.catId==other.catId;
}
public int hashCode() {
int result;
return 3*this.catId; //any primenumber
}
}