模型类的equals和hashcode应该如何在Hibernate中实现?常见的陷阱有哪些?对于大多数情况,默认实现是否足够好?使用业务密钥有什么意义吗?
在我看来,当考虑到延迟获取、id 生成、代理等时,很难让它在每种情况下都能正常工作。
Hibernate在文档中对何时/如何覆盖equals()
/有一个很好的详细描述hashCode()
Set
它的要点是,如果您的实体将成为 a 的一部分,或者您将要分离/附加其实例,您只需要担心它。后者并不常见。前者通常最好通过以下方式处理:
equals()
/hashCode()
基于业务密钥 - 例如,在对象(或至少会话)生命周期内不会更改的唯一属性组合。equals()
/hashCode()
在主键 IF 设置和对象标识 /System.identityHashCode()
否则。这里的重要部分是您需要在将新实体添加到 Set 并持久化后重新加载;否则您可能会遇到奇怪的行为(最终导致错误和/或数据损坏),因为您的实体可能被分配到与其当前不匹配的存储桶中hashCode()
。我不认为接受的答案是准确的。
要回答原始问题:
对于大多数情况,默认实现是否足够好?
答案是肯定的,在大多数情况下是这样。
您只需要覆盖equals()
并且hashcode()
如果实体将用于Set
(这是非常常见的)并且实体将从休眠会话中分离并随后重新连接到休眠会话(这是休眠的不常见用法)。
接受的答案表明,如果任一条件为真,则需要覆盖这些方法。
最好的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
and 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,而不是基础值,因此不要对属性使用比较,而是使用 getter,因为它们可能会触发加载基础值。
是的,这很难。在我的项目中,equals 和 hashCode 都依赖于对象的 id。这个解决方案的问题是,如果对象还没有被持久化,它们都不起作用,因为 id 是由数据库生成的。就我而言,这是可以容忍的,因为在几乎所有情况下,对象都会立即持久化。除此之外,它工作得很好并且很容易实现。
在 Hibernate 5.2 的文档中,它说您可能根本不想实现 hashCode 和 equals - 取决于您的情况。
一般来说,从同一个会话加载的两个对象如果在数据库中相等,则它们将相等(不实现 hashCode 和 equals)。
如果您使用两个或更多会话,它会变得复杂。在这种情况下,两个对象的相等性取决于您的 equals 方法实现。
此外,如果您的 equals 方法正在比较仅在第一次持久化对象时生成的 ID,您将遇到麻烦。当调用 equals 时,它们可能还不存在。
如果您碰巧覆盖equals
,请确保您履行其合同:-
和 override hashCode
,因为它的合同依赖于equals
实施。
Joshua Bloch(Collection 框架的设计者)强烈要求遵守这些规则。
如果您不遵守这些合同,则会产生严重的意外影响。例如,由于一般合同未履行,List#contains(Object o)
可能会返回错误的值。boolean
引用文章中的一句重要的话:
我们建议使用业务键相等来实现 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
}
}