添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

第03课:定义查询方法(Defining Query Methods)

Spring Data JPA 的最大特色,利用方法名定义查询方法,我们本篇内容将围绕这个来展开详细讲解。

定义查询方法的配置方法

由于 Spring JPA Repository 的实现原理是采用动态代理的机制,所以我们介绍两种定义查询方法,从方法名称中可以指定特定用于存储的查询和更新,或通过使用 @Query 手动定义的查询,取决于实际对数据的操作,只需要实体 Repository 继承 Spring Data Common 里面的 Repository 接口即可,就像前面我们讲的一样。如果你想有其他更多默认通用方法的实现,可以选择 JpaRepository、PagingAndSortingRepository、CrudRepository 等接口,也可以直接继承我们后面要介绍的 JpaSpecificationExecutor、QueryByExampleExecutor,QuerydslPredicateExecutor 和自定义 Response,都可以达到同样的效果。

如果不想扩展 Spring 数据接口,还可以使用它来注解存储库接口 @RepositoryDefinition,扩展 CrudRepository 公开了一套完整的方法来操纵实体。如果希望对所暴露的方法有选择性,只需将要暴露的方法复制 CrudRepository 到域库中即可,其实也是自定义 Repository 的一种。

示例:选择性地暴露CRUD方法
@NoRepositoryBeaninterface
MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {
    T findOne(ID id); 
    T save(T entity);
interface UserRepository extends MyBaseRepository<User, Long> {
     User findByEmailAddress(EmailAddress emailAddress);

在此实例中,您为所有域存储库定义了一个公共基础接口,并将其暴露出来,findOne(…) 和 save(…) 这些方法将由 Spring Data 路由到你提供的 MyBaseRepository 的基本 Repository 实现中。在 JPA 的默认情况下,SimpleJpaRepository 作为上面两个接口的实现类,所以 UserRepository 现在将能够保存用户,并通过 ID 查找单个,以及触发查询以 Users 通过其电子邮件地址查找。

综上所述,得出以下两单:

  • MyRepository Extends Repository 接口就可以实现 Defining Query Methods 的功能。
  • 继承其他 Repository 的子接口,或者自定义子接口,可以选择性的暴漏 SimpleJpaRepository 里面已经实现的基础公用方法。
  • 方法的查询策略设置

    通过下面的命令来配置方法的查询策略:

    @EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
    

    其中,QueryLookupStrategy.Key 的值一共就三个:

  • Create:直接根据方法名进行创建,规则是根据方法名称的构造进行尝试,一般的方法是从方法名中删除给定的一组已知前缀,并解析该方法的其余部分。如果方法名不符合规则,启动的时候会报异常。
  • USE_DECLARED_QUERY :声明方式创建,即本书说的注解的方式。启动的时候会尝试找到一个声明的查询,如果没有找到将抛出一个异常,查询可以由某处注释或其他方法声明。
  • CREATE_IF_NOT_FOUND :这个是默认的,以上两种方式的结合版。先用声明方式进行查找,如果没有找到与方法相匹配的查询,那用 Create 的方法名创建规则创建一个查询。
  • 除非有特殊需求,一般直接用默认的,不用管。以 Spring Boot 项目为例,更改其配置方法如下:

    @EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
    public class Example1Application {
       public static void main(String[] args) {
          SpringApplication.run(Example1Application.class, args);
    

    QueryLookupStrategy 是策略的定义接口,JpaQueryLookupStrategy 是具体策略的实现类,类图如下:

    查询方法的创建

    内部基础架构中有个根据方法名的查询生成器机制,对于在存储库的实体上构建约束查询很有用,该机制方法的前缀 find…By、read…By、query…By、count…By 和 get…By 从所述方法和开始分析它的其余部分(实体里面的字段)。

    感兴趣的读者可以到类 org.springframework.data.repository.query.parser.PartTree 查看相关源码的逻辑和处理方法,关键源码如下:

    引入子句可以包含其他表达式,例如在 Distinct 要创建的查询上设置不同的标志,然而,第一个 By 作为分隔符来指示实际标准的开始,在一个非常基本的水平,可以定义实体性条件,并与它们串联 And 和 Or。

    一句话概况,带查询功能的方法名有查询策略(关键字)+ 查询字段 + 一些限制性条件组成。

    看例子如下,可以直接 Controller 里面进行调用看看效果:

    interface PersonRepository extends Repository<User, Long> {
       // and的查询关系
       List<User> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
       // 包含distinct去重,or的sql语法
       List<User> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
       List<User> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
       // 根据lastname字段查询忽略大小写
       List<User> findByLastnameIgnoreCase(String lastname);
       // 根据lastname和firstname查询equal并且忽略大小写
       List<User> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); 
      // 对查询结果根据lastname排序
       List<User> findByLastnameOrderByFirstnameAsc(String lastname);
       List<User> findByLastnameOrderByFirstnameDesc(String lastname);
    

    解析方法的实际结果取决于创建查询的持久性存储。但是,有一些一般的事情要注意:

  • 表达式通常是可以连接运算符的属性遍历,可以使用组合属性表达式 And 和 or,还可以得到这样的运算关键字为支撑 Between、LessThan、GreaterThan、Like 为属性表达式,受支持的操作员可能因数据存储而异,需要注意一下。
  • 该方法解析器支持设置一个 IgnoreCase 标志个别特性(例如,findByLastnameIgnoreCase(…))或对于支持忽略大小写(通常是一个类型的所有属性 String 情况下,如 findByLastnameAndFirstnameAllIgnoreCase(…)),是否支持忽略案例可能会因存储而异,需要了解特定于场景的查询方法。
  • 还可以通过 OrderBy 在引用属性和提供排序方向(Asc 或 Desc)的查询方法中附加一个子句来应用静态排序,要创建支持动态排序的查询方法,来影响查询结果。
  • 3.4 关键字列表

    JPQL 表达

    注意除了 find 的前缀之外,我们查看 PartTree 的源码,还有如下几种前缀:

    private static final String QUERY_PATTERN = "find|read|get|query|stream";
    private static final String COUNT_PATTERN = "count";
    private static final String EXISTS_PATTERN = "exists";
    private static final String DELETE_PATTERN = "delete|remove";
    

    使用的时候要配合不同的返回结果进行使用,例如:

    interface UserRepository extends CrudRepository<User, Long> {
         long countByLastname(String lastname);//查询总数
         long deleteByLastname(String lastname);//根据一个字段进行删除操作
         List<User> removeByLastname(String lastname);
    

    大家也可以通过 Intellij IDEA :Edit -> Find -> Find In Path 工具查找到关键字对应的枚举在哪个类里面,如下:

    所以在这里作者介绍一工作中的小技巧,直接查看源码就可以知道框架支持了哪些关键字。

    Type 枚举的关键源码如下:

    public static enum Type {
        BETWEEN(2, new String[]{"IsBetween", "Between"}),
        IS_NOT_NULL(0, new String[]{"IsNotNull", "NotNull"}),
        IS_NULL(0, new String[]{"IsNull", "Null"}),
        LESS_THAN(new String[]{"IsLessThan", "LessThan"}),
        LESS_THAN_EQUAL(new String[]{"IsLessThanEqual", "LessThanEqual"}),
        GREATER_THAN(new String[]{"IsGreaterThan", "GreaterThan"}),
        GREATER_THAN_EQUAL(new String[]{"IsGreaterThanEqual", "GreaterThanEqual"}),
        BEFORE(new String[]{"IsBefore", "Before"}),
        AFTER(new String[]{"IsAfter", "After"}),
        NOT_LIKE(new String[]{"IsNotLike", "NotLike"}),
        LIKE(new String[]{"IsLike", "Like"}),
        STARTING_WITH(new String[]{"IsStartingWith", "StartingWith", "StartsWith"}),
        ENDING_WITH(new String[]{"IsEndingWith", "EndingWith", "EndsWith"}),
        IS_NOT_EMPTY(0, new String[]{"IsNotEmpty", "NotEmpty"}),
        IS_EMPTY(0, new String[]{"IsEmpty", "Empty"}),
        NOT_CONTAINING(new String[]{"IsNotContaining", "NotContaining", "NotContains"}),
        CONTAINING(new String[]{"IsContaining", "Containing", "Contains"}),
        NOT_IN(new String[]{"IsNotIn", "NotIn"}),
        IN(new String[]{"IsIn", "In"}),
        NEAR(new String[]{"IsNear", "Near"}),
        WITHIN(new String[]{"IsWithin", "Within"}),
        REGEX(new String[]{"MatchesRegex", "Matches", "Regex"}),
        EXISTS(0, new String[]{"Exists"}),
        TRUE(0, new String[]{"IsTrue", "True"}),
        FALSE(0, new String[]{"IsFalse", "False"}),
        NEGATING_SIMPLE_PROPERTY(new String[]{"IsNot", "Not"}),
        SIMPLE_PROPERTY(new String[]{"Is", "Equals"});
    ....}
    

    方法的查询策略的属性表达式(Property Expressions)

    属性表达式只能引用托管(泛化)实体的直接属性,如前一个示例所示。在查询创建时,已经确保解析的属性是托管实体的属性,但是,还可以通过遍历嵌套属性定义约束。假设一个 Person 实体对象里面有一个 Address 的属性里面包含一个 ZipCode 属性。

    在这种情况下,方法名为:

    List<Person> findByAddressZipCode(String zipCode);

    创建及其查找的过程是:解析算法首先将整个 part(AddressZipCode)解释为属性,并使用该名称(uncapitalized)检查域类的属性,如果算法成功,则使用该属性,如果不是,则算法拆分了从右侧的驼峰部分的信号源到头部和尾部,并试图找出相应的属性。在我们的例子中,AddressZip 和 Code 如果算法找到一个具有该头部的属性,那么它需要尾部,并从那里继续构建树,然后按照刚刚描述的方式将尾部分割,如果第一个分割不匹配,则算法将分割点移动到左(Address,ZipCode),然后继续。

    虽然这在大多数情况下应该起作用,但算法可能会选择错误的属性。假设 Person 该类也有一个 addressZip 属性,该算法将在第一个分割轮中匹配,并且基本上选择错误的属性,最后失败(因为该类型 addressZip 可能没有 code 属性)。

    要解决这个歧义,可以在方法名称中使用手动定义遍历点,所以我们的方法名称最终会如此:

    List<Person> findByAddress_ZipCode(ZipCode zipCode);

    当然 Spring JPA 里面是将下划线视为保留字符,但是强烈建议遵循标准 Java 命名约定(即不使用属性名称中的下划线,而是使用骆驼案例),属性命名的时候注意下这个特性。

    可以到 PartTreeJpaQuery.class 查询一下相关的 method 的 name 的拆分和实现逻辑,也可以利用开发工具的 Search anywhere 视图输入 PropertyExpression,然后 Find Used 就可以跟出很多源码,然后设置个断点,就可以进行分析了。

    查询结果的处理

    参数选择(Sort/Pageable)分页和排序

    特定类型的参数,Pageable 并动态 Sort 地将分页和排序应用于查询

    案例:在查询方法中使用 Pageable、Slice 和 Sort。

    Page<User> findByLastname(String lastname, Pageable pageable);
    Slice<User> findByLastname(String lastname, Pageable pageable);
    List<User> findByLastname(String lastname, Sort sort);
    List<User> findByLastname(String lastname, Pageable pageable);
    

    第一种方法允许将 org.springframework.data.domain.Pageable 实例传递给查询方法,以动态地将分页添加到静态定义的查询中,Page 知道可用的元素和页面的总数,它通过基础框架里面触发计数查询来计算总数。由于这可能是昂贵的,这取决于所使用的场景,说白了,当用到 Pageable 的时候会默认执行一条 cout 语句。而 Slice 的用作是,只知道是否有下一个 Slice 可用,不会执行count,所以当查询较大的结果集时,只知道数据是足够的,而相关的业务场景也不用关心一共有多少页。

    排序选项也通过 Pageable 实例处理,如果只需要排序,需在 org.springframework.data.domain.Sort 参数中添加一个参数即可,正如看到的,只需返回一个 List 也是可能的。在这种情况下,Page 将不会创建构建实际实例所需的附加元数据(这反过来意味着必须不被发布的附加计数查询),而仅仅是限制查询仅查找给定范围的实体。

    限制查询结果

    案例:在查询方法上加限制查询结果的关键字 First 和 top。

    User findFirstByOrderByLastnameAsc();
    User findTopByOrderByAgeDesc();
    Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
    Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
    List<User> findFirst10ByLastname(String lastname, Sort sort);
    List<User> findTop10ByLastname(String lastname, Pageable pageable);
    

    查询方法的结果可以通过关键字来限制 first 或 top,其可以被可互换使用,可选的数值可以追加到顶部/第一个以指定要返回的最大结果的大小。如果数字被省略,则假设结果大小为 1,限制表达式也支持 Distinct 关键字。此外,对于将结果集限制为一个实例的查询,支持将结果包装到一个实例中 Optional。如果将分页或切片应用于限制查询分页(以及可用页数的计算),则在限制结果中应用。

    查询结果的不同形式(List/Stream/Page/Future)

    Page 和 List 在上面的案例中都有涉及下面将介绍的几种特殊的方式。

    流式查询结果

    可以通过使用 Java 8 Stream 作为返回类型来逐步处理查询方法的结果,而不是简单地将查询结果包装在 Stream 数据存储中,特定的方法用于执行流。

    示例:使用 Java 8 流式传输查询的结果 Stream

    @Query("select u from User u")
    Stream<User> findAllByCustomQueryAndStream();
    Stream<User> readAllByFirstnameNotNull();
    @Query("select u from User u")
    Stream<User> streamAllPaged(Pageable pageable);
    

    注意:流的关闭问题,try cache 是一种用关闭方法。

    Stream<User> stream;
    try {
       stream = repository.findAllByCustomQueryAndStream()
       stream.forEach(…);
    } catch (Exception e) {
       e.printStackTrace();
    } finally {
       if (stream!=null){
          stream.close();
    
    异步查询结果

    可以使用 Spring 的异步方法执行功能异步执行存储库查询,这意味着方法将在调用时立即返回,并且实际的查询执行将发生在已提交给 Spring TaskExecutor 的任务中,比较适合定时任务的实际场景。

    @Async
    Future<User> findByFirstname(String firstname); (1)
    @Async
    CompletableFuture<User> findOneByFirstname(String firstname); (2)
    @Async
    ListenableFuture<User> findOneByLastname(String lastname);(3)
    
  • 使用 java.util.concurrent.Future 的返回类型。
  • 使用 java.util.concurrent.CompletableFuture 作为返回类型。
  • 使用 org.springframework.util.concurrent.ListenableFuture 作为返回类型。
  • 所支持的返回结果类型远不止这些,可以根据实际的使用场景灵活选择,其中 Map 和 Object[] 的返回结果也支持,这种方法不太推荐使用,应为没有用到对象思维,不知道结果里面装的是什么。

    下表列出了 Spring Data JPA Query Method 机制支持的方法的返回值类型。

    某些特定的存储可能不支持全部的返回类型。 只有支持地理空间查询的数据存储才支持 GeoResult、GeoResults、GeoPage 等返回类型。

    Optional 返回 Java 8 或 Guava 中的 Optional 类。查询方法的返回结果最多只能有一个,如果超过了一个结果会抛出 IncorrectResultSizeDataAccessException 的异常 Option Scala 或者 javaslang 选项类型 Stream Java 8 Stream Future Future,查询方法需要带有 @Async 注解,并开启 Spring 异步执行方法的功能。一般配合多线程使用。关系数据库,实际工作很少有用到 CompletableFuture 返回 Java8 中新引入的 CompletableFuture 类,查询方法需要带有 @Async 注解,并开启 Spring 异步执行方法的功能 ListenableFuture 返回 org.springframework.util.concurrent.ListenableFuture 类,查询方法需要带有 @Async 注解,并开启 Spring 异步执行方法的功能 Slice 返回指定大小的数据和是否还有可用数据的信息。需要方法带有 Pageable 类型的参数 在 Slice 的基础上附加返回分页总数等信息。需要方法带有 Pageable 类型的参数 GeoResult 返回结果会附带诸如到相关地点距离等信息 GeoResults 返回 GeoResult 的列表,并附带到相关地点平均距离等信息 GeoPage 分页返回 GeoResult,并附带到相关地点平均距离等信息

    而我们要看引用的那个 Spring Data 的实现子模块,以 Spring Data JPA 为例,看看 JPA 默认帮实现了哪些返回值类型。

    还是通过工具分析 JpaRepository 帮我们实现了哪些返回类型,这样不至于直接看官方文档的时候一头雾水。

    Projections 对查询结果的扩展

    Spring JPA 对 Projections 的扩展的支持,个人觉得这是个非常好的东西,从字面意思上理解就是映射,指的是和 DB 的查询结果的字段映射关系。一般情况下,我们是返回的字段和 DB 的查询结果的字段是一一对应的,但有的时候,需要返回一些指定的字段,不需要全部返回,或者返回一些复合型的字段,还得自己写逻辑。Spring Data 正是考虑到了这一点,允许对专用返回类型进行建模,以便更有选择地将部分视图对象。

    假设 Person 是一个正常的实体,和数据表 Person 一一对应,我们正常的写法如下:

    @Entity
    class Person {
       UUID id;
       String firstname, lastname;
       Address address;
       @Entity
       static class Address {
          String zipCode, city, street;
    interface PersonRepository extends Repository<Person, UUID> {
       Collection<Person> findByLastname(String lastname);
    

    (1)但是我们想仅仅返回其中的 name 相关的字段,应该怎么做呢?如果基于 projections 的思路,其实是比较容易的。只需要声明一个接口,包含我们要返回的属性的方法即可。如下:

    interface NamesOnly {
      String getFirstname();
      String getLastname();
    

    Repository 里面的写法如下,直接用这个对象接收结果即可,如下:

    interface PersonRepository extends Repository<Person, UUID> {
      Collection<NamesOnly> findByLastname(String lastname);
    

    Ctroller 里面直接调用这个对象可以看看结果。

    原理是,底层会有动态代理机制为这个接口生产一个实现实体类,在运行时。

    (2)查询关联的子对象,一样的道理,如下:

    interface PersonSummary {
      String getFirstname();
      String getLastname();
      AddressSummary getAddress();
      interface AddressSummary {
        String getCity();
    

    (3)@Value 和 SPEL 也支持:

    interface NamesOnly {
      @Value("#{target.firstname + ' ' + target.lastname}")
      String getFullName();
    

    PersonRepository 里面保持不变,这样会返回一个 firstname 和 lastname 相加的只有 fullName 的结果集合。

    (4)对 Spel 表达式的支持远不止这些:

    @Component
    class MyBean {
      String getFullName(Person person) {
        …//自定义的运算
    interface NamesOnly {
      @Value("#{@myBean.getFullName(target)}")
      String getFullName();
    

    (5)还可以通过 Spel 表达式取到方法里面的参数的值。

    interface NamesOnly {
      @Value("#{args[0] + ' ' + target.firstname + '!'}")
      String getSalutation(String prefix);
    

    (6)这时候有人会在想,只能用 interface 吗?dto 支持吗?也是可以的,也可以定义自己的 Dto 实体类,需要哪些字段我们直接在 Dto 类当中暴漏出来 get/set 属性即可,如下:

    class NamesOnlyDto {
      private final String firstname, lastname;
    //注意构造方法
      NamesOnlyDto(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
      String getFirstname() {
        return this.firstname;
      String getLastname() {
        return this.lastname;
    

    (7)支持动态 Projections,想通过泛化,根据不同的业务情况,返回不通的字段集合。

    PersonRepository做一定的变化,如下:
    interface PersonRepository extends Repository<Person, UUID> {
      Collection<T> findByLastname(String lastname, Class<T> type);
    

    我们的掉用方,就可以通过 class 类型动态指定返回不同字段的结果集合了,如下:

    void someMethod(PersonRepository people) {
    //我想包含全字段,就直接用原始entity(Person.class)接收即可
      Collection<Person> aggregates = people.findByLastname("Matthews", Person.class);
    //如果我想仅仅返回名称,我只需要指定Dto即可。
      Collection<NamesOnlyDto> aggregates = people.findByLastname("Matthews", NamesOnlyDto.class);
    

    最后,Projections 的应用场景还是挺多的,望大家好好体会,这样可以实现更优雅的代码,去实现不同的场景。不必要用数组,冗余的对象去接收查询结果。

    实现机制介绍

    通过 QueryExecutorMethodInterceptor 这个类的源代码,我们发现,该类实现了 MethodInterceptor 接口,也就是说它是一个方法调用的拦截器, 当一个 Repository 上的查询方法,譬如说 findByEmailAndLastname 方法被调用,Advice 拦截器会在方法真正的实现调用前,先执行这个 MethodInterceptor 的 invoke 方法。这样我们就有机会在真正方法实现执行前执行其他的代码了。

    然而对于 QueryExecutorMethodInterceptor 来说,最重要的代码并不在 invoke 方法中,而是在它的构造器 QueryExecutorMethodInterceptor(RepositoryInformationr、Object customImplementation、Object target) 中。

    最重要的一段代码是这段:

    for (Method method : queryMethods) { 
         // 使用lookupStrategy,针对Repository接口上的方法查询Query
         RepositoryQuery query = lookupStrategy.resolveQuery(method, repositoryInformation, factory, namedQueries); invokeListeners(query);
         queries.put(method, query);
    

    通过这个思路我们就可以找到很多具体的实现方法,其中有个重要类 PartTree,包含了主要算法逻辑,一图胜千言,我们来看一下网友提供的图。

    如果大家有问题需要互相交流:

  • QQ交流群1:240619787
  • QQ交流群2:559701472
  •