09 Spring Data JPA动态SQL及自定义Repository

本文详细介绍了Spring Data JPA中QueryByExampleExecutor和JpaSpecificationExecutor的使用,包括它们的特点、约束、使用场景及实际案例。QueryByExampleExecutor支持动态查询,但不支持过滤条件分组,适合简单的查询场景。JpaSpecificationExecutor基于Criteria API,适用于更复杂的动态查询,但已不再推荐使用Specifications。文章还讨论了自定义Repository的实现,包括如何扩展共享行为和处理逻辑删除等场景。
摘要由CSDN通过智能技术生成

Spring Data JPA动态SQL及自定义Repository

从 JpaRepository 开始的子类,都是 Spring Data 项目对 JPA 实现的封装与扩展。JpaRepository 本身继承 PagingAndSortingRepository 接口,是针对 JPA 技术的接口,提供 flush()、saveAndFlush()、deleteInBatch()、deleteAllInBatch() 等方法。我们来看一下 UML 来对 JpaRespository 有个整体的认识。

img

QueryByExampleExecutor的使用

按实例查询(QBE)是一种用户友好的查询技术,具有简单的接口。它允许动态查询创建,并且不需要编写包含字段名称的查询。只需要继承JpaRepository接口后,自动拥有了实例查询方法。

public interface QueryByExampleExecutor<T> {
		// 根据实例创建一个对象
    <S extends T> Optional<S> findOne(Example<S> var1);
		// 根据实例查找一批对象		
    <S extends T> Iterable<S> findAll(Example<S> var1);
		// 根据实例查找一批对象,且排序
    <S extends T> Iterable<S> findAll(Example<S> var1, Sort var2);
		// 根据实例查找一批对象,且排序和分页
    <S extends T> Page<S> findAll(Example<S> var1, Pageable var2);
		// 根据实例查找,返回符合条件的个数
    <S extends T> long count(Example<S> var1);
		// 根据实例查找,判断是否有符合条件的对象
    <S extends T> boolean exists(Example<S> var1);
}

需要掌握操作Example的用法和API

public interface Example<T> {
    static <T> Example<T> of(T probe) {
        return new TypedExample(probe, ExampleMatcher.matching());
    }

    static <T> Example<T> of(T probe, ExampleMatcher matcher) {
        return new TypedExample(probe, matcher);
    }

    T getProbe();

    ExampleMatcher getMatcher();

    default Class<T> getProbeType() {
        return ProxyUtils.getUserClass(this.getProbe().getClass());
    }
}

在源码中Example主要包括三个内容:

  • Probe:这是具有填充字段的域对象实际实体类,即查询条件的封装类。必填
  • ExampleMatcher:ExampleMatcher有关于匹配特定字段的匹配规则,他可以重复在多个实例中。必填,如果不填为默认的
  • Example:Examle由探针和ExampleMatcher组成的。它用于创建查询。

QueryByExampleExecutor示例

//创建查询条件数据对象
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 共同创建,为讲解方便,再来结合案例先来明确一些定义。

  • Probe:实体对象,在持久化框架中与 Table 对应的域对象,一个对象代表数据库表中的一条记录,如上例中 Customer 对象。在构建查询条件时,一个实体对象代表的是查询条件中的“数值”部分,如要查询姓“Jack”的客户,实体对象只能存储条件值“Jack”。
  • ExampleMatcher:匹配器,它是匹配“实体对象”的,表示了如何使用“实体对象”中的“值”进行查询,它代表的是“查询方式”,解释了如何去查的问题。例如,要查询姓“刘”的客户,即姓名以“刘”开头的客户,该对象就表示了“以某某开头的”这个查询方式,如上例中 withMatcher("name", GenericPropertyMatchers.startsWith())。
  • Example:实例对象,代表的是完整的查询条件。由实体对象(查询条件值)和匹配器(查询方式)共同创建。

再来理解“实例查询”,顾名思义,就是通过一个例子来查询,要查询的是 Customer 对象,查询条件也是一个 Customer 对象,通过一个现有的客户对象作为例子,查询和这个例子相匹配的对象。

QueryByExampleExecutor 的特点及约束

  • 支持动态查询:即支持查询条件个数不固定的情况,如客户列表中有多个过滤条件,用户使用时在“地址”查询框中输入了值,就需要按地址进行过滤,如果没有输入值,就忽略这个过滤条件。对应的实现是,在构建查询条件 Customer 对象时,将 address 属性值置具体的条件值或置为 null。
  • 不支持过滤条件分组:即不支持过滤条件用 or(或)来连接,所有的过滤查件,都是简单一层的用 and(并且)连接,如 firstname = ?0 or (firstname = ?1 and lastname = ?2)。
  • 仅支持字符串的开始/包含/结束/正则表达式匹配和其他属性类型的精确匹配。查询时,对一个要进行匹配的属性(如:姓名 name),只能传入一个过滤条件值,如以 Customer 为例,要查询姓“刘”的客户,“刘”这个条件值就存储在表示条件对象的 Customer 对象的 name 属性中,针对于“姓名”的过滤也只有这么一个存储过滤值的位置,没办法同时传入两个过滤值。正是由于这个限制,有些查询是没办法支持的,例如要查询某个时间段内添加的客户,对应的属性是 addTime,需要传入“开始时间”和“结束时间”两个条件值,而这种查询方式没有存两个值的位置,所以就没办法完成这样的查询。

