最近用到了Hibernate来作为数据处理部分的框架,离线criteria的确很好用,但在分页上确实让人头疼。要想分页就需要记录的总条数,如果只是一般的查询只需要
criteria.setProjection(Projections.rowCount());
就可以了,但如果是distinct的查询,记录条数和你所需查询的列有关,那么可以这样
Projections.countDistinct(propertyName);
问题来了,好像只能有一列啊。没错在标准sql中的distinct count也只能是一列的,但当有多列查询的时候怎么办呢?criteria有个接口可以设定结果值为distinct,就这样
criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
但是这样做的问题在于,他是先执行,后在程序中distinct,这样会导致每页的记录可能不同,而且性能低下。
换个思路在写sql的时候可以这样写
select count(*) from (select distinct xxxxxx)
也就是说在外层套一个count 而真实的查询语句在from子句中,成为一个子查询。那看看如何变成Hibernate的形式吧。我在网站上搜索了好久,终于得出一个结论(经过Hibernate文档证明了)Hibernate不支持from子句中存在子查询,他们认为from应该是一个映射了的对象,子查询和他们的思想不符。
好吧,那再换个思路,Hibernate本身只是一个映射的过程,那么实际执行还是需要JDBC实现。如果是JDBC就需要sql语句,我们把criteria中蕴含的sql语句提取出来,然后加上我们外层的计数部分,然后再像Hibernate一样绑定参数就可以了,因为外层没有参数占位符,所以也不会影响Hibernate按位置绑定参数,因为没有接口,那只能读源码了。果然,hibernate最后通过sql(就是log中看到的那个)生成一个PreparedStatement,并通过方法绑定参数。废话少说,直接来个破冰之旅。
/**
* 因为需要修改sql,就要传入你新的sql,criteria必须是绑定session的
* 因为我们用到了session
* @param sql
* @param criteria
* @return
*/
public static List<List> wrapAndExecute(String sql, Criteria criteria) {
ResultSet rs = null;//最后的结果集
PreparedStatement ps = null;//我们自己的PreparedStatement
Connection connection = null;//session中获取的连接
try {
//先转型
CriteriaImpl criteriaImpl = (CriteriaImpl) criteria;
// 获取SessionImplementor类型的session
SessionImplementor session = criteriaImpl.getSession();
// 获取factory
SessionFactoryImplementor factory = session.getFactory();
// 获取CriteriaQueryTranslator对象,按照Hibernate源码中的方式生成
CriteriaQueryTranslator translator = new CriteriaQueryTranslator(factory, criteriaImpl, criteriaImpl
.getEntityOrClassName(), CriteriaQueryTranslator.ROOT_SQL_ALIAS);
// 从CriteriaQueryTranslator对象中获取保存参数的QueryParameters对象
QueryParameters queryParameters = translator.getQueryParameters();
// 从factory中获取implementors,一般只用一个
String[] implementors = factory.getImplementors(criteriaImpl.getEntityOrClassName());
// 因为只用一个,所以只需要一个loader,按照Hibernate源码中的方式新建
// 注意有些地方由于非public所以需要用反射,反射部分代码就不贴了,注意invokeMethod要从子类找到父类,直到object
// 因为有些方法是写在父类中的
Loader loader = new CriteriaLoader((OuterJoinLoadable) ReflectUtil.invokeMethod(session,
"getOuterJoinLoadable", new Class[] { String.class }, new Object[] { implementors[0] }), factory,
criteriaImpl, implementors[0], ((SessionImpl) session).getLoadQueryInfluencers());
// 按照Hibernate源码方法获取walker
CriteriaJoinWalker walker = new CriteriaJoinWalker((OuterJoinLoadable) factory
.getEntityPersister(implementors[0]), translator, factory, criteriaImpl, criteriaImpl
.getEntityOrClassName(), session.getLoadQueryInfluencers());
// 获取了criteria的sql
String criteriaSql = walker.getSQLString();
// 用传入的sql代替,注意PLACEHOLDER代表criteriaSql的占位符
sql = sql.replace(PLACEHOLDER, criteriaSql);
// 获取session中的连接
connection = session.getJDBCContext().getConnectionManager().getConnection();
// 构建新的PreparedStatement
ps = connection.prepareStatement(sql);
// 注意一定要过滤过参数才能绑定
queryParameters.processFilters(criteriaSql, session);
// 通过反射绑定参数
ReflectUtil.invokeMethod(loader, "bindParameterValues", new Class[] { PreparedStatement.class,
QueryParameters.class, int.class, SessionImplementor.class }, new Object[] { ps, queryParameters,
1, session });
// 获取结果集
rs = session.getBatcher().getResultSet(ps);
......
呼,好了,终于抠出了criteria的东西了。
最后值得注意的是,由于ResultSet和PreparedStatement是自己创建的,所以需要自己维护,就是要注意关闭咯。至于connection由于是session中获取的,可以不用管,否则这个session如果还有其他操作connection关了就要抛错了。