DDK MyBatis Starter
给 MyBatis-Plus 配好一个 DDD 项目需要的默认件:分页、乐观锁、防全表更新删除、雪花 ID、审计字段自动填充,以及可选的多租户;并注册通用仓储依赖的
MapperProvider。
能做什么
| 能力 | 说明 |
|---|---|
| 分页 | PaginationInnerInterceptor,方言、单页上限、越界行为可配 |
| 乐观锁 | 默认开启。PO 上标了 @Version 的字段在更新时带上 WHERE version = ?,这是 AggregateRoot.version() 和 ConcurrentUpdateException 生效的前提,见通用仓储 |
| 防全表更新删除 | 默认开启。不带 WHERE 的 update / delete 直接抛异常 |
| 雪花 ID | 作用于 @TableId(type = IdType.ASSIGN_ID) 的主键,机器号可配 |
| 审计字段 | 插入、更新时自动填充 createTime、updateTime、createBy、updateBy |
| 多租户 | 可选。每条 SQL 自动追加租户条件,取不到租户时拒绝执行 |
| 行级数据权限 | 可选。受控表的查询、更新、删除按操作者的数据范围收窄,取不到操作者时拒绝执行(0.8.0 起) |
MapperProvider | GenericRepositoryImpl 做 Entity ↔ PO 转换时依赖它。它在 ddk-core 的 jar 里,应用的组件扫描扫不到,所以由本 starter 注册:用通用仓储时要引这个 starter,而不是只引 ddk-mybatis |
MyBatis / MyBatis-Plus 本身的装配(SqlSessionFactory、Mapper 扫描、mybatis-plus.* 配置)来自 mybatis-plus-spring-boot4-starter,本 starter 不参与。
引入方式
<dependency>
<groupId>com.domaindrivenkit</groupId>
<artifactId>ddk-mybatis-starter</artifactId>
</dependency>还需要数据库驱动和 spring.datasource.* 配置。Mapper 接口的扫描由应用自己声明(@MapperScan,或在接口上加 @Mapper)。
操作者上下文
审计字段和多租户都从同一个地方取「当前是谁在操作」:ddk-core 里的 OperatorContext。
flowchart LR
R[HTTP 请求] --> F[OperatorContextFilter<br/>OperatorResolver → Operator]
J[定时任务 / 消息监听] -->|OperatorContext.runAs| C
F --> C[(OperatorContext)]
C --> A[审计字段<br/>createBy / updateBy]
C --> T[租户条件<br/>WHERE tenant_id = ?]Web 请求:应用声明一个
OperatorResolverBean,把请求解析成Operator。DDK 不假设认证方式,可以从 Spring Security 的上下文、网关透传的请求头或 JWT 里取。Web starter 发现这个 Bean 后,在请求期间设置上下文,结束时清除。java@Bean OperatorResolver operatorResolver() { return request -> { String userId = request.getHeader("X-User-Id"); return userId == null ? null : Operator.of(userId, request.getHeader("X-Tenant-Id")); }; }定时任务、消息监听:没有请求,自己指定身份:
OperatorContext.runAs(Operator.of("system", tenantId), () -> service.closeExpired())。换线程:上下文保存在线程上。0.9 起
ddk-core为它注册了 Micrometer 的上下文访问器,Tracer Starter 注册的ContextPropagatingTaskDecorator会把操作者(连同租户)带进@Async方法和 Spring Boot 创建的线程池;关掉ddk.tracer.async-propagation.enabled或者没有引入 tracer starter 时不会传播。应用自己创建的线程不经过装饰器,用OperatorContext.wrap(task)。runAs/callAs结束时恢复之前的操作者。请求线程来自线程池,如果只设置不清除,上一个请求的操作者会留给下一个请求。
审计字段
PO 上声明字段并标上填充策略即可:
@TableField(fill = FieldFill.INSERT)
private Long createBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;- 字段名固定为
createTime、updateTime、createBy、updateBy。时间字段是LocalDateTime,操作者字段是String或Long,其他类型会报错并指出字段。 - 创建人只在为空时填写,调用方显式设置的值(例如数据迁移)不会被覆盖;修改人在每次更新时换成当前操作者。
- 没有操作者时两个操作者字段保持为空,不会填一个占位值冒充。
多租户
ddk:
mybatis:
tenant:
enabled: true
column: tenant_id
ignore-tables: [ t_dict, t_tenant ]开启后,MyBatis-Plus 的租户插件给每条语句加上租户条件:查询、更新、删除追加 tenant_id = ?,插入自动写入租户列。租户取自 OperatorContext 里操作者的 tenantId。
取不到租户时语句直接失败,而不是不加条件放行。 少了租户条件的查询会读到所有租户的数据,而且不会有任何报错,这类问题必须在开发阶段就暴露:
tenant = OperatorContext.current().tenantId
没有操作者,或操作者没有租户
→ IllegalStateException,提示三种处理办法
否则
→ 追加 tenant_id = <tenant>两种豁免都要显式声明:
- 不分租户的表(字典表、租户表本身)列在
ignore-tables里; - 个别需要跨租户的语句,在 Mapper 方法上加 MyBatis-Plus 的
@InterceptorIgnore(tenantLine = "true")。
租户插件排在插件链的最前面,乐观锁、分页都作用在已经带上租户条件的 SQL 上。
行级数据权限
0.8.0 起。
多租户把数据按租户切开。在一个租户内部,多数业务系统还要再切一刀:销售主管看本部门的合同,普通员工只看自己创建的,管理员看全部。这类条件靠每个查询手写最容易漏,而漏掉的后果是数据泄露,却不会让任何东西失败。
模型
DataScope(在 ddk-core 里)从两个维度描述一个操作者能看到什么:数据属于哪个组(部门、门店、项目,由业务自己定义),以及数据由谁创建。两个维度是「或」的关系。
| 范围 | 能看到 |
|---|---|
DataScope.all() | 全部 |
DataScope.ofGroups(ids) | 这些组的数据 |
DataScope.ownOnly() | 自己创建的 |
DataScope.ofGroups(ids).andOwn() | 两者满足其一 |
DataScope.none() | 什么都看不到 |
用法
DDK 不知道角色和组织关系怎么存,所以由应用通过一个 Bean 回答一个问题:这个操作者的数据范围是什么。
@Bean
DataScopeResolver dataScopeResolver(RoleService roles) {
return operator -> roles.dataScopeOf(operator.id());
}ddk:
mybatis:
data-scope:
enabled: true
tables: [t_contract, t_invoice] # 只有列在这里的表受控之后这些表上的查询、更新、删除都会被追加条件,例如 (c.dept_id IN (10, 20) OR c.create_by = 3)。按主键读取、分页的计数、带别名的联表查询同样受限;范围之外的数据 updateById、deleteById 返回 0。
受控表上的 SELECT / UPDATE / DELETE
OperatorContext 里没有操作者 → 抛出异常(和多租户一样,拒绝执行而不是放行)
范围是「全部」 → 语句不变
否则追加条件 → (组 IN (...) OR 创建人 = ?)
两个维度都不适用 → 追加 1 = 0,什么都读不到要知道的几点
DataScopeResolver要自己缓存。 每条涉及受控表的语句都会调用它一次。实现里如果查库,查询落在受控的表上还会再次进入它。后台任务和消息消费方没有操作者。 它们访问受控表时要用
OperatorContext.runAs(...)指定一个系统操作者,并让DataScopeResolver对它返回DataScope.all()。个别语句的豁免:在 Mapper 方法上标
@InterceptorIgnore(dataPermission = "true")。列名可以按表覆盖,也可以声明某张表没有组这一列:
yamlddk: mybatis: data-scope: columns: t_note: group-column: "" # 这张表只按创建人控制 owner-column: author numeric-owner-id: false # 这一列存的是字符串插入不受控。 数据权限只管读取和改动已有的行;新数据写到哪个组,由业务代码决定。
范围之外的聚合在通用仓储看来是「不存在」。
find返回空,update影响 0 行,后者会被当成并发冲突报出来。开启后必须提供
DataScopeResolver,否则应用启动失败。
配置项
| 配置项 | 默认值 | 说明 |
|---|---|---|
ddk.mybatis.db-type | mysql | 分页方言。用 PostgreSQL、Oracle、达梦、H2 等数据库时必须改 |
ddk.mybatis.max-page-size | 500 | 单页最大条数,超过会被静默截断 |
ddk.mybatis.overflow | false | 页码超过总页数时是否回到第一页。默认返回空结果 |
ddk.mybatis.optimistic-locker | true | 乐观锁插件 |
ddk.mybatis.block-attack | true | 防全表更新删除插件 |
ddk.mybatis.worker-id / datacenter-id | 无 | 雪花 ID 的机器号与数据中心号(0–31),两个要同时设置。多实例部署时应当显式指定 |
ddk.mybatis.tenant.enabled | false | 多租户 |
ddk.mybatis.tenant.column | tenant_id | 租户列名 |
ddk.mybatis.tenant.numeric-id | true | 租户列是否为数值类型;为 false 时按字符串比较 |
ddk.mybatis.tenant.ignore-tables | 空 | 不分租户的表,不区分大小写 |
ddk.mybatis.data-scope.enabled | false | 是否启用行级数据权限 |
ddk.mybatis.data-scope.tables | 空 | 受控的表,不区分大小写 |
ddk.mybatis.data-scope.group-column | dept_id | 记录数据属于哪个组的列 |
ddk.mybatis.data-scope.owner-column | create_by | 记录数据由谁创建的列 |
ddk.mybatis.data-scope.numeric-group-id | true | 组的标识是否为数值类型 |
ddk.mybatis.data-scope.numeric-owner-id | true | 创建人的标识是否为数值类型 |
ddk.mybatis.data-scope.columns.<表名>.* | 空 | 按表覆盖上面四项;列名设为空字符串表示这张表没有这个维度 |
mybatis-plus.* | MyBatis-Plus | Mapper XML 位置、逻辑删除字段、主键策略等,由 MyBatis-Plus 自己的 starter 处理 |
装配条件与降级行为
- 自动配置类:
com.ddk.mybatis.starter.config.DdkMybatisAutoConfiguration(0.9 及以前叫MybatisPlusAutoConfiguration),classpath 上有MybatisPlusInterceptor时生效。 MapperProvider、IdentifierGenerator、MybatisPlusInterceptor、MetaObjectHandler四个 Bean 都带@ConditionalOnMissingBean,应用声明同类型的 Bean 即可替换。- 自己声明
MybatisPlusInterceptor会替换掉整个插件链:分页、乐观锁、防全表更新删除、多租户、数据权限都要自己重新加上。 - 插件顺序固定为:多租户 → 数据权限 → 乐观锁 → 防全表更新删除 → 分页。分页放在最后,计数语句因此也带着租户和数据权限的条件。
- 本 starter 自身不连接数据库;数据源由 Spring Boot 的自动配置创建。
不适用的场景 / 已知问题
- 单页上限是静默截断:请求
size=1000不会报错,会被压到max-page-size条返回。 - 审计字段的名字和类型是约定好的。字段名不同(如
gmtCreate)或时间字段用Date时不会填充,也不会报错。 - 多租户只在默认的插件链里生效。自己声明
MybatisPlusInterceptor后要自己加租户插件。手写的原生 JDBC(JdbcTemplate)不经过 MyBatis-Plus,同样不会有租户条件。 - 数据权限只有「组」和「创建人」两个维度,两者取或。更复杂的规则(例如按金额、按状态、多个条件取且)不在范围内;手写的原生 JDBC 同样不受控。
- 逻辑删除没有额外封装:用 MyBatis-Plus 自带的
@TableLogic即可。它与通用仓储的配合没有专门的测试。 - 多数据源:与 DB starter 组合时,MyBatis-Plus 只绑定
@Primary数据源,其余数据源要自己配SqlSessionFactory。