一、前言
在 Java 后端开发中,操作数据库是绕不开的核心环节。早期的 JDBC 编程需要手动拼接 SQL、处理结果集、管理连接,代码繁琐且容易出错。ORM(对象关系映射)框架的出现彻底改变了这一局面,而 MyBatis 凭借其灵活性和轻量级特性,已成为国内 Java 生态中最主流的持久层框架之一。
MyBatis 的核心思想是将 SQL 从 Java 代码中剥离出来,写在 XML 配置文件中,由框架负责参数映射和结果集封装。这种"SQL 与代码分离"的设计,让开发者既能享受 ORM 的便利,又能完全掌控每一条 SQL 的执行逻辑。
本文将系统地介绍 MyBatis XML 的方方面面------从基础概念到高级特性,从语法原理到实战示例,力求让每一位读者都能真正理解和掌握这门"数据库操作的描述语言"。

二、简介
2.1 什么是 MyBatis
MyBatis 是一款优秀的半自动化 ORM 框架,前身为 Apache 的 iBatis 项目,2010 年迁移到 GitHub 后更名为 MyBatis。它通过 XML 或注解的方式将 Java 对象与数据库表进行映射,屏蔽了底层 JDBC 的繁琐操作。

2.2 什么是 MyBatis XML
MyBatis XML 是 MyBatis 框架定义的一套基于 XML 标准的 SQL 映射描述语言。开发者在 XML 文件中编写 SQL 语句、定义参数映射规则和结果集映射规则,MyBatis 框架在运行时解析这些 XML 文件,自动完成以下工作:

bash
Java 方法调用
↓
MyBatis 解析 XML 中对应的 SQL
↓
将 Java 参数映射到 SQL 占位符
↓
执行 SQL
↓
将数据库返回的结果映射为 Java 对象
↓
返回给调用者
一个最简单的 MyBatis XML 文件如下:
xml
<?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.mapper.UserMapper">
<select id="selectById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
2.3 MyBatis 与 MyBatis-Plus 的关系

|
维度
|
MyBatis
|
MyBatis-Plus
|
| --- | --- | --- |
|
定位
|
基础 ORM 框架
|
MyBatis 的增强工具
|
|
XML 语法
|
自身定义
|
完全兼容 MyBatis,无新增 XML 标签
|
|
CRUD
|
需手写 SQL
|
内置通用 Mapper,简单 CRUD 零 SQL
|
|
动态 SQL
|
提供 if/where/foreach 等
|
直接继承,同时提供 Java API 的条件构造器
|
|
额外能力
|
无
|
分页插件、代码生成器、逻辑删除等
|
结论:MyBatis-Plus 的 XML 写法和 MyBatis 完全一致。 学好 MyBatis XML,MyBatis-Plus 的 XML 部分自然就会了。
三、特点
3.1 SQL 与代码分离
SQL 写在 XML 中,Java 代码只负责调用接口方法,修改 SQL 不需要重新编译 Java 代码。
3.2 真正的 SQL 可控
不像 Hibernate 等全自动化 ORM 框架会自动生成 SQL,MyBatis 让开发者直接编写原生 SQL,对每一条语句拥有完全的控制权。这意味着:
-
• 可以充分利用数据库特有的语法和函数
-
• 可以针对慢查询做精细的 SQL 优化
-
• 可以执行复杂的多表关联和子查询
3.3 动态 SQL 能力
MyBatis XML 提供了 <if>、<where>、<choose>、<foreach>、<trim>、<set> 等动态标签,可以根据参数的不同动态生成不同的 SQL 语句,避免了大量字符串拼接的痛苦。
3.4 强大的结果映射
通过 <resultMap> 可以将复杂的数据库查询结果(包括多表关联、嵌套对象、集合)精确映射为 Java 对象,支持一对一、一对多、鉴别器等高级映射关系。
3.5 学习成本低
语法直观,本质上就是"在 XML 里写 SQL",加上少量的框架标签。对于已经熟悉 SQL 的开发者来说,上手非常快。
3.6 插件生态丰富
MyBatis 提供了插件(Interceptor)机制,可以拦截 SQL 执行的各个阶段。MyBatis-Plus 在此基础上提供了分页插件、乐观锁插件、防全表更新插件等开箱即用的能力。
四、功能
MyBatis XML 提供的核心功能可以归纳为以下几类:
bash
┌──────────────────────────────────────────────────────┐
│ MyBatis XML 功能全景图 │
├──────────────────────────────────────────────────────┤
│ │
│ 📌 基础 CRUD │
│ ├── SELECT 查询(单条 / 列表 / 分页) │
│ ├── INSERT 插入(单条 / 批量 / 返回主键) │
│ ├── UPDATE 更新(全量更新 / 选择性更新) │
│ └── DELETE 删除(单条 / 批量) │
│ │
│ 📌 动态 SQL │
│ ├── 条件判断 <if> │
│ ├── 多选一 <choose><when><otherwise> │
│ ├── 智能 WHERE <where> │
│ ├── 智能 SET <set> │
│ ├── 自定义裁剪 <trim> │
│ └── 循环遍历 <foreach> │
│ │
│ 📌 结果映射 │
│ ├── 基本映射 resultType / resultMap │
│ ├── 一对一关联 <association> │
│ ├── 一对多关联 <collection> │
│ └── 鉴别器 <discriminator> │
│ │
│ 📌 复用与引用 │
│ ├── SQL 片段 <sql> / <include> │
│ └── 缓存配置 <cache> / <cache-ref> │
│ │
│ 📌 参数处理 │
│ ├── 预编译参数 #{} │
│ ├── 字符串替换 ${} │
│ └── 特殊符号 CDATA │
│ │
│ 📌 分页查询 │
│ ├── 物理分页 LIMIT / ROW_NUMBER() │
│ └── 逻辑分页 RowBounds / PageHelper │
│ │
└──────────────────────────────────────────────────────┘
五、这种 XML 语法的归属------MyBatis 特有的,还是通用标准?
这是一个非常好的问题,也是很多初学者容易混淆的地方。答案是:需要分层理解。
5.1 第一层:XML 本身 --- 通用国际标准
XML(eXtensible Markup Language,可扩展标记语言)是由 W3C(万维网联盟) 于 1998 年发布的国际标准。它只规定了标签的书写规则:

