深入浅出 MyBatis XML:从入门到精通

一、前言

在 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>

  • • 每个标签有哪些属性:idresultTypeparameterType

  • • 标签之间的嵌套关系:<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 个转义实体:

|

字符

|

转义写法

|

示例

|

| --- | --- | --- |

| < | &lt;** | age &lt; 60 |

| > | &gt; | age &gt; 18 |

| & | &amp; | name &amp; age |

| " | &quot; |

用于属性值中

|

| ' | ' |

用于属性值中

|

xml 复制代码
<select id="selectByAgeRange" resultType="User">
    SELECT * FROM user
    WHERE age &gt;= #{ageMin} AND age &lt;= #{ageMax}
</select>

优点:标准 XML 写法,所有 XML 工具都支持。

缺点 :SQL 可读性下降,尤其是条件多的时候满屏 &gt;``&lt;,维护困难。

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 三种方案对比

|

方案

|

可读性

|

安全性

|

适用场景

|

| --- | --- | --- | --- |

|

转义字符 &gt;``&lt;

|

|

|

特殊符号少且分散时

|

|

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

|

自动去掉开头多余的 ANDOR

|

|

内部无内容

|

整个 <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>

当只传入 nameemail 时,生成的 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 javaTypecollection 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 中 < 导致解析错误

|

特殊符号未转义

|

使用 &lt; 转义或 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 与注解的选择策略------你已经具备了应对绝大多数业务场景的能力。

剩下的就是在实际项目中多写、多练、多踩坑------真正的熟练来自于实践,而非记忆

相关推荐
摇滚侠16 小时前
Codebuddy 官网 Codebuddy IntelliJ IDEA 插件 阅读笔记 2
java·笔记·intellij-idea
snow@li16 小时前
MyBatis:动态 SQL 全景梳理
数据库·sql·mybatis
gugucoding17 小时前
24. 【Java】枚举:更安全的常量
java·开发语言
Kyrie_kk17 小时前
Java--BigInteger 超大整数计算
java·算法
w678200717 小时前
,攻击者通过构造特殊的XML使其包含恶意外部实体。外部实体可以为服务器敏感文件,也可以为网络请求等,之后利用方式类似于文件包含和SS ...
xml·服务器·网络
开发者联盟league17 小时前
Java 通过 JNA 调用 C++ DLL:关键流程总结
java·c++·jna
z落落17 小时前
T-SQL 事务(Transaction)
java·数据库·sql
库克克18 小时前
【C++】智能指针
java·开发语言·c++
lemon_sjdk18 小时前
Spring WebFlux 响应式编程深度解析:从架构选型到核心抽象
java·spring·架构
就改了18 小时前
Mybatis快速入门大全(详细版)
java·spring boot·mybatis