☰
MyBatis实战:从环境搭建到增删改查,详解动态SQL与缓存
2026/10/8 3:53:19 网站建设 项目流程

说句实在话,MyBatis 在国内 Java 后端圈子里的普及率高到什么程度呢?你随便打开一个招聘 JD,十有八九都写着“熟悉 SSM/Spring Boot + MyBatis”。它核心就干一件事:把数据库增删改查从繁琐的 JDBC 模板代码里解放出来,让你用接口方法直接操作数据库,同时还能保留 SQL 的可控性。这篇东西就是写给两类人看的:一类是刚接触 MyBatis、想从零跑通第一个增删改查的初学者,另一类是用了挺久但一直没搞懂 #{} 和 ${}、resultType 和 resultMap、一级缓存和二级缓存这些细节的同学。我会从一个真实的项目视角出发,把环境搭建、增删改查实现、常见坑位全部过一遍,不说废话,全部可落地。

1. 为什么大家都在用 MyBatis:先搞清楚它是干什么的

1.1 JDBC 的痛点,MyBatis 怎么解决

在讲 MyBatis 之前,很值得先回忆一下原生的 JDBC 操作数据库到底有多烦。基本流程是这样的:加载驱动、建立 Connection、创建 Statement 或 PreparedStatement、手动拼接 SQL、执行、遍历 ResultSet 取数据、最后还得记得关闭连接。这套流程本身不复杂,但问题非常明显。

第一是重复代码太多。一个最简单的按 id 查询用户的方法,从加载驱动到释放资源,大概要写二十多行,而且每次写都大同小异。第二是 SQL 和 Java 代码强耦合。SQL 写成字符串拼在 Java 里,改一个字段就要改代码重新编译,维护成本很高。第三是结果集映射全靠手动。从 ResultSet 里一个一个 getString、getInt 取出来再 set 到对象里,字段一多就是纯体力活,还特别容易漏字段。第四是连接管理容易出问题。忘关连接、异常时连接泄漏,在并发高的时候是灾难。

MyBatis 的解决思路其实很朴素,把 SQL 独立到 XML 或注解里,把 Java 方法的参数通过占位符绑定到 SQL 上,再把查询结果自动映射成实体对象。你只需要关注 SQL 本身和业务逻辑,剩下的连接管理、参数设置、结果集处理由框架代劳。打个比方,JDBC 像自己买菜切菜炒菜洗碗,MyBatis 像请了个靠谱的后厨,你只需要把菜单写好、点好菜,等着上菜就行。这个比喻很贴切,也解释了为什么它能在国内流行这么多年——它没有把 SQL 完全藏起来,反而给了你足够的控制权。

1.2 核心组件和一次完整请求的流转

要真正用好 MyBatis,脑子里得有这张地图。一次典型的增删改查请求,大致会经过这些组件:

  • SqlSessionFactoryBuilder:读取配置文件,构建 SqlSessionFactory,通常一个应用只执行一次。
  • SqlSessionFactory:全局单例,负责创建 SqlSession。
  • SqlSession:代表一次数据库会话,可以理解成 JDBC 里的 Connection 的增强版,还封装了执行 SQL、事务控制、一级缓存的能力。
  • Executor:MyBatis 内部真正执行 SQL 的组件,分为 SimpleExecutor、ReuseExecutor、BatchExecutor 三种。
  • MappedStatement:封装了一条 SQL 的所有元数据,包括 id、参数类型、返回值类型、SQL 语句本身。
  • Mapper 接口:你写的 Java 接口,MyBatis 通过动态代理生成实现类,接口方法调用最终会解析到对应的 MappedStatement。

整个流程走下来,其实就是三个字:绑、执、映。绑,就是把 Mapper 接口方法绑定到 MappedStatement;执,就是通过 Executor 执行 SQL;映,就是把 ResultSet 结果映射成对象。理解这三点,再去看什么 MyBatis 面试题都不怵,因为所有机制都是围绕它展开的。

1.3 什么项目适合 MyBatis,什么不适合

MyBatis 并不是银弹。它最擅长的是中小型项目、SQL 复杂、需要精细化调优的场景,尤其是那种查询条件多、报表多、需要手写复杂 SQL 的业务。因为它把 SQL 暴露出来,DBA 和开发可以针对慢查询做精准优化。

