一、执行流程
二、分页
在 MyBatis 中实现分页主要有 4 种 常见方式。根据实现层级的不同,它们可以分为:逻辑分页 (内存分页)和 物理分页(数据库分页)。
以下是具体实现方式及其优缺点的详细对比:
1. 物理分页:使用分页插件(如 PageHelper)
这是目前 生产环境中最推荐、最常用 的方式。它通过 MyBatis 提供的拦截器接口(Interceptor),在动态生成 SQL 时,自动拦截并根据当前数据库类型(MySQL, Oracle, PostgreSQL 等)重写 SQL,拼接上对应的分页关键字(如 LIMIT 或 ROWNUM)。
-
实现方式 :
javaPageHelper.startPage(1, 10); // 核心代码:原理是基于 ThreadLocal 传递分页参数 List<User> list = userMapper.selectAll(); PageInfo<User> pageInfo = new PageInfo<>(list); // 包装后可获取总页数、总条数等 -
优点 :
- 代码侵入性极低 :不需要在 XML 中手动写
LIMIT,也不用在 Mapper 接口中传入分页参数。 - 多数据库支持:插件会自动识别数据库类型,自动切换分页方言,便于数据库迁移。
- 功能完善 :能自动帮你执行一条
COUNT(*)语句获取总条数。
- 代码侵入性极低 :不需要在 XML 中手动写
-
缺点 :
- 异步或多线程限制 :由于基于
ThreadLocal传递参数,如果在startPage之后开启了新线程执行 SQL,分页会失效。 - 浅层安全隐患 :如果调用了
startPage但后面因为条件判断没有执行 SQL,会导致ThreadLocal中的数据残留,污染下一次查询。
- 异步或多线程限制 :由于基于
2. 物理分页:原生 SQL 传参
在 XML 的 SQL 语句中,手动拼接数据库特定的分页关键字(如 MySQL 的 LIMIT #{offset}, #{limit})。
-
实现方式 :
XML<select id="selectByPage" resultType="User"> SELECT * FROM t_user LIMIT #{offset}, #{limit} </select> -
优点 :
- 执行效率最高:没有任何框架和插件的封装损耗,最直接。
- 绝对可控:SQL 怎么写就怎么执行,适合对性能要求极高的复杂全表关联分页。
-
缺点 :
- 不具备通用性:换了数据库(比如从 MySQL 换到 Oracle)后,必须重写 XML 中的分页 SQL。
- 代码冗余 :每个需要分页的业务都要手动写
LIMIT,且如果需要总条数,还必须单独再写一个<select id="count">的 SQL。
3. 逻辑分页:使用 RowBounds
MyBatis 内置的对象,不需要修改 SQL,通过传入一个 RowBounds 参数提示 MyBatis 进行分页。
-
实现方式 :
javaint offset = 0; int limit = 10; RowBounds rowBounds = new RowBounds(offset, limit); List<User> list = sqlSession.selectList("selectAll", null, rowBounds); -
优点 :
- 使用简单:不需要引入第三方依赖,不需要改动任何 XML 语句。
-
缺点 :
- 内存浪费,性能极差 :它的底层原理是 把数据库中满足条件的所有数据一次性全部查询到内存中 ,然后在内存中通过游标跳过前面的数据,截取需要的部分。如果数据量有百万级,会直接导致 OOM(内存溢出) 。因此,严禁在生产大数据量场景下使用。
4. 物理分页:MyBatis-Plus 分页插件
如果你使用的不是原生 MyBatis,而是其增强版 MyBatis-Plus,它内置了专用的分页拦截器。
-
实现方式 :
javaIPage<User> page = new Page<>(1, 10); IPage<User> userPage = userMapper.selectPage(page, queryWrapper); -
优点 :
- 完全面向对象:不需要手写任何 SQL,配合条件构造器使用非常流畅。
- 防止内存溢出:底层同样是拦截器改写 SQL 为物理分页。
-
缺点 :
- 框架绑定:必须依赖 MyBatis-Plus 生态,如果是纯原生 MyBatis 项目无法直接使用。
总结与选型指南
| 分页方式 | 类型 | 适用场景 | 推荐指数 |
|---|---|---|---|
| PageHelper 插件 | 物理分页 | 绝大多数传统 MyBatis 项目,支持多数据库 | ⭐⭐⭐⭐⭐ (最推荐) |
| MyBatis-Plus 分页 | 物理分页 | 使用了 MP 增强框架的项目 | ⭐⭐⭐⭐⭐ (MP项目首选) |
| 原生 SQL 传参 | 物理分页 | 单一数据库且对 SQL 性能有极致压榨要求的场景 | ⭐⭐⭐⭐ (适合核心高并发接口) |
| RowBounds | 逻辑分页 | 仅适用于测试环境,或数据量固定极小(如几十条)的离线统计 | ⭐ (生产环境禁用) |
三、$ 和 # 的核心区别
在 MyBatis 的 XML 映射文件中,$ 和 # 是两种完全不同的参数占位符,其底层原理和安全性有本质区别:
1. 编译与处理机制(根本区别)
- # (预编译占位符) :
MyBatis 会将 # 替换为 JDBC 的问号 ? 占位符。它使用 PreparedStatement 进行预编译 ,在执行时才把实际的参数值安全地塞进去。- 示例:SELECT * FROM user WHERE name = #{name}
- 底层转换:SELECT * FROM user WHERE name = ?(参数值会自带引号,如 '张三')
- $ (字符串拼接符) :
MyBatis 会在解析 XML 时,直接把参数以纯字符串 的形式直接拼接、替换到 SQL 语句中,不进行任何加工。- 示例:SELECT * FROM user WHERE name = '${name}'
- 底层转换:SELECT * FROM user WHERE name = '张三'
2. 安全性( SQL 注入风险)
- # 能绝对防御 SQL 注入。因为它是预编译的,数据库会把传入的值仅仅当作普通的"文本数据"来处理,绝对不会将其作为 SQL 命令执行。
- $ 存在严重的 SQL 注入风险 。如果传入的变量用户可控,攻击者可以构造恶意参数更改原本的 SQL 逻辑。
- 漏洞示例:若输入参数为 1 OR 1=1,使用 WHERE id = ${id} 拼接后变成 WHERE id = 1 OR 1=1,导致整张表的数据直接泄露。
3. 性能表现
- # 性能更好。由于采用预编译,数据库可以缓存该 SQL 的执行计划。下次遇到相同的 SQL 结构(仅参数不同)时,无需重新解析,大大提升高并发下的执行效率。
- $ 性能较低。每次参数改变,SQL 的字符串结构就会改变,数据库必须重新编译和生成执行计划。
四、什么时候必须用 $ ?
虽然 # 既安全又高效,但在一些动态改变 SQL 结构 的场景下,# 会因为自动加单引号或语法限制而失效,此时必须使用 $:
- 动态表名 / 列名 :当表名或字段名需要从前端动态传入时。
- 错误:SELECT * FROM #{tableName} -> 报错(解析成 SELECT * FROM 'user',语法错误)。
- 正确:SELECT * FROM ${tableName}
- 动态排序( ORDER BY ) :当排序字段和升降序需要动态指定时。
- 错误:ORDER BY #{sortField} #{sortOrder} -> 报错(解析成 ORDER BY 'create_time' 'DESC')。
- 正确:ORDER BY {sortField} {sortOrder}
⚠️ 安全避坑指南 :一旦在项目中使用 ,**必须在后端代码中做严格的白名单校验**。例如限制 sortField 只能是 id 或 create_time,限制 sortOrder 只能是 ASC 或 DESC,绝对不能把前端传来的原始字符串直接丢进 中。
五、MyBatis一级缓存和二级缓存

MyBatis 缓存机制的核心目的是减少数据库查询次数以提高系统性能,它包含了一级缓存(Local Cache)和二级缓存(Global Cache)两层结构 。
1、 一级缓存与二级缓存核心对比
| 特性 | 一级缓存(本地缓存) | 二级缓存(全局缓存) |
|---|---|---|
| 作用域 | SqlSession 级别 | Mapper / Namespace 级别 |
| 默认状态 | 默认开启 | 默认关闭 |
| 生命周期 | 随 SqlSession 的创建而创建,销毁而结束 | 随应用生命周期存在,可跨 SqlSession |
| 存储介质 | 本地内存(HashMap) | 本地内存,或第三方缓存(Redis / Ehcache) |
| 脏数据风险 | 分布式或多会话环境下极易产生脏数据 | 多表联查时可能产生脏数据 |
2、 一级缓存(SqlSession 级别)
- 工作原理
- 一级缓存由
BaseExecutor中的localCache维护,其底层是一个简单的 HashMap。 - 当同一个
SqlSession执行两次相同的 SQL 查询时,第一次会访问数据库并写入缓存;第二次则直接从缓存中获取数据,不再触发数据库查询。
- 失效场景
- 执行了增删改(CUD)操作 :同一个
SqlSession内如果执行了insert、update或delete并提交,缓存会被自动清空,防止读到过期数据。 - 手动清空缓存 :调用了
sqlSession.clearCache()方法。 - 跨 SqlSession 访问 :不同的
SqlSession之间缓存完全隔离。 - 配置了 flushCache=true :在对应的
<select>标签中显式配置了清空缓存。
3、 二级缓存(Mapper 级别)
- 工作原理
- 二级缓存的作用域是 Namespace(即同一个 Mapper 文件) ,多个不同的
SqlSession可以共用同一个二级缓存。 - 它的查询流程优先于一级缓存:二级缓存 ➡️ 一级缓存 ➡️ 数据库。
- 重要机制 :当
SqlSession执行close()或commit()之后,一级缓存中的数据才会真正刷新到二级缓存中。
-
开启步骤
-
全局总开关 :在
mybatis-config.xml中配置<setting name="cacheEnabled" value="true"/>(通常默认已开启)。 -
Mapper 映射文件声明 :在需要开启的
XxxMapper.xml文件中添加<cache />标签。 -
实体类序列化 :缓存的 POJO 类必须实现
Serializable接口,因为二级缓存可能涉及反序列化克隆对象的行为。
4、 避坑指南:为什么生产环境不建议使用?
在实际的企业级微服务开发中,技术团队通常不建议使用 MyBatis 自带的缓存机制:
- 分布式环境脏数据 :MyBatis 缓存属于本地缓存 。在多实例部署下,A 服务修改了数据库,B 服务的 MyBatis 缓存并不会感知,从而导致 B 服务持续读取脏数据。
- 多表联查限制 :二级缓存基于 Namespace。如果 A Mapper 的查询关联了 B 表,而 B Mapper 对 B 表进行了更新,A Mapper 的二级缓存不会刷新,导致数据错乱。
- 替代方案 :生产环境通常推荐直接关闭 MyBatis 缓存(或将一级缓存级别设为
STATEMENT),并采用 Redis 等分布式缓存来统一管理业务缓存。
六、MyBatis和MyBatis Plus的核心区别
1. CRUD 操作(增删改查)
- MyBatis :所有的增删改查操作,即使是最基础的"根据 ID 查询"、"插入一条数据",也必须在 XML 文件中手写 SQL 语句,或者在接口上使用
@Select等注解。 - MyBatis-Plus :提供了内置的
BaseMapper<T>和IService<T>接口。只要你的 Mapper 接口继承了BaseMapper,就自动拥有了单表基础的增、删、改、查、批量操作等 30 多个通用方法 ,完全不需要写一行 XML 或者是 SQL 语句。
2. 条件查询与条件构造器
-
MyBatis :多条件组合查询需要使用 XML 中的动态 SQL 标签(如
<if>、<where>、<choose>)来拼接 SQL。 -
MyBatis-Plus :引入了 Wrapper(条件构造器) 。开发者可以使用 链式编程 的纯 Java 代码来构建复杂的查询条件,例如:
java// 转换为 SQL: WHERE age > 18 AND name LIKE '%张%' List<User> list = userMapper.selectList(new LambdaQueryWrapper<User>() .gt(User::getAge, 18) .like(User::getName, "张"));这避免了在 XML 中编写繁琐且容易出错的判断标签。
3. 核心功能特性(MP 独有)
MyBatis 仅仅是一个半自动的 ORM 映射工具,而 MyBatis-Plus 在此基础上扩展了大量企业级开发的实用功能:
- 自动主键生成:内置了雪花算法(Sequence)、UUID、自增等多种主键生成策略。
- 逻辑删除 :支持配置逻辑删除(如将
deleted字段从0改为1),配置后所有的查询、修改都会自动过滤掉已删除的数据,无需手动加WHERE deleted = 0。 - 自动填充(公共字段处理) :支持在插入或更新时,自动填充
create_time、update_time、operator等公共字段,无需手动set。 - 乐观锁插件 :内置
@Version注解,自动处理并发更新时的版本号对比。 - 代码生成器:提供了一键生成 Entity、Mapper、XML、Service、Controller 的工具,极大缩短了新模块的开发时间。
4. 分页实现
- MyBatis :原生不支持物理分页,通常需要引入第三方插件(如
PageHelper),或者在 XML 里手动拼写LIMIT。 - MyBatis-Plus :自带功能完善的分页插件 ,只需配置一个拦截器,即可直接在代码中传入
Page对象进行面向对象的物理分页。
优缺点与适用场景对比
| 维度 | MyBatis | MyBatis-Plus |
|---|---|---|
| SQL 控制度 | 💯 极高。所有 SQL 手写,适合对 SQL 性能要求极其苛刻、需要频繁进行复杂多表联查的系统。 | 📈 中高。单表和轻度多表用 Java 代码搞定;遇到极复杂的 SQL,依然可以回归手写 XML。 |
| 开发效率 | 🐌 较低。大量时间浪费在编写基础单表的增删改查 SQL 和 XML 标签上。 | ⚡ 极高。基础单表全自动,代码生成器可以秒级生成整套基础结构。 |
| 项目维护性 | 🛠️ 较好。SQL 集中在 XML 中,DBA(数据库管理员)可以直观地审查和调优所有 SQL。 | 🧩 视情况而定。Java 代码里混杂了大量的条件构造器(Wrapper),业务复杂时,代码看起来不够直观。 |
| 适用场景 | 1. 互联网大厂核心高并发业务 2. 数据库表结构极度复杂、多表关联频繁的项目 3. 团队中有严格的 SQL 审查机制 | 1. 中小型项目、后台管理系统、微服务应用 2. 快速迭代、表结构多为单表或轻度关联的项目 3. 想要极力提升搬砖效率的开发团队 |
| [优缺点与适用场景对比] |

