Spring中Mybatis、MybatisPlus是怎么样结合工作的?
引子:一个常见的困惑
在 Spring Boot 项目中,我们几乎每天都在用 MyBatis 或 MyBatis-Plus
操作数据库。只要在 Mapper 接口上写几个方法,在 Service 里
@Autowired 注入,就能直接调用。一切看起来理所当然。
但当你开始追问下面这些问题时,事情就变得有意思了:
UserMapper只是一个接口,没有实现类,为什么能被注入?BaseMapper里的insert、selectById方法,我从来没有写过 SQL,它们是怎么执行的?@MapperScan到底做了什么?它和 MyBatis-Plus 重写的MybatisMapperRegistry是怎么关联上的?MybatisPlusAutoConfiguration和@MapperScan是什么关系?- 多数据源时为什么还要配置
sqlSessionFactoryRef和sqlSessionTemplateRef?- MyBatis-Plus 的
saveBatch为什么性能不理想?有没有真正的批量插入方案?
这篇文章就沿着这些问题,把 Spring、MyBatis、MyBatis-Plus 三者的协作关系一层层拆开,最后落到多数据源配置和批量插入的实战上。
第一部分:MyBatis 核心原理
1.1 MyBatis 的两个阶段
MyBatis 的工作可以分成两个阶段:
- 启动阶段:解析所有 SQL(XML 或注解),把每条 SQL
翻译成一个
MappedStatement对象,注册到全局容器Configuration中。 - 运行阶段:根据 Mapper 接口的方法名,从
Configuration中查出对应的MappedStatement,执行 SQL,处理结果。
理解这两个阶段的分界,是理解 MyBatis 的关键。
1.2
MappedStatement:一条 SQL 的说明书
在 MyBatis 中,每个
<select>、<insert>、<update>、<delete>
标签(或注解),启动时都会被解析成一个 MappedStatement
对象。
它包含:
| 字段 | 含义 |
|---|---|
id |
唯一标识,格式
接口全限定名.方法名 |
sqlSource |
SQL 来源,含动态标签 |
resultMaps |
结果映射规则 |
parameterMap |
参数映射规则 |
statementType |
STATEMENT /
PREPARED / CALLABLE |
keyGenerator |
主键生成策略 |
cache |
二级缓存配置 |
容器在 Configuration 中:
1 | public class Configuration { |
Configuration 是整个 MyBatis
的全局大脑,所有元数据都在这里。执行 SQL 时,MyBatis 用
id 查出对应的
MappedStatement,按说明书执行。
1.3
启动阶段:MapperRegistry 与
MapperAnnotationBuilder
启动阶段的核心任务是:把 XML 或注解中定义的 SQL 解析成
MappedStatement,注册到
Configuration.mappedStatements 容器里。
MapperRegistry:接口与代理工厂的注册表
MapperRegistry 是 Configuration
的一个字段,维护一个 Map:
1 | public class MapperRegistry { |
- key:Mapper 接口的
Class对象,如UserMapper.class - value:
MapperProxyFactory,用来为这个接口生成 JDK 动态代理对象
它的核心方法 addMapper() 做了两件事:
1 | public <T> void addMapper(Class<T> type) { |
MapperAnnotationBuilder:SQL
注解的翻译器
MapperAnnotationBuilder 负责解析一个 Mapper
接口中所有方法上的 SQL 注解(或关联的 XML),生成对应的
MappedStatement。
它的核心方法 parse() 大致流程:
1 | public void parse() { |
在 parseStatement() 中,它会读取注解、构建
SqlSource、创建 MappedStatement,最终调用
configuration.addMappedStatement(...) 注册到全局容器。
两者分工:
MapperRegistry:管”接口 → 代理工厂”的注册,并触发解析。MapperAnnotationBuilder:管”接口方法 → MappedStatement”的解析和注册。
1.4 运行阶段:动态代理与 SQL 执行
启动阶段完成后,Configuration 中已经存好了所有 SQL
描述。运行阶段要解决的是:如何把接口方法调用翻译成对
MappedStatement 的执行?
答案就是 JDK 动态代理。
MapperProxyFactory:代理对象的工厂
1 | public class MapperProxyFactory<T> { |
MapperProxy:方法调用的翻译器
1 | public class MapperProxy<T> implements InvocationHandler, Serializable { |
MapperMethod 在构造时会:
- 解析方法签名(
select/insert/update/delete); - 从
Configuration.mappedStatements中查找对应的MappedStatement。
完整调用链:
1 | userMapper.selectById(1L) |
第二部分:MyBatis-Plus 的增强机制
2.1
一个问题:BaseMapper 的方法为什么能调用?
到这里,MyBatis 的核心原理已经清楚了。但一个新的问题出现了:
UserMapper 接口继承了
BaseMapper<User>,而 BaseMapper
里定义了一大堆方法(insert、selectById、updateById
等),这些方法在 XML 或注解里都没有对应的
SQL。它们是怎么能执行的?
答案就在 MyBatis-Plus 的动态注入机制。
2.2 MyBatis-Plus 的切入点:重写两个核心类
MyBatis-Plus 重写了两个关键类:
| MyBatis 原生类 | MyBatis-Plus 重写类 | 目的 |
|---|---|---|
MapperRegistry |
MybatisMapperRegistry |
在 addMapper() 中改用 MP
自己的解析器 |
MapperAnnotationBuilder |
MybatisMapperAnnotationBuilder |
在 parse() 中额外为
BaseMapper 通用方法注入 SQL |
MybatisMapperRegistry.addMapper() 的实现:
1 |
|
MybatisMapperAnnotationBuilder.parse() 的增强逻辑:
1 |
|
而 AbstractMethod.injectMappedStatement()
内部,会根据实体类的
TableInfo(表名、字段映射等元数据)拼出 SQL 模板,创建
MappedStatement,注册到
Configuration.mappedStatements。
所以”动态注入”的本质是:在启动阶段,把原本不存在的
SQL(以 MappedStatement 形式)提前注册到
Configuration 中。运行时调用
userMapper.insert() 时,MyBatis 就像执行普通 XML SQL
一样执行它。
2.3 这些重写类是怎么进入 MyBatis 启动流程的?
MyBatis-Plus 重写了 MapperRegistry 和
MapperAnnotationBuilder,但 MyBatis 启动流程是 MyBatis
自己的代码,它是怎么”偷梁换柱”的?
答案在于 MybatisConfiguration。
MybatisConfiguration 继承自 MyBatis 原生的
Configuration,但做了两件事:
- 初始化自己的
MybatisMapperRegistry:它用一个MybatisMapperRegistry的实例,覆盖了父类中默认的MapperRegistry字段。 - 重写
addMapper方法:当 MyBatis 启动流程调用configuration.addMapper()时,实际调用的是MybatisConfiguration重写后的版本,该方法会将调用委托给MybatisMapperRegistry。
1 | public class MybatisConfiguration extends Configuration { |
而 MybatisConfiguration 又是通过
MybatisSqlSessionFactoryBean 创建的。这个 FactoryBean 由
Spring Boot 的 MybatisPlusAutoConfiguration
创建,替换了原生的 SqlSessionFactoryBean。
完整链路:
1 | Spring Boot 启动 |
为什么是 MybatisPlusAutoConfiguration 而不是
MybatisAutoConfiguration?
因为引入 mybatis-plus-boot-starter
时,MybatisAutoConfiguration 所在的
jar(mybatis-spring-boot-autoconfigure)根本不在 classpath
上。Spring Boot 只能扫描到
MybatisPlusAutoConfiguration,它创建的是
MybatisSqlSessionFactoryBean,从而把 MyBatis-Plus
的全套自定义实现注入了 MyBatis 的启动流程。
第三部分:Spring 集成层
3.1 核心问题:Mapper 接口没有实现类,怎么注入?
现在,MyBatis 核心层已经清楚了:启动时解析
SQL,运行时通过动态代理执行。 MyBatis-Plus
也清楚了:在启动解析阶段,多注册一批
MappedStatement。
但还有一个问题:UserMapper 是一个接口,我们
@Autowired 注入它时,注入的到底是什么?谁负责创建它?
答案在 Spring 集成层。MyBatis 提供了
SqlSession 等核心对象,但要和 Spring
的容器、事务、依赖注入无缝整合,需要 mybatis-spring 这个桥梁。
3.2
MapperFactoryBean:Spring 层的 FactoryBean
MapperFactoryBean 是 mybatis-spring 提供的类,实现了
Spring 的 FactoryBean<T>:
1 | public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { |
FactoryBean 是 Spring
的一个特殊接口:当 Spring 容器需要注入 UserMapper
类型的 Bean 时,实际调用的是 MapperFactoryBean.getObject()
返回的对象,而不是 MapperFactoryBean 本身。
getObject() 内部调用
getSqlSession().getMapper(),最终会走到 MyBatis 核心层的
MapperRegistry.getMapper(),由
MapperProxyFactory 创建代理对象。
3.3
隐藏的关键:MapperFactoryBean.checkDaoConfig()
MapperFactoryBean 继承自
SqlSessionDaoSupport,后者继承自 Spring 的
DaoSupport。DaoSupport 实现了
InitializingBean,会在 Bean 属性注入完成后调用
afterPropertiesSet(),进而调用
checkDaoConfig()。
MapperFactoryBean 重写了
checkDaoConfig():
1 |
|
这一步是 Spring 集成层和 MyBatis-Plus
增强层的连接点:MapperFactoryBean
在初始化时,调用了
configuration.addMapper(UserMapper.class),从而触发
MyBatis-Plus 的 MybatisMapperRegistry,完成 SQL 注入。
3.4
SqlSessionTemplate:线程安全的会话封装
SqlSessionTemplate 是 mybatis-spring 对
SqlSession 的线程安全封装。它本身不持有真正的
SqlSession,而是通过一个动态代理
SqlSessionInterceptor 来管理:
1 | public class SqlSessionTemplate implements SqlSession { |
SqlSessionInterceptor 的 invoke()
方法每次都会:
- 从
SqlSessionFactory获取一个SqlSession(如果当前有 Spring 事务,则复用事务中的SqlSession); - 执行方法调用;
- 提交或回滚;
- 关闭
SqlSession(如果是非事务场景)。
所以 SqlSessionTemplate
是线程安全的,它可以作为单例 Bean 被所有 Mapper 共享。
3.5
为什么同时定义 SqlSessionFactory 和
SqlSessionTemplate?
从技术上讲,只定义 SqlSessionFactory
也能跑通。MapperFactoryBean 继承自
SqlSessionDaoSupport,而
SqlSessionDaoSupport.setSqlSessionFactory() 会自动创建一个
SqlSessionTemplate。
但把它独立成 Bean 有明确的好处:
| 好处 | 说明 |
|---|---|
| 单例共享 | 整个应用只有一个
SqlSessionTemplate,所有 MapperFactoryBean
共用同一个实例 |
统一配置
ExecutorType |
可以通过配置项统一指定
ExecutorType,影响所有 Mapper 的执行方式 |
| 可被其他组件注入 | 任何需要直接执行 SQL 的组件都可以
@Autowired SqlSessionTemplate |
| 支持用户覆盖 | @ConditionalOnMissingBean
允许用户自定义 SqlSessionTemplate 来替换默认实现 |
一句话:SqlSessionFactory
是”造车的工厂”,SqlSessionTemplate
是”可以共享的车”。自动配置两个都定义,是为了让整个应用共用同一辆”车”。
第四部分:@MapperScan
的生效原理
4.1 核心问题
前面提到,MapperFactoryBean
负责创建代理对象,但它是怎么被注册到 Spring
容器中的?我们从来没有手动写过
@Bean MapperFactoryBean,为什么 UserMapper
就能被注入?
答案就是 @MapperScan。
4.2
第一环:@MapperScan 是个”伪装”的 @Import
1 |
|
它的核心是 @Import(MapperScannerRegistrar.class)。Spring
在解析配置类时,会递归查找所有注解上的
@Import,把其中的类当作”额外要处理的组件”。
所以 @MapperScan 本身不干活,它只是告诉
Spring:“请把 MapperScannerRegistrar
这个类也纳入你的处理流程。”
4.3
第二环:MapperScannerRegistrar 注册
MapperScannerConfigurer
MapperScannerRegistrar 实现了 Spring 的
ImportBeanDefinitionRegistrar
接口。这个接口是 Spring 为第三方框架提供的“手动注册
BeanDefinition” 的扩展点。
它的核心方法 registerBeanDefinitions() 做了一件事:
- 读取
@MapperScan注解上的属性(basePackages、sqlSessionFactoryRef、sqlSessionTemplateRef等)。 - 创建一个
MapperScannerConfigurer的BeanDefinition,把这些属性设置进去。 - 把这个
BeanDefinition注册到 Spring 容器中。
4.4
第三环:MapperScannerConfigurer 触发扫描
MapperScannerConfigurer 实现了
BeanDefinitionRegistryPostProcessor
接口。这个接口比普通的 BeanFactoryPostProcessor
更早执行——它在所有 Bean 定义加载完成之后、Bean
实例化之前,允许你继续向容器中注册新的
BeanDefinition。
它的 postProcessBeanDefinitionRegistry()
方法做了两件事:
- 创建
ClassPathMapperScanner:这是一个专门用于扫描 Mapper 接口的扫描器,继承自 Spring 的ClassPathBeanDefinitionScanner。 - 把
@MapperScan中的属性传递给它:包括sqlSessionFactoryBeanName、sqlSessionTemplateBeanName、annotationClass等。 - 调用
scanner.scan(basePackages):开始扫描指定包路径下的接口。
4.5
第四环:ClassPathMapperScanner 改写 BeanDefinition
这是整个流程中最核心的一步。
当 doScan() 找到 UserMapper
这个接口后,它生成的原始 BeanDefinition 的
beanClass 是 UserMapper.class。但
ClassPathMapperScanner 会立刻改写这个
BeanDefinition:
1 | // 在 processBeanDefinitions() 中 |
这意味着:Spring 容器中名为 userMapper
的 Bean,其 beanClass 不再是 UserMapper,而是
MapperFactoryBean,并且
UserMapper 被作为构造参数传了进去。
同时,ClassPathMapperScanner 还会根据
@MapperScan 中是否指定了 sqlSessionFactoryRef
或 sqlSessionTemplateRef,把对应的 Bean 名称也设置到
BeanDefinition 的属性中,后续注入时会用到这些名称。
4.6
第五环:MapperFactoryBean 初始化,触发 SQL 注入
Spring 容器启动到实例化阶段,开始创建 userMapper 这个
Bean。它发现 beanClass 是
MapperFactoryBean,于是:
- 实例化
MapperFactoryBean。 - 注入
sqlSessionFactory和sqlSessionTemplate。 - 调用
afterPropertiesSet()→checkDaoConfig()。
关键就在这里:checkDaoConfig() 调用了
configuration.addMapper(UserMapper.class),触发了第二部分的
MyBatis-Plus 增强逻辑:
1 | MapperFactoryBean.checkDaoConfig() |
4.7
@MapperScan 与 MybatisMapperRegistry
的关联
这是整个体系中最关键的连接点,之前分开讲容易让人困惑。
@MapperScan负责在 Spring 层注册MapperFactoryBean;MapperFactoryBean初始化时调用configuration.addMapper();- 这个调用进入 MyBatis-Plus 重写的
MybatisMapperRegistry,触发 SQL 注入。
@MapperScan
是”触发器”,MybatisMapperRegistry 是”执行者”,连接它们的是
MapperFactoryBean 的初始化过程。
- 没有
@MapperScan,MapperFactoryBean不会被注册,addMapper()不会被调用,MybatisMapperRegistry永远不会被触发。 - 没有
MybatisMapperRegistry,addMapper()会走原生逻辑,BaseMapper的通用方法不会被注入。
两者缺一不可。
4.8 完整链路
1 | Spring 容器 refresh() |
第五部分:MybatisPlusAutoConfiguration
与 @MapperScan 的关系
5.1 两条独立的路径
很多人在学到这里时会感到困惑:MybatisPlusAutoConfiguration
和 @MapperScan 到底什么关系?怎么感觉没关联上?
核心答案:它们是两条独立的路径,最终在
MapperFactoryBean
这里汇合。MybatisPlusAutoConfiguration
负责”造引擎”,@MapperScan 负责”装轮子”,两者通过
SqlSessionFactory / SqlSessionTemplate 这两个
Bean 连接起来。
路径一:MybatisPlusAutoConfiguration
—— 造引擎
它的职责是创建基础设施 Bean:
1 |
|
它产出两个关键 Bean:
SqlSessionFactory:内部持有MybatisConfiguration,启动时负责解析 SQL、注册MappedStatement。SqlSessionTemplate:线程安全的SqlSession封装,后续被MapperFactoryBean使用。
注意:MybatisPlusAutoConfiguration
不负责注册任何 Mapper 接口。它不知道
UserMapper 的存在。
路径二:@MapperScan ——
装轮子
它的职责是把 Mapper 接口注册为
MapperFactoryBean:
1 |
它通过 MapperScannerRegistrar →
MapperScannerConfigurer →
ClassPathMapperScanner,把 UserMapper 接口的
BeanDefinition 改写为 MapperFactoryBean。
注意:@MapperScan 不负责创建
SqlSessionFactory。它只管扫描接口、注册
BeanDefinition。
5.2
汇合点:MapperFactoryBean
两条路径在 MapperFactoryBean 这里汇合。
MapperFactoryBean 继承自
SqlSessionDaoSupport,需要注入
SqlSessionFactory 或 SqlSessionTemplate:
- 单数据源下:容器里只有一个
SqlSessionFactory和一个SqlSessionTemplate(由MybatisPlusAutoConfiguration创建),Spring 按类型自动注入。 - 多数据源下:容器里有多个同类型 Bean,必须通过
@MapperScan的sqlSessionFactoryRef和sqlSessionTemplateRef显式指定。
5.3 为什么会产生”没关联上”的感觉
因为它们确实没有直接的代码调用关系:
MybatisPlusAutoConfiguration里没有任何一行代码提到@MapperScan。@MapperScan的源码里也没有任何一行代码提到MybatisPlusAutoConfiguration。
它们是通过 Spring 容器的依赖注入机制 间接关联的:
MybatisPlusAutoConfiguration把SqlSessionFactory和SqlSessionTemplate注册到容器中。@MapperScan把MapperFactoryBean注册到容器中。- Spring 在实例化
MapperFactoryBean时,发现它需要SqlSessionFactory,于是从容器中查找并注入。
这就像”发电厂”和”冰箱”的关系:发电厂不关心谁在用它的电,冰箱也不关心电从哪个发电厂来,它们通过”电网”(Spring 容器)连接在一起。
5.4
多数据源时,MybatisPlusAutoConfiguration 还需要吗?
答案是:仍然会被加载,但它创建的
SqlSessionFactory 和 SqlSessionTemplate Bean
会被跳过。
只要 mybatis-plus-boot-starter 在 classpath
上,MybatisPlusAutoConfiguration 就会被 Spring Boot
的自动配置机制发现并处理。但它的关键 Bean 方法上都标了
@ConditionalOnMissingBean:
1 |
|
当你在多数据源配置中手动声明了 pgsqlSqlSessionFactory 和
pgsqlSqlSessionTemplate 后:
MybatisPlusAutoConfiguration.sqlSessionFactory()发现容器中已有SqlSessionFactory类型的 Bean,跳过。MybatisPlusAutoConfiguration.sqlSessionTemplate()发现容器中已有SqlSessionTemplate类型的 Bean,跳过。
但自动配置类仍然会被加载,它还有其他作用:
| 作用 | 说明 |
|---|---|
@EnableConfigurationProperties(MybatisPlusProperties.class) |
绑定 mybatis-plus.*
配置项 |
注册
MybatisPlusInterceptor 等扩展 Bean |
分页、乐观锁等插件 |
注册
SqlSessionFactory 的定制器 |
如
ConfigurationCustomizer、MybatisPlusPropertiesCustomizer |
最关键的问题:你手动创建的
SqlSessionFactory 用的是什么 FactoryBean?
- 如果用
MybatisSqlSessionFactoryBean(正确):MyBatis-Plus 的动态注入功能全部生效。 - 如果用原生的
SqlSessionFactoryBean(错误):BaseMapper的通用方法不会被注入,调用userMapper.insert()会报Invalid bound statement错误。
第六部分:多数据源实战
6.1 问题的引出
单数据源场景下,@MapperScan 不需要指定
sqlSessionFactoryRef 和
sqlSessionTemplateRef,因为容器里只有一个
SqlSessionFactory 和一个
SqlSessionTemplate,Spring 可以按类型自动注入。
但在多数据源场景下,容器里存在多个同类型的 Bean,自动注入就会因为类型不唯一而报错。这时就必须显式指定。
6.2 一个典型的多数据源配置(有问题的版本)
1 |
|
6.3 这个配置有什么问题?
问题在于:没有指定
sqlSessionTemplateRef,也没有定义
pgsqlSqlSessionTemplate Bean。
根据前面的原理,MapperScannerConfigurer 在创建
MapperFactoryBean 时,会根据 @MapperScan
的属性决定如何注入 SqlSessionTemplate:
- 如果指定了
sqlSessionTemplateRef:MapperFactoryBean直接使用容器中已存在的SqlSessionTemplateBean。 - 如果只指定了
sqlSessionFactoryRef:MapperFactoryBean会用这个 Factory 自己创建一个私有的SqlSessionTemplate。 - 如果两者都没指定:按类型自动注入,多个同类型 Bean 时报错。
- 如果两者同时指定:启动后会提示
Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.,所以,最佳实践是只指定sqlSessionTemplateRef
所以,只指定 sqlSessionFactoryRef 会导致:每个
Mapper 接口都会持有一个私有的 SqlSessionTemplate
实例。
这会导致两个问题:
- 无法复用与统一管理:每个 Mapper 都持有一个私有的
SqlSessionTemplate,无法享受共享 Bean 带来的内存优化和统一配置(如ExecutorType)的好处。 - 潜在的启动失败风险:如果容器中还存在其他
SqlSessionTemplateBean,你的 Mapper 在自动注入时可能会因类型不唯一而导致启动失败。
6.4 正确的多数据源配置
一个完整的多数据源配置,需要为每个数据源提供
DataSource、SqlSessionFactory、SqlSessionTemplate
和 TransactionManager 这一整套 Bean。
1 |
|
6.5 多数据源配置的要点
| 要点 | 说明 |
|---|---|
| 每个数据源一套完整 Bean | DataSource +
SqlSessionFactory + SqlSessionTemplate +
TransactionManager |
@MapperScan
显式引用 |
指定
sqlSessionTemplateRef,明确绑定 |
@Primary
标记主数据源 |
避免自动注入时类型不唯一 |
@Qualifier
精确注入 |
在 Bean 方法参数上使用,避免歧义 |
第七部分:MyBatis-Plus 批量插入实战
7.1 saveBatch
的局限:伪批量
MyBatis-Plus 的 ServiceImpl 中提供了
saveBatch 方法,用起来很方便:
1 | userService.saveBatch(userList); |
但它的底层实现是:
1 | for (T entity : entityList) { |
它并没有合并
SQL,只是减少了事务提交次数。每条记录仍然是一条独立的
INSERT 语句,网络往返次数并没有减少。
7.2
rewriteBatchedStatements=true:驱动层的”打包员”
要真正提升 saveBatch 的性能,必须配合 JDBC 驱动参数
rewriteBatchedStatements=true。它的原理是:MySQL JDBC
驱动的 executeBatchInternal() 方法中,当
batchHasPlainStatements == false 且
rewriteBatchedStatements == true 时,会走
executeBatchedInserts() 路径,把多条结构相同的
INSERT 在内存中拼成一条多值 SQL:
1 | INSERT INTO user (name, age) VALUES (?, ?), (?, ?), (?, ?), ...; |
这样一次网络往返就能插入所有数据。但这种方式依赖驱动层重写,且只对
INSERT 等特定语句有效。
7.3
insertBatchSomeColumn:真批量,动态注入的产物
MyBatis-Plus 提供了一个官方扩展方法
insertBatchSomeColumn,它从 SQL 生成阶段
就直接构建多值 INSERT,不依赖 JDBC 驱动的重写。
它的原理:回顾第二部分,MyBatis-Plus 通过重写
MybatisMapperAnnotationBuilder,在启动时遍历
BaseMapper 的通用方法,调用每个 AbstractMethod
的 injectMappedStatement() 方法,把生成的
MappedStatement 注册到 Configuration 中。
insertBatchSomeColumn 正是 AbstractMethod
的一个子类。它的 injectMappedStatement() 会:
- 通过
tableInfo.getAllInsertSqlColumnMaybeIf()获取列名脚本; - 通过
tableInfo.getAllInsertSqlPropertyMaybeIf()获取属性表达式; - 拼接出带
<foreach>的 SQL 模板:
1 | INSERT INTO user (name, age) VALUES |
- 创建
MappedStatement并注册到Configuration.mappedStatements中。
运行时,MyBatis 的 <foreach> 标签会把
list 展开,最终只生成一条多值
SQL,一次网络往返发送给数据库。
使用示例:
步骤 1:自定义 SQL 注入器
1 |
|
步骤 2:定义自定义 BaseMapper
1 | public interface MyBaseMapper<T> extends BaseMapper<T> { |
步骤 3:业务 Mapper 继承
1 | public interface UserMapper extends MyBaseMapper<User> {} |
步骤 4:调用
1 | userMapper.insertBatchSomeColumn(userList); |
7.4 两种方案对比
| 特性 | saveBatch +
rewriteBatchedStatements |
insertBatchSomeColumn |
|---|---|---|
| 谁在合并 | JDBC 驱动 | MyBatis-Plus(SQL 生成阶段) |
| 合并时机 | SQL 发送前 | SQL 生成时 |
| 是否依赖 JDBC 参数 | 是 | 否 |
| SQL 形式 | 多条独立 INSERT(驱动重写) | 始终一条多值 INSERT |
| 性能 | 中 | 高 |
一句话总结:saveBatch
是”伪批量”,需要驱动帮忙才能合并;insertBatchSomeColumn
是”真批量”,从一开始就只生成一条 SQL。后者正是 MyBatis-Plus
动态注入机制的绝佳案例。
7.5 多数据源下的批量插入
在多数据源配置中,我们为每个数据源都定义了独立的
SqlSessionFactory 和
SqlSessionTemplate。insertBatchSomeColumn 作为
AbstractMethod,同样会被注入到每个
SqlSessionFactory 对应的 Configuration
中。因此,只要你的 Mapper 继承了自定义的
MyBaseMapper,批量插入方法就能正常工作。
总结
回到最初的问题,我们把整个体系串起来:
MyBatis 核心层: - Configuration
是全局容器,MappedStatement 是每条 SQL 的说明书; - 启动时
MapperRegistry + MapperAnnotationBuilder 把
SQL 解析成 MappedStatement 注册到
Configuration; - 运行时 MapperProxyFactory +
MapperProxy 通过 JDK 动态代理把接口方法调用翻译成对
MappedStatement 的执行。
MyBatis-Plus 增强层: - 重写
MapperRegistry 和
MapperAnnotationBuilder,在启动解析阶段把
BaseMapper 通用方法的 MappedStatement
也注册进去; - 通过 MybatisConfiguration 和
MybatisSqlSessionFactoryBean 替换原生实现,无缝嵌入 MyBatis
启动流程; - insertBatchSomeColumn
是动态注入机制的典型应用,从 SQL 生成阶段就构建多值
INSERT,实现真·批量插入。
Spring 集成层: - MapperFactoryBean
实现 Spring 的 FactoryBean,让 Mapper 接口能作为 Bean
被注入; - MapperFactoryBean.checkDaoConfig() 调用
configuration.addMapper(),是连接 Spring 层和 MyBatis-Plus
增强层的关键; - SqlSessionTemplate 提供线程安全的
SqlSession 封装; - @MapperScan 通过
@Import + ImportBeanDefinitionRegistrar +
BeanDefinitionRegistryPostProcessor 三个扩展点,把 Mapper
接口批量注册为 MapperFactoryBean。
两条路径的汇合: -
MybatisPlusAutoConfiguration
造引擎(SqlSessionFactory、SqlSessionTemplate);
- @MapperScan 装轮子(MapperFactoryBean); -
两者通过 Spring 容器的依赖注入在 MapperFactoryBean
处汇合。
多数据源场景: - 每个数据源需要一套完整的
DataSource + SqlSessionFactory +
SqlSessionTemplate + TransactionManager; -
@MapperScan 中同时指定 sqlSessionFactoryRef 和
sqlSessionTemplateRef,明确绑定; - 只指定
sqlSessionFactoryRef 会导致每个 Mapper 持有私有
SqlSessionTemplate,虽然能跑,但不够规范。
一句话概括整个体系:MyBatis 在启动时把 SQL 解析成
MappedStatement 存入 Configuration,运行时通过
JDK 动态代理把接口方法调用翻译成对 MappedStatement
的执行;MyBatis-Plus 在启动解析阶段”偷偷”多注册一批
MappedStatement,并用这套机制实现了真·批量插入等实用功能;Spring
则通过 @MapperScan 和 MapperFactoryBean
把这些接口装配成可注入的
Bean。三者各司其职,共同构成了我们日常使用的持久层框架。