Files
cn-rdms/rdms-framework/rdms-spring-boot-starter-mybatis/README.md
2026-03-11 19:32:37 +08:00

15 KiB
Raw Blame History

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,则自动改写为更适合当前数据库的 AUTOINPUT
  4. 同一个后处理器顺手补齐 Quartz 的 driverDelegateClass
  5. QueryWrapperX.limitNMyBatisUtils.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 层

LambdaQueryWrapperXQueryWrapperXMPJLambdaWrapperX 都提供了 xxxIfPresent 系列方法,例如:

  1. likeIfPresent
  2. eqIfPresent
  3. inIfPresent
  4. betweenIfPresent

这样 Service 或 Mapper 默认方法可以直接链式拼接条件,空值自动跳过。这样可以把“查询条件存在才拼 SQL”这件事收口到 Wrapper而不是散落在业务代码里做大量 if (param != null)

6. 基础字段治理优先于业务字段治理

BaseDODefaultDBFieldHandler 体现了这个模块在数据治理上的几个固定要求:

  1. 所有 DO 默认带 createTimeupdateTimecreatorupdaterdeleted
  2. 插入和更新时自动补时间
  3. 已登录用户存在时自动补创建人和更新人
  4. 逻辑删除统一使用 deleted,其未删除/已删除值由 yaml 中的 logic-not-delete-value: 0logic-delete-value: 1 配置
  5. BaseDO.clean() 用于清空前端可能误传回来的审计字段

这说明“审计字段一致性”被视为框架职责,而不是每个表、每个 Service 自己维护。

7. 常见字段存储形式做成透明 TypeHandler

模块内置了几类 TypeHandler

  1. EncryptTypeHandler:字符串字段透明加解密
  2. StringListTypeHandler
  3. LongListTypeHandler
  4. LongSetTypeHandler
  5. IntegerListTypeHandler
  6. JacksonTypeHandlerObjectMapper 统一注入

这里的取向是数据库里允许保留“逗号分隔字符串”“JSON”“密文”等存储形式但业务对象层尽量继续使用自然的数据结构。

例如:

@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 这样的逗号分隔字符串, 那么业务代码里仍然可以分别按普通 StringList<String> 来使用它们,字段转换交给 TypeHandler 处理。

8. 把“查询之后的展示翻译”也并入数据库 starter

RdmsTranslateAutoConfigurationTranslateUtils 说明这个模块并不只关心“查出来”,还关心“查出来后如何转成面向前端的 VO”。
这是一种比较明显的项目式封装方式:把 DAL 和 VO 翻译放在同一个 starter方便后台管理类系统直接复用。

如果开发中需要这种能力,可以按下面的方式使用:

  1. 在 VO 里同时定义“原始值字段”和“展示字段”,例如 userIduserName
  2. 在原始值字段上增加 @Trans,声明要根据哪个对象、取哪个字段、回填到哪个展示字段
  3. 查询完成后,先把 DO 转成 VO
  4. 在返回前触发翻译。适合注解方式的接口可使用 @TransMethodResult,不适合注解方式的场景可手动调用 TranslateUtils.translate(...)

示例:

public class OperateLogRespVO implements VO {

    @Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName")
    private Long userId;

    private String userName;
}
@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 后手动调用:

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. 引入依赖

通常业务模块直接依赖:

<dependency>
    <groupId>com.njcn</groupId>
    <artifactId>rdms-spring-boot-starter-mybatis</artifactId>
</dependency>

如果项目需要自动填充 creatorupdater,运行时还应提供安全模块里的登录上下文能力。

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

示例:

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. 如果没有配置主数据源 URLIdTypeEnvironmentPostProcessor 就无法自动判断数据库类型
  3. encryptor.password 只在使用 EncryptTypeHandler 时需要

3. DO 写法

推荐让 DO 继承 BaseDO,并在需要时启用 autoResultMap = true

@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

@TableField(typeHandler = StringListTypeHandler.class)
private List<String> toMails;

4. Mapper 写法

推荐所有 Mapper 继承 BaseMapperX<T>,把分页和条件拼装直接写在默认方法里:

@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

return selectList(new QueryWrapperX<NotifyMessageDO>()
        .eq("user_id", userId)
        .eq("user_type", userType)
        .eq("read_status", false)
        .orderByDesc("id")
        .limitN(size));

5. 手动触发 VO 翻译

当场景不适合用注解自动翻译时,可以手动调用:

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 基座”去理解,这个模块的设计就会非常清晰。