☰
MyBatis核心原理与实战:从ORM思想到SQL映射,避开常见坑
2026/10/21 10:05:27 网站建设 项目流程

1. 上手之前,先把 ORM 这层窗户纸捅破

聊 MyBatis 之前,必须先聊 ORM。很多新手一上来就着急写 SQL 映射,结果连“为什么要有这东西”都没整明白,遇到问题全靠猜。我当初学 Java 后端时,第一个数据库框架不是 MyBatis,而是先在 JDBC 里摸爬滚打了一圈,回头再看 MyBatis,瞬间通透。

Java 是面向对象语言,但数据库是关系型思维,这两者天然就有差距——类里有对象嵌套,表里只有外键关联;程序里用 List、Map 组织数据,数据库返回的是二维结果集。这种“阻抗不匹配”就是 ORM(Object Relational Mapping,对象关系映射)要解决的问题:把数据库表结构映射成 Java 对象,把对对象的操作翻译成 SQL。

市面上主流方案分两类:全自动 ORM 和半自动 ORM。全自动的代表是 Hibernate,你把对象一改,它自动生成 SQL,开发效率高,但“自动”是把双刃剑——SQL 不可控,性能问题难排查,复杂查询绕圈子。MyBatis 走的是另一条路,它把 JDBC 中那些重复劳动(连接管理、参数绑定、结果集映射、异常处理)全部封装好,但 SQL 语句保留在 XML 或注解里,由开发者亲自掌控。

所以你理解 MyBatis 的定位就一句话:SQL 自己写,映射框架帮你干杂活。它不是一个全自动的 ORM 工具,而是帮你在 Java 对象和数据库记录之间建立“半自动”桥梁的持久层框架——你要给它说清楚“怎么查”,它负责“怎么执行、怎么塞进对象”。

我见过不少人在项目里用 MyBatis,CRUD 写得飞起,但问他“查询返回几万条数据时内存怎么控制”“字段名和属性名不一致时怎么优雅处理”“多表联查怎么避免 N+1”,一问三不知。问题的根源就是只学了配置,没吃透思想。这篇博文不打算给你堆文档,我会按自己实战的路线,从 ORM 思想、核心配置、SQL 映射三大块,把 MyBatis 里面的弯弯绕绕讲明白,还会把踩过的坑一并列出来,帮你省掉那些不值得浪费的时间。

2. 为什么 JDBC 够用,我们还要 MyBatis

2.1 手写 JDBC 的重复劳动到底有多烦

