乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

script 元素:在注解映射器中启用动态 SQL

导学

在前面的章节中,我们学习了 XML 映射器中的动态 SQL——<if>、<where>、<set>、<foreach> 等标签让 SQL 具备了条件判断和循环能力。然而,很多开发者更偏爱注解方式编写 Mapper,因为它省去了 XML 文件的维护成本,代码更加紧凑直观。

但注解方式有一个天然的限制:Java 注解的值是字符串常量,无法直接嵌入 XML 标签。如果你尝试在 @Select 中写 <if>,MyBatis 会把它当成纯文本 SQL 的一部分,导致语法错误。

script 元素正是为了解决这一痛点而生。它允许你在注解的字符串值中,用 <script> 标签包裹标准的动态 SQL 片段,从而让注解映射器也能享受到动态 SQL 的全部能力。

定义与作用

script 是 MyBatis 提供的一个特殊 XML 根标签,专门用于注解映射器的字符串值中。它的作用是将包裹在内的内容识别为动态 SQL 片段,交由 MyBatis 的动态 SQL 引擎解析,而不是当作普通 SQL 文本直接提交给数据库。

当 MyBatis 解析到带有 <script> 的注解字符串时,会将其交给 XMLLanguageDriver 处理,内部的 <if>、<where>、<set>、<foreach> 等标签会被正常解析并生成最终 SQL。

核心原理

解析流程

关键说明

组件职责
RawLanguageDriver处理不含 <script> 的注解字符串,直接作为静态 SQL
XMLLanguageDriver处理含 <script> 的注解字符串,解析动态 SQL 标签
DynamicSqlSource根据运行时参数,通过 OGNL 计算动态标签条件,生成最终 SQL
OgnlCache缓存 OGNL 表达式解析结果,避免重复编译,提升性能

MyBatis 在启动时会为每个注解方法选择合适的 LanguageDriver。一旦检测到字符串以 <script> 开头,就会自动切换到 XML 解析模式,因此开发者无需额外配置。

script 元素属性说明

属性是否必填说明
无显式属性—<script> 本身不需要任何属性,它只是一个标识根标签
内部可嵌套标签—<if>、<choose>、<when>、<otherwise>、<where>、<set>、<trim>、<foreach>、<bind> 等所有 XML 动态 SQL 标签

示例一:@Select 中使用 script + if 实现动态条件查询

场景说明

需要根据学生姓名和专业的可选条件查询学生信息。当传入参数中有 name 或 major 时,SQL 自动追加 WHERE 条件;当两个参数都为空时,查询全部学生。

操作前数据

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

Mapper 接口代码

package com.flying.mapper;

import com.flying.entity.Student;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface StudentMapper {

    /**
     * 根据可选条件动态查询学生
     * @param name  学生姓名(可选)
     * @param major 专业(可选)
     * @return 符合条件的学生列表
     */
    @Select("<script>"
          + "SELECT id, name, age, major, score FROM student"
          + "<where>"
          + "  <if test='name != null'> AND name = #{name} </if>"
          + "  <if test='major != null'> AND major = #{major} </if>"
          + "</where>"
          + "</script>")
    List<Student> findByCondition(@Param("name") String name, @Param("major") String major);
}

生成的 SQL 语句展示

场景 A:两个参数都传入

传入参数:name = "大翔", major = "计算机科学"

SELECT id, name, age, major, score FROM student WHERE name = ? AND major = ?

场景 B:只传入 name

传入参数:name = "白歌", major = null

SELECT id, name, age, major, score FROM student WHERE name = ?

场景 C:两个参数都为 null

传入参数:name = null, major = null

SELECT id, name, age, major, score FROM student

执行结果

场景 A 结果:

idnameagemajorscore
1大翔22计算机科学95.50

场景 B 结果:

idnameagemajorscore
2白歌21软件工程88.00

场景 C 结果:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

分析

<where> 标签在这里起到了关键作用:当内部没有任何 <if> 条件满足时,它不会生成 WHERE 关键字;当有条件满足时,它会自动处理开头的 AND 或 OR,确保 SQL 语法正确。这避免了手动拼接 SQL 时容易出现的 "WHERE AND" 语法错误。

示例二:@Update 中使用 script + set 实现动态更新

场景说明

更新学生信息时,只更新传入的非空字段。例如,只想修改小崔的年龄,而不修改其他字段,此时 SQL 应该只包含 age 的更新逻辑。

操作前数据

idnameagemajorscore
3小崔20计算机科学92.00

Mapper 接口代码