xml
<!-- 开始标签和结束标签 -->
<标签名 属性名="属性值">内容</标签名>
<!-- 自闭合标签 -->
<标签名 属性名="属性值"/>
<!-- 注释 -->
<!-- 这是 XML 注释 -->
XML 本身不定义任何具体的业务标签,它只是一套「容器格式规范」,就像一张标准的白纸------在上面写中文还是英文,取决于书写者。
5.2 第二层:MyBatis 标签体系 --- MyBatis 自己定义的私有词汇
MyBatis 通过 DTD 文件(Document Type Definition,文档类型定义)定义了自己的一套 XML 标签。你在 XML 文件头部看到的声明,就是引用了这个 DTD:
xml
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
这个 DTD 文件规定了:
-
• 允许使用哪些标签:
<select>、<insert>、<if>、<where>等 -
• 每个标签有哪些属性:
id、resultType、parameterType等 -
• 标签之间的嵌套关系:
<if>必须写在<where>里面
这些标签不是 XML 标准的一部分,纯粹是 MyBatis 框架自己定义的。 把 <select> 写到 Spring 配置文件里,Spring 是不认识的。
MyBatis 的设计非常聪明,它内置了一套 "本地优先" 的加载策略-,流程如下:
需要注意 :官网曾公告过,
mybatis.org这个网站曾出现过短暂的服务不可用情况-35。但官方也强调,只要配置正确,MyBatis 会从本地的 Jar 包读取 DTD,不会受到网站宕机的影
5.3 第三层:OGNL 表达式 --- 来自 Apache 的表达式语言
MyBatis XML 中还有一个隐藏层:
xml
<if test="name != null and name != ''">
test="" 里面的内容既不是 XML 语法,也不是 MyBatis 发明的,而是 OGNL(Object-Graph Navigation Language) ,一个由 Apache 维护的开源表达式语言,专门用于在 Java 对象上进行属性访问和逻辑判断。
5.4 总结:三层合一
bash
MyBatis XML 文件 = XML 语法规范(W3C 标准)
+ MyBatis 自定义标签(MyBatis DTD)
+ OGNL 表达式(Apache 开源项目)
用一个类比来理解:
|
层次
|
类比
|
| --- | --- |
|
XML 语法(W3C)
|
纸张规格(A4、方格纸)
|
|
MyBatis 标签(DTD)
|
一套专用的公文模板
|
|
OGNL 表达式
|
写在模板里的逻辑判断语言
|
5.5 拓展:Java 生态中到处都是这种模式
利用 XML 容器 + 私有标签的模式在 Java 世界中非常普遍:
|
框架
|
自定义标签
|
用途
|
| --- | --- | --- |
|
MyBatis
| <select>
, <if>, <foreach>
|
SQL 映射
|
|
Spring
| <bean>
, <property>, [aop:config](aop:config)
|
IoC/AOP 配置
|
|
Maven
| <dependency>
, <plugin>, <build>
|
项目构建
|
|
Tomcat
| <Server>
, <Connector>, <Context>
|
服务器配置
|
|
Android
| <LinearLayout>
, <TextView>
|
UI 布局
|
|
MyBatis-Plus
| 无新增
,完全复用 MyBatis 的标签
|
增强工具
|
它们都遵守 XML 的通用语法规则,但标签体系各自独立,互不通用。
六、语法详细介绍
6.1 文件基本结构
每个 MyBatis XML 文件都遵循以下结构:
xml
<?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.mapper.UserMapper">
<!-- 在此编写 SQL 映射语句 -->
</mapper>
|
组成部分
|
说明
|
| --- | --- |
|
XML 声明
|
指定版本和编码,固定写法
|
|
DOCTYPE 声明
|
引用 MyBatis DTD,提供标签校验和 IDE 补全
|
| <mapper>
根标签
| namespace
必须对应 Mapper 接口的全限定类名
|
6.2 四大基础标签
<select> --- 查询
xml
<select id="方法名" resultType="返回类型" parameterType="参数类型">
SQL 语句
</select>
|
属性
|
含义
|
是否必填
|
| --- | --- | --- |
| id |
对应 Mapper 接口中的方法名
|
是
|
| resultType |
查询结果映射的 Java 类型
|
与 resultMap 二选一
|
| resultMap |
引用自定义的结果映射
|
与 resultType 二选一
|
| parameterType |
参数的 Java 类型
|
否(通常省略,MyBatis 自动推断)
|
<insert> --- 插入
xml
<insert id="方法名" useGeneratedKeys="true" keyProperty="id">
SQL 语句
</insert>
|
属性
|
含义
|
| --- | --- |
| useGeneratedKeys |
是否使用数据库自增主键
|
| keyProperty |
自增主键回填到实体类的哪个字段
|
<update> --- 更新
xml
<update id="方法名">
SQL 语句
</update>
<delete> --- 删除
xml
<delete id="方法名">
SQL 语句
</delete>
6.3 参数占位符:#{} 与 ${}
这是 MyBatis 中最容易混淆也最容易出错的知识点:
#{} |
${} |
|
|---|---|---|
| 本质 | ||
预编译参数,生成 ? 占位符 |
||
| 字符串直接替换拼接 | ||
| 安全性 | ||
| 防止 SQL 注入 | 存在 SQL 注入风险 | |
| 底层实现 | ||
PreparedStatement |
Statement |
|
| 适用场景 | ||
| 参数值(绝大多数场景) | ||
| 动态表名、动态列名、ORDER BY 字段 | ||
xml
<!-- 安全写法:参数值统一用 #{} -->
SELECT * FROM user WHERE id = #{id}
-- 实际执行:SELECT * FROM user WHERE id = ?
-- 参数:[10]
<!-- ${} 仅用于无法使用占位符的场景 -->
SELECT * FROM ${tableName} ORDER BY ${sortColumn} ${sortOrder}
-- 实际执行:SELECT * FROM user ORDER BY age DESC
黄金法则:能用 **
#{}的地方就用#{},只在动态表名、列名、排序方向等场景才用${}****。**
6.4 特殊符号处理与 CDATA
XML 解析器有一套自己的语法规则,以下字符在 XML 中具有特殊含义:
|
字符
|
XML 中的含义
|
直接使用的后果
|
| --- | --- | --- |
| < |
标签开始
|
解析器误认为新标签开头,解析报错
|
| > |
标签结束
|
部分情况下解析异常
|
| & |
实体引用开头
|
后续内容被当成实体名解析
|
| " |
属性值界定符
|
截断属性值
|
| ' |
属性值界定符
|
截断属性值
|
6.4.1 为什么 SQL 中会遇到这些符号
SQL 中大量使用这些符号作为运算符:
sql
-- 小于、大于
WHERE age > 18
WHERE age < 60
WHERE price >= 100 AND price <= 500
-- 尖括号(动态 SQL 中的比较)
WHERE score > #{minScore}
如果直接写在 XML 中,解析器会把 > 当成标签的结束符,把 < 当成新标签的开始,导致解析失败。
6.4.2 方案一:转义字符
XML 预定义了 5 个转义实体:
|
字符
|
转义写法
|
示例
|
| --- | --- | --- |
| < | <** | age < 60 |
| > | > | age > 18 |
| & | & | name & age |
| " | " |
用于属性值中
|
| ' | ' |
用于属性值中
|
xml
<select id="selectByAgeRange" resultType="User">
SELECT * FROM user
WHERE age >= #{ageMin} AND age <= #{ageMax}
</select>
优点:标准 XML 写法,所有 XML 工具都支持。
缺点 :SQL 可读性下降,尤其是条件多的时候满屏 >``<,维护困难。
6.4.3 方案二:CDATA 段(推荐)
CDATA(Character Data)段告诉 XML 解析器: "这段内容全部当做纯文本,不要做任何解析" 。
语法格式:
xml
<![CDATA[ 这里面的内容全部是纯文本,原样保留 ]]>
实际使用:
xml
<select id="selectByAgeRange" resultType="User">
SELECT * FROM user
WHERE age <![CDATA[ >= ]]> #{ageMin}
AND age <![CDATA[ <= ]]> #{ageMax}
</select>
生成的 SQL:
sql
SELECT * FROM user WHERE age >= 10 AND age <= 60
CDATA 内部可以自由书写特殊符号,不会被 XML 解析器误解析。
6.4.4 方案三:巧妙利用 SQL 关键字替代
对于比较运算符,可以换一种等价的 SQL 写法来完全避免特殊符号:
xml
<!-- 方式 1:使用 BETWEEN(推荐用于范围查询) -->
<select id="selectByAgeRange" resultType="User">
SELECT * FROM user
WHERE age BETWEEN #{ageMin} AND #{ageMax}
</select>
<!-- 方式 2:使用不涉及特殊符号的 SQL 函数 -->
<select id="selectByAge" resultType="User">
SELECT * FROM user
WHERE GREATEST(age, #{minAge}) = age
</select>
BETWEEN完全不需要<``>符号,是最干净的方案。
6.4.5 CDATA 的注意事项
1. CDATA 不能嵌套
xml
<!-- 错误!CDATA 不支持嵌套 -->
<![CDATA[
age > 18 <![CDATA[ AND ]]> name = #{name}
]]>
2. CDATA 内不能出现 **]]>**字符串
因为 ]]> 是 CDATA 段的结束标记。如果 SQL 中恰好包含 ]]>(极少见),需要拆分 CDATA 段或使用转义。
3. CDATA 与动态 SQL 标签共存时要注意
动态 SQL 标签(<if>、<where>、<foreach> 等)必须在 CDATA 外部,因为它们需要被 XML 解析器识别:
xml
<!-- 正确:CDATA 只包裹包含特殊符号的纯 SQL 部分 -->
<where>
<if test="minAge != null">
AND age <![CDATA[ > ]]> #{minAge}
</if>
<if test="maxAge != null">
AND age <![CDATA[ < ]]> #{maxAge}
</if>
</where>
<!-- 错误!整个 SQL 被 CDATA 包裹后,<if> 等标签变成了纯文本,不会被解析 -->
<![CDATA[
<where>
<if test="minAge != null">
AND age > #{minAge}
</if>
</where>
]]>
最佳实践:CDATA 的包裹范围要尽可能小,只包裹包含特殊符号的那部分 SQL 片段。
6.4.6 三种方案对比
|
方案
|
可读性
|
安全性
|
适用场景
|
| --- | --- | --- | --- |
|
转义字符 >``<
|
差
|
好
|
特殊符号少且分散时
|
|
CDATA 段
|
好
|
好
|
含大量比较运算符的复杂 SQL
|
|
SQL 关键字替代
|
最好
|
好
| BETWEEN
、IN 等可替代的场景
|
实际项目中的常见做法 :优先用 SQL 关键字替代(如
BETWEEN),无法替代时用CDATA包裹,简单场景可用转义字符。
6.5 动态 SQL 标签
动态 SQL 是 MyBatis 最强大、最核心的特性。它解决了传统开发中大量字符串拼接 SQL 的痛点。
6.5.1 <if> --- 条件判断
根据条件决定是否拼接某段 SQL:
xml
<select id="selectByCondition" resultType="User">
SELECT * FROM user
WHERE 1 = 1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</select>
test 属性中使用 OGNL 表达式 进行判断。常用写法:
xml
<!-- 判空 -->
<if test="name != null">
<!-- 判空 + 空字符串 -->
<if test="name != null and name != ''">
<!-- 判断集合非空 -->
<if test="list != null and list.size() > 0">
<!-- 比较数值 -->
<if test="age != null and age >= 18">
<!-- 判断字符串相等(注意用双引号包裹单引号) -->
<if test="status == '1'">
<!-- 逻辑组合 -->
<if test="(name != null or email != null) and deleted == 0">
6.5.2 <where> --- 智能 WHERE 关键字
<if> 示例中的 WHERE 1 = 1 写法不够优雅,<where> 可以完美替代:
xml
<select id="selectByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</where>
</select>
<where> 的智能行为:
|
情况
|
行为
|
| --- | --- |
|
内部有内容
|
自动添加 WHERE 关键字
|
|
第一个条件带 AND
|
自动去掉开头多余的 AND 或 OR
|
|
内部无内容
|
整个 <where> 标签不输出任何内容
|
6.5.3 <set> --- 智能 SET 关键字
更新语句专用,自动处理末尾多余的逗号:
xml
<update id="updateSelective">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
当只传入 name 和 email 时,生成的 SQL:
sql
UPDATE user SET name = '张三', email = 'zs@qq.com' WHERE id = 1
末尾多余的逗号被自动去掉了。
6.5.4 <choose>``<when>``<otherwise> --- 多选一
类似 Java 的 switch-case 结构,只执行第一个满足条件的分支:
xml
<select id="selectByPriority" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="name != null and name != ''">
AND name = #{name}
</when>
<when test="email != null and email != ''">
AND email = #{email}
</when>
<otherwise>
AND status = 1
</otherwise>
</choose>
</where>
</select>
执行逻辑:
-
• 传了
name→ 按名字查询 -
• 没传
name,但传了email→ 按邮箱查询 -
• 都没传 → 查所有状态为 1 的记录
6.5.5 <trim> --- 自定义裁剪
<where> 和 <set> 本质上是 <trim> 的特化形式。当你需要更灵活的控制时,可以直接使用 <trim>:
xml
<!-- 等价于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</trim>
<!-- 等价于 <set> -->
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</trim>
<!-- 自定义:给内容加前缀和后缀 -->
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="name != null">#{name},</if>
<if test="age != null">#{age},</if>
</trim>
四个属性的含义:
|
属性
|
作用
|
示例
|
| --- | --- | --- |
| prefix |
在整体内容前添加
| WHERE
、SET、(
|
| suffix |
在整体内容后添加
| )
、END
|
| prefixOverrides |
去掉内容开头的指定字符
| AND
、OR
|
| suffixOverrides |
去掉内容结尾的指定字符
| , |
6.5.6 <foreach> --- 循环遍历
处理集合/数组参数的利器,最常见的两个场景是 IN 查询 和 批量插入。
IN 查询:
xml
<select id="selectByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
生成的 SQL:
sql
SELECT * FROM user WHERE id IN (1, 2, 3, 5, 8)
批量插入:
xml
<insert id="batchInsert">
INSERT INTO user (name, age, email) VALUES
<foreach collection="list" item="u" separator=",">
(#{u.name}, #{u.age}, #{u.email})
</foreach>
</insert>
生成的 SQL:
sql
INSERT INTO user (name, age, email) VALUES
('张三', 20, 'zs@qq.com'),
('李四', 25, 'ls@qq.com'),
('王五', 30, 'ww@qq.com')
**<foreach>**属性详解:
|
属性
|
含义
|
| --- | --- |
| collection |
要遍历的集合。如果参数用 @Param("ids") 修饰则写 ids;如果直接传 List 则写 list;如果传数组则写 array
|
| item |
遍历时每个元素的变量名
|
| index |
遍历时的索引变量名(可选)
|
| open |
循环开始前拼接的字符串
|
| close |
循环结束后拼接的字符串
|
| separator |
每个元素之间的分隔符
|
6.5.7 <bind> 标签
在 XML 中创建变量,可以简化复杂表达式:
xml
<select id="selectByNameLike" resultType="User">
<bind name="pattern" value="'%' + name + '%'"/>
SELECT * FROM user WHERE user_name LIKE #{pattern}
</select>
<bind> 还可以用来做数据库兼容------不同数据库的拼接函数不同:
xml
<!-- MySQL 用 CONCAT,Oracle 用 ||,通过 bind 统一处理 -->
<bind name="pattern" value="'%' + name + '%'"/>
SELECT * FROM user WHERE user_name LIKE #{pattern}
6.5.8 <sql> 和 <include> --- SQL 片段复用
将重复出现的 SQL 片段抽取出来,避免代码冗余:
xml
<!-- 定义片段 -->
<sql id="baseColumns">
id, user_name, age, email, create_time
</sql>
<sql id="queryCondition">
<if test="name != null and name != ''">
AND user_name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="status != null">
AND status = #{status}
</if>
</sql>
<!-- 引用片段 -->
<select id="selectByCondition" resultType="User">
SELECT <include refid="baseColumns"/>
FROM user
<where>
<include refid="queryCondition"/>
</where>
</select>
<select id="countByCondition" resultType="int">
SELECT COUNT(*)
FROM user
<where>
<include refid="queryCondition"/>
</where>
</select>
6.6 结果映射 <resultMap>
6.6.1 何时需要 resultMap
当数据库字段名和 Java 属性名不一致 时,或者需要做关联映射时。
xml
<resultMap id="userResultMap" type="com.example.entity.User">
<!-- 主键用 id 标签 -->
<id property="id" column="id"/>
<!-- 普通字段用 result 标签 -->
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
<result property="createTime" column="create_time"/>
</resultMap>
<select id="selectAll" resultMap="userResultMap">
SELECT id, user_name, age, email, create_time
FROM user
</select>
快捷方式 :在 MyBatis 配置中开启
mapUnderscoreToCamelCase,下划线自动转驼峰,大部分情况就不需要手动写resultMap了。
6.6.2 一对一关联 <association>
一个用户属于一个部门:
xml
<resultMap id="userWithDept" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<!-- 一对一:关联部门 -->
<association property="dept" javaType="Dept">
<id property="id" column="dept_id"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>
<select id="selectUserWithDept" resultMap="userWithDept">
SELECT
u.id, u.user_name, u.age,
d.id AS dept_id, d.dept_name AS dept_name
FROM user u
LEFT JOIN dept d ON u.dept_id = d.id
WHERE u.id = #{id}
</select>
6.6.3 一对多关联 <collection>
一个部门有多个用户:
xml
<resultMap id="deptWithUsers" type="Dept">
<id property="id" column="id"/>
<result property="deptName" column="dept_name"/>
<!-- 一对多:关联用户列表 -->
<collection property="users" ofType="User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
</collection>
</resultMap>
<select id="selectDeptWithUsers" resultMap="deptWithUsers">
SELECT
d.id, d.dept_name,
u.id AS user_id, u.user_name, u.age
FROM dept d
LEFT JOIN user u ON d.id = u.dept_id
WHERE d.id = #{id}
</select>
association用javaType,collection用ofType,不要搞混。
6.6.4 嵌套查询(延迟加载)
除了上面的 JOIN 一次性查出所有数据外,还可以分步查询:
xml
<!-- 第一步:查询用户 -->
<resultMap id="userWithDeptLazy" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<!-- 第二步:根据 dept_id 触发另一条 SQL 查询部门 -->
<association property="dept"
column="dept_id"
select="com.example.mapper.DeptMapper.selectById"/>
</resultMap>
默认是立即加载 (查用户时同时查部门)。开启延迟加载后,只有真正访问 user.getDept() 时才执行第二条 SQL:
yaml
mybatis:
configuration:
lazy-loading-enabled: true
aggressive-lazy-loading: false
6.7 分页查询
分页是实际项目中最常见的需求之一。MyBatis 本身没有内置分页功能,但提供了多种实现方案。
6.7.1 物理分页 vs 逻辑分页
|
类型
|
原理
|
优点
|
缺点
|
| --- | --- | --- | --- |
| 物理分页 |
在 SQL 层面限制返回行数(如 LIMIT)
|
性能好,只查需要的数据
|
依赖数据库方言
|
| 逻辑分页 |
查出全部数据,在 Java 内存中截取
|
与数据库无关
|
数据量大时内存溢出、性能差
|
生产环境几乎只用物理分页 ,逻辑分页(如
RowBounds)仅在小数据量场景下适用。
6.7.2 原生 SQL 分页(MySQL)
最基础的方式------直接在 SQL 中写 LIMIT:
xml
<!-- MySQL 分页 -->
<select id="selectByPage" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
对应的 Mapper 接口和分页参数:
java
List<User> selectByPage(@Param("name") String name,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
调用时计算 offset:
java
int pageNum = 3; // 第 3 页
int pageSize = 10; // 每页 10 条
int offset = (pageNum - 1) * pageSize; // offset = 20
List<User> users = userMapper.selectByPage("张", offset, pageSize);
生成的 SQL:
sql
SELECT * FROM user WHERE name LIKE '%张%' ORDER BY create_time DESC LIMIT 20, 10
配合总数查询实现完整分页:
xml
<!-- 查询总数(配合分页使用) -->
<select id="countByCondition" resultType="int">
SELECT COUNT(*) FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
</select>
java
// 完整分页逻辑
int total = userMapper.countByCondition("张");
int totalPages = (total + pageSize - 1) / pageSize;
List<User> records = userMapper.selectByPage("张", offset, pageSize);
// 封装分页结果
PageResult<User> pageResult = new PageResult<>();
pageResult.setTotal(total);
pageResult.setPages(totalPages);
pageResult.setPageNum(pageNum);
pageResult.setPageSize(pageSize);
pageResult.setRecords(records);
6.7.3 不同数据库的分页语法对比
|
数据库
|
分页语法
|
示例
|
| --- | --- | --- |
| MySQL | LIMIT offset, size | LIMIT 0, 10 |
| PostgreSQL | LIMIT size OFFSET offset | LIMIT 10 OFFSET 0 |
| Oracle 12c+ | OFFSET n ROWS FETCH NEXT m ROWS ONLY | OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY |
| Oracle 旧版 |
嵌套 ROWNUM 子查询
|
见下方示例
|
| SQL Server | OFFSET n ROWS FETCH NEXT m ROWS ONLY |
同 Oracle 12c
|
Oracle 旧版分页写法(嵌套子查询):
xml
<select id="selectByPageOracle" resultType="User">
SELECT * FROM (
SELECT t.*, ROWNUM rn FROM (
SELECT * FROM user ORDER BY create_time DESC
) t
WHERE ROWNUM <![CDATA[ <= ]]> #{endRow}
)
WHERE rn <![CDATA[ > ]]> #{startRow}
</select>
注意这里用到了 CDATA 来处理
<=和>这些特殊符号。
6.7.4 MyBatis 的 RowBounds 逻辑分页
MyBatis 内置了 RowBounds 参数,实现内存级逻辑分页:
java
// Mapper 接口(不需要 XML 中写分页逻辑)
List<User> selectAll(RowBounds rowBounds);
java
// 调用:跳过前 20 条,取 10 条
RowBounds rowBounds = new RowBounds(20, 10);
List<User> users = userMapper.selectAll(rowBounds);
XML 中无需任何分页相关 SQL:
xml
<select id="selectAll" resultType="User">
SELECT * FROM user ORDER BY create_time DESC
</select>
原理:MyBatis 会执行完整 SQL 查出所有数据,然后在 Java 层截取需要的那一页。
注意:数据量大时性能极差,仅适用于小数据量场景。
6.7.5 MyBatis-Plus 分页插件(推荐)
MyBatis-Plus 提供了开箱即用的物理分页插件,支持多种数据库自动适配。
第一步:配置分页插件
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件,指定数据库类型
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(DbType.MYSQL)
);
return interceptor;
}
}
第二步:使用内置分页方法(零 XML)
java
// 直接调用 BaseMapper 的 selectPage
Page<User> page = new Page<>(1, 10); // 第 1 页,每页 10 条
IPage<User> result = userMapper.selectPage(page, null);
// 带条件的分页
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "张")
.eq("status", 1)
.orderByDesc("create_time");
IPage<User> result = userMapper.selectPage(page, wrapper);
第三步:自定义 XML 分页查询
当需要写复杂 SQL 时,可以在 XML 中使用分页:
xml
<!-- Mapper 接口方法 -->
<!-- IPage<User> selectUserPage(IPage<User> page, @Param("query") UserQuery query); -->
<select id="selectUserPage" resultType="User">
SELECT u.id, u.user_name, u.age, u.email, d.dept_name
FROM user u
LEFT JOIN dept d ON u.dept_id = d.id
<where>
<if test="query.name != null and query.name != ''">
AND u.user_name LIKE CONCAT('%', #{query.name}, '%')
</if>
<if test="query.status != null">
AND u.status = #{query.status}
</if>
</where>
ORDER BY u.create_time DESC
</select>
java
// 调用时只需要传入 Page 对象,MyBatis-Plus 自动追加 LIMIT 语句
Page<User> page = new Page<>(1, 10);
UserQuery query = new UserQuery();
query.setName("张");
query.setStatus(1);
IPage<User> result = userMapper.selectUserPage(page, query);
// 获取结果
result.getRecords(); // 当前页数据
result.getTotal(); // 总记录数
result.getPages(); // 总页数
result.getCurrent(); // 当前页码
result.getSize(); // 每页大小
MyBatis-Plus 的分页插件会自动执行两条 SQL:先查总数,再查当前页数据。你不需要手动写
COUNT(*)和LIMIT。
6.7.6 PageHelper 分页插件(第三方)
如果不使用 MyBatis-Plus,也可以使用 PageHelper:
第一步:引入依赖
xml
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
第二步:使用(通过 ThreadLocal 自动拦截)
java
// 开启分页:紧接在查询方法之前调用
PageHelper.startPage(1, 10);
// 执行普通查询(不需要写分页 SQL)
List<User> users = userMapper.selectAll();
// 结果自动被包装为 Page 类型
Page<User> page = (Page<User>) users;
page.getTotal(); // 总记录数
page.getPages(); // 总页数
page.getResult(); // 当前页数据
原理 :
PageHelper.startPage()通过 ThreadLocal 设置分页参数,MyBatis 执行 SQL 时,PageHelper 的拦截器会自动修改 SQL,追加LIMIT子句和COUNT查询。
6.7.7 分页方案对比
|
方案
|
类型
|
侵入性
|
数据库兼容
|
推荐程度
|
| --- | --- | --- | --- | --- |
|
原生 SQL LIMIT
|
物理
|
无
|
单一数据库
|
简单场景可用
|
|
RowBounds
|
逻辑
|
低
|
全兼容
|
不推荐生产使用
|
|
MyBatis-Plus 分页插件
|
物理
|
低
|
多数据库自动适配
| 强烈推荐 |
|
PageHelper
|
物理
|
低
|
多数据库自动适配
|
推荐(非 Plus 项目)
|
选型建议:使用 MyBatis-Plus 的项目首选其内置分页插件;纯 MyBatis 项目推荐 PageHelper。
七、使用示例
下面通过一个完整的「用户管理系统」场景,把所有语法串联起来。
7.1 实体类
java
@Data
public class User {
private Long id;
private String userName;
private Integer age;
private String email;
private Long deptId;
private Integer status;
private Date createTime;
private Date updateTime;
}
7.2 Mapper 接口
java
@Mapper
public interface UserMapper {
List<User> selectUserList(UserQuery query);
int countUser(UserQuery query);
User selectById(Long id);
List<User> selectByIds(@Param("ids") List<Long> ids);
int insertUser(User user);
int batchInsert(@Param("list") List<User> users);
int updateUser(User user);
int deleteById(Long id);
int deleteByIds(@Param("ids") List<Long> ids);
}
7.3 完整的 XML 文件
xml
<?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.mapper.UserMapper">
<!-- ==================== 可复用片段 ==================== -->
<!-- 通用查询字段 -->
<sql id="baseColumns">
id, user_name, age, email, dept_id, status, create_time, update_time
</sql>
<!-- 通用查询条件 -->
<sql id="queryCondition">
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="ageMin != null">
AND age <![CDATA[ >= ]]> #{ageMin}
</if>
<if test="ageMax != null">
AND age <![CDATA[ <= ]]> #{ageMax}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="deptId != null">
AND dept_id = #{deptId}
</if>
<if test="status != null">
AND status = #{status}
</if>
</sql>
<!-- ==================== 结果映射 ==================== -->
<resultMap id="userResultMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
<result property="deptId" column="dept_id"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<!-- ==================== 查询 ==================== -->
<!-- 条件分页查询 -->
<select id="selectUserList" resultMap="userResultMap">
SELECT <include refid="baseColumns"/>
FROM user
<where>
<include refid="queryCondition"/>
</where>
<choose>
<when test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy} ${orderDir}
</when>
<otherwise>
ORDER BY create_time DESC
</otherwise>
</choose>
LIMIT #{offset}, #{pageSize}
</select>
<!-- 统计总数 -->
<select id="countUser" resultType="int">
SELECT COUNT(*)
FROM user
<where>
<include refid="queryCondition"/>
</where>
</select>
<!-- 根据 ID 查询 -->
<select id="selectById" resultMap="userResultMap">
SELECT <include refid="baseColumns"/>
FROM user
WHERE id = #{id}
</select>
<!-- 根据 ID 列表查询 -->
<select id="selectByIds" resultMap="userResultMap">
SELECT <include refid="baseColumns"/>
FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- ==================== 插入 ==================== -->
<!-- 单条插入(返回自增主键) -->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userName != null">user_name,</if>
<if test="age != null">age,</if>
<if test="email != null">email,</if>
<if test="deptId != null">dept_id,</if>
<if test="status != null">status,</if>
create_time,
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="userName != null">#{userName},</if>
<if test="age != null">#{age},</if>
<if test="email != null">#{email},</if>
<if test="deptId != null">#{deptId},</if>
<if test="status != null">#{status},</if>
NOW(),
</trim>
</insert>
<!-- 批量插入 -->
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (user_name, age, email, dept_id, status, create_time)
VALUES
<foreach collection="list" item="u" separator=",">
(#{u.userName}, #{u.age}, #{u.email},
#{u.deptId}, #{u.status}, NOW())
</foreach>
</insert>
<!-- ==================== 更新 ==================== -->
<!-- 选择性更新 -->
<update id="updateUser">
UPDATE user
<set>
<if test="userName != null and userName != ''">
user_name = #{userName},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="email != null and email != ''">
email = #{email},
</if>
<if test="deptId != null">
dept_id = #{deptId},
</if>
<if test="status != null">
status = #{status},
</if>
update_time = NOW(),
</set>
WHERE id = #{id}
</update>
<!-- ==================== 删除 ==================== -->
<!-- 根据 ID 删除 -->
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
<!-- 根据 ID 列表批量删除 -->
<delete id="deleteByIds">
DELETE FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
7.4 调用示例
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void demo() {
// 1. 条件查询
UserQuery query = new UserQuery();
query.setUserName("张");
query.setStatus(1);
query.setOffset(0);
query.setPageSize(10);
List<User> users = userMapper.selectUserList(query);
// 2. 根据 ID 查询
User user = userMapper.selectById(1L);
// 3. 插入
User newUser = new User();
newUser.setUserName("赵六");
newUser.setAge(28);
newUser.setEmail("zl@example.com");
userMapper.insertUser(newUser);
System.out.println("新用户 ID: " + newUser.getId()); // 主键已回填
// 4. 批量插入
List<User> batchUsers = new ArrayList<>();
// ... 添加多个 User 对象
userMapper.batchInsert(batchUsers);
// 5. 选择性更新(只更新传了值的字段)
User updateUser = new User();
updateUser.setId(1L);
updateUser.setEmail("newemail@example.com");
userMapper.updateUser(updateUser); // 只更新 email
// 6. 批量删除
userMapper.deleteByIds(Arrays.asList(1L, 2L, 3L));
}
}
八、XML 与注解如何选择
MyBatis 同时支持 XML 和注解两种方式来编写 SQL 映射。很多开发者会纠结到底该用哪种,下面从多个维度进行对比分析。
8.1 注解方式快速回顾
先看一个用注解实现的 Mapper 接口:
java
@Mapper
public interface UserMapper {
// 简单查询
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
// 简单插入
@Insert("INSERT INTO user (name, age) VALUES (#{name}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
// 简单更新
@Update("UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}")
int updateUser(User user);
// 简单删除
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteById(Long id);
// 动态 SQL(注解方式,用 <script> 包裹)
@Select("<script>" +
"SELECT * FROM user" +
"<where>" +
" <if test='name != null'>AND name = #{name}</if>" +
" <if test='age != null'>AND age = #{age}</if>" +
"</where>" +
"</script>")
List<User> selectByCondition(UserQuery query);
}
8.2 全方位对比
|
对比维度
|
XML 方式
|
注解方式
|
| --- | --- | --- |
| SQL 可读性 |
高,SQL 格式清晰,有语法高亮
|
低,长 SQL 挤在一行字符串里
|
| 动态 SQL |
原生支持,标签语法直观
|
需用 <script> 包裹,写在字符串中,无 IDE 提示
|
| 复杂查询 |
多表关联、子查询清晰可读
|
嵌套复杂 SQL 时代码几乎不可维护
|
| SQL 与代码分离 |
完全分离,修改 SQL 不动 Java 代码
|
SQL 和 Java 代码混在一起
|
| IDE 支持 |
XML 中 SQL 有语法高亮和校验
|
字符串中的 SQL 基本无提示
|
| 维护成本 |
SQL 多了容易文件膨胀,但有 <sql> 复用
|
简单场景维护方便,复杂场景维护困难
|
| 版本管理 |
XML 文件独立,git diff 清晰
|
SQL 修改和 Java 代码混在同一个 diff 中
|
| 学习门槛 |
需要了解 XML 和 MyBatis 标签语法
|
零额外学习成本
|
| 执行性能 |
完全相同
|
完全相同
|
8.3 选择决策树
bash
你的 SQL 复杂吗?
│
├── 不复杂(单表 CRUD,无动态条件)
│ └── ✅ 用注解,简洁高效
│
├── 有动态 SQL(if/where/foreach 等)
│ └── ✅ 用 XML,注解写动态 SQL 痛苦且易错
│
├── 多表关联查询
│ └── ✅ 用 XML,可读性和维护性远胜注解
│
└── 需要 resultMap 映射(一对一、一对多)
└── ✅ 用 XML,注解方式实现复杂映射非常笨拙
8.4 业界主流做法
大多数企业项目的实际做法是两者结合使用:
java
@Mapper
public interface UserMapper {
// 简单查询 → 注解
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
// 简单插入 → 注解
@Insert("INSERT INTO user (name, age) VALUES (#{name}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
// 复杂查询 → XML(方法签名声明在这里,SQL 写在 XML 中)
List<User> selectUserList(UserQuery query);
// 复杂关联查询 → XML
User selectUserWithDept(Long id);
// 批量操作 → XML
int batchInsert(@Param("list") List<User> users);
}
|
场景
|
推荐方式
|
理由
|
| --- | --- | --- |
|
单表简单查询/插入/删除
| 注解 |
一目了然,无需跳转 XML
|
|
带条件的动态查询
| XML | <if>
<where> 标签比字符串拼接清晰百倍
|
|
多表关联 / 子查询
| XML |
复杂 SQL 在字符串中无法维护
|
|
需要 resultMap 映射
| XML |
注解方式对复杂映射支持很差
|
|
批量操作
| XML | <foreach>
是批量操作的最佳选择
|
|
需要 SQL 片段复用
| XML | <sql>
<include> 天然支持复用
|
8.5 使用 MyBatis-Plus 时的最佳实践
如果项目使用了 MyBatis-Plus,情况又有所不同:
bash
简单 CRUD → MyBatis-Plus 内置方法(零 SQL、零 XML、零注解)
中等复杂 → MyBatis-Plus 条件构造器(Java API,无需 XML)
高度复杂 → XML 自定义 SQL
java
// 简单 CRUD:直接继承 BaseMapper,什么都不用写
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
// 使用时
userMapper.selectById(1L); // 单条查询
userMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L)); // 批量查询
userMapper.insert(user); // 插入
userMapper.updateById(user); // 更新
userMapper.deleteById(1L); // 删除
// 中等复杂:条件构造器
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "张")
.between("age", 18, 30)
.eq("status", 1)
.orderByDesc("create_time");
List<User> users = userMapper.selectList(wrapper);
// 高度复杂:自定义 XML
List<User> users = userMapper.selectComplexReport(params);
总结:简单场景用注解或框架内置方法,复杂场景用 XML,不纠结、不教条,按需选择。
九、进阶技巧与最佳实践
9.1 防止全表更新
MyBatis-Plus 提供了防全表更新插件,但理解原理很重要:
xml
<!-- 危险!缺少 WHERE 条件会导致全表更新 -->
<update id="updateAll">
UPDATE user SET status = #{status}
<!-- 忘了写 WHERE ... -->
</update>
MyBatis-Plus 的防全表更新插件:
java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
9.2 #{} 中指定 JDBC 类型
xml
<!-- 指定 JDBC 类型可以避免某些数据库驱动的兼容问题 -->
#{age, jdbcType=INTEGER}
#{name, jdbcType=VARCHAR}
9.3 嵌套查询的两种方式对比
|
方式
|
优点
|
缺点
|
| --- | --- | --- |
|
嵌套结果(JOIN)
|
一次 SQL 完成,性能好
|
SQL 较长,resultMap 较复杂
|
|
嵌套查询(分步)
|
SQL 简洁,可延迟加载
|
N+1 查询问题
|
十、常见问题排查
|
问题
|
原因
|
解决方案
|
| --- | --- | --- |
| BindingException: Invalid bound statement |
XML 的 namespace 或 id 与接口不匹配
|
检查 namespace 是否为接口全限定名,id 是否与方法名一致
|
|
XML 修改后不生效
|
XML 没有编译到 target 目录
|
检查文件路径是否在 resources 对应包下,或 clean 后重新构建
|
| Parameter 'xxx' not found |
参数名不匹配
|
给方法参数加 @Param("xxx") 注解
|
|
字段值为 null
|
字段名映射不正确
|
检查 resultMap 或开启驼峰映射
|
|
动态 SQL 条件不生效
|
OGNL 表达式写错
|
字串用 != '',数值只用 != null
|
|
XML 中 < 导致解析错误
|
特殊符号未转义
|
使用 < 转义或 CDATA 包裹
|
|
分页插件不生效
|
未配置分页插件
|
检查是否注册了 MybatisPlusInterceptor
|
| $
导致 SQL 注入
|
错误使用了 ${}
|
参数值应统一用 #{}
|
十一、速记口诀
bash
查询 SELECT,插入 INSERT,
更新 UPDATE,删除 DELETE。
参数统一用 #{号井花括号},防注入保安全。
${} 只用于动态列名和排序,
滥用必有 SQL 注入风险。
特殊符号莫忘记,CDATA 来帮你。
< > & 直接写,解析报错找转义。
动态条件 <where> 帮你加 WHERE,
动态更新 <set> 帮你去逗号。
<if> 做判断,<choose> 做多选,
<foreach> 循环,批量操作显神通。
<sql> 抽片段,<include> 做复用,
代码整洁不重复。
<association> 一对一关联映射,
<collection> 一对多关联映射。
主键标签 <id>,字段标签 <result>,
<resultMap> 让映射不再困扰。
分页查询选方案,物理分页是首选。
LIMIT 最基础,插件更省心。
简单查询用注解,复杂查询用 XML,
灵活搭配效率高,不纠结来不教条。
十二、结束语
MyBatis XML 本质上并不复杂------它的核心思想就是把 SQL 写在 XML 文件里,然后通过一些框架标签实现参数映射和动态生成。但正是这种简单的设计,赋予了它极高的灵活性:你可以在 XML 中写任意复杂的 SQL,可以精确控制每一条语句的执行行为,可以根据业务条件动态组装查询逻辑。
掌握了本文介绍的这些知识点------从基础的四大标签、参数占位符,到动态 SQL 的七种标签,再到特殊符号处理、分页查询方案、结果映射技巧,以及 XML 与注解的选择策略------你已经具备了应对绝大多数业务场景的能力。
剩下的就是在实际项目中多写、多练、多踩坑------真正的熟练来自于实践,而非记忆。
