先说个很多人都经历过的场景:你辛辛苦苦把一个项目从 JDBC 改成 MyBatis,写好了各种 Mapper 接口和 XML,代码跑起来也正常。可一旦把它塞进 Spring 容器里统一管理,就冒出一堆“神奇”问题——SqlSession 关不干净、事务不生效、Mapper 接口报找不到实现类、连接池老是超时。多数人此时的第一反应是把锅甩给 MyBatis 或 Spring,其实根子往往出在两者的“协作方式”上。
这篇博文就从 Spring 整合 MyBatis 这件事讲起。你不光能学会完整配置步骤,还能明白每一步背后的逻辑——为什么这样配置、事务怎么接管、Mapper 接口为什么能被注入、遇到绑定异常该怎么排查。走一遍之后再回首,你会发现这不仅是配几个 bean 的事,而是理解 Spring 和持久层框架之间分工的一次机会。适合刚从 MyBatis 单机测试转向 Spring 整合项目的人,也适合项目里已经在用但踩过不少坑的开发者。
1. 为什么非整合不可:原生 MyBatis 的麻烦事
1.1 每次 CRUD 都要手动管理 SqlSession 的痛
先别急着上配置,得先搞清楚我们到底在解决什么问题。原生 MyBatis 的常用套路是这样的:从 SqlSessionFactory 里开一个 SqlSession,执行完 SQL 后手动 commit,最后老老实实 close。你要是这么写过,心里一定有数——这流程基本是复制粘贴,稍微漏一步,连接就泄漏,跑到后面整个应用卡死。
走到 Service 层更难受。一个业务方法里往往要操作两张表,意味着要先开一个 SqlSession,查完 A 表,再查 B 表,然后把业务判断逻辑夹在中间,最后统一提交。这只是事务需求没那么复杂的情况。真要遇上嵌套调用、异常回滚,代码里全是 try-catch-finally,你还没开始写业务逻辑,先被资源管理折磨到怀疑人生。
原生方式还有一个问题:SqlSession 本身是线程不安全的。写 Demo 的时侯无所谓,但放到 Web 应用里,一个请求对应一个线程,你不能让所有请求共享同一个 SqlSession,更不能每次请求都手动管理开关。这种重复且容易出错的劳动,根本不该由人来执行。
1.2 整合前后的核心变化
Spring 整合 MyBatis 之后,最大的变化就是 MyBatis 的生命周期完全交给 Spring 管理。
你会发现项目里不再有人手动去开 SqlSession 和 close,而是注入了一个叫 SqlSessionTemplate 的东西。它是线程安全的,内部会自动管理 SqlSession 的创建、绑定到当前线程、提交或回滚、以及最终关闭。你写 Mapper 接口的时候,也不需要写实现类——Spring 通过动态代理机制,扫描到接口后自动生成代理对象,注入到 Service 层。这也就是为什么 Service 里@Autowired一个 Mapper 接口,它居然能直接用的原因。
事务方面更省心。原生状态下事务边界要自己圈,Spring 接手后你只管在 Service 方法上加@Transactional,框架会自动开启事务、执行 SQL、提交或回滚。底层原理不复杂:整合包里的 SqlSessionTemplate 会感知 Spring 当前的事务状态,如果事务是 Spring 开启的,它就直接加入这个事务,不会自己另开一个,从而保证多条 SQL 在同一个物理连接里执行,真正做到原子性。
2. 环境准备:依赖、版本与项目结构
2.1 Maven 依赖清单与版本选择
整合需要两个核心库:MyBatis 本身和mybatis-spring适配包。前者提供 SQL 映射能力,后者负责把 MyBatis 桥接到 Spring 容器里。
下面是一套比较标准且稳定的依赖组合:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <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>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>版本选择要特别留意:mybatis-spring2.x 对应 MyBatis 3.5+,对应 Spring 5.x;如果你的项目处于 Spring Boot 环境,直接用mybatis-spring-boot-starter就行,它会帮你把版本对齐。但纯 Spring 项目必须手动匹配,我看过不少人拿mybatis-spring1.x 配 Spring 5,启动就报 NoSuchMethodError,基本是版本不兼容的锅。
2.2 连接池和驱动的补充说明
连接池选型上,HikariCP 是当前综合表现不错的选择。配置很简单,核心参数就几个:jdbcUrl、driverClassName、用户名密码,再按需调整最大连接数和超时时间。使用 MySQL 8 及以上版本,驱动类要写com.mysql.cj.jdbc.Driver,jdbcUrl里建议显式带上serverTimezone=Asia/Shanghai和useSSL=false,不然后续时间格式化或 SSL 握手问题很可能让你排查半天。
参数示例:
jdbcUrl=jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 driverClassName=com.mysql.cj.jdbc.Driver username=root password=你的密码 maximumPoolSize=10 connectionTimeout=30000拿数据库连接池类比一下:它相当于一个“电话总机”,应用每次需要数据库连接时,直接从池里取一条“空闲电话线”,用完再放回去,避免了频繁拨号建立连接的开销。HikariCP 内部做得很极致,启动快、吞吐高,对中小型项目完全够用。
3. 配置落地:XML 配置与 Java 配置两种姿势
这一节是核心。两种方式原理相同,选择哪一种看你项目的风格。早期项目偏爱 XML,因为团队成员不一定会写 Java 配置类;新项目大多走 Java 配置,类型安全、重构方便。
3.1 XML 配置方案
创建一个applicationContext.xml,核心内容如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.example.demo"/> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> <property name="maximumPoolSize" value="10"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.example.demo.entity"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> <property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/> </bean> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.demo.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>这段配置里每一条都有讲究。mapperLocations指定了 XML 映射文件的位置,MyBatis 启动时会把你写的 SQL 解析进内存,和 Mapper 接口绑定。typeAliasesPackage用于给实体类起短别名,这样 XML 里写resultType="com.example.demo.entity.User"可以简化为resultType="User"。mapUnderscoreToCamelCase更是必需品,没有它,数据库里的created_at永远映射不到 Java 属性createdAt上。
MapperScannerConfigurer的作用是扫描指定包下的所有 Mapper 接口,把它们注册成 Spring Bean。注意这里我用了sqlSessionFactoryBeanName而不是sqlSessionFactory,这样做的好处是不会强制提前初始化 SqlSessionFactory,能避开一些初始化顺序的潜在坑。
3.2 Java 配置方案
同等作用的 Java 配置类长这样:
@Configuration @MapperScan("com.example.demo.mapper") public class MyBatisConfig { @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setMaximumPoolSize(10); return dataSource; } @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setTypeAliasesPackage("com.example.demo.entity"); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); factoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml")); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class); factoryBean.setConfiguration(configuration); return factoryBean; } @Bean public DataSourceTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }@MapperScan注解替代了 XML 里的 MapperScannerConfigurer,放在配置类上即可,它会扫描指定包以及子包下所有接口,自动注册为 Mapper Bean。这里sqlSessionFactory方法接收一个 DataSource 参数,Spring 会自动把上面定义的 dataSource Bean 注入进来,省去了手工ref的繁琐。
3.3 两种配置的取舍
XML 配置在复杂的条件判断、多环境切换场景下更灵活——改配置不用重新编译,运维直接替换配置文件就行。Java 配置更直观,是类型安全的,写错了编译期就会发现,IDE 跳转也方便。我个人新建项目时倾向 Java 配置,但保持 XML 映射文件存在——Mapper 接口的 SQL 写在 XML 里,动态 SQL 写起来远比注解舒服,这一点后面展开聊。
4. Mapper 层开发:接口与 XML 的配合
4.1 命名空间绑定机制
整合完成不等于项目能跑,Mapper 接口还得和 XML 正确绑定。这里的绑定全靠 XML 根节点里的 namespace 属性。
一个标准的映射文件长这样:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserMapper"> <resultMap id="userMap" type="User"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="email" column="email"/> <result property="createdAt" column="created_at"/> </resultMap> <select id="selectById" resultMap="userMap"> SELECT id, username, email, created_at FROM user WHERE id = #{id} </select> </mapper>namespace 必须与 Mapper 接口的全限定名完全一致,select标签的 id 必须与接口里的方法名一致。MyBatis 启动解析时,会自动从 namespace 定位到对应接口,然后为每个 id 生成代理方法。写错任何一个字符,运行到那一行就会抛出Invalid bound statement (not found)。
4.2 参数映射与结果映射
单个参数可以直接传。多个参数时,最推荐的做法是用@Param注解显式命名:
public interface UserMapper { List<User> selectByCondition(@Param("username") String username, @Param("status") Integer status); }XML 里这样引用:
<select id="selectByCondition" resultType="User"> SELECT * FROM user WHERE username = #{username} AND status = #{status} </select>不写@Param的情况下,MyBatis 会用arg0、arg1或param1、param2作为参数名,极其容易写错。与其依赖这种隐式规则,不如多写一行注解,代码可读性立刻上一个台阶。
结果映射有两条路:一是靠自动映射 +mapUnderscoreToCamelCase配置,适合列名和属性名规规矩矩的表;二是写 resultMap,适合复杂表(连表查询、字段改名、一对一或一对多)。后者的典型场景是字段整理后命名完全对不上,或者需要嵌套结果集的时候——这种场景不是自动映射能解决的,老老实实写 resultMap 才是正解。
4.3 动态 SQL 的写法
这是 MyBatis 相比 JDBC 最有竞争力的地方。按条件查询用户,SQL 的 WHERE 子句在不同入参下可能完全不同。用<if>和<where>组合,比在 Java 代码里手动拼接 SQL 优雅得多:
<select id="selectByCondition" resultType="User"> SELECT * FROM user <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="email != null"> AND email = #{email} </if> </where> ORDER BY id DESC </select><where>会自动处理第一项前面多余的 AND——如果子句以 AND 开头,它会聪明地去掉。不需要你在 Java 里写boolean hasCondition之类的状态位,框架替你干完了。
批量插入也很常用。单条 insert 循环 N 次,数据库连接往返 N 次,效率感人。用一个<foreach>标签,一条语句搞定:
<insert id="batchInsert"> INSERT INTO user(username, email, status) VALUES <foreach collection="list" item="user" separator=","> (#{user.username}, #{user.email}, #{user.status}) </foreach> </insert>这里的 collection 固定写list,表示入参是一个 List。item 是你给集合里每个元素起的名字,后续用user.username获取元素属性。
5. 事务管理:从手动 commit 到 @Transactional
5.1 原生 MyBatis 的事务方式
原生 MyBatis 下手动控制事务,你大概率写过类似代码:拿到 SqlSession 之后,先session.commit(),要么回滚session.rollback(),finally 里还要session.close()。想象一个业务方法里查询、插入、更新三个操作,任何一个出问题都要把前面所有操作撤销——代码会变成什么样?每个分支都来一遍回滚判断,业务没写几行,控制代码堆成山。
5.2 Spring 事务的整合原理
Spring 整合后,你只需要在 Service 方法上加一个注解:
@Service public class UserService { @Autowired private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public void createUserWithProfile(User user, Profile profile) { userMapper.insert(user); // 假设这里抛出了 RuntimeException profileMapper.insert(profile); } }方法执行期间,所有 Mapper 的操作都在同一个事务里。一旦中间出现异常,Spring 检测到后整个事务回滚,前面执行成功的 insert 也会被撤销,不会留下半截数据。
原理不复杂:DataSourceTransactionManager是事务的核心,它管理着数据源连接。@Transactional标注的方法被 Spring AOP 代理——方法进入前从连接池取一个连接开启事务,把连接绑定到当前线程;执行过程中,SqlSessionTemplate 检测到当前线程已有数据库连接,就沿用这个连接而不是取新的;方法正常结束则提交,异常则回滚,最后释放连接并解绑线程。这一整套流程让我直观理解了一个道理:事务的本质不是“在方法上画个圈”,而是牢牢锁定一个数据库连接,让所有 SQL 都在这条连接上串行执行。
5.3 事务不生效的常见原因
事务不生效是比较气人的问题,因为代码不报错、数据也会写入,只是该回滚的时候没回滚。常见原因有四个:
第一,方法被同类内部调用。@Transactional走的是代理机制,同类里一个方法调用另一个方法,调用发生在代理对象内部,相当于绕过了代理——Spring 无法拦到这次调用。要解决,把事务方法挪到另外一个 Service 里,或者自己注入代理对象再调用。
第二,异常被吞掉。事务注解默认只对 RuntimeException 和 Error 回滚,对受检异常(如 Exception)不回滚。你必须在方法里抛异常,并且确保它传播出来,而不是 catch 住。有一个更稳的写法:@Transactional(rollbackFor = Exception.class),这样不管什么异常都会触发回滚,值得养成习惯。
第三,数据库表用了不支持事务的引擎。比如 MySQL 的 MyISAM,本身就没有事务支持,你注解加得再多也白搭。表引擎请用 InnoDB。
第四,事务管理器配置漏了。XML 方案里如果没配置 DataSourceTransactionManager,@Transactional就是个摆设,项目还不一定报错。配置完成后,记得确认<tx:annotation-driven>是否存在。
6. 踩坑实录:第一次整合最容易遇见的五个问题
6.1 绑定异常:报 Invalid bound statement (not found)
这是出现频率最高的报错,没有之一。报错信息直接告诉你哪条 SQL 找不到,但真正的原因往往有几类:
- namespace 没有对应到 Mapper 接口全限定名
- XML 文件没被
mapperLocations扫到。比如你把 XML 放在src/main/resources/mapper/下面,配置却写classpath:mybatis/*.xml,路径错了,文件根本没加载 - 接口方法名和 XML 里的 id 对不上,少个字母都不行
- XML 文件后缀写成了
.xml.txt,或者编译后没有被打进 classpath
排查顺序建议是:先直接看编译产物(target/classes/mapper/)里有没有对应 XML,再看 namespace 和 id。前者经常被忽略,其实很坑。我用过一个项目,IDE 资源目录没配好,XML 在源码目录里躺着,运行时完全找不到,排查了半天才发现是构建配置问题。
6.2 字段映射问题:下划线与驼峰
数据表列名用user_name这种下划线风格,实体类属性用userName这种驼峰风格,这是很常规的操作。MyBatis 默认可不会自动把两者对应起来,需要开启全局配置。
configuration.setMapUnderscoreToCamelCase(true);开启后,自动映射会把user_name转换到userName。但注意,resultMap 里显式定义的字段不受这个开关影响,你写什么映射就是什么,所以如果你用了 resultMap,还是得手动把每个列名和属性名对齐。
还有个小坑:create_time这种字段映射到LocalDateTime createTime没问题,但数据库时间字段类型如果是TIMESTAMP,Java 端要用LocalDateTime或java.sql.Timestamp接收,别用java.util.Date硬顶,容易出现时区偏差。
6.3 为什么 SESSION 事务模式下看不到新数据
MyBatis 的一级缓存是 SqlSession 级别的。没整合之前,你每次手动关闭 SqlSession,缓存随之清空,所以对这个问题不敏感。整合后,SqlSessionTemplate 里的 SqlSession 生命周期和 Spring 事务绑定,如果在一个长事务里反复查询同一行数据,第二次查询只要没有更新操作,就会直接命中缓存,返回旧值。很多人在一个方法里先查一遍、再更新、再查一遍,第二次查出来的还是旧数据,就是这个原因。
处理方法有三种:第一种,在 Mapper 方法上加flushCache="true",强制每次查询刷新缓存;第二种,把大事务拆小,查询和更新分开;第三种,干脆关掉一级缓存(localCacheScope=STATEMENT),性能损失不算大,换取心智简单。在整合环境下,一级缓存是个容易制造“幻觉”的东西,建议项目早期就定好统一策略,不要放任自流。
6.4 SqlSession 为什么线程不安全
原生 MyBatis 的 SqlSession 默认非线程安全,整合后你就不该再手动使用它。写代码时记住:SqlSessionTemplate 才是安全的,它内部对每次操作都会从底层工厂取一个新的 SqlSession,用完立即关闭。这也是为什么整合后你写代码不需要关心 SqlSession 的生命周期——这些都封装在 Template 里了。
如果某天你图省事,把 SqlSessionFactory 注进来,在 Service 里手动开 SqlSession 操作数据库,那就相当于绕开了 Spring 的管理。事务、连接回收全部失效,高点并发下连接泄漏是迟早的事。整合项目的红线之一是:数据库访问只通过 Mapper 接口,不要直接碰 SqlSession。
6.5 连接池配置导致的启动失败
HikariCP 启动时会尝试获取一个连接做连通性检测,如果数据库地址写错、服务没起,或者连接数设置不合理(比如一个测试库只允许 5 个连接,你设置 100),启动阶段就会报异常。这里最容易忽略的是connectionTimeout。默认 30 秒,在极端情况下(数据库负载高、网络抖动)你以为卡死了,其实是在等待连接。诊断方式很简单:看异常堆栈里是否有 HikariPool 相关字样,再用数据库客户端手动连一下,立刻就能定位是配置问题还是网络问题。
7. 实战中的经验补充:日志、分页与代码规范
7.1 开启 SQL 日志,让问题直接显形
排查 SQL 相关问题时,最有效的辅助工具不是 debugger,而是把 MyBatis 的 SQL 日志打开。在配置类里设置:
configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class);这会把每条 SQL、参数、查询结果行数直接打印到控制台。开发阶段我强烈建议开启。你会清楚看到每次执行的 SQL 长什么样、参数装配进 #{...} 后是什么值、最终结果映射成几个对象。一旦怀疑缓存或参数问题,看一眼日志就真相大白。
到生产环境,建议换成 Log4j2 或 Slf4j 实现,按 Logger 级别控制 MyBatis 输出的日志量,避免敏感 SQL 参数全部打进日志文件。比如在 logback 里设置com.example.demo.mapper包为 DEBUG 级别,这样既能看到该包下 SQL,又不会把 Spring 内部的 INFO 日志刷得天昏地暗。
7.2 分页查询的方案与实践
MyBatis 自身没有内置分页插件。最常用的方案是开源分页插件 PageHelper,用法简单到一行代码:
PageHelper.startPage(pageNum, pageSize); List<User> list = userMapper.selectPage(); PageInfo<User> pageInfo = new PageInfo<>(list);原理是拦截器在执行 SQL 前,自动改写原 SQL,包一层 count 查询 + limit 条件,返回总条数和当前页数据。看打印日志时,你会看到它发出两条 SQL:一条SELECT count(0),一条带LIMIT的查询。
不过我得提醒一点:分页插件只对紧跟其后的第一条查询生效。你在 startPage 和查询之间如果执行了其他 SQL 或逻辑,分页就会错位,甚至完全不生效。另外,复杂连表查询、SQL 里有 UNION、GROUP BY 时,count 语句可能会被插件改写得不准确。这类场景我一般不用插件,而是手动写两个方法:一个 count 总数,一个查分页数据,至少心里有数。
7.3 代码分区与命名习惯
整合做完之后,维持代码整洁更重要,不然过两个月自己都看不下去。我从实际项目里总结下来,下面几条习惯值得早期就固定:
- 实体类与数据库表一一对应,放在 entity 包。DTO 和 VO 单独放,别把接口入参和数据库实体混在一起,尤其是字段增减频繁的接口项目。
- Mapper 接口只做单表或简单多表操作,复杂业务逻辑放在 Service 层编排。我在真实项目里见过 Mapper 里写一千行 SQL 的,那不是项目需要,而是把 SQL 当业务逻辑用了。
- XML 的 id 建议和接口方法名保持一致,不要额外起别名(比如方法名
selectByUsername,XML id 却写selectUserByName)。搜索引擎和 IDE 跳转会非常痛苦,全局搜索都救不了你。 - 参数传递用 @Param 显式命名,团队里任何一个人都能一下子看懂
#{username}对应的入参含义,而不是猜 arg0 是谁。
举个例子,一个简单的用户列表分页接口,规范的做法是 controller 接收查询条件对象(Query 对象),service 层调用PageHelper.startPage或手工分页查询,然后组装成页面需要的 VO 返回。Mpper 接口只关注查询本身,不感知分页状态,这样以后替换分页方案也不会牵连业务层。
再聊一个容易被忽略的点:MyBatis 的 XML 路径编写。方法名和 XML id 保持一致真的重要——IDEA 配合 MyBatisX 插件时,接口方法旁边能看到一个绿色跳转箭头,直接跳到对应 XML 标签。没有这种命名一致性,插件的便利也会大打折扣,你连“看 SQL 实现”都得靠手搜。
7.4 为什么建议把动态 SQL 留在 XML 里
很多人刚开始接触 MyBatis 时,觉得注解开发快、代码少,一个@Select就把 SQL 写到接口上了,清爽无比。我当时也这么干过。用着用着就发现,注解方式的动态 SQL 简直是折磨——<script>标签里塞<if>,像把 XML 硬塞到 Java 字符串里,转义、缩进、格式全乱,代码审查时读起来非常费劲。
XML 方式则没有这个问题。哪怕只有一条复杂的动态查询,XML 的层次感也远远好过注解。注解适合固定的单行 SQL,比如@Select("SELECT * FROM user WHERE id = #{id}")这种雷打不动的操作。一旦查询条件有if、foreach、choose,毫不犹豫用 XML。这是我在项目里亲身对比后的结论:注解负责简单查询,XML 负责动态逻辑,两者不冲突,但思想要清晰。
还有一点:SQL 写在哪里,最好由团队约定统一。我在接手过的一个项目里见到过一半注解一半 XML 的混乱场面,一个接口的查询分布在两处,维护成本成倍增加。统一策略之后,团队每个人看代码都省心不少。如果是新建项目,我的建议是一律 XML,没有例外。为什么?因为哪怕现在这个 SQL 很简单,三个月后难保不会加条件、加关联。直接上 XML,后续演进不需要改代码结构,只需要加标签。
8. 整合完成后的一点心得
以上都是配置和代码层面的内容,最后说我个人的一些实际感受。
第一次完整整合 Spring 和 MyBatis 时,会觉得步骤有点多:一个数据源、一个 SqlSessionFactory、一个扫描器、一个事务管理器,各自分工明确。一旦理清“谁负责什么”,整套配置其实很顺——数据源管连接,SqlSessionFactory 管 SQL 解析和执行,扫描器负责把接口变成 Bean,事务管理器负责把多个操作拧成一股绳。配置不难,难的是遇到报错时能猜出是哪一环出了问题。
排查问题的通用路径,我一直遵循一条原则:先看日志,再猜原因。MyBatis 报错信息其实相当精确,无非是绑定失败、映射错误、连接异常这几类。把日志打开,把 SQL 打印出来,问题基本就缩小到很小的范围了。反而是连接池、日志、缓存这些“加分项”,刚开始可以缺,但整合一旦跑通,就要尽早补上,它们决定了你能不能在生产环境安心睡觉。
遇到拿不准的配置时,找一个最小的完整项目从头跑一遍,比在存量项目里瞎试高效得多。把依赖、配置、接口、XML、Service、Controller 从 0 到 1 搭起来,远比读十篇文档印象深刻。你亲手敲过一遍之后,报错在哪个环节、解决方案是什么,心里基本有数了,以后再碰到同类的整合问题都会从容很多。