package com.flying.mapper;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface StudentMapper {

    /**
     * 动态更新学生信息,只更新非空字段
     * @param id    学生ID(必填)
     * @param name  姓名(可选)
     * @param age   年龄(可选)
     * @param major 专业(可选)
     * @param score 成绩(可选)
     * @return 影响的行数
     */
    @Update("<script>"
          + "UPDATE student"
          + "<set>"
          + "  <if test='name != null'> name = #{name}, </if>"
          + "  <if test='age != null'> age = #{age}, </if>"
          + "  <if test='major != null'> major = #{major}, </if>"
          + "  <if test='score != null'> score = #{score}, </if>"
          + "</set>"
          + "WHERE id = #{id}"
          + "</script>")
    int updateStudentDynamic(@Param("id") Integer id,
                             @Param("name") String name,
                             @Param("age") Integer age,
                             @Param("major") String major,
                             @Param("score") Double score);
}

生成的 SQL 语句展示

场景 A:只更新年龄

传入参数:id = 3, age = 21,其余为 null

UPDATE student SET age = ? WHERE id = ?

场景 B:更新姓名和专业

传入参数:id = 3, name = "小崔崔", major = "人工智能"

UPDATE student SET name = ?, major = ? WHERE id = ?

执行结果

场景 A 执行后:

idnameagemajorscore
3小崔21计算机科学92.00

场景 B 执行后:

idnameagemajorscore
3小崔崔21人工智能92.00

分析

<set> 标签与 <where> 类似,它会自动处理末尾多余的逗号。如果没有字段需要更新,<set> 不会生成 SET 关键字,但此时 WHERE id = #{id} 仍然存在,SQL 会变成 UPDATE student WHERE id = ?,这在大多数数据库中是语法错误的。因此,动态更新时通常至少保证有一个字段会被更新,或者在业务层做校验。

示例三:@Insert 中使用 script + foreach 实现批量插入

场景说明

需要一次性插入多条学生记录。使用 <foreach> 遍历传入的列表,生成多组 VALUES 子句,实现批量插入,减少与数据库的交互次数。

操作前数据

当前 student 表已有 5 条记录(id 从 1 到 5)。

Mapper 接口代码

package com.flying.mapper;

import com.flying.entity.Student;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;

public interface StudentMapper {

    /**
     * 批量插入学生记录
     * @param students 学生列表
     * @return 影响的行数
     */
    @Insert("<script>"
          + "INSERT INTO student (name, age, major, score) VALUES"
          + "<foreach collection='students' item='s' separator=','>"
          + "  (#{s.name}, #{s.age}, #{s.major}, #{s.score})"
          + "</foreach>"
          + "</script>")
    int batchInsertStudents(@Param("students") List<Student> students);
}

调用代码

List<Student> newStudents = Arrays.asList(
    new Student("大翔", 23, "数据科学", 91.5),
    new Student("白歌", 22, "网络安全", 89.0),
    new Student("小崔", 21, "云计算", 93.5)
);

int rows = mapper.batchInsertStudents(newStudents);
System.out.println("批量插入成功,影响行数:" + rows);

生成的 SQL 语句展示

INSERT INTO student (name, age, major, score) VALUES
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?)

实际参数绑定:

  • 第一组:大翔, 23, 数据科学, 91.5
  • 第二组:白歌, 22, 网络安全, 89.0
  • 第三组:小崔, 21, 云计算, 93.5

执行结果

插入后 student 表数据:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00
6大翔23数据科学91.50
7白歌22网络安全89.00
8小崔21云计算93.50

分析

<foreach> 标签的 collection 属性对应传入的参数名(这里是 students),item 定义了每次迭代的局部变量名(这里是 s),separator 指定了每组之间的分隔符(这里是逗号)。注意在注解中引用列表内对象的属性时,使用 #{s.name} 的形式,其中 s 必须与 item 属性一致。

批量插入相比循环单条插入,性能提升显著。在 MySQL 5.7 中,单次批量插入的 VALUES 组数建议控制在 1000 组以内,避免 SQL 过长导致解析开销过大。

script 注解方式与 XML 方式的对比

对比维度注解 + script 方式XML 方式
代码位置与 Java 接口在一起,紧凑独立的 XML 文件,分散
可读性字符串拼接,复杂 SQL 可读性下降原生 XML 标签,结构清晰
IDE 支持SQL 语法高亮较弱部分 IDE 支持 XML 内 SQL 高亮
动态 SQL 能力完整支持,与 XML 等价完整支持
维护成本修改 SQL 需改 Java 文件,重新编译修改 XML 即可,无需编译
适用场景简单到中等复杂度的 SQL复杂 SQL、团队有 DBA 参与

选择建议

  • 如果项目以注解方式为主,且 SQL 复杂度不高,使用 <script> 是最佳实践,无需为了少量动态 SQL 引入 XML。
  • 如果 SQL 非常长(超过 20 行),或者需要频繁由非 Java 开发人员调整,建议改用 XML 映射器。
  • 混合使用也是常见做法:简单 CRUD 用注解,复杂报表查询用 XML。

