WebFlux的探索与实战 - r2dbc的多表查询

前言

在一个有数据库的项目中,条件查询与多表查询总是同幽灵般如影随形。

好久不见朋友们,我是forte。
本篇文章会以我的 个人经验 来介绍下如何在 Spring WebFlux 中使用 Spring Data R2DBC 进行多表查询。

这次我会以一个自己写的项目作为基础来为各位介绍。如果你想了解如何创建一个 Spring WebFlux 项目,以及如何定义实体类、Repository类等,可以看 上一篇文章,这里便不会重点介绍了。

前排免责:

对于 ‘r2dbc的多表查询’ 这个主题,我不能保证已完全参透或已经给出非常全面的应用场景,因此本文仅供参考。如果你有更好的使用案例、解决方案,欢迎在评论区留言交流讨论😘。

交代项目

既然是以一个我写的某个项目为基础进行介绍,那么我需要先交代一下这个项目的一些信息,比如涉及的表、实体类和简单的功能介绍。

可能会为了便于编撰文章而简化部分细节

这是一个简单的用户认证服务,用来登录、注册、签发token等。
数据库使用的 MySQL。
它的表包括了 账户 - 角色 - 权限 - 资源 4张表,以及连接它们的3张中间表,总共7张表。

表结构

这里是通过工具生成的DDL:

create table fa_account
(id                 int auto_incrementprimary key,username           varchar(200)      not null,zone_id            varchar(255)      not null comment '时区ID值',email              varchar(254)      null,password           varchar(254)      null,status             tinyint default 0 not null,create_time        datetime          not null,last_modified_time datetime          not null,version            int     default 0 not null,constraint fa_account_email_uindexunique (email)
)comment '账户表';create table fa_permission
(id                 int auto_incrementprimary key,name               varchar(100)      not null,category           varchar(100)      null,enable             tinyint default 1 not null,status             tinyint default 0 not null,create_time        datetime          not null,last_modified_time datetime          not null,version            int     default 0 not null
)comment '权限表';create table fa_resource
(id                 int               not nullprimary key,pattern            varchar(500)      not null,type               tinyint           not null,remark             varchar(500)      null,enable             tinyint default 1 not null,status             tinyint default 0 not null,category           varchar(100)      null,create_time        datetime          not null,last_modified_time datetime          not null,version            int     default 0 not null,constraint fa_resource_pattern_uindexunique (pattern)
)comment '资源表';create table fa_permission_resource
(permission_id      int               not null,resource_id        int               not null,remark             varchar(500)      null,enable             tinyint default 1 not null,method             int               not null,create_time        datetime          not null,last_modified_time datetime          not null,version            int     default 0 not null,primary key (permission_id, resource_id),constraint fa_permission_resource_fa_permission_id_fkforeign key (permission_id) references fa_permission (id)on update cascade on delete cascade,constraint fa_permission_resource_fa_resource_id_fkforeign key (resource_id) references fa_resource (id)on update cascade on delete cascade
)comment '权限-资源关联表';create table fa_role
(id                 int auto_incrementprimary key,name               varchar(100)      not null,is_default         tinyint default 0 not null,is_init            tinyint default 0 not null,category           varchar(100)      null,enable             tinyint default 1 not null,status             tinyint default 0 not null,color              int               null,create_time        datetime          not null,last_modified_time datetime          not null,version            int     default 0 not null
)comment '角色表';create table fa_account_role
(account_id         int               not null,role_id            int               not null,enable             tinyint default 0 not null,create_time        datetime          not null,last_modified_time datetime          not null,version            int     default 0 not null,primary key (account_id, role_id),constraint fa_account_role_fa_account_id_fkforeign key (account_id) references fa_account (id)on update cascade on delete cascade,constraint fa_account_role_fa_role_id_fkforeign key (role_id) references fa_role (id)on update cascade on delete cascade
)comment '账户-权限表';create table fa_role_permission
(role_id            int               not null,permission_id      int               not null,enable             tinyint default 0 not null,create_time        datetime          not null,last_modified_time datetime          not null,version            int               not null,primary key (role_id, permission_id),constraint fa_role_permission_fa_permission_id_fkforeign key (permission_id) references fa_permission (id)on update cascade on delete cascade,constraint fa_role_permission_fa_role_id_fkforeign key (role_id) references fa_role (id)on update cascade on delete cascade
)comment '角色-权限关联表';

