WebFlux的探索与实战 - r2dbc的多表查询
在一个有数据库的项目中,条件查询与多表查询总是同幽灵般如影随形。
好久不见朋友们。 本篇文章会以我的 个人经验 来介绍下如何在 Spring WebFlux 中使用 Spring Data R2DBC 进行多表查询。
这次我会以一个自己写的项目作为基础来为各位介绍。如果你想了解如何创建一个 Spring WebFlux 项目,以及如何定义实体类、Repository类等,可以看 上一篇文章,这里便不会重点介绍了。
前排免责:
对于 'r2dbc的多表查询' 这个主题,我不能保证已完全参透或已经给出非常全面的应用场景,因此本文仅供参考。如果你有更好的使用案例、解决方案,欢迎在评论区留言交流讨论😘。
既然是以一个我写的某个项目为基础进行介绍,那么我需要先交代一下这个项目的一些信息,比如涉及的表、实体类和简单的功能介绍。
可能会为了便于编撰文章而简化部分细节
这是一个简单的用户认证服务,用来登录、注册、签发token等。
数据库使用的 MySQL。
它的表包括了 账户 - 角色 - 权限 - 资源 4张表,以及连接它们的3张中间表,总共7张表。
表结构
这里是通过工具生成的DDL:
create table fa_account
(
id int auto_increment
primary 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_uindex
unique (email)
)
comment '账户表';
create table fa_permission
(
id int auto_increment
primary 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 null
primary 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_uindex
unique (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_fk
foreign key (permission_id) references fa_permission (id)
on update cascade on delete cascade,
constraint fa_permission_resource_fa_resource_id_fk
foreign key (resource_id) references fa_resource (id)
on update cascade on delete cascade
)
comment '权限-资源关联表';
create table fa_role
(
id int auto_increment
primary 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_fk
foreign key (account_id) references fa_account (id)
on update cascade on delete cascade,
constraint fa_account_role_fa_role_id_fk
foreign 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_fk
foreign key (permission_id) references fa_permission (id)
on update cascade on delete cascade,
constraint fa_role_permission_fa_role_id_fk
foreign 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 {
@CreatedDate
private Instant createTime;
@LastModifiedDate
private Instant lastModifiedTime;
@Version
@JsonIgnore
private Integer version;
}
// BaseEntity.java
/**
* 公共抽象类。
*/
@Getter
@Setter
@ToString
public class BaseAuditingEntity {
public static final String TABLE_NAME_PREFIX = "fa_";
@Id
private 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;
@JsonIgnore
private 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 不支持关联查询。不过有关这个问题我们稍后再说。
场景重现
接下来,让我们先根据几个查询场景来看看我是如何实现的。