先把时间拨回纯 JDBC 时代。一个最简单的查询用户列表功能,你要写多少行代码?

Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; List<User> users = new ArrayList<>(); try { // 1. 加载驱动(老版本还要 Class.forName) conn = DriverManager.getConnection(url, username, password); // 2. 编写 SQL 并预编译 String sql = "SELECT id, name, email FROM user WHERE status = ?"; ps = conn.prepareStatement(sql); ps.setInt(1, 1); // 3. 执行查询 rs = ps.executeQuery(); // 4. 手动映射结果集——字段取出来,setter 塞进去 while (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setName(rs.getString("name")); user.setEmail(rs.getString("email")); users.add(user); } } catch (SQLException e) { e.printStackTrace(); } finally { // 5. 手动关闭资源 if (rs != null) { try { rs.close(); } catch (SQLException e) { } } if (ps != null) { try { ps.close(); } catch (SQLException e) { } } if (conn != null) { try { conn.close(); } catch (SQLException e) { } } }

这段代码其实不用写完,到第 4 步你已经烦了。每张表都要重复这套流程,每个字段都要手动 getString、setXxx。表结构一旦调整,映射代码跟着改,牵一发动全身。真正让人崩溃的是,90% 的代码都是模板——连接管理有更好的方式(连接池)、资源关闭容易漏(内存泄漏)、异常处理容易吞(排查困难)。

2.2 MyBatis 到底帮你省了什么

我用一个对比表格,把 JDBC 和 MyBatis 干同一件事的差异摊开来说清楚:

环节传统 JDBCMyBatis
连接获取与释放开发者手动管理框架内部处理,自动从连接池获取并归还
SQL 执行硬编码在 Java 代码里写在 XML 映射文件中,改 SQL 不用改 Java
参数绑定ps.setXxx逐一手动设置#{}占位符自动完成,且自带类型转换
结果集映射rs.getXxx逐字段读取resultType/resultMap自动映射成对象
动态 SQL字符串拼接,易错、有注入风险<if>、<where>、<foreach>等标签方式
缓存无内置一级缓存和可扩展的二级缓存
分页手写 LIMIT,不同数据库语法还不同配合分页插件统一处理

你不用把 MyBatis 想得多高深,它本质上就是一个“JDBC 封装器”。核心两条链路:一条是SqlSessionFactory 创建 → SqlSession 获取 → Mapper 接口动态代理 → 执行 SQL,另一条是XML 映射文件解析 → MappedStatement 注册 → 参数处理 → SQL 执行 → 结果集映射。理解这两条链路,后面无论遇到什么诡异问题,你都有了排查的方向。

2.3 选型思考:为什么不是 Spring JDBC Template,不是 Hibernate

每个框架都有自己的生态位。Spring JDBC Template 比原生 JDBC 省事,但结果集映射依然要自己写 RowMapper,复杂对象组装能力几乎为零。Hibernate 让开发者"不写 SQL",但代价是学习曲线陡峭,而且一旦遇到特殊查询,Hibernate 的 HQL 反而绕圈子,市面上大量老系统最终都被迫引入"自定义 SQL"功能来救火。

MyBatis 恰好卡在中间——它不会替你做所有决定,但把你必须做的决定控制在最小范围。你用 MyBatis 写一条 SQL,执行计划和最终的 SQL 语义都是确定的,这对性能调优来说格外重要。数据库是最终瓶颈的系统,SQL 可控性就是生命线。很多大厂的核心交易系统里,MyBatis 的使用率反而高于 Hibernate,原因就在这里:越复杂的系统,越需要 SQL 的确定性。

3. 核心配置逐项拆解,每一项背后都有讲究

3.1 拿到 MyBatis 依赖后,第一件事不是写代码

这步看着我啰嗦,实际坑不少。Maven 项目引入依赖很简单:

<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency>

版本号要留意。3.5.x 系列是当前主流,但不同小版本对 JDK 版本要求不同,比如 3.5.10 之后对 JDK 8 依然兼容,而更早的 3.4.x 对 JDK 17 可能就有兼容问题。如果你用的是 Spring Boot,更推荐 mybatis-spring-boot-starter 这个整合包,版本跟着 Spring Boot 的版本走,省心不少。

数据库驱动别搞漏了。MySQL 8+ 用com.mysql.cj.jdbc.Driver,MySQL 5.x 用com.mysql.jdbc.Driver,这俩在 8.x 驱动版本下都有,但搞混了会直接报 ClassNotFound。连接地址也别忘带参数,比如serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8,时区不配,查询时间字段会有 8 小时的时差,这就是很多新人"我明明存了正确时间,查出来却不对"的经典原因。

3.2 mybatis-config.xml 配置逐个说清楚

MyBatis 的主配置文件有一个严格的标签顺序,这个顺序在 DTD 里写死了。我曾经见过同事把<typeAliases>放在<environments>后面,程序一启动就报错,提示标签顺序不对。所有标签的正确顺序是:

properties、settings、typeAliases、typeHandlers、objectFactory、objectWrapperFactory、reflectorFactory、plugins、environments、databaseIdProvider、mappers

一个实际可用的配置长这样:

<?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> <!-- 读取外部 properties 文件,里面放数据库连接信息 --> <properties resource="db.properties"/> <settings> <!-- 下划线转驼峰,数据库 user_name 自动映射 Java 的 userName --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 开发时打印 SQL,方便调试,生产环境建议关闭 --> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <!-- 类型别名,后面 XML 里写 resultType 就可以用短名 --> <typeAliases> <package name="com.example.demo.entity"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <package name="com.example.demo.mapper"/> </mappers> </configuration>

逐个说重点。

properties标签的价值不只是把数据库配置外置,更关键的是它可以实现"环境隔离":本地开发连本地库、测试环境连测试库、生产环境连生产库,只需要在启动时加载不同的 properties 文件,Java 代码完全不用动。

settings里的mapUnderscoreToCamelCase我建议任何项目都打开。数据库规范一般要求字段名下划线(user_name),Java 规范要求属性名驼峰(userName),这个开关打开后,MyBatis 默认就会自动完成转换。不打开的话,你有两种选择:给查询列起别名SELECT user_name AS userName,或者用resultMap显式映射。第一种写法冗长,第二种写起来繁琐,还不如一行配置解决。

typeAliases是给 XML 文件"减负"用的。没有别名时,你写resultType="com.example.demo.entity.User",有别名后写resultType="User"。注意,如果配置了包扫描,别名默认就是类名的首字母小写形式。你可以在实体类上使用@Alias("xxx")注解来自定义别名,但一般默认就够了。

environments里要注意transactionManager的两种类型:JDBC是让 MyBatis 直接管理事务(commit/rollback 都调 JDBC 接口),适合非 Spring 环境;MANAGED是让容器管理事务,在 Spring 整合环境中通常使用 Spring 的事务管理器,这里配成JDBC问题也不大,因为 Spring 事务会接管连接。真正的关键是dataSource类型:UNPOOLED每次请求都新建连接,性能差;POOLED使用自带连接池,简单项目够用;JNDI在 Web 容器中使用。如果用 Spring Boot,你通常不在这里配数据源,而是由 Spring 的DataSource直接注入。

3.3 Mapper 映射文件的注册方式,决定了你 90% 的报错来源

<mappers>标签的配置方式有四种,踩坑概率从高到低排列:

  1. package包扫描:<package name="com.example.demo.mapper"/>,最推荐。它会自动注册包下所有接口对应的 XML 映射文件,前提是 XML 文件与接口同名且放在同包目录下,或者在 resources 目录中保持相同路径。
  2. resource路径:<mapper resource="mapper/UserMapper.xml"/>,最经典,适合 XML 文件集中在 resources 下的场景。
  3. class接口名:<mapper class="com.example.demo.mapper.UserMapper"/>,必须满足 XML 与接口同包同名,否则 XML 不会被加载。
  4. url绝对路径:基本不用,不推荐。

我用 Maven 项目时的标准做法是:Java 接口放src/main/java/.../mapper/UserMapper.java,XML 映射文件放src/main/resources/mapper/UserMapper.xml。乍看不在同一个目录,但因为 Maven 会把 resources 目录的内容也打进 classpath,最终在 target/classes 下它们还是同包的。所以你也可以在接口上方用@Mapper注解标记,由 MyBatis 自动扫描,但 XML 文件必须落在对应包路径下才稳妥。

这个配置最容易出现的问题就是"接口方法都定义了,一调用就报Invalid bound statement (not found)"。这个错误 99% 的原因是 XML 文件没有被读取,要么是包扫描路径配错,要么是 XML 文件放错了位置。排查思路后面第 5 节我会单独讲。

3.4 SqlSessionFactory 的构建代码别写死在业务里

有了配置文件,Java 端需要一个入口来创建 SqlSession:

String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSessionFactory是一个应用级别应该只创建一次的对象,它内部持有所有 MappedStatement、配置信息、缓存信息,创建代价不小。如果你每次操作都 new 一个,连接池、缓存全部白搭,性能会成倍劣化。正确做法是用单例或交给 Spring 容器管理一个 SqlSessionFactory Bean。

SqlSession则不同,它不是线程安全的,只应该在方法作用域内使用,用完后必须关闭。这就像数据库连接一样——用完不还,连接池迟早枯竭。用 MyBatis 原生的方式写:

try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper userMapper = session.getMapper(UserMapper.class); User user = userMapper.selectById(1L); }