你可以观察到一些特点:

  • 每个表都会以 fa_ 开头。这是它们的一个统一的表前缀。
  • 每个表都包括了 create_timelast_modified_timeversion 字段。它们通过 Spring Data R2DBC: Auditing 来实现一些审计(自动填充、更新之类的)能力。在 Spring Data JPA 里也有它们的身影。
  • 每个表都有 enable 字段。这些表都被设计为可以进行"开关"的, 也包括那些中间表。

实体类

这些表都各自需要一个实体类,也包括那些中间表。
它们的实体类大概是如下的样子(会经过部分简化,并使用了 Lombok):

// BaseAuditingEntity.java
/*** 公共抽象类,但是没有 ID*/
@Getter
@Setter
@ToString
public class BaseAuditingEntity {@CreatedDateprivate Instant createTime;@LastModifiedDateprivate Instant lastModifiedTime;@Version@JsonIgnoreprivate Integer version;
}// BaseEntity.java/*** 公共抽象类。*/
@Getter
@Setter
@ToString
public class BaseAuditingEntity {public static final String TABLE_NAME_PREFIX = "fa_";@Idprivate Long id;
}// Account.java/*** 账户信息*/
@Table(Account.TABLE_NAME)
@Getter
@Setter
@ToString
public class Account extends BaseEntity {public static final String BASE_TABLE_NAME = "account";public static final String TABLE_NAME = BaseEntity.TABLE_NAME_PREFIX + BASE_TABLE_NAME;private String username;private String email;@JsonIgnoreprivate String password;private Integer status;private ZoneId zoneId;
}// Role.java/*** 角色信息*/
@Table(Role.TABLE_NAME)
@Getter
@Setter
@ToString
public class Role extends BaseEntity {public static final String BASE_TABLE_NAME = "role";public static final String TABLE_NAME = BaseEntity.TABLE_NAME_PREFIX + BASE_TABLE_NAME;private String name;private String category;@Column("is_default")private Boolean defaultValue; // = false,@Column("is_init")private Boolean init; // = false,private Boolean enable; // = true,private Integer status; // = 0,private Integer color;
}// AccountRole.java/*** account - role 中间表*/
@Table(AccountRole.TABLE_NAME)
@Getter
@Setter
@ToString
public class AccountRole extends BaseAuditingEntity {public static final String BASE_TABLE_NAME = Account.BASE_TABLE_NAME + "_" + Role.BASE_TABLE_NAME;public static final String TABLE_NAME = BaseEntity.TABLE_NAME_PREFIX + BASE_TABLE_NAME;private Long accountId;private Long roleId;private Boolean enable;
}// Permission.java/*** 权限信息*/
@Table(Permission.TABLE_NAME)
@Getter
@Setter
@ToString
public class Permission extends BaseEntity {public static final String BASE_TABLE_NAME = "permission";public static final String TABLE_NAME = BaseEntity.TABLE_NAME_PREFIX + BASE_TABLE_NAME;private String name;private Boolean enable; private String category; private Integer status; 
}// RolePermission.java/*** role - permission 中间表*/
@Table(RolePermission.TABLE_NAME)
@Getter
@Setter
@ToString
public class RolePermission extends BaseAuditingEntity {public static final String BASE_TABLE_NAME = Role.BASE_TABLE_NAME + "_" + Permission.BASE_TABLE_NAME;public static final String TABLE_NAME = BaseEntity.TABLE_NAME_PREFIX + BASE_TABLE_NAME;private Long roleId;private Long permissionId;private Boolean enable;
}// Resource.java/*** 资源信息*/
@Table(Resource.TABLE_NAME)
@Getter
@Setter
@ToString
public class Resource extends BaseEntity {public static final String BASE_TABLE_NAME = "resource";public static final String TABLE_NAME = BaseEntity.TABLE_NAME_PREFIX + BASE_TABLE_NAME;private String pattern;private String remark;private Integer type;private Boolean enable;private Integer status;private String category;
}// PermissionResource.java/*** permission - resource 中间表*/
@Table(PermissionResource.TABLE_NAME)
@Getter
@Setter
@ToString
public class PermissionResource extends BaseAuditingEntity {public static final String BASE_TABLE_NAME = Permission.BASE_TABLE_NAME + "_" + Resource.BASE_TABLE_NAME;public static final String TABLE_NAME = BaseEntity.TABLE_NAME_PREFIX + BASE_TABLE_NAME;private Long permissionId;private Long resourceId;private String remark;private Boolean enable;private Integer method;
}

如果你熟悉 JPA,那么你可能发现了:在 R2DBC 中,并没有什么 @ManyToOne@ManyToMany 之类的关系注解给你用。在实体类中,你能做的便是定义与数据库基本一致的字段,然后选择性的添加一些注解(例如 @Id, @Version),就这么多。

