这里有关于JPA实体的some discussions以及哪些hashCode()
/ equals()
实现应该用于JPA实体类。大多数(如果不是全部)它们依赖于Hibernate,但我想讨论它们JPA实现中性(顺便说一下,我使用的是EclipseLink)。
所有可能的实现都有各自的优点和缺点:
hashCode()
/ equals()
List
/ Set
业务合同合规性(不变性)据我所知,有三种选择:
Object.equals()
和Object.hashCode()
hashCode()
/ equals()
工作
无法识别相同的对象,动态代理的问题
分离实体没有问题hashCode()
/ equals()
被打破了
正确的身份(适用于所有管理实体)
分离实体的问题hashCode()
/ equals()
被打破了
正确的身份(适用于所有管理实体)
分离实体没有问题我的问题是:
更新1:
通过“hashCode()
/ equals()
被破坏”,我的意思是连续的hashCode()
调用可能返回不同的值,这是(当正确实现时)没有在Object
API文档的意义上被破坏,但是当尝试从中检索已更改的实体时会导致问题一个Map
,Set
或其他基于散列的Collection
。因此,在某些情况下,JPA实现(至少EclipseLink)将无法正常工作。
更新2:
感谢您的回答 - 其中大多数都具有卓越的品质。 不幸的是,我仍然不确定哪种方法对于现实应用程序最好,或者如何确定应用程序的最佳方法。所以,我会保持这个问题的开放性,希望能有更多的讨论和/或意见。
阅读这篇非常好的文章:Don't Let Hibernate Steal Your Identity。
文章的结论是这样的:
当对象持久化到数据库时,对象标识很难正确实现。但是,问题完全源于允许对象在保存之前没有id存在。我们可以通过从对象关系映射框架(如Hibernate)中分配对象ID来解决这些问题。相反,只要实例化对象,就可以分配对象ID。这使对象标识简单且无错误,并减少了域模型中所需的代码量。
这里显然已有非常丰富的答案,但我会告诉你我们做了什么。
我们什么都不做(即不要覆盖)。
如果我们确实需要equals / hashcode来处理集合,我们使用UUID。您只需在构造函数中创建UUID。我们使用@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {
private static final long serialVersionUID = 1L;
@Version
@Column(name = "version", nullable = false)
private int version = 0;
@Column(name = "uuid_least_sig_bits")
private long uuidLeastSigBits = 0;
@Column(name = "uuid_most_sig_bits")
private long uuidMostSigBits = 0;
private transient int hashCode = 0;
public AbstractJpaEntity() {
//
}
public abstract Integer getId();
public abstract void setId(final Integer id);
public boolean isPersisted() {
return getId() != null;
}
public int getVersion() {
return version;
}
//calling UUID.randomUUID() is pretty expensive,
//so this is to lazily initialize uuid bits.
private void initUUID() {
final UUID uuid = UUID.randomUUID();
uuidLeastSigBits = uuid.getLeastSignificantBits();
uuidMostSigBits = uuid.getMostSignificantBits();
}
public long getUuidLeastSigBits() {
//its safe to assume uuidMostSigBits of a valid UUID is never zero
if (uuidMostSigBits == 0) {
initUUID();
}
return uuidLeastSigBits;
}
public long getUuidMostSigBits() {
//its safe to assume uuidMostSigBits of a valid UUID is never zero
if (uuidMostSigBits == 0) {
initUUID();
}
return uuidMostSigBits;
}
public UUID getUuid() {
return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
}
@Override
public int hashCode() {
if (hashCode == 0) {
hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
}
return hashCode;
}
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof AbstractJpaEntity)) {
return false;
}
//UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
//dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even
//if they have different types) having the same UUID is astronomical
final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
}
@PrePersist
public void prePersist() {
// make sure the uuid is set before persisting
getUuidLeastSigBits();
}
}
作为UUID。 UUID是一个更加昂贵的CPU,但与序列化和数据库访问相比便宜。
业务密钥方法不适合我们。我们使用DB生成的ID,临时瞬态tempId和覆盖equal()/ hashcode()来解决这个难题。所有实体都是实体的后代。优点:
缺点:
看看我们的代码:
Object#getClass()
请根据预定义的类型标识符和ID考虑以下方法。
JPA的具体假设:
抽象实体:
http://wiki.fasterxml.com/JugHome
具体实例示例:
@MappedSuperclass
abstract public class Entity implements Serializable {
@Id
@GeneratedValue
@Column(nullable = false, updatable = false)
protected Long id;
@Transient
private Long tempId;
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
private void setTempId(Long tempId) {
this.tempId = tempId;
}
// Fix Id on first call from equal() or hashCode()
private Long getTempId() {
if (tempId == null)
// if we have id already, use it, else use 0
setTempId(getId() == null ? 0 : getId());
return tempId;
}
@Override
public boolean equals(Object obj) {
if (super.equals(obj))
return true;
// take proxied object into account
if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
return false;
Entity o = (Entity) obj;
return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
}
// hash doesn't change in time
@Override
public int hashCode() {
return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
}
}
测试示例:
@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {
@Id @GeneratedValue
private K id;
@Transient
private final String kind;
public AbstractPersistable(final String kind) {
this.kind = requireNonNull(kind, "Entity kind cannot be null");
}
@Override
public final boolean equals(final Object obj) {
if (this == obj) return true;
if (!(obj instanceof AbstractPersistable)) return false;
final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
return null != this.id
&& Objects.equals(this.id, that.id)
&& Objects.equals(this.kind, that.kind);
}
@Override
public final int hashCode() {
return Objects.hash(kind, id);
}
public K getId() {
return id;
}
protected void setId(final K id) {
this.id = id;
}
}
这里的主要优势:
缺点:
static class Foo extends AbstractPersistable<Long> {
public Foo() {
super("Foo");
}
}
笔记:
@Test
public void test_EqualsAndHashcode_GivenSubclass() {
// Check contract
EqualsVerifier.forClass(Foo.class)
.suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
.withOnlyTheseFields("id", "kind")
.withNonnullFields("id", "kind")
.verify();
// Ensure new objects are not equal
assertNotEquals(new Foo(), new Foo());
}
和super()
的实例平等可能取决于应用程序的具体细节。期待您的评论。
我过去总是使用选项1,因为我知道这些讨论并认为在我知道正确的事情之前什么也不做。这些系统仍然运行成功。
但是,下次我可以尝试选项2 - 使用数据库生成的Id。
如果未设置id,则Hashcode和equals将抛出IllegalStateException。
这将防止涉及未保存实体的细微错误意外出现。
人们怎么看待这种方法?
这是使用Java和JPA的每个IT系统中的常见问题。痛点不仅仅是实现equals()和hashCode(),还会影响组织引用实体的方式以及客户端如何引用同一个实体。我已经看到了没有业务关键的痛苦,我写了class A
来表达我的观点。
简而言之:使用一个简短的,人类可读的顺序ID和有意义的前缀作为生成的业务密钥,而不依赖于除RAM之外的任何存储。 Twitter的class B extends A
就是一个非常好的例子。
IMO你有3个实现equals / hashCode的选项
使用应用程序生成的标识是最简单的方法,但有一些缺点
如果您可以解决这些问题,请使用此方法。
为了克服连接问题,可以使用UUID作为自然键和序列值作为主键,但是您可能仍会遇到具有嵌入式ID的组合子实体中的equals / hashCode实现问题,因为您将要基于联接在主键上。使用子实体id中的自然键和引用父实体的主键是一个很好的折衷方案。
my own blog
IMO这是最干净的方法,因为它可以避免所有缺点,同时为您提供一个值(UUID),您可以与外部系统共享而不暴露系统内部。
如果您可以从用户那里获得一个好主意,那么基于业务键实现它,但也有一些缺点
大多数情况下,此业务键将是用户提供的某种代码,而不是多个属性的组合。
IMO您不应该专门使用或使用业务密钥。这是一个很好的附加组件,即用户可以快速搜索该业务键,但系统不应该依赖它进行操作。
基于主键实现它有问题,但也许它不是什么大问题
如果需要将ID公开给外部系统,请使用我建议的UUID方法。如果不这样做,您仍然可以使用UUID方法,但您不必这样做。在equals / hashCode中使用DBMS生成的id的问题源于在分配id之前可能已将对象添加到基于散列的集合的事实。
解决这个问题的显而易见的方法是在分配id之前不要将对象添加到基于散列的集合。我知道这并不总是可行的,因为您可能在分配ID之前需要重复数据删除。要仍然能够使用基于散列的集合,您只需在分配ID后重建集合。
你可以这样做:
Snowflake
我自己没有测试过确切的方法,所以我不确定在前后持续事件中如何更改集合,但这个想法是:
解决此问题的另一种方法是在更新/持久化之后简单地重建所有基于散列的模型。
最后,这取决于你。我个人大多数时候都使用基于序列的方法,只有在需要向外部系统公开标识符时才使用UUID方法。
如果UUID是许多人的答案,为什么我们不只是使用业务层的工厂方法来创建实体并在创建时分配主键?
例如:
@Entity class Parent {
@Id @GeneratedValue Long id;
@NaturalId UUID uuid;
@OneToMany(mappedBy = "parent") Set<Child> children;
// equals/hashCode based on uuid
}
@Entity class Child {
@EmbeddedId ChildId id;
@ManyToOne Parent parent;
@Embeddable class ChildId {
UUID parentUuid;
UUID childUuid;
// equals/hashCode based on parentUuid and childUuid
}
// equals/hashCode based on id
}
这样我们就可以从持久性提供程序中获取实体的默认主键,而我们的hashCode()和equals()函数可以依赖它。
我们还可以声明Car的构造函数受到保护,然后在我们的业务方法中使用反射来访问它们。这样开发人员就不会想要使用new实例化Car,而是通过工厂方法。
怎么回事?
我试着自己回答这个问题,直到我读完这篇文章,特别是DREW之前,我一直对找到的解决方案完全满意。我喜欢他懒惰的方式创建UUID并以最佳方式存储它。
但是我希望增加更多的灵活性,即只有在实体的第一次持久化之前访问hashCode()/ equals()并且具有每个解决方案的优点时,才会创建UUID:
我非常感谢下面对我的混合解决方案的反馈
@Entity class Parent { @Id @GeneratedValue Long id; @OneToMany(mappedBy = "parent") Set<Child> children; // equals/hashCode based on id } @Entity class Child { @EmbeddedId ChildId id; @ManyToOne Parent parent; @PrePersist void postPersist() { parent.children.remove(this); } @PostPersist void postPersist() { parent.children.add(this); } @Embeddable class ChildId { Long parentId; @GeneratedValue Long childId; // equals/hashCode based on parentId and childId } // equals/hashCode based on id }
在实践中,似乎最常使用选项2(主键)。自然和IMMUTABLE业务密钥很少,创建和支持合成密钥太重,无法解决可能永远不会发生的情况。看看@ManagedBean
public class MyCarFacade {
public Car createCar(){
Car car = new Car();
em.persist(car);
return car;
}
}
实现(唯一的东西:public class MyEntity {
@Id()
@Column(name = "ID", length = 20, nullable = false, unique = true)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
@Transient private UUID uuid = null;
@Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
private Long uuidMostSignificantBits = null;
@Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
private Long uuidLeastSignificantBits = null;
@Override
public final int hashCode() {
return this.getUuid().hashCode();
}
@Override
public final boolean equals(Object toBeCompared) {
if(this == toBeCompared) {
return true;
}
if(toBeCompared == null) {
return false;
}
if(!this.getClass().isInstance(toBeCompared)) {
return false;
}
return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
}
public final UUID getUuid() {
// UUID already accessed on this physical object
if(this.uuid != null) {
return this.uuid;
}
// UUID one day generated on this entity before it was persisted
if(this.uuidMostSignificantBits != null) {
this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
// UUID never generated on this entity before it was persisted
} else if(this.getId() != null) {
this.uuid = new UUID(this.getId(), this.getId());
// UUID never accessed on this not yet persisted entity
} else {
this.setUuid(UUID.randomUUID());
}
return this.uuid;
}
private void setUuid(UUID uuid) {
if(uuid == null) {
return;
}
// For the one hypothetical case where generated UUID could colude with UUID build from IDs
if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
}
this.uuidMostSignificantBits = uuid.getMostSignificantBits();
this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
this.uuid = uuid;
}
)。
spring-data-jpa AbstractPersistable
只知道在HashSet / HashMap中操作新对象。相反,选项1(保持for Hibernate implementation use Hibernate.getClass
实施)在public boolean equals(Object obj) {
if (null == obj) {
return false;
}
if (this == obj) {
return true;
}
if (!getClass().equals(ClassUtils.getUserClass(obj))) {
return false;
}
AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
return null == this.getId() ? false : this.getId().equals(that.getId());
}
@Override
public int hashCode() {
int hashCode = 17;
hashCode += null == getId() ? 0 : getId().hashCode() * 31;
return hashCode;
}
之后被打破,这是非常常见的情况。
如果您没有业务密钥并且需要在哈希结构中操纵新实体,则将Object
覆盖为常量,如下面的Vlad Mihalcea所建议的那样。
以下是Scala的简单(和测试)解决方案。
Scala代码:
merge
我总是重写equals / hashcode并根据业务ID实现它。对我来说似乎是最合理的解决方案。请参阅以下link。
总结所有这些内容,这里列出了处理equals / hashCode的不同方法将起作用或不起作用的列表:
编辑:
解释为什么这对我有用:
我们的实体通常有两个ID:
看一看:
equals()
编辑:澄清我关于调用hashCode()
方法的观点。这是一个典型的场景:
@Entity
public class User {
@Id
private int id; // Persistence ID
private UUID uuid; // Business ID
// assuming all fields are subject to change
// If we forbid users change their email or screenName we can use these
// fields for business ID instead, but generally that's not the case
private String screenName;
private String email;
// I don't put UUID generation in constructor for performance reasons.
// I call setUuid() when I create a new entity
public User() {
}
// This method is only called when a brand new entity is added to
// persistence context - I add it as a safety net only but it might work
// for you. In some cases (say, when I add this entity to some set before
// calling em.persist()) setting a UUID might be too late. If I get a log
// output it means that I forgot to call setUuid() somewhere.
@PrePersist
public void ensureUuid() {
if (getUuid() == null) {
log.warn(format("User's UUID wasn't set on time. "
+ "uuid: %s, name: %s, email: %s",
getUuid(), getScreenName(), getEmail()));
setUuid(UUID.randomUUID());
}
}
// equals() and hashCode() rely on non-changing data only. Thus we
// guarantee that no matter how field values are changed we won't
// lose our entity in hash-based Sets.
@Override
public int hashCode() {
return getUuid().hashCode();
}
// Note that I don't use direct field access inside my entity classes and
// call getters instead. That's because Persistence provider (PP) might
// want to load entity data lazily. And I don't use
// this.getClass() == other.getClass()
// for the same reason. In order to support laziness PP might need to wrap
// my entity object in some kind of proxy, i.e. subclassing it.
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (!(obj instanceof User))
return false;
return getUuid().equals(((User) obj).getUuid());
}
// Getters and setters follow
}
当我运行我的测试并看到日志输出我解决了问题:
setUuid()
或者,可以提供单独的构造函数:
User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("[email protected]");
jediSet.add(user); // here's bug - we forgot to set UUID and
//we won't find Yoda in Jedi set
em.persist(user); // ensureUuid() was called and printed the log for me.
jediCouncilSet.add(user); // Ok, we got a UUID now
所以我的例子看起来像这样:
User user = new User();
user.setUuid(UUID.randomUUID());
我使用默认构造函数和setter,但您可能会发现更适合您的双构造函数方法。
如果你想在你的集合中使用@Entity
public class User {
@Id
private int id; // Persistence ID
private UUID uuid; // Business ID
... // fields
// Constructor for Persistence provider to use
public User() {
}
// Constructor I use when creating new entities
public User(UUID uuid) {
setUuid(uuid);
}
... // rest of the entity.
}
,那么同一个实体只能在那里一次,那么只有一个选项:选项2.这是因为按照定义,实体的主键永远不会改变(如果确实有人更新它,它不再是同一个实体)
您应该从字面上理解:由于您的User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time
em.persist(user); // and no log output
基于主键,因此在设置主键之前,不得使用这些方法。因此,在为实体分配主键之前,不应将实体放入集合中。 (是的,UUID和类似的概念可能有助于尽早分配主键。)
现在,理论上也可以使用选项3实现这一点,即使所谓的“业务键”具有可以改变的令人讨厌的缺点:“所有你需要做的就是从集合中删除已插入的实体( s),并重新插入它们。“这是事实 - 但它也意味着,在分布式系统中,您必须确保在数据插入的任何地方都完成了(并且您必须确保执行更新) ,在其他事情发生之前)。您需要一个复杂的更新机制,特别是如果某些远程系统当前无法访问...
如果集合中的所有对象来自同一个Hibernate会话,则只能使用选项1。 Hibernate文档在equals()/hashCode()
章节中非常清楚:
在Session中,应用程序可以安全地使用==来比较对象。
但是,在会话外使用==的应用程序可能会产生意外结果。即使在某些意想不到的地方也可能发生例如,如果将两个分离的实例放入同一个Set中,则两者可能具有相同的数据库标识(即它们代表同一行)。但是,根据定义,JVM标识不能保证处于分离状态的实例。开发人员必须覆盖持久化类中的equals()和hashCode()方法,并实现自己的对象相等概念。
它继续支持备选方案3:
有一点需要注意:永远不要使用数据库标识符来实现相等性。使用业务键,该键是唯一的,通常不可变的属性的组合。如果瞬态对象是持久的,则数据库标识符将更改。如果瞬态实例(通常与分离的实例一起)保存在Set中,则更改哈希码会破坏Set的约定。
如果你这是真的
否则,您可以自由选择选项2。
然后它提到了相对稳定性的必要性:
业务键的属性不必像数据库主键一样稳定;只要对象在同一个Set中,您就必须保证稳定性。
这是对的。我看到的实际问题是:如果你不能保证绝对的稳定性,只要对象在同一个Set中,你怎么能保证稳定性。我可以想象一些特殊情况(比如仅使用集合进行对话,然后将其丢弃),但我会质疑这种情况的一般实用性。
精简版:
我个人已经在不同的项目中使用了所有这三种状态。我必须说选项1在我看来是现实生活中最实用的应用程序。使得体验破坏hashCode()/ equals()符合性会导致许多疯狂的错误,因为在将实体添加到集合之后,每次都会出现平等变化结果发生变化的情况。
但还有其他选择(也有其优缺点):
a)hashCode / equals基于一组不可变的,非空的构造函数分配的字段
(+)所有三个标准都有保证
( - )字段值必须可用于创建新实例
( - )如果必须更改其中一个,则复杂处理
b)hashCode / equals基于由应用程序(在构造函数中)而不是JPA分配的主键
(+)所有三个标准都有保证
( - )你不能利用简单可靠的ID生成策略,如DB序列
( - )如果在分布式环境(客户端/服务器)或应用服务器集群中创建新实体,则会很复杂
c)hashCode / equals基于由实体构造函数分配的equals()/hashCode()
(+)所有三个标准都有保证
( - )UUID生成的开销
根据所使用的算法(可能由DB上的唯一索引检测到),( - )可能会有一点风险,使用两倍相同的UUID
equals
equals和hashCode实现,因为在hashCode
和entity之后这不起作用。Object
。唯一的问题是你需要使用总是返回相同值的merge
实现,如下所示:
use the entity identifier as suggested in this post尽管使用业务密钥(选项3)是最常推荐的方法(hashCode
,“Java Persistence with Hibernate”,第398页),这是我们最常使用的,但是有一个Hibernate错误可以解决这个问题:eager-fetched sets: @Entity
public class Book implements Identifiable<Long> {
@Id
@GeneratedValue
private Long id;
private String title;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book)) return false;
Book book = (Book) o;
return getId() != null && Objects.equals(getId(), book.getId());
}
@Override
public int hashCode() {
return 31;
}
//Getters and setters omitted for brevity
}
。在这种情况下,Hibernate可以在其字段初始化之前向集合添加实体。我不确定为什么这个bug没有得到更多的关注,因为它确实使推荐的业务密钥方法成为问题。
我认为问题的核心是equals和hashCode应该基于不可变状态(引用Hibernate community wiki),并且具有Hibernate管理主键的Hibernate实体没有这样的不可变状态。当瞬态对象变得持久时,主键由Hibernate修改。当Hibernate在初始化过程中对对象进行水合时,业务键也会被修改。
这样只留下选项1,继承基于对象标识的java.lang.Object实现,或者使用James Brundege在HHH-3799(已由Stijn Geukens的回答引用)和Lance Arlaus在Odersky et al.中建议的应用程序管理的主键。
选项1的最大问题是分离的实例无法与使用.equals()的持久实例进行比较。但那没关系; equals和hashCode的契约让开发人员决定每个类的平等意味着什么。所以让equals和hashCode继承自Object。如果需要将分离的实例与持久化实例进行比较,可以为此目的明确创建一个新方法,可能是"Don't Let Hibernate Steal Your Identity"或"Object Generation: A Better Approach to Hibernate Integration"或boolean sameEntity
。
我同意安德鲁的回答。我们在应用程序中执行相同的操作,但不是将UUID存储为VARCHAR / CHAR,而是将其拆分为两个长值。请参阅UUID.getLeastSignificantBits()和UUID.getMostSignificantBits()。
还有一件事要考虑,对UUID.randomUUID()的调用非常慢,所以你可能只想在需要时懒洋洋地生成UUID,例如在持久性或调用equals()/ hashCode()期间
boolean dbEquivalent
正如其他比我聪明的人已经指出的那样,那里有很多策略。虽然大多数应用的设计模式试图破解成功的方式,但似乎是这种情况。如果不使用专门的构造函数和工厂方法完全阻碍构造函数调用,它们会限制构造函数访问。事实上,使用明确的API总是令人愉快。但如果唯一的原因是使equals-和hashcode覆盖与应用程序兼容,那么我想知道这些策略是否符合KISS(Keep It Simple Stupid)。
对我来说,我喜欢通过检查id来覆盖equals和hashcode。在这些方法中,我要求id不为null并很好地记录此行为。因此,在将新实体存储到其他地方之前,将成为开发者合同。不遵守本合同的申请将在一分钟内失败(希望如此)。
但请注意:如果您的实体存储在不同的表中,并且您的提供程序使用主键的自动生成策略,那么您将获得跨实体类型的重复主键。在这种情况下,还要将运行时类型与boolean businessEquals
的调用进行比较,这当然会使两种不同的类型被认为是不相等的。在大多数情况下,这对我来说很合适。