反过来,如果项目以简单的 CRUD 为主,实体关系复杂、需要大量继承和多态映射,那 JPA/Hibernate 这类全自动 ORM 框架会更省力。MyBatis 的灵活是优点也是代价,它需要你自己维护 SQL,如果项目纯 CRUD 且实体特别多,写 XML 的工作量会很大。我的经验是:MyBatis 适合你明确知道自己要什么 SQL 的项目,JPA 适合希望框架替你生成大部分 SQL 的项目。选型没有绝对的对错,看你团队的 SQL 功底和业务特征。

2. 上手前的准备工作:环境搭建与第一个配置文件

2.1 依赖引入和数据库准备

我以 Maven 项目为例,演示一遍最标准的搭建过程。项目结构先准备好:

src/main/java src/main/resources

在 pom.xml 里引入依赖。我用的是 MySQL 数据库,所以需要 MySQL 驱动;MyBatis 用 3.5.16 版本(实际使用时建议去官网或 Maven 仓库查最新稳定版);测试方便的话再加个 JUnit。

<dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies>

注意 MySQL 8.0 的驱动类名和连接串跟 5.x 版本不一样了,驱动类是com.mysql.cj.jdbc.Driver,连接串里建议加上useSSL=false&serverTimezone=Asia/Shanghai,不然很容易因为时区问题报错。我自己刚换 MySQL 8 的时候就被这个坑过一次,报错信息是The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,看着头大,其实就是连接串少了时区参数。

数据库这边,建一张最简单的用户表:

CREATE DATABASE IF NOT EXISTS mybatis_demo DEFAULT CHARACTER SET utf8mb4; USE mybatis_demo; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(100), created_time DATETIME );

表结构故意做得很简单,方便聚焦增删改查本身。id自增主键,username、password必填,email可空,created_time是时间字段。这些都跟你后面的实体类一一对应。

2.2 mybatis-config.xml 核心配置的每项含义

在src/main/resources下建mybatis-config.xml,这是 MyBatis 的全局配置文件。先把完整内容贴出来,再逐行解释:

<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <typeAliases> <package name="com.example.demo.entity"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <package name="com.example.demo.mapper"/> </mappers> </configuration>

几个关键点说一下。

mapUnderscoreToCamelCase:下划线转驼峰。数据库字段created_time,Java 属性createdTime,开启这个设置后就不用写繁琐的 resultMap 了,对增删改查体验的提升非常明显。这个配置我几乎是必开的,不然每个实体类都得配 resultMap,工作量翻倍。

logImpl:打印 SQL 日志。STDOUT_LOGGING意思是直接用控制台输出,适合入门阶段调试。实际项目中一般会接 Logback 或 Log4j2,但入门阶段这个够用了。

typeAliases:给实体类起别名。配了package后,你在 Mapper XML 里写resultType="User",MyBatis 会自动去找com.example.demo.entity.User,不用写全限定类名。这里有个小坑:如果实体类分布在不同包,就配多个<package>,或者干脆用全限定类名,避免同名类冲突。

environments:环境配置。transactionManager用 JDBC,表示事务由 MyBatis 自己管理,后面交给 Spring 的时候会改成 Spring 的事务管理器。dataSource type="POOLED"是 MyBatis 自带的连接池实现,入门阶段够用,生产环境一般会换 Druid 或 HikariCP。

2.3 构建 SqlSessionFactory:写一个工具类

在src/main/java/com/example/demo/util下写一个 MyBatisUtil,用来加载配置并获取 SqlSession。这里有个设计要点:SqlSessionFactory 是重量级对象,每个应用只应该创建一次,所以用静态代码块初始化。

package com.example.demo.util; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class MyBatisUtil { private static SqlSessionFactory sqlSessionFactory; static { try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } public static SqlSession getSqlSession() { return sqlSessionFactory.openSession(); } }

为什么强调单例?因为 SqlSessionFactory 内部包含配置信息、MappedStatement 等大量元数据,频繁创建代价很高,而且没有必要。SqlSession 则不同,它是轻量的、线程不安全的,每次操作都应该新建,用完了就 close。这跟 JDBC 的 Connection 用法逻辑一致,但很多人容易混淆。

写个测试跑一下,验证配置没问题:

package com.example.demo; import com.example.demo.util.MyBatisUtil; import org.apache.ibatis.session.SqlSession; import org.junit.Test; public class MyBatisTest { @Test public void testConnection() { try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) { System.out.println("拿到 SqlSession,连接成功"); } } }

