更好用的MybatisPlus:MybatisFlex(下)

更好用的Mybatis Plus:Mybatis Flex(下)

前言

上篇文章讲了 Mybaits Flex 的基础用法,这次讲一下 Mybatis Flex 的进阶用法,包含了一些 Mybatis Flex 核心的一些功能。(以下简称 MF

逻辑删除

上篇文章讲到了 @Column 注解,其中有个属性为 isLogicDelete ,当这个属性为 true 时,则标识该字段为逻辑删除字段,MF 会识别到该字段并进相应的一些处理。

image.png

当你在实体类中指明了逻辑删除字段后,在查询时 MF 会自动帮你拼接上 WHERE xxx = 0 的 SQL,而在你删除时则不会真正删除表中的数据,而是将你标识为逻辑删除的字段的值改为1,使其查询不到。

假如我们使用 MF 查询某张表:

1
2
3
4
5
@Test
void simpleSelect() {
List<SysUser> sysUsers = userMapper.selectAll();
sysUsers.forEach(System.out::println);
}

其执行 SQL 如下:

1
2
3
4
5
6
SELECT 
*
FROM
sys_user
WHERE
deleted = 0

需要注意的是:当你进行连表查询时,如果你连的表中同样标明了逻辑删除字段也会作为条件拼接。

1
2
3
4
5
6
7
8
9
10
@Test
void complexSelect() {
List<SysUser> sysUserList = userMapper.selectListByQuery(QueryWrapper.create()
.select()
.from(SysUser.class)
.leftJoin(SysDept.class)
.on(SYS_USER.DEPT_ID.eq(SYS_DEPT.DEPT_ID))
.where(SYS_USER.USER_ID.gt(5)));
sysUserList.forEach(System.out::println);
}

执行 SQL 如下:

1
2
3
4
5
6
7
8
SELECT
*
FROM
sys_user
LEFT JOIN sys_dept
ON sys_dept.deleted = 0
AND sys_user.dept_id = sys_dept.dept_id
WHERE sys_user.user_id > 5 AND sys_user.deleted = 0

而当你删除数据时:

1
2
3
4
@Test
void simpleDelete() {
userMapper.deleteById(11);
}

执行 SQL 如下:

1
2
3
4
5
6
UPDATE
sys_user
SET
deleted = 1
WHERE
user_id = 11

乐观锁

当同时对同一条数据进行操作时,我们应该只允许其中一个操作成功,其余操作应该失败掉,而 MF 也提供了相应的解决方案。

@Column 中有一个 version 属性,当其设置为 true 时,MF 会将其视为乐观锁字段,在新增数据时如果该数据没设值,则会默认设为0。而当其进行修改时,会先比对版本是否正确,如果正确才会修改成功,修改成功后会将版本号+1。

image.png

1
2
3
4
5
6
7
8
UPDATE 
account
SET
xxx = xxx,
version = version + 1
WHERE
id = ?
AND version = ?

需要注意的是:在同一张表中,只能有一个乐观锁字段,即:每个实体类中只能有一个被 @Column(version = true) 修饰的字段。

数据填充

当我们对某张表进行 Insert 或者 update 时,我们希望某些字段的值可以自动填充,此时就可以用到 MF 中提供的数据填充的功能,而在 MF 中提供了两种数据填充的方式:@Table 注解和 @Column 注解。

@Table 注解

@Table 注解中提供了 onInsert 属性和 onUpdate 属性来帮助我们自定义插入时的监听器。

image.png

@Table 注解中提供了三个属性来帮我们监听:插入修改设置,我们需要分别继承 MF 的:InsertListenerUpdateListenerSetListener 进行相应的操作,如下:

image.png

image.png

image.png

通过监听用户操作来帮我们完成一些额外的操作。

@Column 注解

@Column 注解中同样提供了数据填充的功能:onInsertValueonUpdateValue

image.png

类似于 Mybatis Plus 中 @TableField 注解中的 fill 属性,但是 MF 中的该功能会更强大,它甚至支持写 SQL 语句来填充值:

image.png

@Table 注解 与 @Column 注解的区别

在使用 @Table 注解中的数据填充功能时,我们设置的值是 基于Java层面的,在准备执行操作时会先触发监听器,监听器将操作执行完成之后,将参数传递至 ORM 进行处理。

而在使用 @Column 注解中的数据填充功能时,我们设置是基于 数据库层面的 ,我们设置的值会直接拼接到 SQL 中。

数据脱敏

在上一篇文章中我们讲了注解 @ColumnMask ,该注解是实现数据脱敏的核心注解,其仅有一个属性 value ,它指定了我们执行脱敏策略的名称。

image.png

而 MF 共提供了9种脱敏规则,如下:

image.png

  • MOBILE :手机号脱敏
  • FIXED_PHONE :固定电话脱敏
  • ID_CARD_NUMBER :身份证号脱敏
  • CHINESE_NAME :中文名脱敏
  • ADDRESS :地址脱敏
  • EMAIL :邮箱脱敏
  • PASSWORD :密码脱敏
  • CAR_LICENSE :车牌号脱敏
  • BANK_CARD_NUMBER :银行卡号脱敏

自定义脱敏规则

除此之外,我们还可以去自定义脱敏规则,我们利用内部提供的 MaskManager 来实现。

1
2
3
4
5
6
MaskManager.registerMaskProcessor("自定义规则名称",
data-> {
//进行脱敏操作
//返回脱敏后的数据
return data;
});

取消脱敏

在某些特定场景下,我们可能需要得到未脱敏的原始数据来进行操作,例如用户登陆时验证用户名与密码是否正确等。

MaskManager 中提供了两种方式来实现该功能:execWithoutMask()skipMask()restoreMask() ,需要注意的是execWithoutMask() 方法执行后会自动还原脱敏,而 skipMask()restoreMask() 需要配套使用才能实现取消脱敏和还原脱敏的操作。

使用方法如下:

1
2
3
4
SysUserMapper mapper = ...;
List<SysUser> userList = MaskManager.execWithoutMask(mapper::selectAll);
//业务操作
//...

又或者使用以下方式来实现:

1
2
3
4
5
6
7
8
9
10
try {
//取消脱敏
MaskManager.skipMask();
List<SysUser> userList = sysUserMapper.selectListByQuery(...);
//业务操作
//...
} finally {
//还原脱敏
MaskManager.retoreMask();
}

多数据源

在某些场景下我们可能会使用到多数据源的方式来实现我们的业务,设置方式如下:

1
2
3
4
5
6
7
8
9
10
mybatis-flex:
datasource:
db1:
url: jdbc:mysql://127.0.0.1:3306/db1
username: root
password: root
db2:
url: jdbc:mysql://127.0.0.1:3306/db2
username: root
password: root

MF 中也提供了两种方式来帮我们切换数据源:

  1. DataSourceKey.use()DataSourceKey.clear()
  2. @UseDataSource("datasourceName")
    • 当该注解加在Mapper上时,则指定该Mapper执行的方法使用指定数据源
    • 当该注解加在Mapper方法上时,则指定该方法使用指定数据源
    • 当该注解加在实体类上时,则指定该实体相关的增删改查使用指定数据源

@DataSourceKey.use()

1
2
3
4
5
6
7
try {
DataSourceKey.use("datasourceName");
//业务操作
//...
} finally {
DataSourceKey.clear();
}

@UseDataSource(“datasourceName”)

Mapper

@UseDataSource("datasourceName") 注解加在Mapper上时,指明该Mapper中所有方法使用指定数据源。

1
2
3
4
5
@UseDataSource("db1")
public interface SysUserMapper extends BaseMapper<SysUser> {
// Mapper方法
//...
}

Mapper方法

@UseDataSource("datasourceName") 注解加在Mapper方法上时,指明该Mapper中的该方法使用指定数据源。

1
2
3
4
5
public interface SysUserMapper extends BaseMapper<SysUser> {

@UseDataSource("db2")
List<SysUser> selectAll();
}

实体类

@UseDataSource("datasourceName") 注解加在实体类上时,指明该实体类所有的增删改查操作使用指定数据源。

1
2
3
4
5
6
@Data
@Table("sys_user")
@UseDataSource("db3")
public class SysUser {
//...
}

读写分离

读写分离是基于 多数据源 来实现的,我们通过一些操作使查询操作访问 数据库A ,而新增、修改、删除等操作在 数据库B 上进行。

假如我们有四个库:masterslave1slave2other ,我们的 增删改 操作在 master 数据源上进行,而查询操作随机在 slave1slave2 上进行,而在 other 库上我们进行一些特定操作。那我们需要进行以下操作:

首先我们要先在配置文件中指定我们的数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mybatis-flex:
datasource:
master:
url: jdbc:mysql://localhost:3306/master
username: root
password: root
slave1:
url: jdbc:mysql://localhost:3306/slave1
username: root
password: root
slave2:
url: jdbc:mysql://localhost:3306/slave2
username: root
password: root
other:
url: jdbc:mysql://localhost:3306/other
username: root
password: root

然后我们就可以来写我们的分片策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyShardingStrategy implements DataSourceShardingStrategy {
@Override
public String doSharding(String currentDataSourceKey,
Object mapper,
Method method,
Object[] methodArgs) {
//对于other库的操作我们直接放行
if("other".equals(currentDataSourceKey)) {
return currentDataSourceKey;
}

//如果mapper方法以 insert、update、delete开头,则在master库中执行
if(StringUtil.startWith(method.getName(), "insert", "update", "delete")) {
return "master";
}

//除去以上方法均在 salve 库中执行
return "slave*";
}
}

动态表名

当用户在对数据进行操作时,向 MF 传入表名,它能够根据上下文信息(例如用户信息、应用信息等)动态修改当前的表。

适用场景如下:

  • 多租户系统,当不同租户拥有不同的表时可使用动态表名来进行操作
  • 分库分表,当我们为减轻数据库压力而使用分库分表时可使用动态表名来进行操作

首先在应用启动时,我们要调用 TableManager.setDynamicTableProcessor() 方法 来配置动态表名处理器即可,如下:

1
2
3
4
5
6
TableManager.setDynamicTableProcessor(new DynamicTableProcessor(){
@Override
public String process(String tableName){
return tableName + "_1";
}
});

我们配置完成后,在对数据库进行 增删改查 操作时,均会调用 process 方法,待获取到新的表名后再进行处理。

在某些情况下,我们想临时修改映射关系,而非通过 process 方法获取,我们可以进行以下操作:

1
2
3
4
5
6
7
try {
TableManager.setHintTableMapping("sys_user", "sys_user_1");
//业务处理
...
} finally {
TableManager.clear();
}

除了动态表名之外,MF 还支持 动态Scheme(模式),与动态表名配置类似,如下:

1
2
3
4
5
6
TableManager.setDynamicSchemeProcessor(new DynamicSchemeProcessor() {
@Override
public String process(String schema) {
return schema + "_1";
}
});

注意:动态Scheme的配置,只对使用了 @Table(schema = "xxx") 注解的实体类有效。

而当我们使用 SpringBoot 时,可以直接通过写配置类的方式进行注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class DynamicConfiguration {
@Bean
public DynamicTableProcessor dynamicTableProcessor() {
DynamicTableProcessor processor = new ....;
return processor;
}

@Bean
public DynamicSchemaProcessor dynamicSchemaProcessor(){
DynamicSchemaProcessor processor = new ....;
return processor;
}
}

数据权限

我们在系统中可能会遇到不同用户、不同角色或者不同部门通过同一个接口查询时,得到的结果却不同的业务场景。MF 提供了 数据权限 的功能来供我们自定义数据权限。

自定义数据方言 IDialect

在自定义数据方言中,我们可以通过重写 forSelectByQuery 方法来构建通过 QueryWrapper 来查询的方法。

1
2
3
4
5
6
7
8
9
10
public class MyDataPermissionDialect extends CommonsDialectImpl {

@Override
public String forSelectByQuery(QueryWrapper wrapper) {
//此处可以获取当前用户信息、角色信息或部门信息
//通过用户信息或部门信息给查询增加条件
wrapper.and(...);
return super.buildSelectSql(wrapper);
}
}

重写IService方法

与 MP 中一样,MF也提供了 IService 接口以及默认的实现类,我们可以通过构建自己的 IServiceImpl 来实现该功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyServiceImpl<M extends BaseMapper<T>, <T>) implements IService<T> {
@Autowried
protected M mapper;

@Override
public BaseMapper<T> getMapper() {
return mapper;
}

@Override
public List<T> list(QueryWrapper wrapper) {
//此处可以获取当前用户信息、角色信息或部门信息
//通过用户信息或部门信息给查询增加条件
return IService.super.list(wrapper);
}
}

