MyBatis-Plus 通用 CRUD 实现原理技术文档
MyBatis-Plus 通用 CRUD 实现原理技术文档
版本:基于 MyBatis-Plus 3.5.5(JDK 17+)
面向:需要阅读源码、定制 BaseMapper、排查“魔法 SQL” 来源的开发人员
1 文档目标
- 说明 通用 CRUD(BaseMapper、IService) 在启动期如何 无 SQL 注入
- 拆解 SQL 模板 → BoundSql → JDBC 的完整链路
- 给出 自定义通用方法 的扩展点与示例
2 术语表
| 术语 | 说明 |
|---|---|
| TableInfo | MyBatis-Plus 对表元数据的运行时封装(表名、主键、字段、填充器等) |
| SqlInjector | 将自定义方法模板注入到 Configuration 的策略接口 |
| AbstractMethod | 一个通用 SQL 方法的最小单元(如 SelectById、Insert) |
| MapperBuilderAssistant | MyBatis 原生助手,负责把 MappedStatement 注册到 Configuration |
3 启动期注入流程
3.1 入口:MybatisPlusAutoConfiguration
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {return new MybatisPlusInterceptor(); // 插件链
}
- 自动配置类会读取
mybatis-plus.global-config.dbConfig.*,填充到GlobalConfig→DbConfig - 随后触发
MybatisConfiguration#addInterceptor()把 DefaultSqlInjector 注册为 Bean。
3.2 DefaultSqlInjector 的职责
public class DefaultSqlInjector extends AbstractSqlInjector {@Overridepublic List<AbstractMethod> getMethodList(Class<?> mapperClass) {return Stream.of(new Insert(),new DeleteById(),new UpdateById(),new SelectById(),...).collect(Collectors.toList());}
}
- 每个
AbstractMethod实现 两个核心方法mappedStatement()构建MappedStatementsqlMethod()返回String模板(占位符由 TableInfo 填充)
3.3 模板 → MappedStatement
以 Insert 为例:
public class Insert extends AbstractMethod {@Overridepublic String sqlMethod(SqlMethod sqlMethod) {return "<script>INSERT INTO %s (%s) VALUES (%s)</script>";}@Overridepublic SqlCommandType sqlCommandType() {return SqlCommandType.INSERT;}@Overridepublic MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {String sql = String.format(sqlMethod(SqlMethod.INSERT_ONE),tableInfo.getTableName(),sqlSelectColumns(tableInfo, false),sqlInsertColumns(tableInfo, false, null));return addInsertMappedStatement(mapperClass, modelClass, sqlMethod(), sql);}
}
- 占位符
%s由TableInfo动态替换 → 运行时无 XML - 最终通过
MapperBuilderAssistant把MappedStatement注册到 Configuration,ID = mapper接口全名 + “.” + methodName
例如com.demo.mapper.UserMapper.insert
4 运行期执行链路
4.1 调用栈
userMapper.insert(user)├─ MapperProxy.invoke()├─ MapperMethod.execute()├─ SqlSession.insert()├─ BaseExecutor.update()├─ SimpleExecutor.doUpdate()├─ PreparedStatementHandler.update()└─ JDBC PreparedStatement.execute()
4.2 SQL 模板填充
- 占位符解析发生在
SqlSource#getBoundSql() - 字段过滤(逻辑删除、填充器)由
TableInfo在ParameterHandler阶段完成 - 乐观锁 会在
beforeUpdate()中追加version = version + 1
5 扩展:自定义通用方法
5.1 步骤
- 继承
AbstractMethod - 注册到自定义
SqlInjector - 在 Mapper 或 Service 层调用
5.2 实战:批量软删除
public class LogicDeleteBatchByIds extends AbstractMethod {@Overridepublic String sqlMethod(SqlMethod sqlMethod) {return "<script>UPDATE %s SET deleted = 1 WHERE id IN (%s)</script>";}@Overridepublic MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {String ids = "#{" + COLLECTION + "}";String sql = String.format(sqlMethod(SqlMethod.LOGIC_DELETE_BATCH_BY_IDS),tableInfo.getTableName(), ids);return addUpdateMappedStatement(mapperClass, modelClass, sqlMethod(), sql);}
}@Mapper
public interface UserMapper extends BaseMapper<User> {int logicDeleteBatchByIds(@Param("ids") List<Long> ids);
}
6 常见问题排查清单
| 现象 | 排查命令 | 可能原因 |
|---|---|---|
| 方法不存在 | Configuration#getMappedStatementIds | 未注册 SqlInjector |
| SQL 字段缺失 | 打印 TableInfo#getAllInsertSqlColumn | 实体未加 @TableField |
| 主键未回填 | 检查 TableInfo#getKeyProperty | 实体主键未加 @TableId |
| 逻辑删除失效 | 查看 TableInfo#isLogicDelete | 全局配置未开启 logicDeleteField |
7 结论
- 通用 CRUD ≠ 魔法:启动期注入模板 + 运行期 TableInfo 填充
- 可插拔:通过
SqlInjector/AbstractMethod可在任意 Mapper 上追加新方法 - 零侵入:不改变 MyBatis 原生执行链,便于与原生 XML 混合使用
深入 TableInfo 与 SqlInjector 源码,即可在团队内快速构建“私有通用方法库”,同时保持与 MyBatis-Plus 官方升级兼容。
