这两天一直百思不得其解的问题终于解决了,这个问题如下:
通过HQL:“select distinct forumGroup from ForumGroup as forumGroup left join fetch forumGroup.forums”查询所有ForumGroup,并将它们的Forum一并抓取出来。查询启用了查询缓存,ForumGroup和Forum都被映射为可缓存的。
第一次执行时,自然不会命中,生成了如下SQL:
/* select
distinct forumGroup
from
ForumGroup as forumGroup
left join
fetch forumGroup.forums */ select
distinct forumgroup0_.id as id3_0_,
forums1_.id as id2_1_,
forumgroup0_.creationTime as creation2_3_0_,
forumgroup0_.description as descript3_3_0_,
forumgroup0_.modifiedTime as modified4_3_0_,
forumgroup0_.name as name3_0_,
forums1_.creationTime as creation2_2_1_,
forums1_.description as descript3_2_1_,
forums1_.groupId as groupId2_1_,
forums1_.modifiedTime as modified4_2_1_,
forums1_.name as name2_1_,
forums1_.groupId as groupId0__,
forums1_.id as id0__
from
ForumGroup forumgroup0_
left outer join
Forum forums1_
on forumgroup0_.id=forums1_.groupId
第二次执行时,按理是应该命中,不会有任何SQL生成,但是实际结果却是:
DEBUG [13178395@qtp-12191562-0] StandardQueryCache.get(142) | returning cached query results
/* load one-to-many oobbs.domainmodel.forum.ForumGroup.forums */ select
forums0_.groupId as groupId1_,
forums0_.id as id1_,
forums0_.id as id2_0_,
forums0_.creationTime as creation2_2_0_,
forums0_.description as descript3_2_0_,
forums0_.groupId as groupId2_0_,
forums0_.modifiedTime as modified4_2_0_,
forums0_.name as name2_0_
from
Forum forums0_
where
forums0_.groupId=?
.
.
.
重复出现上述SQL.
分析上面的日志我发现:1.查询已经命中,这一点是确认的。2.重复出现的SQL是在迭代ForumGroup中试图访问它的Fourm集合时生成的,这是一个典型的N+1次查询问题。
这里让我迷惑的是:Forum被标记为了可缓存,明明是被Fetch出来的,在第一次查询时它们就被加载出来并进入到二级缓存了,为什么在第二次第三次查询时却找不到这些对象,还要重新查数据库呢?经过排查发现,在ForumGroup的Forum集合字段上没有将该集合配制为可缓存。这样,虽然第一次这些Forum都被抓取出来并进入了二级缓存,但是ForumGroup对象的forums集合(一个存放Forum的ID的集合,不是Forum对象的集合)在上次查询时就没有进入二级缓存,现在,这些集合没有保存forum的ID(在第一次查询的结果集中是有这些ID的,但是因为这个forums集合没有配为可缓存的,所以在ForumGroup对象进入二级缓存时,这些forum集合的信息就被舍弃了)。 这样,下一次查询时,虽然查询命中,但是查询结果中的ForumGroup的fourms集合是空的,因而会重新生成SQL查询。
另外,通过Debug,我发现,Query Cache缓存一个查询,其key就不多说了,在log中都会打出,其Value很有意思。上面的这个查询这的value是[5228208135548928, 1, 1, 2],第一个数据是一个时间戳,第二,三,四就是ForumGroup的ID。因为做过表连接,所以ForumGroup_1重复出现了一次。Forum的ID并没有在缓存的value中。由此可以确定,Query Cache只缓存结果集中对象的ID。被Fetch出来的Forum不被视为结果集的一部分,因而没有出现在结果集中。
还会有一些相似的问题可能会出现,比如:
相同条件第一次 list的时候,因为查询缓存中找不到,不管class缓存是否存在数据,总是发送一条sql语句到数据库获取全部数据,然后填充查询缓存和class缓存。但是第二次执行的时候,问题就来了,如果你的class缓存的超时时间比较短,现在class缓存都超时了,但是查询缓存还在,那么list方法在获取id串以后,将会一个一个去数据库load( N+1次 查询问题 )!因此,class缓存的超时时间一定不能短于查询缓存设置的超时时间!如果还设置了发呆时间的话,保证class缓存的发呆时间也大于查询缓存的生存时间。这里还有其他情况,比如class缓存被程序强制evict了,这种情况就请自己注意了。如果以上问题没有处理好,就必然会出现N+1次查询问题。
通过这个问题,总结两点:
1.查询缓存只保存储查询结果集中对象的ID,对象的实例存放在二级缓存中。这种缓存格局可能有两种不一致的情况发生:一.二级缓存中有实体对象,但是查询缓存中没有缓存某些关联对象的ID,这时个会导致重新生成SQL查询数据库。比如上面提到的Forums集合问题。二.查询缓存中有对象的ID,但是这些对象的实例却不在二级缓存中了。比如二级缓存已经失效等等,这也会导致生成SQL查询数据库。
2.对于集合字段,必须显式地使用@Cache标记,集合才会被缓存。注意,这里缓存的并不是集合的元素,而是元素的ID。集合元素能否被缓存取决于元素类有没有声明为可缓存的。 如果没有配制集合为可缓存,那么,即使在第一次查询时它们都进行入了二级缓存,下一次通过宿主类导航这个集合时还是会生成SQL,因为集合没有缓存,也就是所有元素的ID没有缓存,Hibernate不知道宿主对象关联的是那一些元素。虽然这些元素都已经在缓存中了。
从这个例子中我们可以看到:Cache的设置总是静态的全局的,不像Fetch那样可以动态重写。