Rebuild fetches from query-cache hits

Description

I have two domain objects - Employee and Department - and there is a 1:N relationship from Department to Employee. When I join fetch an Employee with its Department, without query cache enabled, it works fine. If I enable query cache for this same query, it bombs with a LazyInitializationException.
Notes:
1) We get this error only if query cache is enabled.
2) We observed the same behaviour on both oscache and ehcache
3) Calling Hibernate.initialize() explicitly after firing the HQL seems to work. The initialization does not fire an extra query though (it seems to pick it from the cache).
4) Setting "lazy=false" on the "many-to-one" mapping also works. However, it wouldn't be acceptable.

Hibernate version: 3.0.5

Mapping documents:

Employee.hbm.xml

<hibernate-mapping>
<class name="tavant.platform.test.domain.Employee"
table="CACHE_ISSUE_EMP" lazy="true" dynamic-update="true" dynamic-insert="true">

<cache usage="read-write" />

<id name="id" column="EMP_ID" type="java.lang.Long"
access="field" unsaved-value="null">
<generator class="increment" />
</id>

<property name="name" type="string" update="true"
insert="true" column="EMP_NAME"/>

<many-to-one name="department" class="tavant.platform.test.domain.Department"
cascade="none" outer-join="auto" update="true" insert="true" column="DEPARTMENT_ID"/>
</class>
</hibernate-mapping>

Department.hbm.xml

<hibernate-mapping>
<class name="tavant.platform.test.domain.Department" table="CACHE_ISSUE_DEP"
lazy="true" dynamic-update="true" dynamic-insert="true">

<cache usage="read-write" />

<id name="id" column="DEPARTMENT_ID"
type="java.lang.Long" access="field">
<generator class="increment"/>
</id>

<property name="name" type="java.lang.String"
update="false" insert="true" column="NAME"/>

<bag name="employees" lazy="true"
inverse="true" cascade="save-update" access="field">
<cache usage="read-write"/>

<key column="DEPARTMENT_ID"/>

<one-to-many class="tavant.platform.test.domain.Employee"/>
</bag>
</class>
</hibernate-mapping>

Code between sessionFactory.openSession() and session.close():

public Employee getEmployeeWithDepartment(String empName) {
Session session = null;
try {
session = sessionFactory.openSession();
Employee emp = (Employee) session.createQuery(
"from Employee e join fetch e.department where e.name = :name")
.setString("name", empName)
.setCacheable(true)
.uniqueResult();
// If I uncomment the next line, this works (even without
// firing an extra query)!
// Hibernate.initialize(emp.getDepartment());
return emp;
} finally {
if (session != null) {
session.close();
}
}
}

// First load employee and populate cahces
Employee emp = test.getEmployeeWithDepartment(EMPLOYEE_NAME);
System.out.println("Employee : " + emp + ", Employee.Department : "
+ emp.getDepartment());

// Now try to make use of the cache
emp = test.getEmployeeWithDepartment(EMPLOYEE_NAME);
System.out.println("Employee : " + emp + ", Employee.Department : "
+ emp.getDepartment());

Full stack trace of any exception that occurs:

org.hibernate.LazyInitializationException: could not initialize proxy - the owning Session was closed
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:53)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:84)
at org.hibernate.proxy.CGLIBLazyInitializer.intercept(CGLIBLazyInitializer.java:134)
at tavant.platform.test.domain.Department$$EnhancerByCGLIB$$67b26899.toString(<generated>)
at java.lang.String.valueOf(String.java:2131)
at java.lang.StringBuffer.append(StringBuffer.java:370)
at tavant.platform.test.client.TestPrefetchRelationWithQueryCacheEnabled.main(TestPrefetchRelationWithQueryCacheEnabled.java:116)

Name and version of the database you are using:
We have noticed this on Oracle and HSQL

The generated SQL (show_sql=true):

#First read, goes fine
Hibernate: select employee0_.EMP_ID as EMP1_0_, department1_.DEPARTMENT_ID as DEPARTMENT1_1_, employee0_.EMP_NAME as EMP2_1_0_, employee0_.DEPARTMENT_ID as DEPARTMENT3_1_0_, department1_.NAME as NAME0_1_ from CACHE_ISSUE_EMP employee0_ inner join CACHE_ISSUE_DEP department1_ on employee0_.DEPARTMENT_ID=department1_.DEPARTMENT_ID where (employee0_.EMP_NAME=? )

#Prints the Employee and Department fine
Employee : [Id : 1, name : testEmployee], Employee.Department : [Id : 1, name : testDepartment]

