|
@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:
SELECT dep.employees FROM Department dep
Neither the Employee nor the Department JPA entities are cached in L2, however the query above is cached.
Then I run the test case:
log.info("============== step 1 ================");
EntityManager em = ... Query q = em.createQuery("SELECT dep.employees FROM Department dep");
... List<Employee> list = q.getResultList();
em.close();
list.iterator().next().getId();
log.info("============== step 2 ================");
EntityManager em = ... Query q = em.createQuery("SELECT dep.employees FROM Department dep");
... List<Employee> list = q.getResultList();
em.close();
list.iterator().next().getId();
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:
public class MyQueryCache extends StandardQueryCache {
public MyQueryCache(Settings settings, Properties props, UpdateTimestampsCache updateTimestampsCache, String regionName)
throws HibernateException {
super(settings, props, updateTimestampsCache, regionName);
}
@Override
public boolean put(QueryKey key, Type[] returnTypes, List result,
boolean isNaturalKeyLookup, SessionImplementor session)
throws HibernateException {
return super.put(key, getCorrectedReturnTypes(returnTypes, session), result, isNaturalKeyLookup, session);
}
@Override
public List get(QueryKey key, Type[] returnTypes,
boolean isNaturalKeyLookup, Set spaces, SessionImplementor session)
throws HibernateException {
return super.get(key, getCorrectedReturnTypes(returnTypes, session), isNaturalKeyLookup, spaces, session);
}
private Type[] getCorrectedReturnTypes(Type[] returnTypes, SessionImplementor session) {
Type[] returnTypesNew = returnTypes;
if ( returnTypes.length == 1 ) {
Type type = returnTypes[0];
if (type instanceof SetType) {
String entityClassName = ((SetType) type).getElementType(session.getFactory()).getName();
returnTypesNew[0] = new ManyToOneType(entityClassName);
}
}
return returnTypesNew;
}
}
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):
INFO: ============== step 1 ================
Hibernate: select [..employee fields removed for brevity..] from CACHE_ISSUE_EMP employee0_ inner join CACHE_ISSUE_DEP department1_ on employee0_.DEPARTMENT_ID=department1_.DEPARTMENT_ID
INFO: ============== step 2 ================
Hibernate: select [..employee fields removed for brevity...] from CACHE_ISSUE_EMP employee0_ where employee0_.EMP_ID=?
Hibernate: select [..employee fields removed for brevity...] from CACHE_ISSUE_EMP employee0_ where employee0_.EMP_ID=?
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.
|