从 JpaRepository 开始的子类,都是 Spring Data 项目对 JPA 实现的封装与扩展。JpaRepository 本身继承 PagingAndSortingRepository 接口,是针对 JPA 技术的接口,提供 flush()、saveAndFlush()、deleteInBatch()、deleteAllInBatch() 等方法
从图中其实可以发现,JPA 的实现类最关键是:SimpleJpaRepository,我们多次提到,还有一个最关键的实现类是 QuerydslJpaRepository,会在后面继续介绍。
从图中还可以看出来,最关键的几个接口 QueryByExampleExecutor、JpaSpecificationExecutor。
从图中还可以好好体会一些接口的用意(暴露那些该暴露的操作方法,而不是一股脑的把所有的方法都暴露给使用的人,因为不是每个场景下面都会用到所有方法。作者感悟:在实际工作中,当我们去设计公共方法或者架构的时候,要充分考虑清楚抽象类和接口的区别及其应用场景。)
按示例查询(QBE)是一种用户友好的查询技术,具有简单的接口,它允许动态查询创建,并且不需要编写包含字段名称的查询。
从 UML 图中,可以看出继承 JpaRepository 接口后,自动拥有了按“实例”进行查询的诸多方法,可见 Spring Data 的团队已经认为了 QBE 是 Spring JPA 的基本功能了,继承 QueryByExampleExecutor 和继承 JpaRepository 都会有这些基本方法,所以 QueryByExampleExecutor 位于 Spring Data Common 中,而 JpaRepository 位于 Spring Data JPA 中。
先来看一下 QueryByExampleExecutor 的源码:
public interface QueryByExampleExecutor<T> { //根据“实例”查找一个对象。 <S extends T> S findOne(Example<S> example); //根据“实例”查找一批对象 <S extends T> Iterable<S> findAll(Example<S> example); //根据“实例”查找一批对象,且排序 <S extends T> Iterable<S> findAll(Example<S> example, Sort sort); //根据“实例”查找一批对象,且排序和分页 <S extends T> Page<S> findAll(Example<S> example, Pageable pageable); //根据“实例”查找,返回符合条件的对象个数 <S extends T> long count(Example<S> example); //根据“实例”判断是否有符合条件的对象 <S extends T> boolean exists(Example<S> example); }
从源码上可以看出,只要了解 Example 基本上就可以掌握它的用法和 API 了。
public class Example<T> { @NonNull private final T probe; @NonNull private final ExampleMatcher matcher; public static <T> Example<T> of(T probe) { return new Example(probe, ExampleMatcher.matching()); } public static <T> Example<T> of(T probe, ExampleMatcher matcher) { return new Example(probe, matcher); } ...... }
我们从源码中可以看出 Example 主要包含三部分内容。
//创建查询条件数据对象 Customer customer = new Customer(); customer.setName("Jack"); customer.setAddress("上海"); //创建匹配器,即如何使用查询条件 ExampleMatcher matcher = ExampleMatcher.matching() //构建对象 .withMatcher("name", GenericPropertyMatchers.startsWith()) //姓名采用“开始匹配”的方式查询 .withIgnorePaths("focus"); //忽略属性:是否关注。因为是基本类型,需要忽略掉 //创建实例 Example<Customer> ex = Example.of(customer, matcher); //查询 List<Customer> ls = dao.findAll(ex); //输出结果 for (Customer bo:ls) { System.out.println(bo.getName()); }
上面例子中,是这样创建“实例”的:Example<Customer> ex = Example.of(customer, matcher);可以看到,Example 对象由 customer 和 matcher 共同创建,为讲解方便,再来结合案例先来明确一些定义。
再来理解“实例查询”,顾名思义,就是通过一个例子来查询,要查询的是 Customer 对象,查询条件也是一个 Customer 对象,通过一个现有的客户对象作为例子,查询和这个例子相匹配的对象。
1、支持动态查询:即支持查询条件个数不固定的情况,如客户列表中有多个过滤条件,用户使用时在“地址”查询框中输入了值,就需要按地址进行过滤,如果没有输入值,就忽略这个过滤条件。对应的实现是,在构建查询条件 Customer 对象时,将 address 属性值置具体的条件值或置为 null。
2、不支持过滤条件分组:即不支持过滤条件用 or(或)来连接,所有的过滤查件,都是简单一层的用 and(并且)连接,如 firstname = ?0 or (firstname = ?1 and lastname = ?2)。
3、仅支持字符串的开始/包含/结束/正则表达式匹配和其他属性类型的精确匹配。查询时,对一个要进行匹配的属性(如:姓名 name),只能传入一个过滤条件值,如以 Customer 为例,要查询姓“刘”的客户,“刘”这个条件值就存储在表示条件对象的 Customer 对象的 name 属性中,针对于“姓名”的过滤也只有这么一个存储过滤值的位置,没办法同时传入两个过滤值。正是由于这个限制,有些查询是没办法支持的,例如要查询某个时间段内添加的客户,对应的属性是 addTime,需要传入“开始时间”和“结束时间”两个条件值,而这种查询方式没有存两个值的位置,所以就没办法完成这样的查询。
1、使用场景
使用一组静态或动态约束来查询数据存储、频繁重构域对象,而不用担心破坏现有查询、简单的查询的使用场景,有时候还是挺方便的。
2、实际使用中我们需要考虑的因素
查询条件的表示,有两部分,一是条件值,二是查询方式。条件值用实体对象(如 Customer 对象)来存储,相对简单,当页面传入过滤条件值时,存入相对应的属性中,没入传入时,属性保持默认值。查询方式是用匹配器 ExampleMatcher 来表示,情况相对复杂些,需要考虑的因素有以下几个:
(1)Null 值的处理
当某个条件值为 Null时,是应当忽略这个过滤条件呢,还是应当去匹配数据库表中该字段值是 Null 的记录?
Null 值处理方式:默认值是 IGNORE(忽略),即当条件值为 Null 时,则忽略此过滤条件,一般业务也是采用这种方式就可满足。当需要查询数据库表中属性为 Null 的记录时,可将值设为 INCLUDE,这时,对于不需要参与查询的属性,都必须添加到忽略列表(ignoredPaths)中,否则会出现查不到数据的情况。
(2)基本类型的处理
如客户 Customer 对象中的年龄 age 是 int 型的,当页面不传入条件值时,它默认是0,是有值的,那是否参与查询呢?
关于基本数据类型处理方式:实体对象中,避免使用基本数据类型,采用包装器类型。如果已经采用了基本类型,而这个属性查询时不需要进行过滤,则把它添加到忽略列表(ignoredPaths)中。
(3)忽略某些属性值
一个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?
ignoredPaths:虽然某些字段里面有值或者设置了其他匹配规则,只要放在 ignoredPaths 中,就会忽略此字段的,不作为过滤条件。
(4)不同的过滤方式
同样是作为 String 值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?
默认配置和特殊配置混合使用:默认创建匹配器时,字符串采用的是精确匹配、不忽略大小写,可以通过操作方法改变这种默认匹配,以满足大多数查询条件的需要,如将“字符串匹配方式”改为 CONTAINING(包含,模糊匹配),这是比较常用的情况。对于个别属性需要特定的查询方式,可以通过配置“属性特定查询方式”来满足要求,设置 propertySpecifiers 的值即可。
(5)大小写匹配
字符串匹配时,有时可能希望忽略大小写,有时则不忽略,如何做到?
defaultIgnoreCase:忽略大小的生效与否,是依赖于数据库的。例如 MySQL 数据库中,默认创建表结构时,字段是已经忽略大小写的,所以这个配置与否,都是忽略的。如果业务需要严格区分大小写,可以改变数据库表结构属性来实现。