try-with-resources 确保 SqlSession 自动关闭,这是最干净的方式。如果是 Spring 整合环境,SqlSession 的创建和关闭由框架管理,但底层原理一样。

4. SQL 映射文件,从 namespace 到 resultMap 的完整实践

4.1 namespace 不是摆设,它是绑定接口的唯一凭证

每个 XML 映射文件都有一个根元素<mapper>,它的namespace属性必须写 Mapper 接口的全限定名:

<mapper namespace="com.example.demo.mapper.UserMapper">

namespace 的作用有两个。第一,它把 XML 中的每条 SQL 和接口方法绑定起来,MyBatis 启动时会扫描这些映射生成 MappedStatement,接口调用时通过动态代理找到对应的 Statement ID。Statement ID 格式就是namespace + "." + id,例如com.example.demo.mapper.UserMapper.selectById。第二,它把多个 XML 映射文件隔离在各自命名空间中,避免方法 ID 冲突。如果你不写 namespace,或者写的和接口不一致,调用接口方法时就会出现Invalid bound statement或者TooManyResultsException之类的报错。

4.2 select 标签的完整参数说明

以一个最常用的查询为例:

<select id="selectById" resultType="User" parameterType="long"> SELECT id, user_name, email, age, create_time FROM user WHERE id = #{id} </select>

