I have a problem with hashCode()
that delegates to uninitialized objects using hibernate.
My data-model looks as follows (the following code is highly pruned to stress the problem and thus broken, do not replicate!):
class Compound {
@FetchType.EAGER
Set<Part> parts = new HashSet<Part>();
String someUniqueName;
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getSomeUniqueName() == null) ? 0 : getSomeUniqueName().hashCode());
return result;
}
}
class Part {
Compound compound;
String someUniqueName;
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getCompound() == null) ? 0 : getCompound().hashCode());
result = prime * result + ((getSomeUniqueName() == null) ? 0 : getSomeUniqueName().hashCode());
return result;
}
}
Please note that the implementation of hashCode()
thoroughly follows the advice as given in the hibernate documentation.
Now if I load an object of type Compound
, it eagerly loads the HasSet
with the parts. This calls the hashCode()
on the parts, which in turn calls the hashCode()
on the compound. However the problem is that at this point, not all values that are considered for creating the hashCode of the compound are yet available. Therefore, the hashCode of the parts changes after initialization is complete, thus braking the contract of the HashSet
and leading to all kinds of difficult-to-track-down errors (like e.g. having the same object in the parts set twice).
So my question is: What is the simplest solution to avoid this problem (I'd like to avoid writing classes for custom loading/initialization)? Do I do anything wrong here entirely?
Edit: Am I missing something here? This seems to be a basic problem, why don't I find anything about it anywhere?
Instead of using the database identifier for the equality comparison, you should use a set of properties for equals() that identify your individual objects. [...] No need to use the persistent identifier, the so called "business key" is much better. It's a natural key, but this time there is nothing wrong in using it! (article from hibernate)
And
It is recommended that you implement equals() and hashCode() using Business key equality. Business key equality means that the equals() method compares only the properties that form the business key. It is a key that would identify our instance in the real world (a natural candidate key). (hibernate documentation)
Edit: This is the stack trace when the loading happens (in case this helps). At that point in time, the attribute someUniqueName
is null and thus the hashCode is calculated wrongly.
Compound.getSomeUniqueName() line: 263
Compound.hashCode() line: 286
Part.hashCode() line: 123
HashMap<K,V>.put(K, V) line: 372
HashSet<E>.add(E) line: 200
HashSet<E>(AbstractCollection<E>).addAll(Collection<? extends E>) line: 305
PersistentSet.endRead() line: 352
CollectionLoadContext.endLoadingCollection(LoadingCollectionEntry, CollectionPersister) line: 261
CollectionLoadContext.endLoadingCollections(CollectionPersister, List) line: 246
CollectionLoadContext.endLoadingCollections(CollectionPersister) line: 219
EntityLoader(Loader).endCollectionLoad(Object, SessionImplementor, CollectionPersister) line: 1005
EntityLoader(Loader).initializeEntitiesAndCollections(List, Object, SessionImplementor, boolean) line: 993
EntityLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 857
EntityLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 274
EntityLoader(Loader).loadEntity(SessionImplementor, Object, Type, Object, String, Serializable, EntityPersister, LockOptions) line: 2037
EntityLoader(AbstractEntityLoader).load(SessionImplementor, Object, Object, Serializable, LockOptions) line: 86
EntityLoader(AbstractEntityLoader).load(Serializable, Object, SessionImplementor, LockOptions) line: 76
SingleTableEntityPersister(AbstractEntityPersister).load(Serializable, Object, LockOptions, SessionImplementor) line: 3293
DefaultLoadEventListener.loadFromDatasource(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 496
DefaultLoadEventListener.doLoad(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 477
DefaultLoadEventListener.load(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 227
DefaultLoadEventListener.proxyOrLoad(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 269
DefaultLoadEventListener.onLoad(LoadEvent, LoadEventListener$LoadType) line: 152
SessionImpl.fireLoad(LoadEvent, LoadEventListener$LoadType) line: 1090
SessionImpl.internalLoad(String, Serializable, boolean, boolean) line: 1038
ManyToOneType(EntityType).resolveIdentifier(Serializable, SessionImplementor) line: 630
ManyToOneType(EntityType).resolve(Object, SessionImplementor, Object) line: 438
TwoPhaseLoad.initializeEntity(Object, boolean, SessionImplementor, PreLoadEvent, PostLoadEvent) line: 139
QueryLoader(Loader).initializeEntitiesAndCollections(List, Object, SessionImplementor, boolean) line: 982
QueryLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 857
QueryLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 274
QueryLoader(Loader).doList(SessionImplementor, QueryParameters) line: 2542
QueryLoader(Loader).listIgnoreQueryCache(SessionImplementor, QueryParameters) line: 2276
QueryLoader(Loader).list(SessionImplementor, QueryParameters, Set, Type[]) line: 2271
QueryLoader.list(SessionImplementor, QueryParameters) line: 459
QueryTranslatorImpl.list(SessionImplementor, QueryParameters) line: 365
HQLQueryPlan.performList(QueryParameters, SessionImplementor) line: 196
SessionImpl.list(String, QueryParameters) line: 1268
QueryImpl.list() line: 102
<my code where the query is executed>
You have a perfect legitimate use case, and indeed it should work. You would have the same problem in regular Java however, if you would set the 'parts' of the Compound object before you set the 'someUniqueName'.
So if you could convince hibernate to set the 'someUniqueName' property before the 'parts' property. Did you experiment with just reordering them in the java class? Or renaming 'parts' to 'zparts'? The hibernate docs just say the order is not guaranteed. I'd file a bug in hibernate to allow to enforce this order...
Another solution that might be easier:
class Part {
public int hashCode() {
//don't include getCompound().hashCode()
return getSomeUniqueName() == null ? 0 : getSomeUniqueName().hashCode();
}
public boolean equals(Object o)
{
if (this == o) return true;
if (!o instanceof Part) return false;
Part part = (Part) o;
if (getCompound() != null ? !getCompound().equals(part.getCompound()) : part.getCompound()!= null)
return false;
if (getSomeUniqueName()!= null ? !getSomeUniqueName().equals(part.getSomeUniqueName()) : part.getSomeUniqueName()!= null)
return false;
return true;
}
}
In the Compound.equals() make sure it also starts with
public boolean equals(Object o)
{
if (this == o) return true;
This should avoid the problem you are having now.
Every property in the hashCode() method should be in the equals() method, but not necessarily the other way around.
From your question I understood that all your model properties which ever participating in hashCode()
method were not loaded by default. In that case If you want all your properties to get loaded then you can follow to ways.
hashCode()
of your model class as it's initializes/loads all model properties.sesstion.get()
instead of session.load()
method, as it won't create any proxy and will load all properties of your model.lazy="false"
for all your properties in mapping.Hope this can solve your problem!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With