先说一些题外话。众所周知,在Java持久层中有三种查询方式,分别是SQL、JPQL、和Criteria API。
这个很好理解,一般小项目并且对安全性、效率性和灵活性没有特别需求的都会采用这个最原始的方法:
// 获得实体管理器 EntityManager em = ... // 建立SQL查询 String getByFirstName = "SELECT * FROM contacts c WHERE c.first_name = ?1"; // 创建查询实例 Query query = em.createNativeQuery(getByFirstName, Contact.class); // 设置查询参数 query.setParameter(1, "John"); // 获取结果 List contacts = query.getResultList();
上面的例子告诉我们3件事:
1)用SQL建立查询,无需学习新的查询语言;
2)创建的查询没有类型安全,在使用前必须计算查询结果;
3)在运行程序前必须验证查询的拼写或语法是否有错误。
JPQL是基于字符串的查询语言,语法类似于SQL。因此学习JPQL相当容易,只要有一定的SQL基础。看下面的代码:
// 获得实体管理器 EntityManager em = ... // 建立JPQL查询 String getByFirstName = "SELECT c FROM Contact c WHERE c.firstName = :firstName"; // 创建查询实例 TypedQuery<Contact> query = em.createQuery(getByFirstName, Contact.class); // 设置查询参数 query.setParameter("firstName", "John"); // 获取结果 List<Contact> contacts = query.getResultList();
上面的例子告诉我们3件事:
1)创建的查询是类型安全的,我们不必计算查询的结果;
2)JPQL查询字符串是易读、易于理解的;
3)创建的查询字符串在编译期间不会被验证。
Criteria API用于解决对接第三方ORM框架时让JPQL标准化。它用于构建查询定义对象,此对象会被翻译成可执行的SQL查询。Criteria API对于创建动态查询是一个极好的工具,它使得创建动态查询更简便,因为我们处理的是对象,而不是处理查询的字符串。缺点在于随着查询的复杂度的增加,查询定义对象的创建也会变得很繁琐,代码会更难读。下面的代码说明了这个问题:
// 获得实体管理器 EntityManager em = ... // 获得Criteria建立器 CriteriaBuilder cb = em.getCriteriaBuilder(); // 建立Criteria查询 CriteriaQuery<Contact> query = cb.greateQuery(Contact.class); // 创建查询Root Root<Contact> root = query.from(Contact.class); // 创建firstName的查询条件,使用静态元模型 Predicate firstNameIs = cb.equal(root.get(Contact_.firstName, "John")); // 指定查询的where条件 query.where(firstNameIs); // 创建查询并获取结果 TypedQuery<Contact> q = em.createQuery(query); List<Contact> contacts = q.getResultList();
上面的例子告诉我们3件事:
1)创建的查询是类型安全的,不必计算查询的结果;
2)代码不如SQL或JPQL那么易读;
3)由于是使用Java API处理,Java编译器会确保查询的语法正确。
目前一般的中大型Java Web项目都会采用Spring和Hibernate框架进行开发,为了迎合统一面向对象化的代码管理,Criteria就显得比较好用了,在查询方法设计上可以灵活的根据Criteria的特点来方便地进行查询条件的组装。现在对Hibernate的Criteria用法进行总结:
Hibernate设计了CriteriaSpecification作为Criteria的父接口,下面提供了Criteria和DetachedCriteria。Criteria和DetachedCriteria的主要区别在于创建的形式不一样,Criteria是在线的,所以它是由Hibernate Session进行创建的;而DetachedCriteria是离线的,创建时无需Session,DetachedCriteria提供了2个静态方法forClass(Class)或forEntityName(Name)进行DetachedCriteria实例的创建。Spring框架提供了getHibernateTemplate().findByCriteria(detachedCriteria)方法可以很方便地根据DetachedCriteria来返回查询结果。Criteria和DetachedCriteria均可使用Criterion和Projection设置查询条件。可以设置FetchMode(联合查询抓取模式),设置排序方式。对于Criteria还可以设置FlushModel(冲刷Session方式)和LockMode(数据库锁模式)。
Hibernate具体的Criteria使用方式这里就不再赘述,官网文档里已经有比较详细的说明了。下面我就只举个平时开发中遇到的有关自定义SQL表达式的例子。
有人说Hibernate不灵活,估计主要就是体现在采用Criteria API时,对于自定义SQL表达式的支持比较缺乏人性化的设计,当然这只是个人之言,也有可能对架构还没有完全吃透。下面我就说下开发中遇到的问题,先上DAO代码:
public List<com.yun.dba.check.view.HostResource> getHostResourceByCondition(List<SearchSupport> searchList, Integer pageNo, Integer pageSize, String orderString) { return super .getByCondition(getSession().createCriteria(HostResource.class, "hr"), searchList, pageNo, pageSize) .addOrder(Order.desc(orderString)) .setProjection( Projections .projectionList() .add(Projections.property("hr." + "id").as("id")) .add(Projections.property("hr." + "statDate").as("statDate")) .add(Projections.property("hr." + "gmtModified").as("gmtModified")) .add(Projections.property("hr." + "hostId").as("hostId")) .add(Projections.property("hr." + "region").as("region")) .add(Projections.property("hr." + "clusterName").as("clusterName")) .add(Projections.property("hr." + "ip").as("ip")) .add(Projections.property("hr." + "dbType").as("dbType")) .add(Projections.property("hr." + "groupName").as("groupName")) .add(Projections.property("hr." + "hostCreated").as("hostCreated")) .add(Projections.property("hr." + "hostAvail").as("hostAvail")) .add(Projections.property("hr." + "clusterAvail").as("clusterAvail")) .add(Projections.property("hr." + "memTotal").as("memTotal")) .add(Projections.property("hr." + "diskTotal").as("diskTotal")) .add(Projections.property("hr." + "activeInsCnt").as("activeInsCnt")) .add(Projections.property("hr." + "memSelled").as("memSelled")) .add(Projections.property("hr." + "diskSelled").as("diskSelled")) .add(Projections.property("hr." + "currInsCnt").as("currInsCnt")) .add(Projections.property("hr." + "diskCurr").as("diskCurr")) .add(Projections.property("hr." + "rssMem").as("rssMem")) .add(Projections.property("hr." + "dbVersion").as("dbVersion")) .add( Projections .sqlProjection( "round(mem_selled/mem_total*100,2) as memselledRate,round(disk_selled/disk_total*100,2) as diskselledRate,round(disk_curr/disk_total*100,2) as diskcurrRate", new String[] { "memselledRate", "diskselledRate", "diskcurrRate" }, new Type[] { StandardBasicTypes.FLOAT, StandardBasicTypes.FLOAT, StandardBasicTypes.FLOAT }))) .setResultTransformer( new AliasToBeanResultTransformer(com.yun.dba.check.view.HostResource.class)) .list(); }
首先这个DAO是个带分页输出的Criteria查询,继承父类分页DAO后对于本表进行属性定义,sqlProjection方法就是Hibernate Criteria API里对自定义SQL表达式的支持方法。然后对输出的对象进行结构改造,基于原来的HostResource表类再增加一些自定义SQL表达式列,把新的HostResource类放在view包里,这么做的意义在于进行Order by操作的时候自定义SQL表达式投影列也可以做到分页排序输出。
看似一切都很完美,但其实不然,在调试中才发现原来Order.desc(orderString)方法竟然不支持对sqlProjection方法中新生成的自定义SQL表达式投影列名进行排序。现在有两种解决方案,一个是放弃Criteria API转而采用createSQLQuery方法对该DAO进行JPQL改造;另一种方法就是深入研究Hibernate Criteria API源码,找出解决方法。最后选择的动力是重构一个JPQL太麻烦,还不如研究源码靠谱些。
查看org.hibernate.criterion.Order类可以发现,toSqlString方法对传入的列名参数及其排序方式进行了处理:
public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { String[] columns = criteriaQuery.getColumnsUsingProjection(criteria, propertyName); Type type = criteriaQuery.getTypeUsingProjection(criteria, propertyName); StringBuffer fragment = new StringBuffer(); for ( int i=0; i<columns.length; i++ ) { SessionFactoryImplementor factory = criteriaQuery.getFactory(); boolean lower = ignoreCase && type.sqlTypes( factory )[i]==Types.VARCHAR; if (lower) { fragment.append( factory.getDialect().getLowercaseFunction() ) .append('('); } fragment.append( columns[i] ); if (lower) fragment.append(')'); fragment.append( ascending ? " asc" : " desc" ); if ( i<columns.length-1 ) fragment.append(", "); } return fragment.toString(); }
可以发现getColumnsUsingProjection方法只是对Criteria已定义的投影列做了处理,对于那些自定义SQL表达式投影列名是排除在外的,这不得不说是个遗憾,所以sqlProjection方法就有点类似于鸡肋的感觉了。
知道了原理接下来解决方法就简单了,直接继承org.hibernate.criterion.Order类,清空toSqlString方法,取消Order by的Criteria投影支持:
import org.hibernate.criterion.Order; import org.hibernate.criterion.CriteriaQuery; import org.hibernate.Criteria; import org.hibernate.HibernateException; public class OrderBySqlFormula extends Order { private String sqlFormula; protected OrderBySqlFormula(String sqlFormula) { super(sqlFormula, true); this.sqlFormula = sqlFormula; } public String toString() { return sqlFormula; } public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { return sqlFormula; } public static Order sqlFormula(String sqlFormula) { return new OrderBySqlFormula(sqlFormula); } }
使用的时候把Order.desc(orderString)改为OrderBySqlFormula.sqlFormula(orderString+" desc")就可以了,唯一的缺点就是Order by的时候所有的列名都不能使用Criteria投影列名了,取而代之的是表中真实的列名,这点需要注意。但不管怎么说还是做到了用最少的代码修改代价达到了既定的需求目标,尽管代码改的有些丑陋。