字段权限

假如我们在某一张表中有多个字段,但是根据不同的用户、部门或角色查询,得到的结果是不同的,例如在 sys_user 表中存有用户的密码信息,但 password 只允许本人与超管查询,此时我们就可以用到 MF 提供的 字段权限 的功能。

上一篇文章我们介绍了 @Table 注解,该注解中有一个属性名为 onSet ,我们可以通过该属性来编写我们自定义的监听器,当我们查询时会先执行 SQL,当我们的 SQL 执行完之后会将结果集映射到实体类中,此时 mybaits 会调用实体类中的 setter 方法,我们的监听器就会生效,我们主动调用 setter 方法时监听器不会生效。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
@Data
@Table(value = "sys_user", onSet = MyPasswordSetListener.class)
public class SysUser {
@Id(keyType = KeyType.Auto)
private Long userId;

private String username;

private String password;
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyPasswordSetListener implements SetListener {
@Override
public Object onSet(Object entity, String property, Object value) {
//查询用户是否拥有查询密码的权限
boolean hasPasswordPermission = hasPasswordPermission();

//若用户不拥有查询密码的权限则直接返回空
if(Boolean.FALSE.equals(hasPasswordPermission)) {
return null;
}

return value;
}
}

字段加密

当我们在数据库表中的某个字段存储为明文,但是想在获取时返回的为加密内容,此时可用到 MF为我们提供的 字段加密 的功能。

同样的,我们的实现也是通过 @Table 中的 onSet 属性来实现的,如下:

1
2
3
4
5
6
7
8
9
10
11
@Data
@Table(value = "sys_user", onSet = MyFieldEncryptionListener.class)
public class SysUser {
@Id(keyType = KeyType.Auto)
private Long userId;

private String username;

private String password;
...
}
1
2
3
4
5
6
7
8
9
public class MyFieldEncryptionListener implements SetListener {
@Override
public Object onSet(Object entity, String property, Object value) {
if(null != value) {
return encrypt(value);
}
return value;
}
}

字典回写

假如我们有一张字典表,字典表里存着一些业务字段,例如存放着性别的值,1表示男,2表示女,其余表示未知,此时在新增或者修改时,我们需要根据该表中配置的信息对前端进行回显。

我们依旧是使用 @Table 中的 onSet 属性来实现,为了保证数据类型一致,我们需要新加一个业务字段来进行回显,而不是使用原有的字段进行回显。

1
2
3
4
5
6
7
8
9
10
@Data
@Table(value = "sys_dict")
public class SysDict {
@Id(keyType = KeyType.Auto)
private Long dictId;

private String value;

private Integer code;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@Table(value = "sys_user",onSet = MyEchoListener.class)
public class SysUser {
@Id(keyType = KeyType.Auto)
private Long userId;

private int gender;

@Column(ignore = true)
private String genderStr;

...
}
1
2
3
4
5
6
7
8
9
10
11
public class MyEchoListener implements SetListener {
@Override
public Object onSet(Object entity, String property, Object value) {
SysUser sysUser = (SysUser) entity;
if("gender".equals(property) && Objects.nonNull(value)) {
String genderStr = getGenderStrByDict(property);
sysUser.setGenderStr(genderStr);
}
return value;
}
}

枚举属性

在某些场景下,我们可能不需要通过字典表去管理我们的状态、性别等,此时我们可以使用枚举进行管理。

在 MF 中提供了一个名为 EnumTypeandler 的内置处理器,当我们没有显示设置时就会默认使用该处理器,但是只使用该处理器只能将枚举中的名字保存至数据库,而当我们需要将枚举中的某个值存入数据库时,我们需要在枚举类中进行一些处理。

1
2
3
4
5
6
7
8
9
@Data
@Table("sys_user")
public class SysUser {
@Id(keyType = KeyType.Auto)
private Long userId;

private MyEnum myEnum;
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum MyEnum {
NORMAL(1, "正常"),
BLOCK_UP(2, "停用"),
FREEZING(3, "冻结"),
LOCK(4, "锁定");

@EnumValue
private int code;

private String statusStr;

MyEnum(int code, String statusStr) {
this.code = code;
this.statusStr = statusStr;
}

//getter
...
}

我们通过 @EnumValue 注解来指定枚举中的哪个属性将被作为值传入数据库,同样的,在读取时,MF 也会自动将数据库中查出来的值映射成枚举。

注意事项:

  • @EnumValue 注解标注的属性,要求必须被 public 修饰,或者拥有 getter 方法。
  • 当枚举中的属性配置了 @EnumValue 时,在 QueryWrapper 构建时,传入枚举,自动使用该值进行 SQL 参数拼接。如下:
1
2
3
4
QueryWrapper wrapper = QueryWrapper.create();
wrapper.select()
.from(SysUser.class)
.where(SYS_USER.MY_ENUM.eq(MyEnum.NORMAL));

其执行 SQL 如下:

1
2
3
4
5
6
SELECT
*
FROM
sys_user
WHERE
my_enum = 1

多租户

简单来讲,多租户就是一个应用可以为多个用户提供服务,且用户与用户之间数据隔离互不干扰。多租户技术要求所有用户共用同一个数据中心,但能提供多个客户端相同甚至可定制化的服务,并且仍然可以保障客户的数据隔离。

多租户的数据隔离有许多种方案,但最为常见的是以列进行隔离的方式。MF 内置的正是通过指定的列(租户ID tenant_id)进行隔离的方案。

当我们使用 MF 的多租户功能时,我们需要进行2个步骤,示例如下:

  1. 通过 @Column(tenantId = true) 来标识租户列
1
2
3
4
5
6
7
8
9
10
@Data
@Table(value = "sys_user")
public class SysUser {
@Id(keyType = KeyType.Auto)
private Long userId;

@Column(tenantId = true)
private Long tenantId;
...
}
  1. 配置 TenantManagerTenantFactory
1
2
3
4
5
6
TenantManager.setTenantFactory(new TenantFactory() {
@Override
public Object[] getTenantIds() {
return ...;
}
});

TenantFactory

TenantFactory 主要的作用为获取当前的租户ID,并在 增删改查 时自动带上 TenantFactory “生产” 的数据。其内部方法如下:

image.png

getTenantIds 要求返回一个数组,原因有如下场景:

  • 场景1:租户对自己的数据进行增删改查,返回的 Object[] 数组只有租户自己的 ID 就可以了。
  • 场景2:租户可以对自己,以及其他租户(比如下级租户)的数据进行增删改查,那么要求返回的 Object[] 必须包含其他租户的 ID。比如某个数据列表, 除了显示租户自己的数据以外,还包含下级租户的数据,这种场景则要求 getTenantIds 返回多个值。
  • 场景3:忽略租户条件,由代码自定义条件查询,此项要求 getTenantIds 返回 null 或者 空数组。

需要注意的是: 在整个应用中,应该 只有一个 TenantFactory 实例,然后再通过其 getTenantIds() 方法里去获取当前的租户 ID。在 Spring 框架中,我们可以通过在 RequestContextHolder 中去获取当前的租户 ID。

1
2
3
4
5
6
7
8
9
public class MyTenantFactory implements TenantFactory {

@Override
public Object[] getTenantIds() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
Long tenantId = attributes.getAttribute("tenantId", RequestAttributes.SCOPE_REQUEST);
return new Object[]{tenantId};
}
}

我们自定义完成之后还要定义一个拦截来器获取 tenantId

1
2
3
4
5
6
7
8
9
10
11
12
public class MyTenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//通过 request 去获取租户 ID
Long tenantId = getTenantIdByReuqest(request);

//设置租户ID到 request 的 attribute
request.setAttribute("tenantId", tenantId);

return true;
}
}