易错场景

错误场景错误表现正确做法
忘记加 <script> 标签@Select 中的 <if> 被当作纯文本,数据库报 SQL 语法错误所有动态 SQL 标签必须用 <script> 包裹
忘记用 @Param 注解参数OGNL 表达式无法识别参数名,报 There is no getter for property named 'xxx'多参数时必须加 @Param,单参数可不加
特殊字符未用 CDATA 包裹< 被 XML 解析器误认为是标签开头,报 XML 解析错误在 <script> 内使用 <![CDATA[ < ]]> 包裹比较运算符
< 和 > 在 OGNL 表达式中直接使用test='age < 18' 导致 XML 解析失败使用 test='age &lt; 18' 或改用 &gt; / &lt;,或者将表达式放入 CDATA
collection 参数名与 @Param 不一致foreach 找不到集合,报 There is no getter for property named 'students'确保 collection='students' 与 @Param("students") 一致

CDATA 使用示例

当 SQL 中需要用到 < 或 > 时,必须使用 CDATA 包裹:

@Select("<script>"
      + "<![CDATA["
      + "SELECT * FROM student WHERE age < #{maxAge} AND score > #{minScore}"
      + "]]>"
      + "</script>")
List<Student> findByRange(@Param("maxAge") int maxAge, @Param("minScore") double minScore);

或者使用 XML 实体转义:

@Select("<script>"
      + "SELECT * FROM student WHERE age &lt; #{maxAge} AND score &gt; #{minScore}"
      + "</script>")

面试考点

Q1:为什么注解方式中不能直接用 <if>,必须用 <script> 包裹?

A:MyBatis 的注解字符串默认由 RawLanguageDriver 处理,直接作为静态 SQL 提交给数据库。只有检测到 <script> 标签时,才会切换到 XMLLanguageDriver,将内容当作 XML 动态 SQL 解析。没有 <script> 标识,MyBatis 不会启动动态 SQL 引擎。

Q2:<script> 方式与 XML 方式的动态 SQL,在运行时性能上有差异吗?

A:没有本质差异。两者最终都会生成 DynamicSqlSource,由相同的 OGNL 引擎解析参数并生成最终 SQL。性能差异主要来自字符串拼接和 XML 解析的启动开销,但在运行时缓存机制(OgnlCache)的作用下,这一差异可以忽略不计。

Q3:在 <script> 中使用了 <where>,但生成的 SQL 仍然多出了一个 AND,可能是什么原因?

A:<where> 标签只能自动处理开头的 AND 或 OR。如果 <if> 条件写在中间位置,且前面的静态 SQL 已经包含了 WHERE 关键字,那么 <where> 不会生效。正确的做法是让 <where> 作为 WHERE 关键字的唯一来源,不要在 <where> 外部再写 WHERE。

Q4:批量插入时,如何控制单次插入的数据量,避免 SQL 过长?

A:在业务层对列表进行分批处理,例如每 500 或 1000 条调用一次 batchInsertStudents。MySQL 5.7 对单条 SQL 的长度有限制(max_allowed_packet 默认 4MB),超过限制会导致连接断开。分批插入既能避免此问题,也能更好地控制事务粒度。

小结

script 元素是连接注解映射器与动态 SQL 世界的桥梁。通过 <script> 标签,开发者可以在 @Select、@Insert、@Update、@Delete 中完整使用 MyBatis 的所有动态 SQL 能力,包括条件判断、动态更新、批量操作等。

核心要点回顾:

  • <script> 是注解方式使用动态 SQL 的唯一入口,不可省略。
  • 多参数时必须使用 @Param 注解,确保 OGNL 表达式能正确识别参数名。
  • SQL 中的特殊字符(<、>)需用 CDATA 或 XML 实体转义,避免解析错误。
  • 注解方式与 XML 方式在运行时能力完全等价,选择哪种方式取决于团队的开发习惯和 SQL 的复杂度。

下一章引子

掌握了 script 元素后,注解映射器已经具备了完整的动态 SQL 能力。但在实际企业开发中,一个常见的挑战是:同一套代码需要部署到不同的数据库环境中——开发用 MySQL,生产用 Oracle,测试用 SQL Server。不同数据库的 SQL 语法差异(如分页、字符串拼接、自增主键)该如何优雅处理?

下一节将介绍 _databaseId 与动态 SQL 的多数据库支持,学习如何在一套 Mapper 中根据数据库类型自动选择对应的 SQL 片段,实现真正的"一份代码,多处运行"。

上一页
bind
下一页
_databaseId 与动态 SQL 的多数据库支持