350 lines
15 KiB
Markdown
350 lines
15 KiB
Markdown
# rdms-spring-boot-starter-mybatis
|
||
|
||
## 模块定位
|
||
`rdms-spring-boot-starter-mybatis` 不是一个“只把 MyBatis-Plus 引进来”的轻量 starter,而是项目里的数据库访问基础设施模块。它把下面几类能力统一收口到一个地方:
|
||
|
||
1. 数据源与事务管理
|
||
2. MyBatis-Plus 与 MyBatis-Plus-Join 的统一装配
|
||
3. 多数据库兼容策略
|
||
4. DO 基类、审计字段自动填充、逻辑删除
|
||
5. Mapper / Wrapper 的统一编码风格
|
||
6. 常用 TypeHandler
|
||
7. Easy-Trans 的 VO 翻译接入
|
||
|
||
从仓库里的实际用法看,这个模块倾向于让业务模块在 DAL 层遵循一套固定约定,而不是每个模块自己重复配置 MyBatis、分页、排序、主键策略和字段填充。
|
||
|
||
## 设计思路
|
||
|
||
## 1. 用一个 starter 统一数据库层约定
|
||
这个模块直接依赖了:
|
||
|
||
1. `druid-spring-boot-3-starter`
|
||
2. `mybatis-plus-spring-boot3-starter`
|
||
3. `dynamic-datasource-spring-boot3-starter`
|
||
4. `mybatis-plus-join-boot-starter`
|
||
5. `easy-trans-spring-boot-starter`
|
||
|
||
这说明模块不是“最小封装”路线,而是把项目默认采用的数据库栈整体打包,业务模块只需要依赖这一个 starter,就能得到一致的运行时行为。
|
||
|
||
## 2. 优先解决“约定不一致”问题,而不是只提供工具类
|
||
模块里最核心的不是某一个工具类,而是整套约定:
|
||
|
||
1. `@MapperScan` 统一扫描 `${rdms.info.base-package}`
|
||
2. `BaseDO` 统一审计字段和逻辑删除字段
|
||
3. `BaseMapperX` 统一分页、批量操作、便捷查询
|
||
4. `LambdaQueryWrapperX` / `QueryWrapperX` 统一动态拼条件写法
|
||
5. `IdTypeEnvironmentPostProcessor` 统一不同数据库下的主键策略
|
||
|
||
这套设计可以让代码生成器、业务模块和框架模块都围绕同一套 DAL 模型工作。
|
||
|
||
## 3. 用“自动推断”减少多数据库切换成本
|
||
模块针对不同数据库运行场景做了几层兼容:
|
||
|
||
1. `pom.xml` 直接预留了 MySQL、Oracle、PostgreSQL、SQL Server、达梦、人大金仓、openGauss 等驱动
|
||
2. `IdTypeEnvironmentPostProcessor` 会根据主数据源 URL 自动判断 `DbType`
|
||
3. 如果 `mybatis-plus.global-config.db-config.id-type=NONE`,则自动改写为更适合当前数据库的 `AUTO` 或 `INPUT`
|
||
4. 同一个后处理器顺手补齐 Quartz 的 `driverDelegateClass`
|
||
5. `QueryWrapperX.limitN` 和 `MyBatisUtils.findInSet` 也在做跨数据库差异适配
|
||
|
||
也就是说,这个模块尽量不让业务代码关心“当前连接的是哪一种数据库”,而是把差异尽量前移到框架层。
|
||
|
||
## 4. 把“业务里高频重复的 Mapper 写法”沉到基类里
|
||
`BaseMapperX<T>` 是这个模块最重要的抽象之一。它在 `BaseMapper` 之上继续补了几类默认能力:
|
||
|
||
1. `selectPage(...)`:直接返回项目统一的 `PageResult`
|
||
2. `selectJoinPage(...)`:把 Join 查询也纳入同一分页模型
|
||
3. `selectOne` / `selectList` / `selectCount` 的便捷重载
|
||
4. `insertBatch` / `updateBatch` 等批量操作
|
||
5. 针对 SQL Server 的批量插入特殊处理
|
||
6. `PAGE_SIZE_NONE` 时的“不分页查询”约定
|
||
|
||
这背后的设计取向很明确:让 Mapper 接口本身承载一部分“轻业务语义”的默认实现,减少 XML 和 Service 层重复代码。
|
||
|
||
## 5. 动态查询要“少写 if”,而不是把 if 搬到 Service 层
|
||
`LambdaQueryWrapperX`、`QueryWrapperX`、`MPJLambdaWrapperX` 都提供了 `xxxIfPresent` 系列方法,例如:
|
||
|
||
1. `likeIfPresent`
|
||
2. `eqIfPresent`
|
||
3. `inIfPresent`
|
||
4. `betweenIfPresent`
|
||
|
||
这样 Service 或 Mapper 默认方法可以直接链式拼接条件,空值自动跳过。这样可以把“查询条件存在才拼 SQL”这件事收口到 Wrapper,而不是散落在业务代码里做大量 `if (param != null)`。
|
||
|
||
## 6. 基础字段治理优先于业务字段治理
|
||
`BaseDO` 和 `DefaultDBFieldHandler` 体现了这个模块在数据治理上的几个固定要求:
|
||
|
||
1. 所有 DO 默认带 `createTime`、`updateTime`、`creator`、`updater`、`deleted`
|
||
2. 插入和更新时自动补时间
|
||
3. 已登录用户存在时自动补创建人和更新人
|
||
4. 逻辑删除统一使用 `deleted`,其未删除/已删除值由 yaml 中的 `logic-not-delete-value: 0` 和 `logic-delete-value: 1` 配置
|
||
5. `BaseDO.clean()` 用于清空前端可能误传回来的审计字段
|
||
|
||
这说明“审计字段一致性”被视为框架职责,而不是每个表、每个 Service 自己维护。
|
||
|
||
## 7. 常见字段存储形式做成透明 TypeHandler
|
||
模块内置了几类 `TypeHandler`:
|
||
|
||
1. `EncryptTypeHandler`:字符串字段透明加解密
|
||
2. `StringListTypeHandler`
|
||
3. `LongListTypeHandler`
|
||
4. `LongSetTypeHandler`
|
||
5. `IntegerListTypeHandler`
|
||
6. `JacksonTypeHandler` 的 `ObjectMapper` 统一注入
|
||
|
||
这里的取向是:数据库里允许保留“逗号分隔字符串”“JSON”“密文”等存储形式,但业务对象层尽量继续使用自然的数据结构。
|
||
|
||
例如:
|
||
|
||
```java
|
||
@TableName(value = "infra_mail_account", autoResultMap = true)
|
||
public class MailAccountDO extends BaseDO {
|
||
|
||
@TableField(typeHandler = EncryptTypeHandler.class)
|
||
private String password;
|
||
|
||
@TableField(typeHandler = StringListTypeHandler.class)
|
||
private List<String> toMails;
|
||
}
|
||
```
|
||
|
||
如果数据库里的 `password` 存的是密文、`to_mails` 存的是 `a@xx.com,b@xx.com` 这样的逗号分隔字符串,
|
||
那么业务代码里仍然可以分别按普通 `String` 和 `List<String>` 来使用它们,字段转换交给 `TypeHandler` 处理。
|
||
|
||
## 8. 把“查询之后的展示翻译”也并入数据库 starter
|
||
`RdmsTranslateAutoConfiguration` 和 `TranslateUtils` 说明这个模块并不只关心“查出来”,还关心“查出来后如何转成面向前端的 VO”。
|
||
这是一种比较明显的项目式封装方式:把 DAL 和 VO 翻译放在同一个 starter,方便后台管理类系统直接复用。
|
||
|
||
如果开发中需要这种能力,可以按下面的方式使用:
|
||
|
||
1. 在 VO 里同时定义“原始值字段”和“展示字段”,例如 `userId` 和 `userName`
|
||
2. 在原始值字段上增加 `@Trans`,声明要根据哪个对象、取哪个字段、回填到哪个展示字段
|
||
3. 查询完成后,先把 DO 转成 VO
|
||
4. 在返回前触发翻译。适合注解方式的接口可使用 `@TransMethodResult`,不适合注解方式的场景可手动调用 `TranslateUtils.translate(...)`
|
||
|
||
示例:
|
||
|
||
```java
|
||
public class OperateLogRespVO implements VO {
|
||
|
||
@Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName")
|
||
private Long userId;
|
||
|
||
private String userName;
|
||
}
|
||
```
|
||
|
||
```java
|
||
@TransMethodResult
|
||
public CommonResult<PageResult<OperateLogRespVO>> pageOperateLog(...) {
|
||
PageResult<OperateLogDO> pageResult = operateLogService.getOperateLogPage(pageReqVO);
|
||
return success(BeanUtils.toBean(pageResult, OperateLogRespVO.class));
|
||
}
|
||
```
|
||
|
||
上面的含义是:返回结果里先保留 `userId`,然后在翻译阶段根据 `userId` 查到对应用户的 `nickname`,再回填到 `userName`。
|
||
如果不是常规接口返回场景,例如导出 Excel,可以在转换为 VO 后手动调用:
|
||
|
||
```java
|
||
TranslateUtils.translate(BeanUtils.toBean(list, OperateLogRespVO.class));
|
||
```
|
||
|
||
## 自动装配链路
|
||
|
||
## 1. Spring Boot 自动配置入口
|
||
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
|
||
|
||
1. `com.njcn.rdms.framework.datasource.config.RdmsDataSourceAutoConfiguration`
|
||
2. `com.njcn.rdms.framework.mybatis.config.RdmsMybatisAutoConfiguration`
|
||
3. `com.njcn.rdms.framework.translate.config.RdmsTranslateAutoConfiguration`
|
||
|
||
## 2. EnvironmentPostProcessor 入口
|
||
`META-INF/spring.factories`
|
||
|
||
1. `com.njcn.rdms.framework.mybatis.config.IdTypeEnvironmentPostProcessor`
|
||
|
||
这意味着模块启动时会先做环境预处理,再进入正常的自动配置流程。
|
||
|
||
## 核心组件
|
||
|
||
| 组件 | 作用 |
|
||
| --- | --- |
|
||
| `RdmsDataSourceAutoConfiguration` | 开启事务管理,并在启用 Druid 监控页时注册过滤器去掉广告脚本 |
|
||
| `RdmsMybatisAutoConfiguration` | 提前于 MyBatis-Plus 完成装配,统一 `@MapperScan`、分页插件、字段填充、主键生成器、`JacksonTypeHandler` |
|
||
| `IdTypeEnvironmentPostProcessor` | 根据主数据源 URL 推断数据库类型,自动设置 `id-type` 和 Quartz Delegate |
|
||
| `BaseDO` | 统一 DO 基类,内置审计字段、逻辑删除和 Easy-Trans 兼容处理 |
|
||
| `DefaultDBFieldHandler` | 自动填充创建人、更新人、创建时间、更新时间 |
|
||
| `BaseMapperX` | 统一分页、Join 分页、批量操作、便捷查询 |
|
||
| `LambdaQueryWrapperX` / `QueryWrapperX` / `MPJLambdaWrapperX` | 提供 `IfPresent` 风格的条件拼装能力 |
|
||
| `JdbcUtils` / `MyBatisUtils` | 统一数据库类型探测、分页构造、排序拼装、跨库 SQL 片段 |
|
||
| `EncryptTypeHandler` 等 | 处理密文、列表、集合等特殊字段映射 |
|
||
| `TranslateUtils` | 在不能用注解自动翻译时,手动触发 Easy-Trans 翻译 |
|
||
|
||
## 模块特征
|
||
从实现细节和业务模块的用法看,这个 starter 有几个比较明确的特征:
|
||
|
||
1. 它承担“项目默认 DAL 规范”的角色,而不是一个可随意裁剪的通用组件。
|
||
2. 它偏向“约定优于配置”,例如主键策略、Mapper 扫描、分页结果、审计字段都由框架先定好。
|
||
3. 它存在一定程度的强绑定,例如绑定动态数据源、Druid、Easy-Trans、Security 上下文,以换取更少的样板代码。
|
||
4. 它在多数据库兼容上投入较多,适合数据库切换、国产库适配或私有化部署场景。
|
||
5. 它优先抽象后台管理系统里高频出现的分页、筛选、列表页、字典翻译、逻辑删除、审计字段等能力。
|
||
|
||
## 如何使用
|
||
|
||
## 1. 引入依赖
|
||
通常业务模块直接依赖:
|
||
|
||
```xml
|
||
<dependency>
|
||
<groupId>com.njcn</groupId>
|
||
<artifactId>rdms-spring-boot-starter-mybatis</artifactId>
|
||
</dependency>
|
||
```
|
||
|
||
如果项目需要自动填充 `creator`、`updater`,运行时还应提供安全模块里的登录上下文能力。
|
||
|
||
## 2. 基础配置
|
||
最小可工作的配置重点有四个:
|
||
|
||
1. `rdms.info.base-package`
|
||
2. `spring.datasource.dynamic.primary`
|
||
3. `spring.datasource.dynamic.datasource.<primary>.url`
|
||
4. `mybatis-plus.global-config.db-config.id-type`
|
||
|
||
示例:
|
||
|
||
```yaml
|
||
rdms:
|
||
info:
|
||
base-package: com.njcn.rdms.module.system
|
||
|
||
spring:
|
||
datasource:
|
||
dynamic:
|
||
primary: master
|
||
datasource:
|
||
master:
|
||
url: jdbc:mysql://127.0.0.1:13306/rdms?useSSL=false&serverTimezone=Asia/Shanghai
|
||
username: root
|
||
password: 123456
|
||
|
||
mybatis-plus:
|
||
global-config:
|
||
db-config:
|
||
id-type: NONE
|
||
logic-delete-value: 1
|
||
logic-not-delete-value: 0
|
||
type-aliases-package: ${rdms.info.base-package}.dal.dataobject
|
||
encryptor:
|
||
password: your-16-bytes-key
|
||
|
||
mybatis-plus-join:
|
||
banner: false
|
||
|
||
easy-trans:
|
||
is-enable-global: false
|
||
```
|
||
|
||
说明:
|
||
|
||
1. `id-type: NONE` 是这里推荐的“自动判断模式”
|
||
2. 如果没有配置主数据源 URL,`IdTypeEnvironmentPostProcessor` 就无法自动判断数据库类型
|
||
3. `encryptor.password` 只在使用 `EncryptTypeHandler` 时需要
|
||
|
||
## 3. DO 写法
|
||
推荐让 DO 继承 `BaseDO`,并在需要时启用 `autoResultMap = true`:
|
||
|
||
```java
|
||
@TableName(value = "infra_data_source_config", autoResultMap = true)
|
||
@KeySequence("infra_data_source_config_seq")
|
||
public class DataSourceConfigDO extends BaseDO {
|
||
|
||
private Long id;
|
||
private String name;
|
||
private String url;
|
||
private String username;
|
||
|
||
@TableField(typeHandler = EncryptTypeHandler.class)
|
||
private String password;
|
||
}
|
||
```
|
||
|
||
如果字段在库里按逗号分隔字符串保存,也可以直接挂列表型 `TypeHandler`:
|
||
|
||
```java
|
||
@TableField(typeHandler = StringListTypeHandler.class)
|
||
private List<String> toMails;
|
||
```
|
||
|
||
## 4. Mapper 写法
|
||
推荐所有 Mapper 继承 `BaseMapperX<T>`,把分页和条件拼装直接写在默认方法里:
|
||
|
||
```java
|
||
@Mapper
|
||
public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
|
||
|
||
default PageResult<AdminUserDO> selectPage(UserPageReqVO reqVO,
|
||
Collection<Long> deptIds,
|
||
Collection<Long> userIds) {
|
||
return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>()
|
||
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
|
||
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
|
||
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
|
||
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
|
||
.inIfPresent(AdminUserDO::getDeptId, deptIds)
|
||
.inIfPresent(AdminUserDO::getId, userIds)
|
||
.orderByDesc(AdminUserDO::getId));
|
||
}
|
||
}
|
||
```
|
||
|
||
如果需要跨库兼容的 `limit n`,当前实现建议使用 `QueryWrapperX`:
|
||
|
||
```java
|
||
return selectList(new QueryWrapperX<NotifyMessageDO>()
|
||
.eq("user_id", userId)
|
||
.eq("user_type", userType)
|
||
.eq("read_status", false)
|
||
.orderByDesc("id")
|
||
.limitN(size));
|
||
```
|
||
|
||
## 5. 手动触发 VO 翻译
|
||
当场景不适合用注解自动翻译时,可以手动调用:
|
||
|
||
```java
|
||
List<OperateLogRespVO> result = TranslateUtils.translate(list);
|
||
```
|
||
|
||
## 典型开发流程
|
||
在这个 starter 的设计下,比较顺畅的开发流程基本是:
|
||
|
||
1. 配置好多数据源和 `rdms.info.base-package`
|
||
2. DO 继承 `BaseDO`
|
||
3. Mapper 继承 `BaseMapperX`
|
||
4. 查询条件优先使用 `WrapperX` 系列拼装
|
||
5. 分页统一返回 `PageResult`
|
||
6. 特殊字段优先通过 `TypeHandler` 做透明映射
|
||
7. VO 展示转换需要字典或名称翻译时再接 Easy-Trans
|
||
|
||
## 注意事项
|
||
|
||
1. `IdTypeEnvironmentPostProcessor` 依赖 `spring.datasource.dynamic.primary` 和对应数据源的 `url`,否则无法自动识别数据库类型。
|
||
2. 当 `id-type` 被识别为 `INPUT` 时,自动配置还会注册对应数据库的 `IKeyGenerator`。这类场景下实体通常还需要正确声明 `@KeySequence`。
|
||
3. `DefaultDBFieldHandler` 会调用安全上下文工具获取登录用户。如果运行时没有对应安全能力,创建人和更新人自动填充就不能按预期工作。
|
||
4. `EncryptTypeHandler` 依赖 `mybatis-plus.encryptor.password`。密钥一旦更换,历史密文可能无法解密,生产环境不应硬编码。
|
||
5. `easy-trans.is-enable-global` 在示例配置里默认关闭,说明默认没有把全局响应翻译作为高优先级能力。
|
||
6. `QueryWrapperX.limitN` 是跨数据库兼容封装,但它仍然是基于数据库方言分支处理,不适合无限扩展到所有数据库。
|
||
7. 这个模块的职责边界偏“大而全”,适合本项目统一使用;如果要抽成通用开源组件,需要先拆掉对动态数据源、安全上下文、Easy-Trans 的强耦合。
|
||
|
||
## 总结
|
||
这个模块的核心价值,不是简单提供几个 MyBatis 工具类,而是把数据库层的共性问题一次性沉到框架层:
|
||
|
||
1. 启动时怎么自动装配
|
||
2. 多数据库怎么少改代码
|
||
3. 分页、排序、条件查询怎么统一写
|
||
4. 审计字段和逻辑删除怎么保持一致
|
||
5. 特殊字段怎么透明映射
|
||
6. 查询结果怎么更顺滑地进入 VO 展示层
|
||
|
||
如果把它当成一个“带强约束的项目级 DAL 基座”去理解,这个模块的设计就会非常清晰。
|