最后我们需要将拦截器注册到 Spring 容器中。

1
2
3
4
5
6
7
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyTenantInterceptor());
}
}

如果我们使用的是 SpringBoot 时,我们可以直接通过 @Configuration 来配置。

1
2
3
4
5
6
7
@Configuration
public class MyTenantConfig {
@Bean
public TenantFactory tenantFactory() {
return new MyTenantFactory();
}
}

忽略多租户

在某些场景下,我们可能需要忽略多租户来对数据库进行操作,此时我们可以使用 TenantManager 中的 withoutTenantCondition 方法来完成。

1
2
SysUserMapper mapper = ...;
List<SysUser> userList = TenantManager.withoutTenantCondition(mapper::selectAll);

我们来看一下 withoutTenantCondition 方法的内部实现方式,如下:

image.png

里面通过调用 ignoreTenantCondition 方法和 restoreTenantCondition 方法来完成多租户的忽略与恢复。

那么我们也可以通过 ignoreTenantConditionrestoreTenantCondition 方法来完成代码比较复杂的情况,需要注意的是:ignoreTenantCondition 方法和 restoreTenantCondition 方法需要配套使用

1
2
3
4
5
6
7
try {
TenantManager.ignoreTenantCondition();
//相应的业务操作
...
} finally {
TenantManager.restoreTenantCondition();
}

