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.pswp.cn/news/718628.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/718628.shtml
英文地址,請注明出處:http://en.pswp.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…