ExampleMatcher 源码解读

public class ExampleMatcher {
NullHandler nullHandler; 
StringMatcher defaultStringMatcher; //默认
boolean defaultIgnoreCase; //默认大小写忽略方式
PropertySpecifiers propertySpecifiers; //各属性特定查询方式
Set<String> ignoredPaths; //忽略属性列表
//Null值处理方式,通过构造方法,我们发现默认忽略
   NullHandler nullHandler;
   //字符串匹配方式,通过构造方法可以看出默认是DEFAULT(默认,效果同EXACT),EXACT(相等)
   StringMatcher defaultStringMatcher;
   //各属性特定查询方式,默认无特殊指定的。
   PropertySpecifiers propertySpecifiers;
   //忽略属性列表,默认无。
   Set<String> ignoredPaths;
   //大小写忽略方式,默认不忽略。
   boolean defaultIgnoreCase;
   @Wither(AccessLevel.PRIVATE) MatchMode mode;
//通用、内部、默认构造方法。
   private ExampleMatcher() {
      this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.<String>emptySet(), false,
            MatchMode.ALL);
   }
   //Example的默认匹配方式
   public static ExampleMatcher matching() {
      return matchingAll();
   }
public static ExampleMatcher matchingAll() {
   return new ExampleMatcher().withMode(MatchMode.ALL);
}
......
}

关键属性分析

  • nullHandler:Null 值处理方式,枚举类型,有两个可选值,INCLUDE(包括)、IGNORE(忽略)。
    • 标识作为条件的实体对象中,一个属性值(条件值)为 Null 时,是否参与过滤;
    • 当该选项值是 INCLUDE 时,表示仍参与过滤,会匹配数据库表中该字段值是 Null 的记录;
    • 若为 IGNORE 值,表示不参与过滤。
  • defaultStringMatcher:默认字符串匹配方式,枚举类型,有 6 个可选值,DEFAULT(默认,效果同 EXACT)、EXACT(相等)、STARTING(开始匹配)、ENDING(结束匹配)、CONTAINING(包含,模糊匹配)、REGEX(正则表达式)。
    • 该配置对所有字符串属性过滤有效,除非该属性在 propertySpecifiers 中单独定义自己的匹配方式。
  • defaultIgnoreCase:默认大小写忽略方式,布尔型,当值为 false 时,即不忽略,大小不相等。
    • 该配置对所有字符串属性过滤有效,除非该属性在 propertySpecifiers 中单独定义自己的忽略大小写方式。
  • propertySpecifiers:各属性特定查询方式,描述了各个属性单独定义的查询方式,每个查询方式中包含4个元素:属性名、字符串匹配方式、大小写忽略方式、属性转换器。
    • 如果属性未单独定义查询方式,或单独查询方式中,某个元素未定义(如字符串匹配方式),则采用 ExampleMatcher 中定义的默认值,即上面介绍的 defaultStringMatcher 和 defaultIgnoreCase 的值。
  • ignoredPaths:忽略属性列表,忽略的属性不参与查询过滤。

(3)字符串匹配举例

字符串匹配方式 对应 JPQL 的写法
Default& 不忽略大小写 firstname=?1
Exact& 忽略大小写 LOWER(firstname) = LOWER(?1)
Staring& 忽略大小写 LOWER(firstname) like LOWER(?0)+'%'
Ending& 不忽略大小写 firstname like '%'+?1
Containing 不忽略大小写 firstname like '%'+?1+'%'

QueryByExampleExecutor 使用场景 & 实际的使用

使用场景

使用一组静态或动态约束来查询数据存储、频繁重构域对象,而不用担心破坏现有查询、简单的查询的使用场景,有时候还是挺方便的。

实际使用中我们需要考虑的因素

查询条件的表示,有两部分,一是条件值,二是查询方式。条件值用实体对象(如 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 数据库中,默认创建表结构时,字段是已经忽略大小写的,所以这个配置与否,都是忽略的。如果业务需要严格区分大小写,可以改变数据库表结构属性来实现。

实际使用案例说明

(1)无匹配器的情况

  • 要求:查询地址是“河南省郑州市”,且重点关注的客户。
  • 说明:使用默认匹配器就可以满足查询条件,则不需要创建匹配器。
//创建查询条件数据对象
Customer customer = new Customer();
customer.setAddress("河南省郑州市"
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值