当然,除了以上两种情况之外,假如 TenantFactory 返回为空的情况下也会忽略多租户。

全局配置

除了单独配置完,我们也可以通过配置 FlexGlobalConfig 来实现多租户的全局配置。

1
FlexGlobalConfig.getDefaultConfig().setTenantColumn("tenant_id");

当我们使用此方法进行全局配置时,可省略 @Column(tenantId = true) 的注解。

注意事项

需要注意的是:当我们在新增时,如果实体类中标注了多租户字段,那么我们对这个字段设置任何值,最终都会被 TenantFactory 返回的内容所覆盖,假如 TenantFactory 返回的是一个数组,那么默认会取第一个值去填充,如果 TenantFacotry 返回为空或 null ,则会保留设置的值传入数据库。

示例如下:

1
2
3
SysUser user = new SysUser();
user.setTenantId(999);
userMapper.insert(user);

对于以上代码,会有两种情况:

  1. TenantFactory 返回的值不为空时,则默认取第一个值给 tenantId
  2. TenantFactory 返回的值为空或 null 时,tenantId 的值为 999

而当我们对实体类中标注了多租户的字段时,其所有通过 MF 来进行的查询、修改和删除均会带上租户的条件,执行 SQL 示例如下:

1
2
3
4
5
DELETE FROM
sys_user
WHERE
user_id = ?
AND tenant_id = ?

TenantFactory 返回的租户ID为数组时,执行 SQL 如下:

1
2
3
4
5
DELETE FROM
sys_user
WHERE
user_id = ?
AND tenant_id IN (?,?,...,?);

修改与查询同上所述。

总结

总的来说,Mybatis-Flex 为我们提供了许多的方法与功能供我们扩展,相对来说还是很强大的,希望文章能够帮助到各位,感谢观看。