Caching Fails with Composite-Ids Containing Nested, Complex Entities

Description

Description of Failing Test Case Scenario

Preconditions: An entity class is mapped that uses a composite-id that contains a nested entity class. Only the composite-id class implements equals/hashcode, not the nested entity class.

Steps to Reproduce:
1. open session and fetch object using composite-id
2. open new session and fetch same object again using different instance of composite-id but with same identity

Invalid Postconditions: On second retrieve, Hibernate fails to get the object from the cache and unnecessarily reloads the object. CachKeys containing different instances of the composite-id always fail to be equal even though they have the same persistent identity.

Attachment Contents

Code fix is attached as well as a Junit test case that reproduces the problem and validates the fix. The full Hibernate suite was also executed with no impact.

Attachment contains:

New Test Method:
org.hibernate.test.cache.BaseCacheProviderTestCase.testQueryCacheComplexItem

New Test Entity Items:
org\hibernate\test\cache\ComplexItem.hbm.xml
org.hibernate.test.cache.ComplexItem
org.hibernate.test.cache.ComplexItemPK

Code Fix:
org.hibernate.cache.CacheKey (see FIX comments)

Problem and Fix Details

Hibernate generally strives to use persistent identifiers for managing object identity rather than the equals/hashcode methods implemented by entity classes. While it is good practice to implement equals/hashcode, Hibernate does not generally force users to do this.

When wrapping a composite-id object, the current implementation of CacheKey fails to recurse through nested complex entities to query for equality based on persistent identity. Instead, when the recursion algorithm hits a complex entity, it invokes equals directly on that entity rather than further recursing through the identifier object.

Notably, the recursion logic for equals is not symmetrical with the recursion logic for hashcode, which does recurse through identifier objects. So, while CacheKey never invokes hashcode on nested complex entities, it does invoke equals on these entities.

A simple fix to this inconsistency is to store the factory parameter passed to CacheKey and later pass that parameter to the overloaded method:
Type.isEqual(Object x, Object y, EntityMode entityMode, SessionFactoryImplementor factory).

This fix restores symmetry to equals and hashcode behavior. By calling this overloaded method, the thread of execution will enter EntityType. isEqual(Object x, Object y, EntityMode entityMode, SessionFactoryImplementor factory), which correctly recurses through complex identifiers.

Design Principles

Hibernate should strive to behave predictably even in scenarios where users do not follow best practices.

Hibernate should strive to be as forgiving as possible as long there is no negative consequence caused by such forgiveness.

Hibernate should behave as consistently as possible. If Hibernate does not generally rely user-implemented equals/hashcode, it is best to avoid exceptions to this rule wherever possible.

Possible Future Enhancement

Mapping composite-ids that contain complex entities can cause deep object graphs to be cached as part of CacheKey. This is unsettling because of it's potential to consume memory unnecessarily and unpredictably.

Currently, CacheKey caches the hashcode by recursing through a complex graph of identifier objects. Perhaps, it would also be possible for CacheKey to cache an object graph of identifier objects whose leaves hold primitive values. This would further add symmetry between hashcode and equals and lighten the load for caching composite-ids that hold entity classes.

Robustly supporting composite-ids that hold complex identifiers seems like a worthwhile design goal.

Attachments

2
  • 06 Mar 2010, 12:53 AM
  • 11 Jun 2007, 04:08 PM

Activity

penweiJuly 24, 2015 at 8:22 AM

new vision hibernate4 has exist this problem?i'm use hibernate 4.1.6 still this problem

Brett MeyerJuly 8, 2014 at 3:10 PM

Bulk rejecting stale issues. If this is still a legitimate issue on ORM 4, feel free to comment and attach a test case. I'll address responses case-by-case. Thanks!

Brett MeyerApril 7, 2014 at 5:43 PM

In an effort to clean up, in bulk, tickets that are most likely out of date, we're transitioning all ORM 3 tickets to an "Awaiting Test Case" state. Please see http://in.relation.to/Bloggers/HibernateORMJIRAPoliciesAndCleanUpTactics for more information.

If this is still a legitimate bug in ORM 4, please provide either a test case that reproduces it or enough detail (entities, mappings, snippets, etc.) to show that it still fails on 4. If nothing is received within 3 months or so, we'll be automatically closing them.

Thank you!

ChristopherFMarch 6, 2010 at 12:53 AM

We're actually seeing a much more serious manifestation of this issue in our production systems, where composite keyed entities containing an uninitialized proxy are being put into the cache and breaking on a CacheKey hashCode() collision.

This is because the initial put() operation requires only a CacheKey.hashCode() call that does not invoke the entity hashCode() method or initialize the lazy proxy, but an operation with a hashCode() collision requires a CacheKey.hashCode() call and a CacheKey.equals() comparison, that latter of which does invoke the entity equals() method and attempts initialization of the lazy proxy outside it's associated session and throws a "org.hibernate.LazyInitializationException: could not initialize proxy - no Session", crippling the application until the cache is manually flushed. If the CacheKey hashCode() and equals() implementations were at least symmetrical with respect to invoking entity methods, the proxy would either be initialized before the first put(), or would not be initialized by any cache operations.

See attached test project that reproduces the issue.

Shihab HamidMay 28, 2009 at 7:39 AM

For the simple case mentioned above, we were able to work around the problem by mapping the composite-id to a component class.

Rejected

Details

Assignee

Reporter

Original estimate

Time tracking

No time logged

Affects versions

Priority

Created June 11, 2007 at 4:08 PM
Updated July 24, 2015 at 8:22 AM
Resolved July 8, 2014 at 3:10 PM

Flag notifications