Criteria Query,译为条件查询,可理解为程序式、函数式查询。为什么这样说呢?写SQL语句也是编程,但是,SQL就没有什么类、函数这些编程概念了。而Criteria Query就是把查询用一系列对象、方法的调用组合起来,最终结果也得到一条SQL语句。这样做的好处主要是:不用拼接语句,不用写一句很长的代码。写过SQL的人都知道,一个SQL可能很长,但是通常都不是一口气写下来的,而是一小段一小段的写,然后组合出来的,Criteria Query也是这样一点点的“构建”,符合编程习惯,还便于除错。
Criteria Query其实和JPQL是一样的,只不过通常对于JPQL中的很多概念,一般会很少去注意它们的名称,但是在Criteria Query里,都得用一个一个像Expression、Predicate这样的接口及其方法来表达,所以感觉很复杂,但如果把它和JPQL对照起来看,就容易理解了。《Hibernate实战(第二版)》这本书讲Criteria Query的时候就是这么干的,这里必须赞一下。但是要记住是跟JPQL/HQL对照,而不是跟普通 的SQL对照,因为这两者虽然相似,但又不同,主要差别在于:JPQL是面向对象的,而SQL不是。在SQL中可以很自由的关联和投影(选择结果列),JPQL就不是,虽然也可以选择一序列对象或属性作为结果,但如果投影的结果不是一个实体(Entity)的话,那返回的就是一个Object[],这基本上就失去Persistence的意义了。所以通常JPQL是这样写的:
SELECT i FROM Item i;
对应的Native SQL是这样写的
SELECT * FROM item;
然而,一般的SQL教科书都会建议写明字段列表,一个是语义明确,另一个是减少不必要的数据读取和传输。
JPQL/HQL与Native SQL的另一个重要区别是:Native SQL里面别名是可选的,一般只在复杂的查询需要关联或有关联子查询时定义别名,甚至也可以不定义,因为如果没有别名那表或视图本来的名称就是别名。而在JPQL/HQL里,别名是必须的,这里和Criteria Query对照起来就好理解了,因为必须传递一个参数,别名就是这个参数的变量名(形参)。其实Criteria Query和JPQL先有哪个还真不好说,背后中的实现有可能是把JPQL转化为Criteria Query再实现的。
了解了这两个差别,下面这种怪异的写法就很好理解了:
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery criteria = builder.createQuery( Person.class );
Root root = criteria.from( Person.class );
一开始很不理解为什么createQuery时已经指定了Person.class,后面还要定义一个root并且传给其构建工厂的参数还是Person.class。其实这两处的意义是不一样的,root这里从工厂方法名称来看,要好理解一点,相当于SQL的主表,在Oracle SQL里,会有FROM t1,t2这种同时从两个表里面查询的写法,同样Criteria Query也可以定义两个Root的,当然最好是用Join,实际上Join这个接口就是扩展自Root接口的。而CreateQuery的参数,实际上是指明了查询结果的类型,一般都会是查询实体(主表)的类别,当然也不一定,如果非要查不是实体的东西,比如String也是可以的,当然最后执行查询得到的结果会是List
总结下来就是,SQL可以查的,JPQL也可以查,只是语法不同而已,结合面向对象的特点,这点语法上的差异还是不难理解的。至于CriteriaQuery,由于要用编程语句来表示,会用到很多接口,但这些接口都是与查询中的语法概念一一对应的,结合API资料,参考一下别人写的,熟悉了就好了。
正因为通常用JPQL都是“select 实体 from 实体”,所以Spring Data JPA就把CriteriaQuery封装成了JpaSpecificationExecutor接口,里面定义了findOne、findALL、count等方法,只需要定义一个接口扩展实例化的JpaSpecificationExecutor接口,无需编写实现,像CrudRepository接口一样,Spring会自动提供实现,非常好用。当然,一个查询的最关键部分也就是查询条件,是必须作为参数传递给要调用的方法的,这一部分在Spring被封装成了Specification接口,虽然是只有一个类型参数的模板,但是实现它倒也不是很容易,主要是因为,虽然封装了,但是查询条件是不可能脱离Query存在的,所以要写好Specification接口的实现,必须搞懂或者基本搞懂CriteriaQuery,在Specification接口的唯一虚函数toPredicate中,(Root,CriteriaQuery>,CriteriaBuilder)这三个是要作为参数传递的,虽然不用像前面的代码例子那样去写语句生成这些对象,只需要实例化JpaSpecificationExecutor接口,Spring就会创建好这些对象,但是难点从来就不是对象创建而是使用。所以,还是得好好理解CreateQuery是怎样一步步生成Query(语句)的才行。
最后,采用Criteria Query或者其他的编程语句式查询而不是直接写SQL,其实就是为了根据复杂的条件灵活地生成查询条件。Criteria Query里的Predicate(Spring封装成了Specification)是可以组合的,Predicate扩展了Expression
粘一段实际代码。
@PostMapping("/user")
public String listuser(QueryUserForm queryparam,Model model) {
List users = userRepository.findAll((root, query, builder) -> {
List predicates = new ArrayList();
if (!queryparam.getUsername().isEmpty()) {
predicates.add(builder.like(root.get("username"), "%" + queryparam.getUsername() + "%"));
}
if (!"000000".equals(queryparam.getDistrict_code())) {
predicates.add(builder.equal(root.get("district").get("code"), queryparam.getDistrict_code()));
}
if (queryparam.getOrganization_id() != 0) {
predicates.add(
builder.equal(root.get("organization").get("id"), queryparam.getOrganization_id()));
}
return builder.and(predicates.toArray(new Predicate[predicates.size()]));
});
model.addAttribute(users);
return "manage/usermanage";
}
这段代码用到了 λ \lambda λ表达式和“闭包”,这样子就不用在Business层引用QueryUserForm对象,虽然它只是一个POJO对象,但因为它是用来接受前端查询参数的,所以把它归属为Web层而不应该放在Business层,同时,也不用在Web层引入Root,CriteriaQuery>,CriteriaBuilder这些应该是Business(数据层)的对象。相当于在controller里真的只是写了一些查询条件,应该不算业务逻辑吧,因为又没有改变什么。这个例子也可以当作“闭包”应用的一个具体例子吧。