进学阁

业精于勤荒于嬉,行成于思毁于随

0%

构建一个通用的数据访问模块

当构建一个通用的数据访问模块的时候,一般需要处理分页,乐观锁、以及插入或者修改时一些公共字段默认值填充,以及慢查询处理定制化(虽然mysql自带的有这方面功能但是通知的灵活性不如自定义更实用)

处理分页、乐观锁

1
2
3
4
5
6
7
8
9
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//分页
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}

插入或者修改设置默认字段

首先定义基类BaseEntity,这里定义了createTime,updateTime,delFlag三个默认配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
@SuperBuilder
@NoArgsConstructor
public class BaseEntity {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

/**
* 删除标志
*/
@TableField(fill = FieldFill.INSERT)
private Integer delFlag;
}

MetaObjectHandler接口是mybatisPlus为我们提供的的一个扩展接口,我们可以利用这个接口在我们插入或者更新数据的时候,为一些字段指定默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DailyMartMetaObjectHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "delFlag", Integer.class, DelEnum.NORMAL.code());
}

@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}

/**
* 2023/10/28 调整
* Mybatis-plus的自动更新要求待更新的字段为null,否则不更新。这就导致select 后 更新时无法自动更新 updateTime
*/
@Override
public MetaObjectHandler strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) {
Object obj = fieldVal.get();
if (Objects.nonNull(obj)) {
metaObject.setValue(fieldName, obj);
}
return this;
}

将MetaObjectHandler注入

1
2
3
4
5
6
7
8
/**
* 在 插入/更新 数据时自动填充默认值
*/
@Bean
public PomeloMetaObjectHandler myMetaObjectHandler() {
return new PomeloMetaObjectHandler();
}

慢查询处理

mysql的慢查询可以通过很多地方拦截,比如数据库本身就可以通过配置来实现拦击并打印为日志,一部分开源的数据库连接池也可以实现,比如阿里的druid。这些虽然都可以实现慢查询拦截,但是这些属于第三方,不够灵活所以我们这里可以使用Mybatis提供的拦截器接口Interceptor来自己实现拦截,自己是实现时可以结合消息队列来完成阀值式处理,这些为后话,我们先来进行一个简单的实现

定义一个配置文件实体

1
2
3
4
5
6
7
8
@ConfigurationProperties(prefix = "pomelo.sql.slow")
@Data
public class SlowSQLProperties {

private boolean enable = false;

private long cost = 1000L;
}

实现Interceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Slf4j
@Intercepts({
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
@RequiredArgsConstructor
public class SlowSqlInterceptor implements Interceptor {
private final SlowSQLProperties slowSQLProperties;
@Override
public Object intercept(Invocation invocation) throws Throwable {

Object target = invocation.getTarget();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
StatementHandler statementHandler = (StatementHandler) target;
try {
return invocation.proceed();
} finally {
stopWatch.stop();
long costTime = stopWatch.getTotalTimeMillis();

if (costTime >= slowSQLProperties.getCost()) {
BoundSql boundSql = statementHandler.getBoundSql();

String sql = boundSql.getSql();

// 20230901 去掉sql中的换行符
sql = sql.replaceAll("\\n+", " ");

log.warn("WARN !!!,监测到慢查询SQL:[{}] 执行耗时 {} ms ", sql, costTime);
}

}
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

}

注入mybatis插件

1
2
3
4
5
6
7
8
/**
* 注入mybatis插件,可以统计SQL执行耗时
*/
@Bean
@ConditionalOnProperty(name = "pomelo.sql.slow.enable", havingValue = "true", matchIfMissing = true)
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return configuration -> configuration.addInterceptor(new SlowSqlInterceptor(slowSQLProperties));
}

替换ID生成器

分布式ID一文中我们自定义了ID,这里修改一下mybatis的id的生成器

1
2
3
4
5
6
7
8
9
10
11
public class IdGenerator implements IdentifierGenerator {
@Override
public Number nextId(Object entity) {
return (Long)IDUtils.generateKey();
}

@Override
public String nextUUID(Object entity) {
return IDUtils.get32UUID();
}
}

注入生成器

1
2
3
4
5
6
7
/**
* 替换Mybatis-plus的算法生成器
*/
@Bean
public IdGenerator identifierGenerator() {
return new IdGenerator();
}

统一扫描地址

当我们使用mybatis时需要在每个服务启动类上都需要通过@MapperScan注解指定Mapper对象的包路径地址。如果各个模块的命名规则之间遵循统一的命名规范,那么我们可以在配置主类中直接设置Mapper对象的包路径地址,这样SpringBoot启动类就不再需要依赖此注解了