参数选择(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 {@IdUUID id;String firstname, lastname;Address address;@Entitystatic 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 的应用场景还是挺多的,望大家好好体会,这样可以实现更优雅的代码,去实现不同的场景。不必要用数组,冗余的对象去接收查询结果。