这里用了 try-with-resources,SqlSession 实现了 Closeable,可以自动关闭。如果控制台没有任何异常,说明环境基本通了。

3. 增删改查完整实操:从 Mapper 接口到 SQL 语句

3.1 实体类和 Mapper 接口定义

实体类放在com.example.demo.entity包下,字段跟表结构对应。我用的是驼峰命名,配合mapUnderscoreToCamelCase就能自动映射。

package com.example.demo.entity; import java.time.LocalDateTime; public class User { private Integer id; private String username; private String password; private String email; private LocalDateTime createdTime; // getter / setter 略,实际项目用 Lombok @Data }

然后定义 Mapper 接口,放在com.example.demo.mapper包下:

package com.example.demo.mapper; import com.example.demo.entity.User; import java.util.List; public interface UserMapper { int insert(User user); int deleteById(Integer id); int updateById(User user); User selectById(Integer id); List<User> selectList(); }

这里要强调一下:Mapper 接口本身没有任何实现类,MyBatis 会通过 JDK 动态代理在运行时生成一个实现。你只需要保证三点:接口的全限定名和 XML 的 namespace 一致;接口方法名和 XML 的 id 一致;方法返回值类型和 XML 的 resultType 兼容。这三点任何一个对不上,都会报错,后面我会专门讲排错。

3.2 insert 语句:主键回填怎么做

插入语句的核心难点是主键回填。表结构里id是自增主键,但很多时候业务上需要在插入之后立刻拿到这个新 id,比如新增用户之后返回给前端,或者往下走关联逻辑。MyBatis 提供了useGeneratedKeys和keyProperty两个属性来解决。

<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_user (username, password, email, created_time) VALUES (#{username}, #{password}, #{email}, #{createdTime}) </insert>

执行完 insert 之后,传入的 User 对象的 id 字段会被自动回填。测试代码:

User user = new User(); user.setUsername("zhangsan"); user.setPassword("123456"); user.setEmail("zhangsan@example.com"); user.setCreatedTime(LocalDateTime.now()); userMapper.insert(user); System.out.println("插入成功,主键 id = " + user.getId());

你一定会好奇这里useGeneratedKeys和keyProperty的作用机制。useGeneratedKeys=true告诉 MyBatis,数据库会生成主键,需要把生成的主键拿回来;keyProperty指定回填到传入对象的哪个属性。底层实际上是通过 JDBC 的getGeneratedKeys()方法获取自增主键。这个方法在 MySQL 的 AUTO_INCREMENT 上没问题,PostgreSQL 的 SERIAL 也可以,但像 Oracle 这种用序列的方式就不好使,需要改用<selectKey>标签。

3.3 delete 和 update:动态 SQL 的雏形

删除比较简单,按主键删:

<delete id="deleteById" parameterType="integer"> DELETE FROM t_user WHERE id = #{id} </delete>

注意parameterType="integer"是 MyBatis 的别名机制,大小写不敏感,int、integer、java.lang.Integer都能识别。实际开发中,delete 操作还要考虑逻辑删除还是物理删除的问题,这里先不展开,先把物理删除跑通。

更新操作要多讲一点。真正业务里,前端传过来的对象往往只有部分字段有值,其他字段是 null。如果直接写死所有字段:

<update id="updateById" parameterType="User"> UPDATE t_user SET username = #{username}, password = #{password}, email = #{email}, created_time = #{createdTime} WHERE id = #{id} </update>

那只要传入对象的某个属性是 null,数据库里这一列就被覆盖成 NULL 了。我在实际项目里见过太多次这种事故,特别是那种“前端只改了邮箱,结果用户名被清空了”的情况。更稳妥的做法是用动态 SQL,只更新非空字段:

<update id="updateById" parameterType="User"> UPDATE t_user <set> <if test="username != null">username = #{username},</if> <if test="password != null">password = #{password},</if> <if test="email != null">email = #{email},</if> <if test="createdTime != null">created_time = #{createdTime},</if> </set> WHERE id = #{id} </update>

<set>标签会自动处理多余的逗号,这是 MyBatis 动态 SQL 里比较贴心的设计。如果所有字段都是 null,SQL 会变成UPDATE t_user WHERE id = ?,这在 MySQL 里会报语法错误,所以实际使用中建议在代码层做校验,至少保证有一个字段在更新。

3.4 select 查询:参数传递的多种姿势

查询是增删改查里花样最多的部分。先看最简单的按 id 查询:

<select id="selectById" parameterType="integer" resultType="User"> SELECT id, username, password, email, created_time FROM t_user WHERE id = #{id} </select>

因为开启了mapUnderscoreToCamelCase,created_time能自动映射到createdTime。再看查询列表:

<select id="selectList" resultType="User"> SELECT id, username, password, email, created_time FROM t_user </select>

这里有几个点要讲清楚。

第一,当 Mapper 接口方法只有一个参数时,XML 里的#{id}、#{xxx}写什么都行,MyBatis 都能拿到值,但为了可读性建议保持跟方法参数名一致。当参数多于一个时,情况就不一样了,比如:

List<User> selectByCondition(String username, String email);

XML 里直接写#{username}会报错,因为 MyBatis 不知道这个username对应哪个参数。解决办法有三种:用@Param注解、用param1/param2、或者封装成对象。

用@Param是最推荐的方式:

List<User> selectByCondition(@Param("username") String username, @Param("email") String email);

这样在 XML 里写#{username}、#{email}就能准确绑定。如果你不想用注解,MyBatis 也支持#{param1}、#{param2},但可读性很差,多人协作时容易乱。最优雅的方式其实是封装成一个查询对象,比如UserQuery,把条件都放进去,XML 里通过#{username}直接取属性,这样既清晰又方便扩展分页参数。

第二,模糊查询的 LIKE 怎么传参也有讲究。很多人会写成WHERE username LIKE '%${username}%',这确实能跑通,但有 SQL 注入风险。正确做法是用#{}配合拼接:

<select id="selectByKeyword" resultType="User"> SELECT id, username, password, email, created_time FROM t_user WHERE username LIKE CONCAT('%', #{keyword}, '%') </select>

或者传入参数时就在 Java 侧拼好%keyword%,XML 里WHERE username LIKE #{keyword}。用CONCAT的好处是参数始终保持预编译,安全且没有兼容性问题。

第三,多表联查时如果几个表有同名字段,自动映射就可能出错。这时候要么在 SQL 里给列起别名,要么使用显式的 resultMap。这属于进阶范畴,放后面细说。

4. 参数绑定与结果映射的细节:别在阴沟里翻船

4.1 #{} 和 ${} 的本质区别

这是 MyBatis 面试里必考、实际开发里必踩的坑。#{}是预编译占位符,MyBatis 会把 SQL 里的#{}替换成?,然后通过 PreparedStatement 的 setXXX 方法安全地设置参数。参数值不参与 SQL 语句的拼装,所以能有效防止 SQL 注入。

${}是字符串拼接,MyBatis 会直接把参数值原样拼到 SQL 语句中。比如ORDER BY ${sortColumn},传入username,最终 SQL 变成ORDER BY username。如果传入的是username; DROP TABLE t_user;--,后果不堪设想。这里不是危言耸听,很多注入案例都是因为开发图省事用了${}。

正因为如此,能用#{}的地方绝对不用${}。但有两个场景必须用${}:一是动态排序字段,二是动态表名(比如按月分表)。这两个场景本质上都是 SQL 片段而不是值,占位符没意义。用的时候一定记住:字段名、表名这类东西只能内部传参,绝不能直接接收前端传来的原始值,必须做白名单校验。我自己的做法是写一个排序字段映射,把允许排序的字段名放在一个 Map 里,传入的字符串必须命中 Map 的 key 才拼接,否则用默认值。

4.2 resultType 和 resultMap 怎么选

简单来说,resultType 是自动映射,resultMap 是手动映射。resultType 适合数据库字段和 Java 属性一一对应、或者靠mapUnderscoreToCamelCase能转过来的情况。它的底层其实也是在构建一个隐式的 resultMap,只是省了你写 XML 的功夫。

resultMap 适合字段对不上、有多表联查、有复杂嵌套关系的场景。比如:

<resultMap id="userResultMap" type="User"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="email" column="email"/> </resultMap>

如果遇到列名不一致,还可以用column起别名的方式兜底:

<select id="selectById" resultType="User"> SELECT id, username, password, email, created_time AS createdTime FROM t_user WHERE id = #{id} </select>

我的建议是:能用 resultType 就别用 resultMap,少写一堆 XML;但一旦涉及联查、聚合、一对多嵌套,该上 resultMap 就上,千万别硬扛。resultMap 最强大的地方在于association和collection,比如一个订单关联多个订单明细,可以用嵌套结果映射一次查出来,避免 N+1 查询问题。不过这个复杂度较高,入门阶段先把基础映射搞明白。

4.3 打印 SQL 的配置方法

排查增删改查问题的时候,最需要的就是 SQL 日志。入门阶段用STDOUT_LOGGING就够了,在mybatis-config.xml的 settings 里配置:

<settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>

配置好之后,执行每个 Mapper 方法都会在控制台打印类似这样的内容:

==> Preparing: SELECT id, username, password, email, created_time FROM t_user WHERE id = ? ==> Parameters: 1(Integer) <== Columns: id, username, password, email, created_time <== Row: 1, zhangsan, 123456, zhangsan@example.com, 2025-01-01 10:00:00 <== Total: 1

这里有个非常实用的排查技巧:如果Preparing后面的 SQL 跟你预期的不一样,说明是 XML 里的 SQL 写错了;如果 SQL 对但查不到数据,那就是参数问题或者数据本身不存在;如果查询结果字段是 null,那就是结果映射的问题。三步定位,能省下大量调试时间。

在 Spring Boot 项目里,一般不用 STDOUT_LOGGING,而是直接配置日志级别:

logging.level.com.example.demo.mapper=debug

第二种方式的好处是只打印 Mapper 包下面的 SQL,日志量可控,也不会打印结果集细节。

4.4 动态 SQL 的核心能力:if、where、foreach

动态 SQL 是 MyBatis 的杀手锏,也是面试必问的。初学者容易把它当成“拼接字符串的模板”,但其实它解决的是很实际的场景:查询条件不确定、更新字段不确定、批量操作参数数量不确定。

if标签配合where标签做条件查询是最常见的组合:

<select id="selectByCondition" resultType="User"> SELECT id, username, password, email, created_time FROM t_user <where> <if test="username != null and username != ''"> AND username = #{username} </if> <if test="email != null and email != ''"> AND email = #{email} </if> </where> </select>

<where>标签会自动处理首个条件前面的 AND,非常方便。如果你自己拼 SQL,很容易出现WHERE AND ...这种语法错误,很多新手在这里卡过。

foreach标签处理 IN 查询也很常用:

<select id="selectByIds" resultType="User"> SELECT id, username, password, email, created_time FROM t_user WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

这里collection应该填方法参数名,如果方法签名是List<Integer> selectByIds(@Param("ids") List<Integer> ids),那 collection 就是ids。foreach 还有index属性可以拿到循环下标,在批量 update 时会用到。

动态 SQL 的原理其实不复杂,本质上就是 OGNL 表达式结合 XML 标签做 SQL 拼接。但用好了能极大减少方法数量,比如统一的用户查询接口,只写一个 select 就能覆盖精确查询、模糊查询、组合查询各种场景。

5. 常见问题与排查技巧实录

5.1 经典报错:BindingException、ParameterType

我把自己和身边同事踩过的坑整理一下。

第一个是org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.demo.mapper.UserMapper.insert。这个报错 90% 的原因是接口方法和 XML 里的 id 对不上,或者 namespace 没写对。排查步骤非常固定:先检查 XML 文件名是不是UserMapper.xml,再检查 namespace 是不是接口的全限定名,再检查 XML 里的 id 是不是接口方法名。还有一种可能,Maven 构建时把 XML 文件漏掉了。解决办法是在 pom.xml 里配置 resources,把src/main/java下的 xml 也打包进去,或者把 XML 统一放在src/main/resources下。

第二个是There is no getter for property named 'id' in class 'java.lang.Integer'。出现这个报错,通常是因为方法参数上有@Param("id"),但 XML 里写成了#{userId},两边对不上。多参数绑定时,@Param的名字就是 XML 里的唯一依据,务必保持一致。

第三个是我见过很多新手问题:parameterType写错。比如方法入参是User对象,XML 里写parameterType="integer",运行时不一定会立刻报错,但参数绑定就会出问题。入门阶段建议先严格按照参数类型写,不要图省事。

还有一个容易忽略的:resultType 写成实体类本身,没有带包名,而全局配置里又没有 typeAliases 扫描。这时候会报TypeAliasRegistry相关的异常,告诉你找不到对应的类。配置 typeAliases 或者写全限定类名都能解决。

5.2 缓存相关的坑(一级缓存、二级缓存)

热词里有 mybatis 缓存和二级缓存实现,这里专门说两句。

一级缓存是 SqlSession 级别的,默认开启,无法关闭。同一个 SqlSession 里执行同一条 SQL 且参数相同,第二次会直接命中缓存,不再查数据库。这个机制本身没毛病,但有个经典坑:在一个 SqlSession 里先查询用户,然后执行了插入或更新操作,MyBatis 会清空一级缓存。如果你在同一个 SqlSession 里先查、再改、再查,第二次查询会重新走数据库,这其实是框架在帮你避免脏数据。

二级缓存是 namespace 级别的,默认不开启。开启方式是在 Mapper XML 里加<cache/>标签,之后这个 Mapper 下的查询结果会被缓存到二级缓存区域。二级缓存的坑更隐蔽:如果一个表被多个 Mapper 操作,而每个 Mapper 各自开了二级缓存,当一个 Mapper 更新数据后,其他 Mapper 的缓存不会自动失效,很容易查出脏数据。

我的观点很明确:单体小项目、读多写少、并发不高的场景,可以谨慎开二级缓存;只要表可能被多个 Mapper 操作,就别开。真需要缓存,直接用 Redis,别让 MyBatis 干这活。很多人面试喜欢背二级缓存原理,但实际项目里它往往是麻烦的制造者。

5.3 与 Spring Boot 整合时的几个注意点

现在真实项目里,很少会有人像上面那样手写SqlSessionFactoryBuilder,基本都是 Spring Boot + MyBatis 的组合。整合时的注意点也说说。

首先,依赖上要引入mybatis-spring-boot-starter,而不是直接在 Spring Boot 里引官方 mybatis,否则要手动配置很多东西。引入 starter 之后,在application.properties里配置数据源和 Mapper 扫描:

spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis.mapper-locations=classpath:mapper/*.xml mybatis.type-aliases-package=com.example.demo.entity # 打印 SQL mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

然后在启动类上加@MapperScan("com.example.demo.mapper"),或者在每个 Mapper 接口上直接加@Mapper注解。前者适合接口数量多的项目,后者适合接口少且想避免扫描粒度问题的场景。我一般倾向于启动类上加@MapperScan,因为接口多了以后逐个加注解太麻烦。

Spring Boot 整合时还有个连接池选择问题。HikariCP 默认就配好了,性能业内顶尖,不用额外配置。Druid 的优势是监控能力,可以看 SQL 执行统计、慢查询、活跃连接数。我的建议是:没特殊需求就用 HikariCP,如果确实需要监控 SQL 和连接池状态,再考虑换 Druid。工具选择要服务于需求,不要为了赶潮流而替换。

5.4 Maven 打包漏掉 XML 的坑

这个坑我遇到太多次了,必须要单独拿出来说。Maven 项目如果 XML 文件放在src/main/java目录下跟 Java 源码放一起,默认打包时是不会被复制到 classes 目录的。结果就是本地 IDE 里跑得好好的,一打成 jar 包部署到服务器,就报Invalid bound statement,排查半天发现 classes 目录里根本没有 Mapper XML。

解决办法有两种。最简单的是把 XML 统一放到src/main/resources/mapper/目录下,然后在配置里指定:

<mappers> <package name="com.example.demo.mapper"/> </mappers>

或者 Spring Boot 配置里:

mybatis.mapper-locations=classpath:mapper/*.xml

第二种方法是在 pom.xml 里显式声明:

<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>

我个人强烈推荐第一种,把 XML 和 Java 分开,目录结构干净,也能避免很多无谓的问题。很多人为了省事喜欢把 XML 放在接口同级目录下,但部署阶段踩坑的概率太高了。

最后再分享一个我个人的习惯。每次新项目引入 MyBatis,我都会先把增删改查这四个基础方法写在同一个 Mapper 里完整跑通一遍,刻意看一眼控制台打印的 SQL 有没有意外,比如该参数化的地方被拼接了、该走索引的字段没生效、日期字段映射对不对。这个流程跑通之后,后面再加多表查询、动态 SQL、复杂结果映射,心里都有底了。MyBatis 这个框架,入门门槛确实不高,难点全在细节上,比如主键回填、多参数绑定、缓存失效、XML 打包路径这些位置。你踩过一次坑,把原因记下来,后面就顺了。如果你看完这篇内容,能把第一批坑提前绕过去,那这篇文章的价值就体现出来了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询