做过几年 Java 后端的人,多半都跟 MyBatis 打过交道。尤其是手头维护着几个老项目,或者要在一个非 Spring Boot 的传统 Spring 工程里硬塞一套持久层框架时,就会发现网上讲“Spring 整合 MyBatis”的文章很多,但大多是贴一段 XML 配置然后说“配置好了,能用”,至于为什么这样配、少了哪一步会踩什么坑、底层在启动时到底干了什么,往往语焉不详。
这篇就把 Spring 和 MyBatis 整合这件事从头到尾拆开讲。我会从整合思路讲起,把数据源、SqlSessionFactoryBean、Mapper 扫描、事务、XML 映射文件这些核心环节逐一过一遍,再附上我在实际项目里排查过的几个典型问题。无论你是刚接触 SSH/SSM 的老古董项目,还是准备给现有 Spring 工程引入 MyBatis,这篇文章应该能帮你少走不少弯路。
1. 先把整合思路理清楚:Spring 和 MyBatis 各自该干什么
很多人一上来就急着写配置,结果配了一堆发现启动报错,其实问题出在根本不知道这两个框架的分工上。
MyBatis 本身是一个独立的持久层框架,它的核心是 SqlSessionFactory。你给它一个数据源、一堆 Mapper XML 映射文件,它就能帮你执行 SQL、封装结果集。但它本身不关心对象管理和事务边界,这两件事恰恰是 Spring 的强项。
所以整合的本质,就是把 MyBatis 的核心对象交给 Spring 容器管理,让 Spring 帮你完成对象的创建、依赖注入和事务控制。当你写@Autowired private UserMapper userMapper;时,这个UserMapper其实不是你自己 new 出来的实现类,而是 MyBatis 通过 JDK 动态代理生成的代理对象,由 Spring 容器负责创建并注入到你业务代码里。
具体到整合时要做的事,可以拆成四步:
- 配置一个数据源 DataSource,告诉 MyBatis 连接哪台数据库。
- 用 Spring 的 SqlSessionFactoryBean 创建 SqlSessionFactory,并喂给它数据源和映射文件路径。
- 通过 MapperScannerConfigurer 或者
@MapperScan让 Spring 扫描 Mapper 接口,自动生成代理对象。 - 配置事务管理器,让 MyBatis 的每一次数据库操作都能纳入 Spring 事务管理。
把这四件事想明白了,后面所有配置都只是给这四步填充细节而已。
另外一个容易被忽略的点:这里说的 Spring 整合 MyBatis,其实是指 mybatis-spring 这个桥接库。它做了两件关键的事,一是让 SqlSessionFactory 的创建过程适配 Spring 的 Bean 生命周期,二是提供了 SqlSessionTemplate,一个线程安全的 SqlSession 封装。在老式用法里,你要自己拿 SqlSession 执行 SQL 还得手动 close,但通过 SqlSessionTemplate,你可以在 Spring 管理的环境中安全地使用 MyBatis,SqlSession 的打开和关闭都由它托管,配合 Spring 事务同步机制,还能让 SqlSession 跟事务绑定在同一个线程上。这个机制搞明白了,你就不会在整合后还傻傻地手动关闭 SqlSession 了。
2. 依赖配置与版本选择
无论你用什么构建工具,第一步都是把依赖引进来。这里我以 Maven 为例,给出最常用的一套组合。
<!-- Spring 核心依赖,一般项目里已经会有,这里按整合所需列全 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.31</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.31</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- 连接池 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <!-- 数据库驱动,以 MySQL 8 为例 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>这里有两个版本细节必须提醒一下。
第一,mybatis-spring 2.x 要求 Spring 5.x 或以上,如果你项目里还是 Spring 4,那得老老实实降级到 mybatis-spring 1.3.x。同理,mybatis-spring 1.3.x 也不能配 Spring 5 使用,否则启动时会因为方法签名不一致报错。最省心的做法是:Spring 5 对应 mybatis-spring 2.0.x,Spring 6 对应 mybatis-spring 3.0.x。
第二,MySQL 驱动在 8.0.31 之后改了包名,旧的com.mysql.jdbc.Driver在 8.0 系列里已经不建议使用了,新的驱动类名是com.mysql.cj.jdbc.Driver。很多老项目复制粘贴旧配置,启动时总是报加载不到驱动类,十有八九就是这个原因。
如果你是 Spring Boot 项目,整合会省事很多,mybatis-spring-boot-starter 自动完成大部分装配工作,但还是要去理解底层的这几个 Bean,否则一旦遇到多数据源、动态数据源这种复杂场景,光靠自动配置根本玩不转。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency>这个启动器会根据配置文件里的spring.datasource.*参数自动创建数据源和 SqlSessionFactory,并且自动扫描@Mapper注解的接口。不过要注意,这个 starter 不会自动扫描 XML 映射文件,需要额外配置mybatis.mapper-locations=classpath:mapper/*.xml告诉它映射文件在哪。
3. 数据源与 SqlSessionFactoryBean 的配置原理
依赖引进来之后,写配置的顺序也挺讲究。我一般习惯先配数据源,再配 SqlSessionFactory,最后配事务和 Mapper 扫描。这个顺序不是随便排的,因为后边的 Bean 都要引用前边的 Bean。
3.1 数据源配置:不要再用 DriverManagerDataSource
很多教程为了方便,直接写一个DriverManagerDataSource,这玩意儿在测试里没问题,但真上了生产环境就是灾难,因为它每次获取连接都新建物理连接,完全没有任何池化能力。
实际项目里我更喜欢用 HikariCP。它足够轻量,性能在同级连接池里属于第一梯队,Spring Boot 2 默认用的也是它。
@Bean public DataSource dataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("root"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); config.setMaximumPoolSize(10); config.setMinimumIdle(5); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); return new HikariDataSource(config); }有几个参数我建议你要么搞清楚含义、要么就别乱调。
maximumPoolSize:连接池中允许的最大连接数,不是越大越好。连接数跟数据库的并发处理能力、SQL 执行时长强相关。设得太大,数据库扛不住;设得太小,高并发时请求全在等连接。我自己的经验是从CPU核心数 * 2 + 磁盘IO等待系数这个方向去估算,但最终还是要靠压测调。对一个常规业务系统,10 到 20 基本够用。maxLifetime:连接的最大存活时间,要小于数据库自身的wait_timeout。我踩过一次坑,MySQL 默认wait_timeout是 8 小时,但连接池里的连接存活时间设置得比这个长,夜里流量一低,MySQL 把空闲连接断了,池子还蒙在鼓里,早上高峰期一到,一堆请求报“连接已被关闭”,那叫一个酸爽。idleTimeout:空闲连接的超时时间,只在minimumIdle < maximumPoolSize时才会生效,如果最小空闲数和最大连接数相等,这个参数会被忽略。
3.2 SqlSessionFactoryBean:真正的整合核心
SqlSessionFactoryBean 是 mybatis-spring 提供的 FactoryBean 实现,它实现了 Spring 的FactoryBean接口。平时我们@Autowired注入 SqlSessionFactory 时,注进来的是getObject()返回的对象,而不是 SqlSessionFactoryBean 本身。这个机制要心里有数,否则排查问题时容易犯迷糊。
@Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml")); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); factoryBean.setTypeAliasesPackage("com.example.entity"); return factoryBean; }这里我要多说两句,几个属性各有用处。
dataSource:不用多说,没有数据源一切都是空谈。configLocation:指向 mybatis 的全局配置文件。如果你习惯在 Spring 里把 MyBatis 所有设置都写在 XML 或者注解里,也可以不设置这个,但我建议保留一个mybatis-config.xml,像mapUnderscoreToCamelCase、日志实现、懒加载开关这些设置放这里,后续好维护。mapperLocations:指向 Mapper 的 XML 映射文件。这个属性特别容易漏,漏了的典型报错就是Invalid bound statement (not found),MyBatis 启动时根本不知道你写的 XML 映射文件在哪里。多数据源场景下,不同 SqlSessionFactory 要分别指定各自的 XML 路径,这也是一个经典坑点。typeAliasesPackage:如果实体类的包路径下有很多类,建议配置这个属性,XML 里的 resultType 就可以直接写类名,不用写全限定名。
3.3 mybatis-config.xml 里的常用配置
我的标准mybatis-config.xml长这样:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="jdbcTypeForNull" value="NULL"/> <setting name="logImpl" value="SLF4J"/> </settings> <typeAliases> <package name="com.example.entity"/> </typeAliases> </configuration>这几个 setting 的坑我挨个说一下。
mapUnderscoreToCamelCase设置为 true 后,user_name可以自动映射到userName,省掉一大堆 resultMap。但不是所有情况都能靠它兜底,像user_name和userName能转,可你要是把userName写成username,或者数据库字段是u_name,它照样歇菜,还是老老实实写 resultMap。
jdbcTypeForNull设置为 NULL,是解决一个经典问题的。当你向数据库插入 null 值时,如果不指定 jdbcType,MyBatis 在某些数据库下会报“JDBC 3.0 无法处理 null 值”之类的错误。老版本的 Oracle 驱动尤其敏感。MySQL 下一般还好,但既然配了能省事,何乐而不为。
logImpl建议设成 SLF4J,让 MyBatis 的日志输出跟项目里的日志框架统一,排查 SQL 问题时你会感谢这个决定。日志输出 SQL 对排查问题极其重要,项目上线后出问题,第一件事往往就是去日志里找那条执行慢的 SQL。
4. Mapper 扫描的三种方式与代理对象生成原理
SqlSessionFactory 创建好了,接下来要让 Spring 容器认识你的 Mapper 接口。这一步有几种做法,我按推荐程度讲。
4.1 推荐做法:@MapperScan 注解
直接在配置类上加注解,最省事:
@Configuration @MapperScan(basePackages = "com.example.mapper") public class MyBatisConfig { // 上面那些 Bean 定义 }@MapperScan会扫描指定包下的所有接口,并为它们创建代理对象注册到 Spring 容器。这里有一个细节值得注意:@MapperScan默认会使用容器里的 SqlSessionFactory,如果你配置了多个 SqlSessionFactory,需要指定sqlSessionFactoryRef属性,明确告诉它用哪一个。
4.2 XML 方式:MapperScannerConfigurer
老项目里没有注解配置类,用的是 XML 配置,那就要通过 MapperScannerConfigurer 来做:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.mapper"/> </bean>注意这个类有BeanDefinitionRegistryPostProcessor的能力,会在 Spring 容器初始化早期介入。所以你在它内部引用其他 Bean 时,用字符串名字更安全,直接引用实例可能会因为初始化顺序问题导致注入失败。
实际项目中我曾经见过因为 XML 里写了两个 SqlSessionFactory,而 MapperScannerConfigurer 没有指定sqlSessionFactoryBeanName,结果有的 Mapper 跑到了错误的 SqlSessionFactory 上,SQL 执行时连接的库完全是另一个库。所以多数据源场景下,每个数据源都要配一个 MapperScannerConfigurer,并且分别指定sqlSessionFactoryBeanName。
4.3 注解方式:@Mapper 注解逐个标记
如果 Mapper 接口数量少,也可以在接口上加@Mapper注解。但它必须配合@MapperScan指定的包路径一起用,否则 Spring 不会自动扫描到它。反过来,你只要用了@MapperScan,接口上的@Mapper加不加都无所谓。
4.4 Mapper 代理对象的生成原理
我一直觉得,理解了 Mapper 的代理机制,排查问题会快很多。当 Spring 扫描到UserMapper接口时,MapperScannerConfigurer会把它注册为MapperFactoryBean,这个 FactoryBean 在getObject()时调用SqlSession.getMapper(UserMapper.class),最终 JDK 动态代理生成一个 UserMapper 的代理实例。
这个代理实例不是你写的实现类,对它做断点调试时,实际栈帧会走 MapperProxy 的 invoke 方法。排查“为什么这个 SQL 没走我预期的逻辑”时,看到这个类别慌,这是正常流程。代理对象会根据你调用的方法名,从 Configuration 里找到对应的 MappedStatement,然后执行 SQL。
如果这个查找过程出了问题,比如方法名跟 XML 里的 id 对不上,就会抛出Invalid bound statement (not found)。注意这个异常的定位思路,你先检查接口名全限定名和 XML namespace 是否一致,再检查方法名和 XML id 是否一致,最后看 mapperLocations 有没有扫到 XML。
5. 事务管理:Spring 事务和 MyBatis 是怎么协作的
整合过程中,事务这块最容易出问题,而且出了问题往往还不是立即报错,而是静默地不生效,这种问题排查起来很抓狂。
5.1 配置事务管理器
事务管理器的配置很直接:
@Bean public DataSourceTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager manager = new DataSourceTransactionManager(); manager.setDataSource(dataSource); return manager; }事务管理器必须跟 SqlSessionFactory 使用同一个 DataSource。如果它们各用各的数据源,事务就会完全失效,因为 MyBatis 操作的是 A 数据源的连接,而事务管理的是 B 数据源的连接,两边各管各的,回滚的时候什么都回滚不了。
MyBatis 与 Spring 事务的协作机制是这样的:当你给某个方法加了@Transactional,Spring 在进入方法前会从数据源获取一个数据库连接,绑定到当前线程。MyBatis 执行 SQL 时,SqlSessionTemplate 通过TransactionSynchronizationManager感知到当前线程已经有连接了,就会复用这个连接,而不是重新从连接池取。事务提交时,Spring 统一提交;事务回滚时,Spring 统一回滚。
所以这里有个推论:如果你绕开了 Spring 事务,比如直接在 MyBatis 的 SqlSession 上调用commit(),在 Spring 管理的事务里反而会出问题,因为这打断了 Spring 对连接的统一管理。实际开发中,不要在业务代码里手动调用 SqlSession 的 commit、rollback、close 这些方法,让 Spring 管就好。
5.2 加 @Transactional 的姿势
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { orderMapper.insert(order); // 调用另一个 Mapper 的更新方法 stockMapper.deductStock(order.getProductId()); } }rollbackFor = Exception.class这笔配置强烈建议写上。Spring 事务默认只对 RuntimeException 回滚,如果你方法里抛了一个自定义的检查异常,事务不会回滚,数据就静默地提交了。这是事务不回滚的几个经典原因之一。
还有自调用问题。同一个类里,一个方法调用另一个被@Transactional标注的方法,事务注解是不会生效的。因为 Spring 的 AOP 代理机制是通过代理对象调用时才会拦截,而你通过this.xxx()调用,走的是当前对象本身,不是代理对象。解决方式要么把事务方法放到另一个 Bean 里,要么注入自身的代理,要么用AopContext.currentProxy()这种方式自己取代理。但最推荐的做法还是尽量避免同类自调用,让事务边界清晰。
5.3 事务只读提示的坑
如果你配置了事务的readOnly = true,但方法里执行了 insert/update/delete 操作,MySQL 下一般不会立刻报错,但在某些数据库驱动下会警告“Connection is read-only”。这通常不是配置写错了,而是你的事务方法被不恰当地标记成了只读,而实际上里面写了数据。
5.4 数据库表引擎也要注意
事务回滚还有个前置条件:表引擎要支持事务。MySQL 的 MyISAM 引擎不支持事务,不管 Spring 怎么配置,执行插入后一样会落库,回滚完全没有效果。排查回滚失效的前提,先确认表是不是 InnoDB。
6. Mapper 接口与 XML 映射文件的配合细节
很多新手在写 Mapper 的时候会纠结:SQL 到底写在接口注解里还是写在 XML 里?我个人的建议是:复杂 SQL 一律写 XML,简单 SQL 可以用注解。理由很简单,SQL 一旦复杂起来,注解形式的可读性会直线下降,而且动态 SQL 在注解里写起来很痛苦,字符串拼接会让人崩溃。
6.1 namespace 与 id 的对应规则
<mapper namespace="com.example.mapper.UserMapper"> <select id="selectByName" resultType="User"> select * from user where name = #{name} </select> </mapper>这里的 namespace 必须等于 Mapper 接口的全限定名,select 标签的 id 必须等于接口方法名。错一个,就是上面说过的Invalid bound statement (not found)。
但要注意,这个报错还有一种情况,是接口方法有@Param("name")注解,而 XML 里用了#{name},这种通常没问题。反而是接口方法参数是单个对象,XML 里误用了#{name}而对象里没有 getName 方法时,报的是另一个运行时错误。所以看到这个报错,先按 namespace、id、mapperLocations 三个方向排查,基本能覆盖九成情况。
6.2 resultType 与 resultMap 的选择
字段名和属性名对得上的时候,直接写 resultType,配合mapUnderscoreToCamelCase就能自动映射。字段名对不上的时候,比如数据库是user_name,实体是username,就得用 resultMap 手动指定映射关系。
<resultMap id="UserResultMap" type="User"> <id property="id" column="id"/> <result property="username" column="user_name"/> <result property="createTime" column="create_time"/> </resultMap>resultMap 还有一种常见用途是解决一对一、一对多关联查询。一对多时要用<collection>子标签,一对一用<association>。不过我要提醒一句:关联查询在 XML 里写起来方便,性能上要小心 N+1 查询问题。连表查询一次拿回来的结果还好,但如果你用嵌套查询的方式,每条记录都触发一次额外查询,数据一多性能就崩了。遇到这种情况,优先考虑连表查询,或者用分页查询分批加载。
6.3 动态 SQL 的高频场景
MyBatis 的动态 SQL 是我认为它比 JDBC 爽太多的地方。几个高频标签的实际用法:
<select id="selectByCondition" resultType="User"> select * from user <where> <if test="name != null and name != ''"> and name = #{name} </if> <if test="age != null"> and age >= #{age} </if> </where> order by id desc </select>这里<where>标签会自动处理掉多余的 and/or,你不用担心第一个条件前面多出来的 and。<set>标签在 update 语句里的作用同理,自动处理最后的逗号:
<update id="updateUser" parameterType="User"> update user <set> <if test="name != null">name = #{name},</if> <if test="age != null">age = #{age},</if> </set> where id = #{id} </update><foreach>用于 IN 查询:
<select id="selectByIds" resultType="User"> select * from user where id in <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>注意 collection 的值,接口方法参数如果是数组,传array;如果是 List,传list;如果有@Param("ids")注解,直接写注解的名字。这个对应关系经常被忽略,写错之后报错信息又常常看不懂。实际开发中,我强烈建议所有参数都用@Param显式指定名字,既清晰又能避免这种问题。
6.4${}与#{}的选择
这个老生常谈但还是要说。#{}是预编译占位符,MyBatis 会生成?占位符,再通过 PreparedStatement 设置参数值,能够有效防止 SQL 注入。${}是直接拼接字符串,设计到 SQL 注入风险。
什么场景下非用${}不可?大概就是表名、列名这种不能作为预编译参数的部分,例如动态分页排序时按动态列名排序:
<select id="selectOrderBy" resultType="User"> select * from user order by ${orderColumn} ${orderDir} </select>这种写法只能保证传入的值是白名单内的常量,绝不能直接把前端传来的字符串拼进去。我一般会在代码里做一个映射校验,把允许的列名枚举出来,比如"createTime" -> "create_time",假若前端传的值不在映射表里,就直接给默认值。这是对自己系统的保护。
7. 常见问题与排查实录
整合完成之后,真正让人头疼的是各种运行时问题。我把自己实际踩过、帮别人排查过的几个高频问题列出来,按排查顺序写,希望能帮你省点时间。
7.1 Invalid bound statement (not found)
这是高频中的高频。排查步骤如下:
- 确认 Mapper 接口的类路径跟 XML 的 namespace 完全一致,包括大小写。
- 确认接口方法名跟 XML 的 id 完全一致,包括大小写。
- 确认 SqlSessionFactoryBean 的 mapperLocations 是否覆盖到了你的 XML 文件路径。如果 XML 放在
src/main/java而不是src/main/resources,还要在 pom 里加资源配置,否则打包后 XML 根本没有进入 classpath。 - 确认你的 XML 文件没有损坏,可以打开看看 mapper 标签是否正确闭合。
有一个经验可以参考:我在 IDEA 里新建 Mapper XML 后,有时因为模块结构问题,XML 没被自动复制到 target 目录,导致运行时报找不到绑定。解决办法就是在 pom.xml 的 build 节点里显式声明 resources。
<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> </build>7.2 Mapped Statements collection does not contain value for ...
这个报错跟上面那个很相似,但定位更明确,说配置里没有那个 id。常见原因是 XML 文件没有被正确扫描到,或者 XML 里的 select 标签 id 写错了。确认方向跟上面一样,重点看 mapperLocations。
7.3 事务不生效或回滚失败
按下面顺序排查,基本能覆盖:
- 确认事务管理器注入的数据源跟 SqlSessionFactory 的数据源是同一个。
- 确认方法被 Spring AOP 代理调用,而不是同类自调用。
- 确认你抛出的异常被
rollbackFor覆盖住了,或者改成@Transactional(rollbackFor = Exception.class)。 - 确认表是 InnoDB。
- 确认
@EnableTransactionManagement这个注解加上了(XML 方式的话要加<tx:annotation-driven/>)。 - 如果用了异步方法,还要注意
@Transactional和@Async在同一个方法上时,代理的顺序可能影响事务。
7.4 连接池连接耗尽,getConnection timeout
这个通常不是配置问题,而是代码问题。典型场景是事务方法里执行了一个长时间的 SQL,或者代码里拿到了 SqlSession / Connection 没关闭。排查命令先看数据库的连接数情况,再看应用日志里哪类 SQL 的耗时异常。如果所有 SQL 都正常但连接还是耗尽,去查是不是有地方手动创建了连接没释放。
HikariCP 连接池在连接不够用时会打印 pool 状态相关的日志,能看到 active 和 idle 的数量。假如 active 一直等于 maximumPoolSize 且久高不下,十有八九是泄漏。
7.5 分页查询失效或数据重复
分页插件如 PageHelper 的原理,是基于拦截器在执行 SQL 前改写 SQL,并在 ThreadLocal 存放分页参数。它的一个典型坑是线程池复用导致分页参数串到下一次查询,解决方式是确认在 finally 里调用了PageHelper.clearPage()。
还有一个常见场景:查总数和查列表的 SQL 被拦截器改写得不符合预期,比如你主动加了 distinct,分页插件统计 count 时也会原样翻一遍,导致 count 结果不准。这时候要检查你的 SQL 是不是写了太复杂的 group by 或 distinct 语句。
7.6 缓存问题
MyBatis 自带一级缓存是 SqlSession 级别的,SqlSessionTemplate 在 Spring 整合中每次执行 SQL 可能开启新 SqlSession,所以一级缓存的作用范围在一个事务内基本有效,跨事务就别指望它能帮你扛读压力。二级缓存是跨 SqlSession 级别的,但实际项目中我对二级缓存一般采取保守态度,因为一旦数据更新,缓存失效策略很容易出问题,尤其在多实例部署的微服务架构下,节点间的缓存一致性根本没法保证。真需要缓存,优先考虑业务层缓存(比如 Redis)或者应用层缓存,而不是依赖 MyBatis 的二级缓存。
8. 补充一种更现代的配置方式
如果你不想写那一大堆配置类或 XML 配置,也可以完全用注解的方式搞定。核心配置类就像前面第 3 章写的那样,但也可以把 SqlSessionFactoryBean 的创建隐藏到更精简的写法里。这里有一个常见的误区:很多人以为用了@MapperScan就可以不用配 SqlSessionFactoryBean,其实不是,@MapperScan只是帮你扫描 Mapper 接口,SqlSessionFactory 仍然是必须要有的。在传统 Spring(非 Boot)项目里,这两者是配合关系,不是替代关系。
还有不少人问,能不能不写 mybatis-config.xml,把所有设置都放在 SqlSessionFactoryBean 上?可以。
@Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); org.apache.ibatis.session.Configuration ibatisConfig = new org.apache.ibatis.session.Configuration(); ibatisConfig.setMapUnderscoreToCamelCase(true); ibatisConfig.setJdbcTypeForNull(JdbcType.NULL); factoryBean.setConfiguration(ibatisConfig); return factoryBean; }这种写法的好处是少一个配置文件,项目结构更紧凑,适合小项目。但全局配置一旦复杂起来,维护性反而没有 XML 好。取舍标准很简单:项目小、配置少,注解方式方便;项目大、协作人多、配置项多,XML 方式一目了然。
9. 写在最后的几点个人经验
把这些年在 Spring 里用 MyBatis 的经验浓缩一下,有几条我觉得比任何配置都重要。
第一条,把 SqlSessionTemplate 当成 MyBatis 在 Spring 环境里的第一公民。很多时候你会发现在业务代码里能拿到 SqlSessionTemplate,直接用它执行 SQL。但这只是极少数异步任务、批量操作等特殊场景下才会做的事,正常业务开发中,优先通过 Mapper 接口操作,保持代码整洁。
第二条,SQL 日志必须打出来。配置好logImpl之后,每一条 SQL 和传入的参数都会原样打印。你排查问题时看得最多的不是业务日志,就是这些 SQL 日志。统计慢 SQL 时,也是从这些日志里捞出来分析。如果一个系统开了 SQL 日志生产环境会刷屏,那至少线上要保留慢 SQL 日志。
第三条,MyBatis 的 XML 映射文件最好在同一目录统一管理,不要跟实体类、接口混在一起。我习惯放在src/main/resources/mapper/下,一个模块一个子目录。这样 mapperLocations 的配置既简单又可靠,还能避免打包时因为资源目录配置导致 XML 缺失。
第四条,也是我最想强调的一点:不要迷信网上那些一键生成的代码生成器。它们生成的 Mapper、XML、实体类,问题不在能不能跑,而在于生成的 SQL 里经常混着${}、一个实体类里堆了十几个字段导致查询全表扫描、update 语句把所有字段都更新一遍。真正写 SQL 的时候,还是要按业务需要的字段来。MyBatis 的灵活性恰恰在这里,你要什么就查什么,别一股脑 select *。
整合 Spring 和 MyBatis 这件事,其实技术难度不算高,复杂的是各种隐性的整合细节。数据源、SqlSessionFactory、Mapper 扫描、事务管理,这四个环节环环相扣,任何一环出问题,表现出的现象又往往让人摸不着头脑。我写这篇文章的初衷,就是希望能有人把这些坑一次性踩平。如果你照着配置下来还遇到问题,优先看日志,从启动时的 Mapper 注册日志和 SQL 执行日志入手,绝大多数问题都能看出端倪。