用hibernate分页的时候遇到了个问题,就是获取总页数。
现在流行的办法是用Criteria,在排序前先分组计数并返回值,然后在添加排序、分页。举例来说:
Session session = sessionFactory.getCurrentSession();
Criteria c = session.createCriteria(UserModel.class);
c.add(Restrictions.ge("id", 10));
c.add(Restrictions.le("id",20));
int t = ((Long) c.setProjection(Projections.rowCount())
.uniqueResult()).intValue();
c.setProjection(null);
c.setMaxResults(4);
c.addOrder(Order.desc("id"));
注意中间有一个个setProjection(null),用来把之前的分组取消。这算是一个hack吧,虽然在排序前进行总数计数能节约一部分性能,但依然是两次查询。例子中的选择条件是相当简单的,如果复杂一些呢,再来几个many-to-one或者one-to-many子查询呢?
针对我用的是mysql,百度到一种解决方案[1]:用select found_rows(),可以得到上一次查询的结果数。如果在查询中加上SQL_CALC_FOUND_ROWS,就可以忽略limit,也就是说得到我们要的总条目数,也就能得到总页数!
但奈何SQL_CALC_FOUND_ROWS并不是hql关键字,语法解析的时候就被pass了。于是乎花了一天的时间追踪hibernate源码(大部分时间都浪费在官网下的源码,各种莫名其妙的错误,而且还少类!后来用了在grepcode.com下的hibernate源码),又简单学习了hibernate所用语言识别工具antlr,终于将SQL_CALC_FOUND_ROWS成功加入hibernate的抽象语法树AST。
所用hibernate版本号:4.1.6 final
所用依赖包见:http://grepcode.com/snapshot/repo1.maven.org/maven2/org.hibernate/hibernate-core/4.1.6.Final/
流程:
(1)配置antlr的环境变量,将antlr的jar包添加至环境变量的classpath中[2]
(2)修改hql.g(如果在grepcode上下载的源码,这些中间文件在jar包的一级目录里,如果是Hibernate官网上下的,在/project/hibernate-core/src/main/antlr中[3]):
在tokens代码块中添加:
SELECTCOUNT="sql_calc_found_rows";
selectClause
: SELECT^ // NOTE: The '^' after a token causes the corresponding AST node to be the root of the sub-tree.
{ weakKeywords(); } // Weak keywords can appear immediately after a SELECT token.
(SELECTCOUNT)? (DISTINCT)? ( selectedPropertiesList | newExpression | selectObject )
;
(3)修改hql-sql.g
修改selectClause的规则定义:
selectClause!
: #(SELECT { handleClauseStart( SELECT ); beforeSelectClause(); } (s:SELECTCOUNT)? (d:DISTINCT)? x:selectExprList ) {
#selectClause = #([SELECT_CLAUSE,"{select clause}"], #s, #d, #x);
}
;
(4)修改sql-gen.g
修改selectClause的规则定义:
selectClause
: #(SELECT_CLAUSE (countRows)? (distinctA)? ( selectColumn )+ )
;
在其后添加一条新的规则:
countRows
: SELECTCOUNT { out("sql_calc_found_rows "); }
;
(5)按序用antlr运行上述3个词法文件
(6)将生成的*.java(一共7个)放至org.hibernate.hql.internal.antlr中,覆盖原文件。
(7)修改org.hibernate.hql.internal.ast.tree.SelectClause的getFirstSelectExpression方法:
protected AST getFirstSelectExpression() {
AST n = getFirstChild();
// Skip 'DISTINCT' and 'ALL', so we return the first expression node.
while ( n != null && ( n.getType() == SqlTokenTypes.DISTINCT || n.getType() == SqlTokenTypes.ALL || n.getType() == SqlTokenTypes.SELECTCOUNT ) ) {
n = n.getNextSibling();
}
return n;
}
(8)大功告成,在hql中就可以使用 sql_calc_found_rows 了!~
关于found_rows()的并行支持,考虑到hibernate中用了事务,加上mysql推出此函数的目的就是用于获取总页数的场景,心里稍宽慰。
有文章说SQL_CALC_FOUND_ROWS并不一定提升查询性能[5],也有人做了类似实验并给出解释[6]。我懒,没自己做实验证实,凭感觉肯定用这种官方提供的专属函数要比两次查询快。跟着感觉走~
参考:
[1]:http://www.ooso.net/archives/342
[2]:http://www.ibm.com/developerworks/cn/java/j-lo-antlr/
[3]:http://calvin.iteye.com/blog/92007
[4]:antlr2.7.5中文文档:http://ishare.iask.sina.com.cn/download/explain.php?fileid=15440125
[5]:http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/
[6]:http://hi.baidu.com/thinkinginlamp/item/b122fdaea5ba23f614329b14