换言之,首先你要明白:R2DBC 不支持关联查询。不过有关这个问题我们稍后再说。

场景重现

接下来,让我们先根据几个查询场景来看看我是如何实现的。

1. 分步查询: 某账户的全量信息

上文我们提到,表结构中共有四级:账户 - 角色 - 权限 - 资源,它们都是互相多对多的,因此一个 全量 的账户信息,可以大概表示为如下形式(扁平化后):

public record AccountFullView(Account account,List<Role> roles,List<Permission> permissions,List<Resource> resources
) {
}

那么接下来,准备一个 Service, 来实现根据某个 account_id 来查询对应账户的全量信息。

首先,简单交代一下思路。由于 R2DBC 本身并不支持直接进行关联查询,那么我们只能退而求其次,
将这些数据分步查询。也就是说,我们:

  1. 先查询账户(Account)信息
  2. 根据账户信息,查询所有角色(Role)信息
  3. 根据这些角色信息(Set<role_id>),查询所有权限(Permission)信息
  4. 根据这些权限信息(Set<permission_id>),查询所有资源(Resource)信息

在这其中:

  1. 假设不会有大集合数据(比如一个用户关联的角色最多100个)
  2. 由于只是一种单纯的查询,不考虑严格的数据一致性,因此不加事务

那么让我们来准备好这个 Service:

@Service
@RequiredArgsConstructor
public class AccountService {private final AccountRepository accountRepository;private final RoleRepository roleRepository;private final PermissionRepository permissionRepository;private final ResourceRepository resourceRepository;public Mono<AccountFullView> full(Long accountId) {// TODO 实现...return null;}}

然后接下来在 full 中实现逻辑。
回顾上述的步骤,先进行最简单的一步:查询用户信息:

@Service
@RequiredArgsConstructor
public class AccountService {private final AccountRepository accountRepository;private final RoleRepository roleRepository;private final PermissionRepository permissionRepository;private final ResourceRepository resourceRepository;private static class AccountFullViewContext {Account account = null;List<Role> roles = Collections.emptyList();Set<Long> roleIds = Collections.emptySet();List<Permission> permissions = Collections.emptyList();Set<Long> permissionIds = Collections.emptySet();List<Resource> resources = Collections.emptyList();AccountFullView toView() {return new AccountFullView(account, roles, permissions, resources);}}public Mono<AccountFullView> full(Long accountId) {// 准备一个 contextfinal var context = new AccountFullViewContext();final var accountMono = accountRepository.findById(accountId).switchIfEmpty(Mono.error(new NoSuchElementException("Account(id=" + accountId + ")")));// TODO 实现...return null;}
}

上面代码中的 AccountFullViewContext 是一个供 full 内的数据流流转使用的一个 “上下文” 类型,
它会随着流程的一步步推进而逐步完善其内部的各属性,并在最终通过 toView 将结果转化为 AccountFullView

当然,你也可以选择不使用这种上下文的形式而是拆分出各个阶段的结果或者其他更好的方式,如何实现都是可以的。

不得不说,在 Java 中用响应式编程,一个简单的逻辑就可以把你的代码塞得满满当当的…
照着这股劲,将剩下的步骤继续完成!

是的,接下来便是 R2DBC 的地狱了。
首先回顾一下,我们说过,R2DBC 不支持关联查询,同时在一开始我们提到过,这几个表之间的关系都是多对多的,换言之,想查询"用户的所有角色",就需要关联它们的中间表才能做到。

为了贯彻这一小节中我们说的 “分步” 查询,我们接下来要做的是:

  1. 从中间表,查询对应 account_id 的所有 role_id
  2. 根据这些 role_id,再去查询所有角色

那么,我们继续!

要完成这个任务,我们首先得需要一个 AccountRoleRepository, 也就是查询中间表实体 AccountRole 的仓库。我们来创建一个:

public interface AccountRoleRepository extends Repository<AccountRole, Long> {/*** 根据 account id 查询 AccountRole集*/Flux<AccountRole> findAllByAccountId(Long accountId);
}

也许你注意到了,对于一个中间表实体的持久化仓库,我直接使用了 Repository 而不是 R2dbcRepository。这是为什么呢? R2dbcRepository 中提供的那些方法都是基于一个主键ID的,而作为一个中间表,它并没有一个具体的主键字段,所以我们也就不需要那些方法了。

如果你熟悉 JPA, 那么你可能会想要去尝试使用 @Embedded@Id 来实现一个组合式的复合主键类型。而在你准备尝试之前,也许你可以先去看看 spring-projects/spring-data-relational#574,来提前了解它为什么还不支持,以及大家围绕这个问题展开的讨论。

@Service
@RequiredArgsConstructor
public class AccountService {private final AccountRepository accountRepository;private final AccountRoleRepository accountRoleRepository;private final RoleRepository roleRepository;private final PermissionRepository permissionRepository;private final ResourceRepository resourceRepository;private static class AccountFullViewContext {...}public Mono<AccountFullView> full(Long accountId) {// 准备一个 contextfinal var context = new AccountFullViewContext();final var accountMono = accountRepository.findById(accountId).switchIfEmpty(Mono.error(new NoSuchElementException("Account(id=" + accountId + ")")));accountMono.flatMap(account -> {// 初始化 accountcontext.account = account;// 查询得到 rolesvar contextMono = accountRoles(context, account);// TODO permissionsreturn null;});// TODO 实现...return null;}private Mono<AccountFullViewContext> accountRoles(AccountFullViewContext context, Account account) {return accountRoleRepository.findAllByAccountId(account.getId()).map(AccountRole::getRoleId)// 将 AccountRole.roleId 收集为 Set..collect(Collectors.toSet()).flatMap(roleIdSet -> {// 查询所有的角色return roleRepository.findAllById(roleIdSet).collectList().map(roles -> {// 初始化 context 中的属性context.roles = roles;context.roleIds = roleIdSet;return context;});});}}

又是一小步,这样我们便完成了对 Role 的查询。接下来如法炮制,完成剩下的、对 PermissionResource 的查询吧!

最终完整的 Service 内实现大概是这个样子的:

@Service
@RequiredArgsConstructor
public class AccountService {private final AccountRepository accountRepository;private final AccountRoleRepository accountRoleRepository;private final RoleRepository roleRepository;private final RolePermissionRepository rolePermissionRepository;private final PermissionRepository permissionRepository;private final PermissionResourceRepository permissionResourceRepository;private final ResourceRepository resourceRepository;private static class AccountFullViewContext {Account account = null;List<Role> roles = Collections.emptyList();Set<Long> roleIds = Collections.emptySet();List<Permission> permissions = Collections.emptyList();Set<Long> permissionIds = Collections.emptySet();List<Resource> resources = Collections.emptyList();AccountFullView toView() {return new AccountFullView(account, roles, permissions, resources);}}/*** 查询并获取用户的全量'扁平化'信息.*/public Mono<AccountFullView> full(Long accountId) {// 准备一个 contextfinal var context = new AccountFullViewContext();final var accountMono = accountRepository.findById(accountId).switchIfEmpty(Mono.error(new NoSuchElementException("Account(id=" + accountId + ")")));return accountMono.flatMap(account -> {// 初始化 accountcontext.account = account;// 查询各结果并合并return accountRoles(context, account).flatMap(this::rolePermissions).flatMap(this::permissionResources).map(AccountFullViewContext::toView);});}private Mono<AccountFullViewContext> accountRoles(AccountFullViewContext context, Account account) { // 实际上 account 也能省略return accountRoleRepository.findAllByAccountId(account.getId()).map(AccountRole::getRoleId)// 将 AccountRole.roleId 收集为 Set..collect(Collectors.toSet()).flatMap(roleIdSet -> {// 查询所有的角色return roleRepository.findAllById(roleIdSet).collectList().map(roles -> {// 初始化 context 中的属性context.roles = roles;context.roleIds = roleIdSet;return context;});});}private Mono<AccountFullViewContext> rolePermissions(AccountFullViewContext context) {return rolePermissionRepository.findAllByRoleIdIn(context.roleIds).map(RolePermission::getPermissionId).collect(Collectors.toSet()).flatMap(permissionIdSet -> {// 查询所有的权限return permissionRepository.findAllById(permissionIdSet).collectList().map(permissions -> {context.permissionIds = permissionIdSet;context.permissions = permissions;return context;});});}private Mono<AccountFullViewContext> permissionResources(AccountFullViewContext context) {var resourceIds = permissionResourceRepository.findAllByPermissionIdIn(context.permissionIds).map(PermissionResource::getResourceId);// 查询所有资源return resourceRepository.findAllById(resourceIds).collectList().map(resources -> {context.resources = resources;return context;});}
}

2. 有条件的连表查询: 某账户所有符合条件的’资源’

接下来是另一个课题。之前我们提到过,大部分实体表和关联表都有一个 enable 字段代表对应的信息是否"启用",然后如果你仔细观察便会发现,在 PermissionResource (表 fa_permission_resource) 中有一个字段:method

既然你能够坚持阅读到这里,那么为了表示感谢,我将会先来解释一下这个 methodResource 的关系。

根据设计,这个系统中的’资源’,也就是 Resource 是用于在 网关 中进行权限校验的 “路由” 信息。
比如:/hello/world/**, /auth/*/login 之类的。

同时,一个资源可能会被分配给不同的权限(Permission), 这时候便会通过中间表 PermissionResource 来控制这个权限是针对这个资源的那些 访问方式

而这个访问方式便是 Rest API 中的 HTTP method, 它们以比特位的形式记录在 method 中。比如 权限1 允许以 GET 的形式访问 资源1,那么它的 method 便是 0x0001,也就是 1

好了,接下来,我们需要这样一个接口:根据 account_id 查询它对应的全部资源,且要求:

  1. 这些资源以及关联链路上的其他所有(比如 RolePermission 或某个中间表) 的 enable 都要为 true
  2. 如果入参 method 不为 null,则根据位运算计算访问方式与这个参数 完全相同 的资源。也就是使用 method & param.method == method 这种方式进行计算。

那么这个课题,我将会使用一个 整个SQL 来完成。但是我需要提醒你,结果可能并非如你期望的那样。

首先,准备 Service 和方法:

@Service
@RequiredArgsConstructor
public class AccountService {private final R2dbcEntityTemplate entityTemplate; Flux<Resource> accountResources(Long accountId, @Nullable Integer method) {// TODOreturn null;}
}

当你看到它的第一眼,你会觉得它很简短,而后当你看到 R2dbcEntityTemplate,我想你也许已经猜到了事情之后的发展方向。

是的,正如前述,使用 整个SQL 的方式,便是一种大家耳熟能详的方式:拼接SQL字符串。而且这里不仅需要拼接字符串,我们还可能要遇到:

  1. 手动绑定 (bind) SQL变量
  2. 手动映射结果 (Row, RowMetadata)

不过也好在正是因为有 R2dbcEntityTemplate 的存在,我们可以 相对 轻松的完成这些工作。

废话不多说,我们来看下一步的代码:

@Service
@RequiredArgsConstructor
public class AccountService {private final R2dbcEntityTemplate entityTemplate;Flux<Resource> accountResources(Long accountId, @Nullable Integer method) {var builder = new StringBuilder();builder.append("SELECT DISTINCT r.* FROM " + Resource.TABLE_NAME + " r \n" +"LEFT JOIN " + PermissionResource.TABLE_NAME + " pr ON r.id = pr.resource_id AND pr.enable\n" +"LEFT JOIN " + RolePermission.TABLE_NAME + " frp ON pr.permission_id = frp.permission_id AND frp.enable\n" +"LEFT JOIN " + Role.TABLE_NAME + " role ON.role_id = role.id AND role.enable\n" +"LEFT JOIN " + AccountRole.TABLE_NAME + " accr ON role.id = accr.role_id AND accr.enable\n" +"WHERE accr.account_id = :accountId AND r.enable");if (method != null) {builder.append("\n AND pr.method & :method = :method");}// TODO 绑定参数// TODO 查询结果并返回return null;}
}

我们使用一大坨 LEFT JOIN 进行这一串表关系的关联,并通过它们各自的 AND xxx.enable 完成对 enable 的筛选,
并在最后的 WHERE 处通过 accr.account_id = :accountId 来指定目标结果对应的账户ID。

而后,当 method 不为 null 时,直接使用 SQL 的位运算来计算它的条件,也同时为 SQL 中添加了一个 :method 参数。

也许到这时候,这段代码可以为你解释为什么在一开始我定义实体类的时候,要为那些实体类添加它们各自的 表名常量 TABLE_NAME 了。

接着,我们来为这段 SQL 绑定参数:

    Flux<Resource> accountResources(Long accountId, @Nullable Integer method) {var builder = new StringBuilder();builder.append(...);if (method != null) {builder.append(...);}var sql = builder.toString();// 绑定参数var spec = entityTemplate.getDatabaseClient().sql(sql).bind("accountId", accountId);if (method != null) {spec = spec.bind("method", method);}// TODO 查询结果并返回return null;}

使用 R2dbcEntityTemplate 获取到一个 databaseClient 并使用 sql 创建一个执行器后,便可以轻松的为它绑定参数了。

参数绑定完成后,就是执行了,让我们继续:

    Flux<Resource> accountResources(Long accountId, @Nullable Integer method) {var builder = new StringBuilder();builder.append(...);if (method != null) {builder.append(...);}var sql = builder.toString();// 绑定参数var spec = entityTemplate.getDatabaseClient().sql(sql).bind("accountId", accountId);if (method != null) {spec = spec.bind("method", method);}// 查询结果并返回return spec.map((row, meta) -> entityTemplate.getConverter().read(Resource.class, row, meta)).all();}

我们通过 map 来指定对查询结果的行数据(Row, RowMetadata)进行处理,好在 R2dbcEntityTemplate 为我们提供了转化器,它可以快速的将行数据解析为某个指定的类型, 而后便是最终得到数据的方式,all 也就是获取所有的结果。

思考总结

以上便是我遇到的两个使用 R2DBC 进行较为复杂的关系条件查询的最终应用方案了。

其实这两个场景中使用的这两种方法 (分步查询、拼接SQL) 也都是可以互相替代的,但是正如你所见,它们都并不是那么的"友好"。

这时你可能一些疑问,比如是否有能支持关系实体/关系查询的第三方库、为什么 R2DBC 官方不支持关系实体、以及有什么更好增加使用 R2DBC 的体验的方式等等。

这些问题我也思考过,也有问题至今仍在摸索和思考。

是否有能支持关系实体/关系查询的第三方库?

首先:我没有针对性地、长时间地、深入地去搜索、体验过,但我认为应该是有的。

最值得一提的便是 querydsl。虽然我没在 R2DBC 体验过,但是我在 spring-data-jpa 中还是很喜欢 querydsl 的。

与之相关的议题你可以参考:

  • querydsl#2468 (虽然关闭了,但是是因为长时间没有活跃信息而被 bot 关闭的)
  • spring-data-r2dbc#529

结合参考上面这两个议题后可能会发现,似乎 R2DBC 还不支持 querydsl。但是有一个 PR 在最近出现了: OpenFeign/querydsl#292。从名字上不难看出,这是 OpenFeign对querydsl的分支 。为什么要 fork querydsl?为什么要在 fork 在 OpenFeign 组织库下?如果你感兴趣,可以前往 OpenFeign/querydsl 来了解它的更多信息。不管怎样,也许它未来可期。

它在 querydsl#2468 提供了对 R2DBC 的支持。你可以前往这个 issue 来了解更详细的内容。

如果你了解其他的好用的第三方库,也欢迎评论留言分享。

为什么 R2DBC 官方不支持关系实体

其实首先一点是,诸如 @ManyToMany 这类关系注解并非是 Spring 的东西,而是 JPA 的,在 spring-data-jpa 中由 Hibernate 实现的。而 R2DBC 则并不是一个 JPA 的实现,所以实际上没有这些注解是理所当然的。

实际上 spring-data-r2dbc 更像 spring-data-jdbc 一些。

不过,针对 “支持一对一和一对多关系” 这个话题,官方与社区也是有讨论的,并且这个议题从2020年直至今日也依旧活跃:spring-data-r2dbc#356

你如果对这个话题感兴趣,好奇为什么官方迟迟不支持、社区对此有何种愿景(抱怨)与建议、官方针对这个内容又有哪些回应,你可以前往了解一下。

其中也包括了本文章第 2 个场景中使用 R2dbcConverter 处理 SQL 查询结果的方案 (issue评论-771587180) ———— 是的,灵感就是来源于此 issue。

有什么更好增加使用 R2DBC 的体验的方式

1. 使用 Kotlin

如果你使用 Kotlin 来搭配 Java 的任何一个响应式库(也包括 R2DBC 涉及的 reactor),你就会发现原本的那种在 Java 中被响应流式API所折磨的情况不复存在。Kotlin 的协程与挂起,可以让你如同写同步代码一样来控制你的响应式代码。

以我们之前提到的场景 1 为例:

    suspend fun full(userId: Long): UserFullView {val user = accountRepository.findById(userId).awaitSingleOrNull()?: throw EntityNotFoundException("User", userId)val (roles, roleIds) = accountRoles(userId)val (permissions, permissionIds) = rolePermissions(roleIds)val resources = permissionResources(permissionIds)return AccountFullView(user, roles, permissions, resources)}        // 那几个方法省略...

你也不再需要那个 AccountFullViewContext

Kotlin 的魅力不仅如此。如果你对 Kotlin 有兴趣,那么是时候为你安利它了!

  • Kotlin 官网
  • Kotlin 官方文档 (也可在官方文档右上角进入)
  • Kotlin 中文站
  • Koans: 让你熟悉 Kotlin 语法和关键词的一系列练习
  • Learn Kotlin by Example: 一套为 Kotlin 新手设计的官方小而简单的注释示例。无需任何编程语言知识

不瞒你说,本文中最开始说的这个’练手’项目,实际就是用 Kotlin 写的 —— 所有的代码示例,都是临时新建的项目,重新用 Java 又写了一遍 😢

2. 虚拟线程也不错

何必死守 R2DBC 呢?如果你需要使用一个关系型数据库,又希望得到类似响应式库这种避免传统阻塞API带来的性能问题,那么我想 JDK21 的 虚拟线程 也许会更合你的胃口。

虚拟线程允许你使用一如既往同步代码,而享受到自动切换物理线程的好处。你可以在 Oracle文档: Virtual Threads 来了解它,或者…我想现在应该有不少有关它的帖子了吧?去搜搜看,记得选一些靠谱的。

不过先不要着急,本篇文章当下的场景,是我们要在使用数据库的前提下使用虚拟线程。
但是,真的所有数据库驱动的实现都支持虚拟线程吗?

这里根据我所知的信息,为你提供一些参考:

1. MySQL

在 MySQL#95 中,有人为 MySQL 提交了有关支持虚拟线程相关的内容提交,但是截止到写下此篇文章,似乎这个改动尚未被发布。换言之,至少在 mysql-connector-j8.3.0 及以下版本中,它可能对虚拟线程并不友好。不过,这已经在它们的日程中了,终有一天,不是吗?

2. H2

阅读 h2database#3824 可以得知,在 2.2.222 之前,它并不是虚拟线程友好的。而在这个议题本身和与之相关的 h2database#3850 中,议题的发起者与开发者之间的交锋也有着很多有用的信息,可以帮助你了解一个库对于是否需要支持虚拟线程这件事都需要有哪些考量、以及虚拟线程的一些小细节。

3. 其他

如果你知道其他的相关信息资讯,也欢迎在评论区留言交流喔!

尾声

到这里内容就结束了,如果你耐心的看到了最后,那么我十分感谢你对我的认可与支持!

文章疏浅,如有遗漏或错误,欢迎在评论区指正,感谢你的阅读,我们下次再见~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/718628.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[课程]yolov9目标检测封装成类调用

搞定系列&#xff1a;yolov9目标检测封装成类调用 课程地址&#xff1a;https://edu.csdn.net/course/detail/39352 课程介绍课程目录讨论留言 你将收获 学会yolov9封装基本技巧和大体思路 学会yolov9封装类的API调用技巧和自由扩展 学会使用Pycharm调试技巧和运行脚本技…

「连载」边缘计算(二十四)03-04:边缘部分源码(源码分析篇)

&#xff08;接上篇&#xff09; 在Register()函数中对EdgeHub struct的初始化只是对EdgeHub struct中的controller进行初始化。controller的初始化函数具体如下所示。 KubeEdge/edge/pkg/edgehub/controller.go //NewEdgeHubController creates and returns a EdgeHubContro…

uniapp+vue基于Android的图书馆借阅系统qb4y3-nodejs-php-pyton

uni-app框架&#xff1a;使用Vue.js开发跨平台应用的前端框架&#xff0c;编写一套代码&#xff0c;可编译到Android、小程序等平台。 框架支持:springboot/django/php/Ssm/flask/express均支持 前端开发:vue 语言&#xff1a;pythonjavanode.jsphp均支持 运行软件:idea/eclip…

2023天津公租房网上登记流程图,注册到信息填写

2023年天津市公共租赁住房网上登记流程图 小编为大家整理了天津市公共租赁住房网上登记流程&#xff0c;从登记到填写信息。 想要体验的朋友请看一下。 申请天津公共租赁住房时拒绝申报家庭情况会怎样&#xff1f; 天津市住房保障家庭在享受住房保障期间&#xff0c;如在应申…

智慧草莓基地:Java与SpringBoot的技术革新

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

xss.haozi:0x00

0x00没有什么过滤所以怎么写都没有关系有很多解 <script>alert(1)</script>

【Linux取经路】文件系统——inode与软硬链接

文章目录 一、前言二、认识硬件——磁盘2.1 磁盘的存储构成2.2 磁盘的逻辑抽象 三、操作系统对磁盘的使用3.1 再来理解创建文件3.2 再来理解删除文件3.3 再来理解目录 四、硬链接五、软链接六、结语 一、前言 在之前的【Linux取经路】文件系统之被打开的文件——文件描述符的引…

DevStack 基于 Ubuntu 部署 OpenStack

Devstack 简介 DevStack 是一系列可扩展的脚本&#xff0c;用于基于 git master 的最新版本快速调出完整的 OpenStack 环境。devstack 以交互方式用作开发环境和 OpenStack 项目大部分功能测试的基础。 devstack 透过执行 stack.sh 脚本&#xff0c;搭建 openstack 环境&…

AcWing 799. 最长连续不重复子序列

Problem: AcWing 799. 最长连续不重复子序列 文章目录 思路解题方法复杂度Code 思路 这是一个求最长连续不重复子序列的问题。我们可以使用双指针&#xff08;滑动窗口&#xff09;的方法来解决。我们维护一个窗口&#xff0c;并使用一个数组来记录窗口内元素的出现次数。当窗口…

深度学习的一个完整过程通常包括以下几个步骤

深度学习的一个完整过程通常包括以下几个步骤&#xff1a; 问题定义和数据收集&#xff1a; 定义清晰的问题&#xff0c;明确任务的类型&#xff08;分类、回归、聚类等&#xff09;以及预期的输出。收集和整理用于训练和评估模型的数据集。确保数据集的质量&#xff0c;进行预…

车联网产品与应用

在中国&#xff0c;先是小鹏汽车官宣“智驾覆盖城市数量、可用里程以及用户口碑均为行业第一”。后有华为问界官宣OTA&#xff0c;领航功能全国可用路段高达99%&#xff0c;“全国都能用&#xff0c;哪哪都能开”。 似乎分分钟&#xff0c;“自动驾驶”就要干成了。但日新月异的…

Day31|贪心算法1

贪心的本质是选择每一阶段的局部最优&#xff0c;从而达到全局最优。 无固定套路&#xff0c;举不出反例&#xff0c;就可以试试贪心。 一般解题步骤&#xff1a; 1.将问题分解成若干子问题 2.找出适合的贪心策略 3.求解每一个子问题的最优解 4.将局部最优解堆叠成全局最…

【MySQL】深入解析 Buffer Pool 缓冲池

文章目录 1、前置知识1.1、Buffer Pool介绍1.2、后台线程1.2.1、Master Thread1.2.2、IO Thread1.2.3、Purge Thread1.2.4、Page Cleaner Thread 1.3、重做日志缓冲池 2、Buffer Pool 组成2.1、数据页2.2、索引页2.3、undo页2.4、插入缓冲2.5、锁空间2.6、数据字典2.6、自适应哈…

JavaScript之structuredClone现代深拷贝

在JavaScript中&#xff0c;实现深拷贝的方式有很多种&#xff0c;每种方式都有其优点和缺点。今天介绍一种原生JavaScript提供的structuredClone实现深拷贝。 下面列举一些常见的方式&#xff0c;以及它们的代码示例和优缺点&#xff1a; 1. 使用JSON.parse(JSON.stringify(…

代码随想录 二叉树第四周

目录 617.合并二叉树 700.二叉搜索树中的搜索 98.验证二叉搜索树 530.二叉搜索树的最小绝对差 501.二叉搜索树中的众树 236.二叉树的最近公共祖先 617.合并二叉树 617. 合并二叉树 简单 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其…

【Rust】——切片

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

第105讲:Mycat垂直分表实战:从规划到解决问题的完整指南

文章目录 1.垂直分表的背景2.垂直分表案例实战2.1.垂直分表规划2.2.配置Mycat实现垂直分表2.3.重启Mycat2.4.在Mycat命令行中导入数据结构2.5.查看由Mycat分表后每个分片上存储的表2.6.Mycat垂直分表后可能遇到的问题2.7.垂直分表完成 1.垂直分表的背景 我们的商城系统数据库&…

Unity编辑器下如何获取物体(GameObject)的中心位置

注意仅能在编辑器下才能使用该方法 实现方式依靠UnityEditor.Tools提供的参数&#xff0c;具体实现如下&#xff1a; 获取单个物体的中心坐标 public static Vector3 GetGameObjectCenter(GameObject gameObject) {// 选中物体Selection.activeObject gameObject;// 记录当前…

C#中Byte.Parse的用法,如果需要解析含有数字以外的字符,应该如何使用?

在C#中&#xff0c;Byte.Parse用于将字符串解析为byte类型的数字。它的用法如下&#xff1a; byte result Byte.Parse(str);其中&#xff0c;str是要解析的字符串。 如果要解析的字符串含有数字以外的字符&#xff0c;Byte.Parse会抛出一个FormatException异常。为了处理这种…

javaWebssh水利综合信息管理系统myeclipse开发mysql数据库MVC模式java编程计算机网页设计

一、源码特点 java ssh水利综合信息管理系统是一套完善的web设计系统&#xff08;系统采用ssh框架进行设计开发&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCA…