Spring Data JPA 从入门到精通~查询结果的处理

参数选择(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<T> 作为返回类型来逐步处理查询方法的结果,而不是简单地将查询结果包装在 Stream 数据存储中,特定的方法用于执行流。

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

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

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

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 等返回类型。

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

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

Projections 对查询结果的扩展

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

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

@Entity
class Person {
   @Id
   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 的应用场景还是挺多的,望大家好好体会,这样可以实现更优雅的代码,去实现不同的场景。不必要用数组,冗余的对象去接收查询结果。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值