MyBatis

一、执行流程

二、分页

在 MyBatis 中实现分页主要有 4 种 常见方式。根据实现层级的不同,它们可以分为:逻辑分页 (内存分页)和 物理分页(数据库分页)。

以下是具体实现方式及其优缺点的详细对比:

1. 物理分页:使用分页插件(如 PageHelper)

这是目前 生产环境中最推荐、最常用 的方式。它通过 MyBatis 提供的拦截器接口(Interceptor),在动态生成 SQL 时,自动拦截并根据当前数据库类型(MySQL, Oracle, PostgreSQL 等)重写 SQL,拼接上对应的分页关键字(如 LIMITROWNUM)。

  • 实现方式

    java 复制代码
    PageHelper.startPage(1, 10); // 核心代码:原理是基于 ThreadLocal 传递分页参数
    List<User> list = userMapper.selectAll();
    PageInfo<User> pageInfo = new PageInfo<>(list); // 包装后可获取总页数、总条数等
  • 优点

    • 代码侵入性极低 :不需要在 XML 中手动写 LIMIT,也不用在 Mapper 接口中传入分页参数。
    • 多数据库支持:插件会自动识别数据库类型,自动切换分页方言,便于数据库迁移。
    • 功能完善 :能自动帮你执行一条 COUNT(*) 语句获取总条数。
  • 缺点

    • 异步或多线程限制 :由于基于 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 进行分页。

  • 实现方式

    java 复制代码
    int 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,它内置了专用的分页拦截器。

  • 实现方式

    java 复制代码
    IPage<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 结构 的场景下,# 会因为自动加单引号或语法限制而失效,此时必须使用 $

  1. 动态表名 / 列名 :当表名或字段名需要从前端动态传入时。
    • 错误:SELECT * FROM #{tableName} -> 报错(解析成 SELECT * FROM 'user',语法错误)。
    • 正确:SELECT * FROM ${tableName}
  2. 动态排序( 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 级别)

  1. 工作原理
  • 一级缓存由 BaseExecutor 中的 localCache 维护,其底层是一个简单的 HashMap
  • 当同一个 SqlSession 执行两次相同的 SQL 查询时,第一次会访问数据库并写入缓存;第二次则直接从缓存中获取数据,不再触发数据库查询。
  1. 失效场景
  • 执行了增删改(CUD)操作 :同一个 SqlSession 内如果执行了 insertupdatedelete 并提交,缓存会被自动清空,防止读到过期数据。
  • 手动清空缓存 :调用了 sqlSession.clearCache() 方法。
  • 跨 SqlSession 访问 :不同的 SqlSession 之间缓存完全隔离。
  • 配置了 flushCache=true :在对应的 <select> 标签中显式配置了清空缓存。

3、 二级缓存(Mapper 级别)

  1. 工作原理
  • 二级缓存的作用域是 Namespace(即同一个 Mapper 文件) ,多个不同的 SqlSession 可以共用同一个二级缓存。
  • 它的查询流程优先于一级缓存:二级缓存 ➡️ 一级缓存 ➡️ 数据库
  • 重要机制 :当 SqlSession 执行 close()commit() 之后,一级缓存中的数据才会真正刷新到二级缓存中。
  1. 开启步骤

  2. 全局总开关 :在 mybatis-config.xml 中配置 <setting name="cacheEnabled" value="true"/>(通常默认已开启)。

  3. Mapper 映射文件声明 :在需要开启的 XxxMapper.xml 文件中添加 <cache /> 标签。

  4. 实体类序列化 :缓存的 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_timeupdate_timeoperator 等公共字段,无需手动 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. 想要极力提升搬砖效率的开发团队
[优缺点与适用场景对比]

相关推荐
棒棒的唐1 小时前
非常棒的向量数据库chroma的前端管理工具及向量模型配置
前端
程序员-Benothing1 小时前
Java ForkJoinPool 详解:从分治思想到高性能并行计算
java·开发语言·后端·面试·职场和发展
上海安当技术2 小时前
单点登录 SSO 怎么选协议?SAML 2.0 / OAuth 2.0 / OIDC / CAS 对比与 ERP、OA、CRM 接入实战
java·开发语言
mifengxing2 小时前
Java集合与泛型
java·算法·复习笔记
芭拉拉小魔仙2 小时前
前端文件下载与错误处理实战指南
前端
不才不才不不才2 小时前
Spring 源码系列(14): JDK 动态代理 vs CGLIB,以及拦截器责任链
java·开发语言·spring
流云鹤2 小时前
05Java学习day(5)
java·学习
Yao8062 小时前
MinIO自建对象存储,省OSS费用的完整方案
前端·后端
用户3126874877202 小时前
Spring 事件机制到底怎么传播的?ApplicationEvent 源码拆解
java·spring