前言
在一個有數據庫的項目中,條件查詢與多表查詢總是同幽靈般如影隨形。
好久不見朋友們,我是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_time
、last_modified_time
、version
字段。它們通過 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 本身并不支持直接進行關聯查詢,那么我們只能退而求其次,
將這些數據分步查詢。也就是說,我們:
- 先查詢賬戶(
Account
)信息 - 根據賬戶信息,查詢所有角色(
Role
)信息 - 根據這些角色信息(
Set<role_id>
),查詢所有權限(Permission
)信息 - 根據這些權限信息(
Set<permission_id>
),查詢所有資源(Resource
)信息
在這其中:
- 假設不會有大集合數據(比如一個用戶關聯的角色最多100個)
- 由于只是一種單純的查詢,不考慮嚴格的數據一致性,因此不加事務
那么讓我們來準備好這個 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 不支持關聯查詢,同時在一開始我們提到過,這幾個表之間的關系都是多對多的,換言之,想查詢"用戶的所有角色",就需要關聯它們的中間表才能做到。
為了貫徹這一小節中我們說的 “分步” 查詢,我們接下來要做的是:
- 從中間表,查詢對應
account_id
的所有role_id
- 根據這些
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
的查詢。接下來如法炮制,完成剩下的、對 Permission
和 Resource
的查詢吧!
最終完整的 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
。
既然你能夠堅持閱讀到這里,那么為了表示感謝,我將會先來解釋一下這個 method
和 Resource
的關系。
根據設計,這個系統中的’資源’,也就是 Resource
是用于在 網關 中進行權限校驗的 “路由” 信息。
比如:/hello/world/**
, /auth/*/login
之類的。
同時,一個資源可能會被分配給不同的權限(Permission
), 這時候便會通過中間表 PermissionResource
來控制這個權限是針對這個資源的那些 訪問方式。
而這個訪問方式便是 Rest API 中的 HTTP method, 它們以比特位的形式記錄在 method
中。比如 權限1 允許以 GET
的形式訪問 資源1,那么它的 method
便是 0x0001
,也就是 1
。
好了,接下來,我們需要這樣一個接口:根據 account_id
查詢它對應的全部資源,且要求:
- 這些資源以及關聯鏈路上的其他所有(比如
Role
、Permission
或某個中間表) 的enable
都要為true
。 - 如果入參
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字符串。而且這里不僅需要拼接字符串,我們還可能要遇到:
- 手動綁定 (
bind
) SQL變量 - 手動映射結果 (
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-j
的 8.3.0
及以下版本中,它可能對虛擬線程并不友好。不過,這已經在它們的日程中了,終有一天,不是嗎?
2. H2
閱讀 h2database#3824 可以得知,在 2.2.222
之前,它并不是虛擬線程友好的。而在這個議題本身和與之相關的 h2database#3850 中,議題的發起者與開發者之間的交鋒也有著很多有用的信息,可以幫助你了解一個庫對于是否需要支持虛擬線程這件事都需要有哪些考量、以及虛擬線程的一些小細節。
3. 其他
如果你知道其他的相關信息資訊,也歡迎在評論區留言交流喔!
尾聲
到這里內容就結束了,如果你耐心的看到了最后,那么我十分感謝你對我的認可與支持!
文章疏淺,如有遺漏或錯誤,歡迎在評論區指正,感謝你的閱讀,我們下次再見~