tl;dr
See here for how to properly generate equals
and hashCode
for Hibernate. The catch is that with Hibernate you end up with a mix of Entity class and HibernateProxy
subclasses, which will not match the equals
methods suggested in most places.
Overview
Review the article linked above for the technical details of how equals
and hashCode
are used by Hibernate. The key is that these need to reflect the primary key of the entity so that when Hibernate needs an object for a database record it can locate it in its session.
Also, Hibernate uses proxies to allow for lazy loading of relationships, by overriding the getters.
Depending on your fetch strategy, you’ll end up with a mix of Entity classes and HibernateProxy
classes, particularly with @OneToMany
relationships, all based on when the entity was fetched or referenced.
In this example below there is a @OneToMany(fetch = FetchType.EAGER)
relationship (this is the default for JPA). In the screenshot you can see the contents of the List
contains a mix of object classes.
Specifically, you can see Person(id=1)
is duplicated as both a proxy and base Entity.
Now, with @OneToMany(fetch = FetchType.LAZY)
:
Everything is a proxy!
Consequences and Solution
In this particular case I was trying to group the results by person
, which produced invalid results because Person(id=1)
ended up in 2 groups. Interestingly, this error only became apparent when upgrading from Hibernate 5 to Hibernate 6, as the behaviour of @OneToMany
changed from default LAZY
to default EAGER
to conform with the JPA spec.
The terrible bandaide solution would be to force the fetch to always be LAZY
. This basically just hides a landmine for future developers.
The proper solution is to implement an equals
that is aware of HibernateProxy
and checks that the proxied classes are the same. In my case I made a helper class that is used by all entities.
public static <T> boolean equals(T a, Object b, Function<T, Object> keyExtractor) {
if (a == b) {
return true;
}
if (b == null) {
return false;
}
Class<?> bEffectiveClass = b instanceof HibernateProxy
? ((HibernateProxy) b).getHibernateLazyInitializer().getPersistentClass()
: b.getClass();
Class<?> aEffectiveClass = a instanceof HibernateProxy
? ((HibernateProxy) a).getHibernateLazyInitializer().getPersistentClass()
: a.getClass();
if (aEffectiveClass != bEffectiveClass) {
return false;
}
return keyExtractor.apply(a).equals(keyExtractor.apply((T) b));
}
Person.java:
public class Person {
...
@Override
public boolean equals(Object o) {
return EntityHelper.equals(this, o, Person::getId);
}
}