上篇 帖子
Hibernate查询缓存的一个可靠性问题 说发现Hibernate 的 Query Cache 在使用 SQL Query 时的一个可靠性 Bug, 即在关联表数据修改后,无法查询出最新数据的问题。 经过源码分析,找到问题所在,并通过修改 Hibernate 源代码,成功解决了此问题。
Hibernate 3.1, JDK 1.4
1. 问题原因:
走了一下源码,大致找到了问题所在, SQLQueryReturnProcessor 在分析 query 时,用于判断缓存失效的 querySpaces[]只分析出了返回结果对应的表,例如例子中的权限点表:sys_perm, 而sql中的关联表没有分析出来,并放到 querySpaces[] 中。
具体分析:
类 org.hibernate.loader.custom.SQLCustomQuery, 它是解析 SQL Query 的一个包装类。
SQLCustomQuery 类有一个与缓存相关的属性,querySpaces 集合 :
private final Set querySpaces = new HashSet();
通过源码分析,发现 querySpaces 主要用来进行Cache更新检查,querySpaces 存放的是基本VOClass 对应的 tableName, 例如: {SYS_PERM,SYS_USER} . Hibernate 在执行查询时,会检查这个集合中的所有 tableName, 如果该任意一个 tableName 对应 VOClass 二级缓存 有增,删,改的更新操作,即 UpdateTimestampsCache 不是最新的 ,那么该 Query 的 cache 就失效,就会重新去数据库中查询 ID 集合。
SQLCustomQuery 在构造函数中即进行 sql 的解析和querySpaces[]的判断,其中中有这样一段代码:
public SQLCustomQuery(.....) throws HibernateException {
SQLQueryReturnProcessor processor = new SQLQueryReturnProcessor(queryReturns, scalarQueryReturns, factory);
processor.process();
....
SQLLoadable[] entityPersisters = (SQLLoadable[]) processor.getPersisters().toArray( new SQLLoadable[0] );
....
for (int i = 0; i < entityPersisters.length; i++) {
SQLLoadable persister = entityPersisters[i];
//alias2Persister.put( aliases[i], persister );
//TODO:Does not consider any other tables referenced in the query
ArrayHelper.addAll( querySpaces, persister.getQuerySpaces() );
....
}
....
//TODO: Does not consider any other tables referenced in the query
ArrayHelper.addAll( querySpaces, persister.getQuerySpaces() );
其中红色部分是将 persister 的querySpaces[] 赋给 sqlQuery 的 querySpaces, 但是 persister 代表返回结果集类型对应的表,测试用例中是权限表 SYS_PERM; 因此漏了关联表 (Reference Table),这就是问题所在。 我们看到作者 author Gavin King, Max Andersen 也打了 TODO 注释:
//TODO: Does not consider any other tables referenced in the query
“不要考虑其他查询中的关联表”, 看来作者也留下一手,也许是遗忘了该 TODO 的处理。
2. 解决原理
关键就是要对 sql 分析出关联表,将其加入 querySpaces[] , 这样sql query查询时,就能自动检查关联表是否有更新。
分析关联表,用正则表达式解析 sql 中的所有单词 word,并逐个检查word 是否为 sessionFactory 中已经映射的表,凡是映射的表,就作为 reference table 加入 querySpaces[] 。
1) 新增分析类: org.hibernate.loader.custom.SQLQueryReferencesAnalyzer.
/**
* analyze reference table of a specified sql query
* @author Raymond He, Guangzhou China
* Oct 8, 2008
*
*/
public class SQLQueryReferencesAnalyzer {
private static Pattern p = Pattern.compile( "\\w{3,}" );
private static Map sqlquery2ReferenceTableMapping = new HashMap();
private static Map tableName2EntityNameMapping = new HashMap(100);
private static final Log log = LogFactory.getLog( SQLQueryReferencesAnalyzer.class );
public List analyzeSQL(SessionFactory sessionFactory,String sql) {
if(sqlquery2ReferenceTableMapping.containsKey(sql)) {
List refTables = (List)sqlquery2ReferenceTableMapping.get(sql);
if(log.isDebugEnabled())
log.debug("Got ref tables:" + refTables + "\r\n of SQL: " + sql);
return refTables;
}else {
if(tableName2EntityNameMapping.size() == 0) { //init it once
initTableName2EntityNameMapping(sessionFactory);
}
List refTables = new ArrayList(3);
Matcher m = p.matcher(sql);
while(m.find()) {
String word = m.group();
word = word.toUpperCase();
//check if the word is a table name in sessionFactory
//cache table for every sessionFactory independently, for multi sessionFactory env.
String key = "SF" + sessionFactory.hashCode() + "_" + word;
if(tableName2EntityNameMapping.containsKey( key )) {
if(log.isDebugEnabled()) log.debug("word is table: "+ word);
refTables.add( word);
}
}
if(log.isDebugEnabled())
log.debug("To cache sqlquery2ReferenceTableMapping, ref tables:" + refTables + "\r\n of SQL: " + sql);
//cache it
sqlquery2ReferenceTableMapping.put(sql, refTables);
return refTables;
}
}
2) 调用reference table分析类,将关联表加入 querySpaces[]
在 SQLCustomQuery 中 完成 Gavin King 的 TODO 任务。
//2008-10-6,Raymond He fix bug here,old text: Does not consider any other tables referenced in the query
/**start: analyze ref tables and add it to querySpaces ***********/
SQLQueryReferencesAnalyzer referencesAnalyzer = new SQLQueryReferencesAnalyzer();
List refTables = referencesAnalyzer.analyzeSQL(factory, sql);
for (int k = 0; k < refTables.size(); k++) {
querySpaces.add(refTables.get(k));
}
/**end ***********/
3. 验证
还是之前那个测试用例,观察日志:
Execute No. 0 ********************
2008-10-08 17:32:50,140 [DEBUG](AbstractBatcher.java,346) - select this.PERMCODE as PERM1_15_0_, this.MODULECODE as MODULE2_15_0_, this.PERMTYPECODE as PERM3_15_0_, this.PERMNAME as PERM4_15_0_, this.PERMDESC as PERM5_15_0_, this.PORTNO as PORT6_15_0_ from (select t.perm_code as permCode,
t.module_code as moduleCode,
t.perm_name as permName,
t.perm_desc as permDesc,
t.port_no as portNo,
t.perm_type_code as permTypeCode
from sys_perm t join sys_role_perm o
on t.perm_code = o.perm_code
where o.role_code = ? ) this
(No.0)result size:1
Execute No. 1 ********************
(No.1)result size:1
2008-10-08 17:32:50,187 [DEBUG](AbstractBatcher.java,346) - delete from SYS_ROLE_PERM where PERM_CODE=? and ROLE_CODE=?
Execute No. 2 ********************
2008-10-08 17:32:50,187 [DEBUG](AbstractBatcher.java,346) - select this.PERMCODE as PERM1_15_0_, this.MODULECODE as MODULE2_15_0_, this.PERMTYPECODE as PERM3_15_0_, this.PERMNAME as PERM4_15_0_, this.PERMDESC as PERM5_15_0_, this.PORTNO as PORT6_15_0_ from (select t.perm_code as permCode,
t.module_code as moduleCode,
t.perm_name as permName,
t.perm_desc as permDesc,
t.port_no as portNo,
t.perm_type_code as permTypeCode
from sys_perm t join sys_role_perm o
on t.perm_code = o.perm_code
where o.role_code = ? ) this
(No.2)result size:0
注意到第3次又执行了sql 语句,并且 (No.2)result size:0, 表明第二次查询后, 删除了 角色授权记录,因此第3次查询 角色 STESTOR 授权限结果为 0 。 表明成功修复此问题。
4. fix包及源码下载:
用法:
1) remove one class from hibernate3.jar
org.hibernate.loader.custom.SQLCustomQuery.class
2) add hibernate3-sqlquerycache-fix.jar to classpath, it provides another SQLCustomQuery impl to solve the bug.