WebFlux的探索与实践 - r2dbc的分页查询
自从上次立下这系列的FLAG之后就再也不想碰了。今天难得早起出门面试,回家之后突发奇想打算再写点儿什么敷衍一下,于是便有了这篇文章。
前言
虽然响应式API更加适合流式列表的查询,但是分页这东西可是很常见的。
也没什么前言可说,反正就是一篇介绍如何在 Spring WebFlux 中使用 Spring Data R2DBC 进行分页查询的文章。如果喜欢,还望点个赞喵~
文章会从创建项目开始,你要是没啥兴趣,就往下划划。
准备
总而言之,先创建个项目,并且要加上 WebFlux
、R2DBC
和一个支持 R2DBC
的数据库驱动。
至于驱动的选择,你可以去 R2DBC官方网站的这里 看看。
你可以去 start.spring.io 去整个项目下来,我选择使用 gradle
构建项目,
这里是我的项目配置:
gradle.build.kts
plugins {
java
id("org.springframework.boot") version "3.0.3"
id("io.spring.dependency-management") version "1.1.0"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
compileOnly("org.projectlombok:lombok")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-webflux")
runtimeOnly("com.h2database:h2")
runtimeOnly("io.r2dbc:r2dbc-h2")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
testRuntimeOnly("com.h2database:h2")
testRuntimeOnly("io.r2dbc:r2dbc-h2")
}
tasks.withType<Test> {
useJUnitPlatform()
}
这里我选择使用 H2
数据库作为演示用的数据库,其他内容一律默认。
实体类与Repository
按照惯例,首先建个表用作示例:
schema.sql
DROP TABLE IF EXISTS foo;
CREATE TABLE IF NOT EXISTS foo(
id int auto_increment not null primary key comment 'id',
name varchar(20) not null default '' comment '名称',
size int not null default 0 comment '大小'
)
data.sql
INSERT INTO foo(name, size) VALUES ('name1', 1);
INSERT INTO foo(name, size) VALUES ('name2', 2);
INSERT INTO foo(name, size) VALUES ('name3', 3);
INSERT INTO foo(name, size) VALUES ('name4', 4);
INSERT INTO foo(name, size) VALUES ('name5', 114);
INSERT INTO foo(name, size) VALUES ('name6', 514);
INSERT INTO foo(name, size) VALUES ('name7', 19);
INSERT INTO foo(name, size) VALUES ('name8', 10);
INSERT INTO foo(name, size) VALUES ('name9', 11);
INSERT INTO foo(name, size) VALUES ('name10', 12);
INSERT INTO foo(name, size) VALUES ('name11', 13);
INSERT INTO foo(name, size) VALUES ('name12', 14);
INSERT INTO foo(name, size) VALUES ('name13', 15);
INSERT INTO foo(name, size) VALUES ('name14', 16);
INSERT INTO foo(name, size) VALUES ('name15', 17);
foo
是什么意思呢?我也不清楚,但是反正我们这次要去分页查询这个 foo
的表。
接下来,整个对应的实体类吧:
/**
* 数据库 foo 对应实体类
*
* @param id 主键
* @param name 名称
* @param size 大小
*/
public record Foo(@Id Integer id, String name, Integer size) {
}
然后给这个实体类提供一个对应的 Repository
实现。或者更准确的说,是 ReactiveRepository
的实现:
/**
* {@link Foo} 的 Repository 实现
* @author ForteScarlet
*/
@Repository
public interface FooRepository extends R2dbcRepository<Foo, Integer> {
}
顺带一提,R2dbcRepository<T, ID>
实现了下述三个基础接口:
ReactiveCrudRepository<T, ID>
ReactiveSortingRepository<T, ID>
ReactiveQueryByExampleExecutor<T>
那么这样就完成了吗?并没有。通常情况下,一个最简化的、整体性的分页数据应该包括 数据总量
和 分页数据列表
这两个信息,那么让我们首先来提供一个 Paged
类型:
/**
* 分页数据体
*
* @param total 数据总量
* @param data 数据列表
*/
public record Paged<T>(long total, List<T> data) {
}
接下来,因为我们之前的 FooRepository
中已经包含了查询数据总量的 count
,所以接下来我们只需要一个查询分页列表数据的方法就好了。十分幸运,R2DBC Repositories 的 Query Methods
支持我们直接这么写:
@Repository
public interface FooRepository extends R2dbcRepository<Foo, Integer> {
/**
* 分页查询 foo
* @param pageable 分页信息
* @return paged foo flux
*/
Flux<Foo> findAllBy(Pageable pageable);
}
直接在接口中增加一个如上所示的 findAllBy
并提供一个分页参数即可。当然,因为我们在用 r2dbc
,所以返回值应该是响应式的 Flux
类型。
这里的 Pageable
是Spring所提供的类型,所以可以直接拿来用。
接下来让我们来试试效果。先查询总数,再查询列表,然后将他们合并为一个 Paged
:
@SpringBootTest
class WebfluxR2dbcPageableDemoApplicationTests {
@Test
void pagedTest(@Autowired FooRepository repository) {
// 第一页的三条数据
var paged = PageRequest.of(0, 2);
repository.count().flatMap(total -> repository
.findAllBy(paged)
.collectList()
.map(list -> new Paged<>(total, list)))
.as(StepVerifier::create)
.consumeNextWith(System.out::println) // 控制台输出
.verifyComplete();
}
}
输出:
Paged[total=15, data=[Foo[id=1, name=name1, size=1], Foo[id=2, name=name2, size=2]]]
在这个单元测试中,我们首先准备了一个代表 第一页的三条数据
的分页信息。其中,PageRequest
是Spring提供的 Pageable
的一个基本的实现类,所以直接借来用了。
我们首先通过 repository.count
查询数据库数据总数 Mono<Integer>
, 再通过 flatMap
进行下一步,也就是查询列表。
查询列表使用了我们之前的 findAllBy(Pageable)
,然后使用 collectList
将其收集为一个 Mono<List<Foo>>
。
之后便是将总数和列表合并为了 Paged
,然后交给下游。
还是蛮简单的,不是吗?
简单条件查询
但是仅此而已吗?有些时候我们希望分页查询的结果是存在条件的,比如我们想要根据 name
的包含查询来查询结果。
那么接下来让我们来对 FooRepository
稍作调整,添加几个新函数:
/**
* 分页查询包含 name 的 foo
* @param name Foo的name,包含查询
* @param pageable 分页信息
* @return paged foo flux
*/
Flux<Foo> findAllByNameContains(String name, Pageable pageable);
/**
* 查询包含 name 的 foo 总数
* @param name Foo的name,包含查询
* @return count
*/
Mono<Long> countByNameContains(String name);
可以看到, 新的两个函数与之前的不同的是,它们都是以 ByNameContains
结尾,并且都多了一个 String name
参数。
这里的 ByNameContains
是 Spring Repositories Query Methods
的关键字(keyword)之一,Spring会根据你的关键字自行处理SQL。更多的关键字你可以去它们的文档 阅读,IDEA的智能提示也会帮你一把:
这些就是另外的话题了.回到正题,让我们再来试试这加了条件的分页查询是如何的:
@Test
void selectByNameTest(@Autowired FooRepository repository) {
// 第一页的三条数据
var paged = PageRequest.of(0, 2);
// 查询包含 'name1' 的内容
repository.countByNameContains("name1").flatMap(total -> repository
.findAllByNameContains("name1", paged)
.collectList()
.map(list -> new Paged<>(total, list)))
.as(StepVerifier::create)
.consumeNextWith(System.out::println) // 控制台输出
.verifyComplete();
}
与之前的测试用例没什么太大的区别,只不过是更换了一下方法名,然后添加了一个新的参数。
控制台输出:
2023-03-01T12:38:49.072+08:00 DEBUG 21376 --- [ Test worker] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT COUNT(FOO.ID) FROM FOO WHERE FOO.NAME LIKE $1]
2023-03-01T12:38:49.089+08:00 DEBUG 21376 --- [ Test worker] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT FOO.ID, FOO.NAME, FOO.SIZE FROM FOO WHERE FOO.NAME LIKE $1 LIMIT 2]
Paged[total=7, data=[Foo[id=1, name=name1, size=1], Foo[id=10, name=name10, size=12]]]
从 DEBUG 日志可以看到,Spring生成的SQL中为我们添加了 WHERE FOO.NAME LIKE $1
的查询条件,这也就说明我们的方法是可行的。