id对应接口方法名,必须一致。parameterType可以省略,MyBatis 能通过接口方法的反射推断出来,但写上也不影响阅读。resultType是映射目标类型,它只配置了类,MyBatis 会自动将列名与属性名按规则映射(配合mapUnderscoreToCamelCase开关效果更佳)。#{id}是预编译占位符,MyBatis 会把它解析成?,然后通过 JDBC 的setXxx方法设置参数,这个过程天然防止了 SQL 注入。

这里要特别强调#{}与${}的天壤之别。#{}是预编译参数占位符,安全;${}是字符串直接拼接,如果参数来自用户输入,会产生 SQL 注入风险。我见过有人图方便,在 ORDER BY 子句里拼${orderBy},结果被注入了一段; DROP TABLE user;——生产环境的数据直接没了。如果必须动态排序,用白名单校验:

public void query(String orderBy, Direction dir) { // 只允许传入 "id"、"create_time" 等固定字段并映射 String safeColumn = allowList.getOrDefault(orderBy, "id"); // 排序方向只允许 ASC/DESC 枚举 }

${}不是完全不能用,但只能在完全受控的场景下用(比如动态列名、表名),且必须经过校验。任何直接拼用户输入的写法,都是往坟头蹦迪。

4.3 参数传递的几种姿势,别再只会 @Param

Mapper 接口方法传参数,常见有三种方式:

第一种,单个参数(非 POJO 类型),直接传:

User selectById(Long id);

XML 里#{任意名字}都能取到值。

第二种,多个参数,必须用@Param命名:

List<User> selectByNameAndAge(@Param("name") String name, @Param("age") Integer age);

不使用@Param时,MyBatis 会默认生成param1、param2这种位置名称,XML 里写#{param1}也能取到,但可读性极差,我强烈不建议这样做。@Param本质上给每个参数起了一个更强的名字,推荐每个多参数方法都加上。

第三种,直接传 POJO 或 Map:

List<User> selectByCondition(UserQuery query);

XML 里可以写#{name}、#{minAge},MyBatis 会自动从对象的 getter 中取值。Map 传参虽然灵活,但可读性很差,团队协作时别人根本不知道这个 Map 里需要哪些 key,所以遇到复杂条件查询,优先定义一个查询对象。

4.4 resultType 与 resultMap 的选择,别总觉得 resultMap 万能

resultType处理简单映射足够,但遇到列名和属性名对不上、嵌套对象、一对多集合这些场景时,就必须上resultMap。一个典型场景:查询用户及其订单列表。

<resultMap id="userWithOrdersMap" type="User"> <id column="id" property="id"/> <result column="user_name" property="userName"/> <result column="email" property="email"/> <!-- 一对多:一个用户对应多笔订单 --> <collection property="orders" ofType="Order"> <id column="order_id" property="id"/> <result column="order_no" property="orderNo"/> <result column="amount" property="amount"/> </collection> </resultMap> <select id="selectUserWithOrders" resultMap="userWithOrdersMap"> SELECT u.id, u.user_name, u.email, o.id AS order_id, o.order_no, o.amount FROM user u LEFT JOIN orders o ON u.id = o.user_id WHERE u.id = #{id} </select>

这里有三个关键细节。

第一,<id>标签不仅是主键标识,更是一个去重标志。MyBatis 拼接嵌套结果时,会对这个 id 做判断:如果用户已经构建过,后续同 id 的记录只追加到对应订单列表中。

第二,collection里的列一定要通过AS取别名,否则两个表都叫id,映射结果必定错乱。我在实际项目中已经见过太多次"找不到 orderId"的报错,最后发现就是列名冲突。

第三,resultMap定义的位置最好在mapper根元素下、SQL 语句之前,同一个 map 可以被多个查询复用。resultType和resultMap只能二选一,不能同时写。如果你定义了resultMap,就不要再用resultType,否则 MyBatis 会直接报错。

4.5 insert、update、delete 与事务边界

写操作的 XML 结构大体相似,但有个高频需求必须掌握:插入一条记录后马上要拿到自增主键。MySQL 中这么做:

<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO user (user_name, email, age) VALUES (#{userName}, #{email}, #{age}) </insert>

useGeneratedKeys="true"表示使用数据库生成的主键,keyProperty指定把生成后的主键值回填到入参对象的哪个字段上。执行完 insert 后,user.getId()直接就是新记录的主键。如果不写这两个属性,你得再查一次或者拼SELECT LAST_INSERT_ID(),纯属绕远路。

还有selectKey这种"老式"写法,目前在 MySQL 自增主键场景已经没必要,但如果你是 Oracle 序列或者其他不支持自增的数据库,可以这样:

<insert id="insertUser"> <selectKey keyProperty="id" order="BEFORE" resultType="long"> SELECT my_sequence.NEXTVAL FROM DUAL </selectKey> INSERT INTO user (id, user_name, email) VALUES (#{id}, #{userName}, #{email}) </insert>

update 和 delete 没什么神秘感,#{id}传值即可。但事务边界这个概念必须强调:MyBatis 原生环境下,sqlSession.commit()才真正提交事务。执行 update/delete 之后如果不 commit,其他连接是看不到数据变化的,而且连接归还到连接池后,下一次复用这个连接时,你可能会看到"脏数据"。Spring 环境下用@Transactional管理事务,但底层原理是一致的。

4.6 动态 SQL,让查询条件不再"写死"

动态 SQL 是 MyBatis 最实用的功能之一。最简单的按条件查询:

<select id="selectByCondition" resultType="User"> SELECT id, user_name, email, age, create_time FROM user <where> <if test="userName != null and userName != ''"> AND user_name LIKE CONCAT('%', #{userName}, '%') </if> <if test="age != null"> AND age = #{age} </if> <if test="startTime != null"> AND create_time &gt;= #{startTime} </if> </where> ORDER BY id DESC </select>

<where>标签有两个隐藏能力:如果里面所有条件都不成立,它不会拼接WHERE关键字;如果第一个条件成立且以AND开头,它会自动把AND去掉。换句话说,你不用再写WHERE 1=1这种老式技巧,<where>自动帮你处理掉了。

<set>标签同理,用于 update 动态语句,会自动去掉末尾逗号:

<update id="updateUser"> UPDATE user <set> <if test="userName != null">user_name = #{userName},</if> <if test="email != null">email = #{email},</if> <if test="age != null">age = #{age},</if> </set> WHERE id = #{id} </update>

批量插入用<foreach>:

<insert id="batchInsert"> INSERT INTO user (user_name, email, age) VALUES <foreach collection="list" item="user" separator=","> (#{user.userName}, #{user.email}, #{user.age}) </foreach> </insert>

collection="list"表示入参是 List 类型,item是每次遍历的引用名,separator是元素之间的分隔符。批量插入时注意 SQL 的长度限制和批量条数,一次别整太多,3000 条已经是不少 MySQL 默认max_allowed_packet的临界值了,稳健一点 500–1000 条一批。

<choose>、<when>、<otherwise>组合可以应付"优先级条件":

<select id="selectByPriority" resultType="User"> SELECT * FROM user <where> <choose> <when test="id != null"> id = #{id} </when> <when test="userName != null"> user_name = #{userName} </when> <otherwise> status = 1 </otherwise> </choose> </where> </select>

动态 SQL 相当于给 SQL 加了一层"逻辑模板"能力,让一条语句适配多种查询场景。它会成为维护重点,所以写的时候一定要保持每个<if>条件逻辑简单,别层层嵌套,否则三个月后你自己都看不懂要查什么数据。

5. 项目中踩过的坑,整理成速查表给你

5.1 经典报错汇总与解决思路

报错信息真实原因解决办法
Invalid bound statement (not found)Mapper 接口与 XML 映射没绑定成功检查 namespace 是否完全匹配接口全限定名;检查 XML 是否在 resource 路径下正确加载;检查 Spring 扫包路径是否覆盖 Mapper 接口
TooManyResultsException查询返回了多条记录但方法返回值是单个对象SQL 查询条件需要能唯一定位一条,或把方法返回类型改为 List
Unknown column 'user_name' in 'field list'数据库字段名写错或大小写问题核对表结构;MySQL 在 Linux 下表名字段名区分大小写
Parameter 'xxx' not found多参数方法没加 @Param给每个参数加上 @Param 注解
org.apache.ibatis.binding.BindingException接口绑定出问题与 Invalid bound statement 排查思路类似,重点看包扫描配置
日期数据差了 8 小时 / 时区不对JDBC URL 没加 serverTimezone 参数URL 加上serverTimezone=Asia/Shanghai

5.2 N+1 查询问题,新手最容易沾沾自喜的坑

我的"查询用户及其订单"例子如果写成"先查用户列表,再循环查每个用户的订单",就是一个典型的 N+1 问题:一条主查询 + N 条子查询,数据量大时数据库连接和响应时间直接爆炸。解决方式有三种:

第一种,联表一次查完,用<collection>拼结果,前面已经写过。适合父子数据量都不大的场景。

第二种,先查主表再查关联表,最后在内存中组装。先查询全部用户SELECT * FROM user,再根据用户 ID 集合查订单SELECT * FROM orders WHERE user_id IN (...),然后在 Java 代码里把订单塞进对应用户。这个方法我用得最多,因为 MySQL 对 IN 的优化出色,而且每次查询返回的数据量可控。

第三种,使用分步查询加懒加载,<collection select="...">的方式,但是这个方案在循环中还是会发起多次查询,实际性能可能更差,一般不推荐在核心列表接口里用。

5.3 分页实现的正确姿势

MyBatis 原生没有内置分页插件,最土的方法是自己拼LIMIT #{offset}, #{pageSize}。但这有两个问题:不同数据库方言不同;每次都要单独计算 count(*) 和 offset。更推荐使用 PageHelper 这类分页插件,它基于 MyBatis 的拦截器机制,自动拦截带有 Page 参数的查询,改写 SQL 并执行 count 查询。

我个人的实践体会是:分页插件虽方便,但遇到复杂 SQL(含 group by、union、嵌套子查询)时,count 结果可能不准确,需要手动 SQL 重写或关闭自动 count。所以生产级项目里,我会把列表分页拆成两条查询:一条查询总数、一条查询数据,用自己写的PageResult<T>对象封装,SQL 完全可控,逻辑反而简单。分页条数做上限校验,超过 100 条直接默认 20,防止有人恶意拉全表。

5.4 一级缓存与二级缓存:用好了是蜜糖,用错了是砒霜

MyBatis 的 SqlSession 级别一级缓存默认开启,同一个 SqlSession 内执行相同 SQL,第二次直接从缓存拿结果。听起来很美好,但有个大坑:如果查询结果变化了,一级缓存不会自动感知(除非显式调用 clearCache 或执行了 update/insert/delete)。在一个长事务里做"查询-更新-再查询",第二次查询拿到的是缓存中的旧数据,就出大问题了。

二级缓存跨 SqlSession,默认关闭,需要你在 XML 中配置<cache/>才开启。我个人的态度是:默认不开二级缓存,分布式环境下尤其别开。因为多个应用实例各自维护缓存,数据一致性无法保证,最好把缓存交给 Redis 这类外部框架。MyBatis 内置缓存适合单机、读多写少、数据一致性要求不高的场景,比如配置字典表。

我自己早年负责过一个报表模块,开了二级缓存后出现统计数字迟迟不更新的诡异问题,查了半天才意识到是缓存作祟。从那以后就定了个原则:凡是数据敏感、实时性要求高的查询,一律不用 MyBatis 缓存,改走外部缓存并显式设置过期时间。

6. Day46 的作业建议与后续路线

到这里,MyBatis 的核心三块内容——ORM 思想、核心配置、SQL 映射——你已经全部过了一遍。但这篇文章只能算把你领进门,真正让技能长在身上的方式只有一个:亲手敲一遍。

你可以按这个顺序做一个小练习:建一张用户表和一张订单表,用原生 JDBC 写一个查询,体会重复劳动;再用 MyBatis 重写一遍,体会封装价值;最后实现一个查询用户 + 订单列表的接口,分别用联表 resultMap 和两条查询两种方式实现,对比差异。整个过程中把常见报错都触发一遍,比如故意配错 namespace、故意漏加 @Param、故意把 resultType 写成不存在的类——踩过一遍的坑,以后才会真正避开。

做完这些,我的建议是下一步把 MyBatis 和 Spring Boot 整合起来练手。整合之后你会发现大部分配置都被自动装配了,但本文讲的核心原理在整合环境下依然成立,无非是 SqlSessionFactory 的创建交给了容器、Mapper 扫描交给了自动配置。如果哪一天排查底层问题,你能回头想到这篇文章讲的这些配置和映射机制,那说明 MyBatis 这根主线,你已经稳稳地攥在手里了。

写这篇文章时我回忆了一下,自己也曾经为一行#{}写成${}付出了不小的代价,也曾经因为resultMap里漏了<id>导致数据少了好几条却浑然不知。MyBatis 这东西,你用好了它能让 CRUD 开发效率翻倍,用不好它也能在深更半夜给你上一课。希望这篇内容能让你少熬几个夜,那就值了。

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

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

立即咨询