#Second read bombs!
org.hibernate.LazyInitializationException: could not initialize proxy - the owning Session was closed
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:53)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:84)
at org.hibernate.proxy.CGLIBLazyInitializer.intercept(CGLIBLazyInitializer.java:134)
[..]

Please have a look at the post http://forum.hibernate.org/viewtopic.php?t=955839 for more details and follow ups.

Kindly help. I am attaching an Eclipse Project containing the TestCase. The main file is TestPrefetchRelationWithQueryCacheEnabled.

Thanks,
Vikas

Environment

None

Activity

Show:
Julien Kronegg
October 6, 2013, 7:56 PM

@Erkki: did you try the method suggested in my post from 11/nov./11 7:07 PM ?

Erkki Mickelsson
October 6, 2013, 8:52 PM
Edited

@Julien
Yes, although with Hibernate 4.2. The patch fixes the LazyInitializationException for cases where the association can be found from cache. But if the association has expired from the cache (and the query has not) it will act just like before, creating a proxy for the association and thus in detached state causing a LazyInitializationException during proxy access.

Julien Kronegg
August 28, 2014, 1:58 PM

@Steve, @Erkki:
I finaly found some time to experiment further on this issue. I'm using a variant of the query and test case described in my post from 11/nov./2011 7:07 PM:

Neither the Employee nor the Department JPA entities are cached in L2, however the query above is cached.

Then I run the test case:

Digging into the org.hibernate.cache.StandardQueryCache, it appears that during the first step, the StandardQueryCache.put(QueryKey key, Type[] returnTypes, List result, boolean isNaturalKeyLookup, SessionImplementor session) method is called with a returnTypes=[org.hibernate.type.SetType(Employee)]. The SetType.disassemble(value,session,owner) method is used to provide a cacheable representation of the List<Employee>. However, the SetType.disassemble() method always returns null because the provided owner is null. Consequently, the cacheRegion is updated with a cacheable as an ArrayList containing a timestamp and result.size() null values (instead of the Employee.id for each List item).

During the 2nd step, the StandardQueryCache.get(QueryKey key, Type[] returnTypes, boolean isNaturalKeyLookup, Set spaces, SessionImplementor session) method is called because the query is cached. The cached values are obtained as List cacheable = (List)cacheRegion.get(key). In our case, this list contains the timestamp and the null values. Reassembling the List values lead to a List of null elements instead of a List<Employee>. Consequently, the list.iterator().next().getId(); in the 2nd step raises a NullPointerException.

The cause may be either:

  • the SetType is not capable to assemble/disassemble the data when used for by the StandardQueryCache (it's a design issue)

  • the StandardQueryCache is not called with the correct returnTypes (it's a bug)

As a workaround, I've created the following MyQueryCache which extends the StandardQueryCache as follow:

Basically, this custom query cache implementation replaces the SetType of the returnTypes by the (supposely equivalent) ManyToOneType. I also created the adhoc query cache factory (a copy of StandardQueryCacheFactory which returns a MyQueryCache instaqnce), and configured the persistence.xml by adding a <property name="hibernate.cache.query_cache_factory" value="my.testpackage.MyQueryCacheFactory" />.

Using this custom query cache implementation, the final result is the same when running the first and second steps. However, the behavior is not the same due to the query cache behavior (one query with INNER JOIN for the first step and two simple query for the second step):

The great thing about this workaround is that it does not require to change the Hibernate version. This should match your expectations, Erkki.

Of course, with this simple example, the query cache efficient is not evident since we replace one query by two queries. But this is only a test case.

For reference, I used : Hibernate 3.3.1 (but the StandardQueryCache is very similar other Hibernate versions such as in 4.3.5), EhCache 2.4.3, DB2.

Erkki Mickelsson
September 10, 2014, 5:12 PM

@Julien

I solved all of my LazyInitializationExceptions by using the new ENABLE_LAZY_LOAD_NO_TRANS option, which allows lazy loads outside of session. It works like a charm and simplified my code a lot. Just beware of the few bugs related to it like HHH-7971.

Steve Ebersole
April 29, 2020, 6:35 PM

Still need more tests, but the one I wrote fetching many-to-one worked

Assignee

Steve Ebersole

Reporter

Vikas Sasidharan

Fix versions

Labels

backPortable

None

Suitable for new contributors

None

Requires Release Note

None

Pull Request

None

backportDecision

None

Affects versions

Priority

Major
Configure