Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delegating hash-function to uninitialized delegates in hibernate causes changing hashCode

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>
like image 991
roesslerj Avatar asked Dec 31 '11 10:12

roesslerj


2 Answers

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.

like image 69
GeertPt Avatar answered Oct 26 '22 12:10

GeertPt


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.

  1. By calling getter methods in hashCode() of your model class as it's initializes/loads all model properties.
  2. By using sesstion.get() instead of session.load() method, as it won't create any proxy and will load all properties of your model.
  3. By setting lazy="false" for all your properties in mapping.

Hope this can solve your problem!

like image 33
Pokuri Avatar answered Oct 26 '22 10:10

Pokuri