一、动态 SQL 简介
1.1 什么是动态 SQL
动态 SQL 是 MyBatis 框架提供的一种强大的 SQL 构建机制,它允许开发者在 XML 映射文件或注解中,根据运行时的参数条件动态地构建 SQL 语句。这种机制通过一系列特殊的标签(如 if、choose、where、set 等)来实现条件判断和 SQL 片段拼接。
在实际开发中,动态 SQL 特别适用于以下典型场景:
- 多条件组合查询:例如用户管理系统的搜索功能,可能包含用户名模糊查询、状态筛选、注册时间范围等多个可选条件
- 批量操作:如根据传入的 ID 集合批量更新或删除记录
- 可选字段更新:只更新传入的非空字段,避免全字段覆盖
示例:一个简单的用户查询场景
<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="username != null">
AND username LIKE CONCAT('%',#{username},'%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
1.2 动态 SQL 的作用
1.2.1 减少代码冗余
传统 JDBC 开发中,要为不同的条件组合编写多个 SQL 语句变体。例如有 3 个可选查询条件时,理论上需要准备 8 种 SQL 组合(2^3)。而动态 SQL 通过条件标签只需维护一个基础 SQL 模板,由框架根据参数自动生成最终 SQL,显著减少了代码量。
1.2.2 提高 SQL 灵活性
动态 SQL 支持多种控制结构:
- 条件分支:通过
<if>、<choose>实现 - 循环结构:通过
<foreach>处理集合参数 - SQL 片段:通过
<sql>定义可重用的 SQL 块 - 智能处理:
<where>自动处理前缀 AND/OR,<set>处理更新语句中的逗号
1.2.3 增强 SQL 安全性
MyBatis 动态 SQL 采用预编译机制处理所有参数:
- 参数值都通过
#{}语法绑定,防止 SQL 注入 - 自动处理特殊字符转义
- 类型安全校验,避免类型不匹配错误
对比示例:
// 不安全的字符串拼接(存在SQL注入风险)
String sql = "SELECT * FROM users WHERE id = " + userInput;
// 安全的使用方式(MyBatis自动防护)
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(@Param("id") Integer id);
此外,动态 SQL 还支持 OGNL 表达式,可以实现更复杂的条件判断逻辑,如:
<if test="@com.example.util.MyUtils@isValid(status)">
AND status = #{status}
</if>
二、常用动态 SQL 标签详解
2.1 if 标签
if标签是 MyBatis 中最基础、最常用的动态 SQL 标签,它用于根据条件判断是否将其包含的 SQL 片段添加到最终生成的 SQL 语句中。
语法格式
<if test="条件表达式">
SQL片段
</if>
其中,test属性用于指定条件表达式,表达式的结果为true时,会将if标签内的 SQL 片段拼接到主 SQL 中;结果为false时,则忽略该 SQL 片段。
代码示例:多条件查询
假设我们有一个用户表user,需要根据用户名(username)和年龄(age)进行多条件查询,当用户名或年龄为空时,忽略对应的查询条件。
Mapper 接口
public interface UserMapper {
List<User> selectUserByCondition(User user);
}
XML 映射文件
<select id="selectUserByCondition" parameterType="com.example.entity.User" resultType="com.example.entity.User">
SELECT id, username, age, email
FROM user
WHERE 1=1
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</select>
详细说明:
WHERE 1=1的作用是避免当所有if条件都不满足时,生成的 SQL 语句中出现多余的WHERE关键字- 当
username不为空且不为空字符串时,会添加用户名模糊查询的条件 - 当
age不为空时,会添加年龄精确查询的条件 - 使用
CONCAT函数实现模糊查询,兼容不同数据库的语法差异
应用场景:
- 后台管理系统的用户查询功能
- 数据报表的筛选条件
- API接口中的可选参数查询
2.2 where 标签
在使用if标签进行多条件查询时,我们通常需要在WHERE子句后添加1=1来避免 SQL 语法错误。而where标签可以自动处理这种情况,它会智能地判断是否需要添加WHERE关键字,并且会去除多余的AND或OR关键字。
语法格式
<where>
<if test="条件表达式1">
AND/OR SQL片段1
</if>
<if test="条件表达式2">
AND/OR SQL片段2
</if>
...
</where>
代码示例:优化多条件查询
使用where标签对上面的多条件查询示例进行优化:
XML 映射文件
<select id="selectUserByCondition" parameterType="com.example.entity.User" resultType="com.example.entity.User">
SELECT id, username, age, email
FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
优化点说明:
- 移除了
WHERE 1=1的固定条件 where标签会自动处理以下情况:- 当所有
if条件都不满足时,不会生成WHERE关键字 - 如果有条件满足,会自动添加
WHERE关键字 - 会智能去除第一个 SQL 片段前多余的
AND或OR
- 当所有
注意事项:
- 每个条件语句前仍需保留
AND或OR where标签只能用于WHERE子句部分- 适用于
SELECT、UPDATE、DELETE等需要WHERE条件的语句
2.3 choose、when、otherwise 标签
choose、when、otherwise标签组合使用,类似于 Java 中的if-else if-else语句,用于实现多条件分支判断。choose标签是父标签,when标签用于指定分支条件,otherwise标签用于指定默认分支(当所有when条件都不满足时执行)。
语法格式
<choose>
<when test="条件表达式1">
SQL片段1
</when>
<when test="条件表达式2">
SQL片段2
</when>
...
<otherwise>
SQL片段n(默认分支)
</otherwise>
</choose>
代码示例:按优先级查询
假设我们需要查询用户信息,查询优先级为:先根据用户 ID(id)查询,如果 ID 为空,则根据用户名(username)查询,如果用户名也为空,则查询年龄大于 18 岁的用户。
XML 映射文件
<select id="selectUserByPriority" parameterType="com.example.entity.User" resultType="com.example.entity.User">
SELECT id, username, age, email
FROM user
<where>
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="username != null and username != ''">
username LIKE CONCAT('%', #{username}, '%')
</when>
<otherwise>
age > 18
</otherwise>
</choose>
</where>
</select>
执行逻辑说明:
- 会按照
when标签的顺序依次判断条件 - 只要有一个
when条件满足,就会执行对应的 SQL 片段,并忽略后续的when和otherwise标签 - 如果所有
when条件都不满足,则执行otherwise标签内的 SQL 片段 - 与
if标签不同,choose结构只会选择一个分支执行
典型应用场景:
- 多级查询优先级设置
- 条件互斥的查询需求
- 默认查询条件的设置
2.4 set 标签
set标签主要用于更新操作(UPDATE语句),它可以自动处理 SQL 语句中多余的逗号(,),并智能地添加SET关键字。
语法格式
<update id="updateUser" parameterType="com.example.entity.User">
UPDATE user
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="email != null and email != ''">
email = #{email}
</if>
</set>
WHERE id = #{id}
</update>
代码示例:动态更新用户信息
假设我们需要更新用户信息,只更新不为空的字段。使用set标签可以避免因某些字段为空而导致的 SQL 语法错误。
Mapper 接口
public interface UserMapper {
int updateUser(User user);
}
XML 映射文件
<update id="updateUser" parameterType="com.example.entity.User">
UPDATE user
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="email != null and email != ''">
email = #{email}
</if>
</set>
WHERE id = #{id}
</update>
功能特点:
- 自动添加
SET关键字 - 智能去除最后一个 SQL 片段后的多余逗号
- 如果所有
if条件都不满足,会生成错误的 SQL(UPDATE user SET WHERE id = ?) - 在实际使用中,需要确保至少有一个字段需要更新,或者在代码中进行判断
最佳实践建议:
- 在业务层先检查更新字段是否为空
- 对于必填字段,不要放在
set标签内 - 可以结合
<trim>标签实现更复杂的更新逻辑 - 适用于部分更新、选择性更新的场景
常见应用:
- 用户信息修改功能
- 数据状态更新
- 批量字段更新操作
2.5 foreach 标签详解
foreach标签是MyBatis中用于循环遍历集合或数组的强大标签,它在处理批量操作时特别有用。通过foreach标签,我们可以轻松实现IN查询、批量插入、批量更新等常见数据库操作。
语法格式详解
<foreach collection="集合/数组参数名"
item="遍历元素别名"
index="索引别名"
open="开始符号"
close="结束符号"
separator="分隔符">
#{遍历元素别名}
</foreach>
属性详细说明
-
collection:
- 指定要遍历的集合或数组的参数名
- 特殊默认值:
- List集合:默认参数名为"list"
- 数组:默认参数名为"array"
- Map集合或自定义对象中的集合属性:直接指定属性名
- 示例:
collection="idList"或collection="array"
-
item:
- 定义遍历过程中当前元素的引用名称
- 在标签内部通过
#{别名}引用元素值 - 示例:
item="id",则在循环体内使用#{id}
-
index(可选):
- 对于List:表示当前元素的索引(从0开始)
- 对于Map:表示键(key)
- 对于数组:表示数组下标
- 示例:
index="i",可用于动态生成字段名
-
open(可选):
- 循环开始前拼接的字符串
- 示例:
open="("会在循环前添加左括号
-
close(可选):
- 循环结束后拼接的字符串
- 示例:
close=")"会在循环后添加右括号
-
separator(可选):
- 元素之间的分隔符
- 示例:
separator=","会在每个元素间添加逗号
应用场景与代码示例
场景1:批量查询(IN查询)
当需要根据多个ID查询用户信息时:
// Mapper接口
public interface UserMapper {
List<User> selectUserByIds(@Param("ids") List<Integer> idList);
}
<select id="selectUserByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
执行时若传入ids=[1,2,3],生成的SQL为:
SELECT * FROM user WHERE id IN (1,2,3)
场景2:批量插入
一次性插入多条用户记录:
// Mapper接口
public interface UserMapper {
int batchInsert(@Param("users") List<User> userList);
}
<insert id="batchInsert">
INSERT INTO user (name, age) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
传入两个用户对象时,生成的SQL:
INSERT INTO user (name, age) VALUES
('张三', 20),
('李四', 22)
场景3:批量更新
使用CASE WHEN实现批量更新:
<update id="batchUpdate">
UPDATE user
SET name = CASE id
<foreach collection="users" item="user">
WHEN #{user.id} THEN #{user.name}
</foreach>
END
WHERE id IN
<foreach collection="users" item="user" open="(" close=")" separator=",">
#{user.id}
</foreach>
</update>
注意事项
- 大数据量时(超过1000条),应考虑分批处理
- Oracle数据库IN查询参数超过1000个时会报错
- 使用@Param注解可以自定义collection属性值
- 嵌套foreach时要注意item命名冲突
2.6 trim 标签详解
trim标签提供了强大的SQL片段处理能力,可以灵活控制SQL语句的拼接方式。它实际上是where和set标签的底层实现。
语法结构
<trim prefix="前缀"
suffix="后缀"
prefixOverrides="需要去除的前缀"
suffixOverrides="需要去除的后缀">
SQL片段
</trim>
属性精解
-
prefix:
- 在整个trim内容前添加的字符串
- 示例:
prefix="WHERE"会添加WHERE关键字
-
suffix:
- 在整个trim内容后添加的字符串
- 示例:
suffix=";"会添加分号
-
prefixOverrides:
- 去除内容开头指定的字符串(多个用|分隔)
- 示例:
prefixOverrides="AND|OR"会去除开头多余的AND或OR
-
suffixOverrides:
- 去除内容结尾指定的字符串
- 示例:
suffixOverrides=","会去除末尾多余的逗号
实际应用示例
示例1:自定义WHERE条件
实现类似where标签的功能:
<select id="findUsers" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="name != null">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</trim>
</select>
执行逻辑:
- 当有条件成立时,添加WHERE关键字
- 自动去除第一个条件前多余的AND/OR
- 若无任何条件,WHERE关键字也不会出现
示例2:动态UPDATE语句
替代set标签的实现:
<update id="updateUser">
UPDATE user
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
</trim>
WHERE id = #{id}
</update>
特点:
- 自动添加SET关键字
- 智能去除最后一个多余的逗号
- 若无任何更新字段,SET不会出现
示例3:复杂条件拼接
实现多条件组合查询:
<select id="search" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND|OR" suffixOverrides="AND|OR">
<if test="param1 != null">
(col1 = #{param1} OR col2 = #{param1}) AND
</if>
<if test="param2 != null">
col3 = #{param2} OR
</if>
</trim>
</select>
优势:
- 同时处理前缀和后缀的多余关键字
- 支持复杂的条件组合
- 自动处理边界情况
高级技巧
-
嵌套使用:
<trim prefix="(" suffix=")" prefixOverrides="AND|OR"> <trim prefixOverrides="AND|OR"> <!-- 条件内容 --> </trim> </trim> -
动态表名:
<trim prefix="FROM" prefixOverrides=","> <if test="table1">, table1</if> <if test="table2">, table2</if> </trim> -
批量操作组合:
<insert id="batchInsert"> <trim prefix="INSERT ALL" suffix="SELECT 1 FROM DUAL"> <foreach collection="list" item="item"> INTO table VALUES (#{item.val1}, #{item.val2}) </foreach> </trim> </insert>
通过灵活组合这些属性,trim标签几乎可以满足所有动态SQL拼接的需求,是MyBatis动态SQL中最强大的基础标签之一。
三、动态 SQL 的高级用法
3.1 结合 OGNL 表达式
MyBatis 动态 SQL 的 test 属性支持 OGNL(Object-Graph Navigation Language)表达式,这是一种功能强大的表达式语言,可以通过对象图导航的方式进行复杂条件判断。OGNL 表达式在 MyBatis 中的应用使得动态 SQL 的条件判断更加灵活和强大,能够处理各种复杂的业务场景。
示例 1:判断集合是否为空
<if test="userList != null and userList.size() > 0">
<!-- 集合不为空时执行的SQL片段 -->
AND user_id IN
<foreach collection="userList" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</if>
说明:
userList != null确保集合对象不为空。userList.size() > 0确保集合中至少有一个元素。- 结合
foreach标签,可以动态生成 IN 子句,适用于批量查询场景。
示例 2:判断字符串是否包含指定字符
<if test="username != null and username.contains('zhang')">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
说明:
username.contains('zhang')使用 OGNL 的字符串方法判断是否包含子串。- 适用于模糊查询场景,比如搜索用户名中包含特定关键字的用户。
示例 3:判断参数是否为指定枚举值
假设我们有一个枚举类 UserStatus,用于表示用户状态:
public enum UserStatus {
ACTIVE, INACTIVE, LOCKED
}
在动态 SQL 中判断用户状态是否为 ACTIVE:
<if test="status == @com.example.enums.UserStatus@ACTIVE">
AND status = #{status}
</if>
说明:
@com.example.enums.UserStatus@ACTIVE是 OGNL 的枚举值引用语法,通过全限定类名访问枚举值。- 适用于状态过滤的场景,比如只查询活跃用户。
3.2 动态 SQL 与 SQL 片段(sql 标签)结合
当多个 SQL 语句中包含相同的 SQL 片段时,可以使用 <sql> 标签将其抽取为公共片段,然后通过 <include> 标签引用。这种方式不仅提高了代码的复用性,还能减少重复代码,便于维护。
代码示例
抽取公共 SQL 片段
<sql id="userColumns">
id, username, age, email, create_time, update_time
</sql>
说明:
id是片段的唯一标识,用于后续引用。- 片段中可以包含任意合法的 SQL 部分,比如字段列表、JOIN 语句等。
引用公共 SQL 片段
<select id="selectUserById" parameterType="java.lang.Integer" resultType="com.example.entity.User">
SELECT <include refid="userColumns"/>
FROM user
WHERE id = #{id}
</select>
<select id="selectAllUser" resultType="com.example.entity.User">
SELECT <include refid="userColumns"/>
FROM user
</select>
<select id="selectUserByCondition" parameterType="map" resultType="com.example.entity.User">
SELECT <include refid="userColumns"/>
FROM user
<where>
<if test="username != null">
AND username = #{username}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
说明:
<include refid="userColumns"/>通过refid引用之前定义的 SQL 片段。- 适用于多表查询时字段列表较长的情况,或者需要统一字段定义的场景。
- 如果字段列表需要修改,只需修改
sql标签中的内容,所有引用该片段的地方都会自动更新。
高级用法:带参数的 SQL 片段
<sql id="orderByClause">
ORDER BY ${orderByField} ${orderByDirection}
</sql>
<select id="selectUserWithOrder" parameterType="map" resultType="com.example.entity.User">
SELECT <include refid="userColumns"/>
FROM user
<include refid="orderByClause"/>
</select>
说明:
${orderByField}和${orderByDirection}是动态参数,使用时需确保参数安全。- 适用于需要动态排序的场景,比如表格列排序功能。
四、动态 SQL 使用注意事项
-
参数类型处理:在MyBatis动态SQL的test属性中进行参数判断时,必须充分考虑参数类型特性。针对字符串类型参数,需要进行双重判断:首先检查是否为null,其次检查是否为空字符串(test="username != null and username != ''")。这种处理方式的原因是:
- null表示参数未被赋值
- 空字符串('')表示参数已被赋值为空值 对于数值类型(如Integer、Long等),则只需进行null判断(test="age != null"),因为数值类型不存在空字符串的概念,如果误加空字符串判断(如test="age != ''")反而会导致表达式求值错误。例如:
<!-- 正确示例 --> <if test="username != null and username != ''">AND user_name = #{username}</if> <if test="age != null">AND user_age = #{age}</if> <!-- 错误示例 --> <if test="age != null and age != ''">...</if> <!-- 数值类型不应判断空字符串 --> -
SQL注入防护:MyBatis的动态SQL机制虽然提供了基础的SQL注入防护,但仍有需要特别注意的情况:
- 使用#符号时(如#{param}),MyBatis会使用预编译语句,自动进行参数转义
- 使用$符号直接拼接时(如ORDER BY ${column}),存在SQL注入漏洞 安全实践建议:
- 90%以上的场景都应该使用#符号
- 必须使用$符号的场景(如动态表名、动态排序字段):
- 应该限制参数值为白名单值(如:test="orderBy == 'create_time' or orderBy == 'update_time'")
- 或对参数值进行严格校验(如只允许字母、数字和下划线) 典型危险场景示例:
<!-- 危险示例:直接拼接用户输入 --> ORDER BY ${userInput} <!-- 安全示例:使用白名单控制 --> <choose> <when test="orderBy == 'name'">ORDER BY user_name</when> <when test="orderBy == 'age'">ORDER BY user_age</when> <otherwise>ORDER BY create_time</otherwise> </choose> -
避免过度复杂的动态SQL:动态SQL虽然灵活,但过度使用会导致:
- 可读性下降:嵌套多层<if>/<choose>标签
- 维护困难:业务逻辑分散在SQL和Java代码中
- 性能问题:可能导致执行计划不稳定 改进方案:
- 将复杂查询拆分为多个简单查询,在Java层组装
- 使用SQL视图或存储过程封装复杂逻辑
- 保持单个动态SQL块不超过3级嵌套 反面教材示例:
<!-- 过度复杂的动态SQL --> <select id="findUsers" resultType="User"> SELECT * FROM users <where> <if test="param1 != null"> AND field1 = #{param1} <if test="param2 != null"> OR field2 = #{param2} <choose> <when test="param3 == 'A'">...</when> <otherwise>...</otherwise> </choose> </if> </if> <!-- 更多嵌套... --> </where> </select> -
批量操作的性能考量:使用foreach标签进行批量操作时需注意:
- 数据库限制:
- MySQL默认max_allowed_packet=4MB
- Oracle有SQL语句长度限制
- SQL Server有参数个数限制(约2100个)
- 性能优化方案:
- 分批处理:每批500-1000条记录
// Java层分批示例 List<User> users = getUsers(); int batchSize = 500; for (int i = 0; i < users.size(); i += batchSize) { List<User> batch = users.subList(i, Math.min(i + batchSize, users.size())); userMapper.batchInsert(batch); }- 调整数据库配置(需DBA配合)
- 使用JDBC批处理模式
- 考虑使用LOAD DATA INFILE(MySQL)等批量导入工具
- 数据库限制:
-
参数传递的一致性:参数命名必须严格匹配,注意:
- Map传参:键名区分大小写
- 对象传参:属性名区分大小写
- @Param注解:指定的别名优先级最高 常见问题场景:
// Java代码 Map<String, Object> params = new HashMap<>(); params.put("userName", "John"); // 注意大小写 params.put("USER_AGE", 25); // 全大写 // 错误示例:大小写不匹配 <if test="username != null">...</if> <!-- 应为userName --> <if test="user_age != null">...</if> <!-- 应为USER_AGE --> // 最佳实践: - 统一命名规范(如全小写下划线) - 使用@Param明确指定参数名 @Select("...") List<User> findUsers(@Param("user_name") String userName); <!-- 对应XML --> <if test="user_name != null">...</if>
五、动态 SQL 在注解方式中的应用
5.1 注解方式的核心原理
通过注解指定一个 "SQL 提供者" 类,该类中包含生成 SQL 语句的静态方法(或非静态方法),MyBatis 会在执行 SQL 时调用该方法获取动态生成的 SQL 语句。这种机制的核心在于:
- 运行时动态构建:SQL 语句不是在编译时确定的,而是在运行时根据参数动态生成
- 类型安全:使用 Java 代码构建 SQL,避免了字符串拼接的错误
- 灵活控制:可以利用 Java 的所有控制结构(条件判断、循环等)构建复杂的 SQL 逻辑
MyBatis 通过 @SelectProvider、@InsertProvider、@UpdateProvider 和 @DeleteProvider 等注解来支持这种动态 SQL 生成方式。
5.2 代码示例:注解方式实现多条件查询
步骤 1:定义 SQL 提供者类
public class UserSqlProvider {
/**
* 多条件查询SQL生成方法
* @param user 查询条件对象
* @return 动态生成的SQL语句
*/
public String selectUserByCondition(User user) {
// 使用MyBatis提供的SQL工具类构建SQL
return new SQL() {{
SELECT("id, username, age, email");
FROM("user");
// 动态添加用户名查询条件
if (user.getUsername() != null && !"".equals(user.getUsername())) {
WHERE("username LIKE CONCAT('%', #{username}, '%')");
}
// 动态添加年龄查询条件
if (user.getAge() != null) {
WHERE("age = #{age}");
}
// 可以继续添加更多条件
if (user.getEmail() != null) {
WHERE("email = #{email}");
}
// 可以添加排序条件
ORDER_BY("id DESC");
}}.toString();
}
}
上述代码中:
SQL是 MyBatis 提供的工具类,通过方法链方式构建 SQL- 使用
{{...}}双括号语法初始化匿名内部类 - 条件判断完全基于 Java 语法,非常灵活
- 可以添加各种子句(WHERE、ORDER BY 等)
步骤 2:在 Mapper 接口中使用注解引用 SQL 提供者
public interface UserMapper {
/**
* 多条件查询用户
* @param user 包含查询条件的用户对象
* @return 匹配的用户列表
*/
@SelectProvider(
type = UserSqlProvider.class, // 指定SQL提供者类
method = "selectUserByCondition" // 指定生成SQL的方法名
)
List<User> selectUserByCondition(User user);
}
注解属性说明:
type:指定 SQL 提供者类的 Class 对象method:指定 SQL 提供者类中生成 SQL 的方法名- MyBatis 3.5+ 版本支持非静态方法
- 方法必须返回 String 类型的 SQL 语句
5.3 注解方式与 XML 方式的对比
| 对比维度 | 注解方式 | XML 方式 |
|---|---|---|
| 可读性 | 复杂 SQL 逻辑在 Java 代码中,结构清晰,IDE 支持好(代码提示、跳转) | SQL 片段与动态标签混合,直观性强,但复杂逻辑可能难以理解 |
| 维护性 | 适合复杂逻辑(如多分支、循环),便于调试,修改需要重新编译 | 适合简单 SQL,修改无需重新编译,可直接热加载 |
| 灵活性 | 支持 Java 代码的所有逻辑(如循环、判断),可以调用其他工具方法 | 依赖 MyBatis 提供的动态标签(if/choose/foreach等),逻辑受限 |
| 性能 | 运行时动态生成 SQL,可能有轻微性能开销 | XML 解析后缓存,性能较好 |
| 适用场景 | 复杂动态 SQL、团队偏好注解开发、需要复杂逻辑控制 | 简单/中等复杂度 SQL、需要频繁修改 SQL 的场景、大型项目团队协作 |
| SQL 复用 | 可通过 Java 方法复用 SQL 片段 | 可通过 <sql> 标签和 <include> 标签复用 SQL 片段 |
实际应用建议:
- 对于简单固定的 SQL,使用 XML 方式更直观
- 对于需要复杂逻辑(如多重条件判断、循环等)的动态 SQL,推荐使用注解方式
- 大型项目中可以混合使用两种方式,根据具体场景选择
六、动态 SQL 常见问题与解决方案
6.1 问题 1:foreach标签遍历集合时提示 "Parameter 'list' not found"
问题描述
在使用MyBatis的foreach标签遍历List集合时,可能会遇到以下错误提示:
org.apache.ibatis.binding.BindingException: Parameter 'list' not found. Available parameters are [arg0, collection, list]
原因分析
-
参数命名问题:
- 当Mapper接口方法的参数为单个List集合且未使用@Param注解时,MyBatis 3.4.6及以下版本默认将参数名识别为"list"
- 但在3.5.0及以上版本中,参数名可能被识别为"collection"或"arg0"
-
@Param注解使用不当:
- 如果使用了@Param注解指定了参数名(如@Param("idList")),则必须确保foreach标签的collection属性值与该注解指定的参数名完全一致
解决方案
方案 1:显式使用@Param注解
// Mapper接口方法
List<User> selectUserByIds(@Param("idList") List<Integer> idList);
<!-- XML映射文件 -->
<select id="selectUserByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="idList" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
方案 2:使用默认参数名
// 不使用@Param注解
List<User> selectUserByIds(List<Integer> ids);
<!-- 根据MyBatis版本选择正确的参数名 -->
<foreach collection="list" item="id" ...> <!-- 3.4.6及以下版本 -->
<foreach collection="collection" item="id" ...> <!-- 3.5.0及以上版本 -->
6.2 问题 2:动态更新时所有字段为空导致SQL语法错误
问题描述
使用set标签动态更新时,如果传入对象的所有字段都为空,会生成如下错误SQL:
UPDATE user SET WHERE id = ?
原因分析
- 动态SQL生成机制:
- set标签会生成SET关键字
- 只有当if条件满足时才会生成对应的字段赋值语句
- 当所有if条件都不满足时,SET后为空,导致语法错误
解决方案
方案 1:业务层校验
public void updateUser(User user) {
// 检查至少一个字段不为null或空字符串
if (Stream.of(
user.getUsername(),
user.getAge(),
user.getEmail()
).allMatch(value -> value == null || (value instanceof String && ((String)value).isEmpty()))) {
throw new BusinessException("至少需要更新一个字段");
}
userMapper.updateUser(user);
}
方案 2:SQL层处理(谨慎使用)
<update id="updateUser">
UPDATE user
<set>
<if test="username != null and username != ''">username = #{username},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null and email != ''">email = #{email},</if>
<!-- 默认更新一个无影响字段 -->
<if test="true">update_time = NOW()</if>
</set>
WHERE id = #{id}
</update>
6.3 问题 3:OGNL表达式判断字符串相等时失效
问题描述
在test属性中判断字符串相等时,如下条件始终返回false:
<if test="status == 'ACTIVE'">
原因分析
- OGNL类型处理:
- 单引号在OGNL中表示char类型
- 当比较字符串时,会导致类型不匹配
- 例如:'ACTIVE'实际上被解释为char[],而不是String
解决方案
方案 1:正确使用引号
<!-- 使用双引号包裹字符串 -->
<if test='status == "ACTIVE"'>
<!-- 或者嵌套引号 -->
<if test="status == 'ACTIVE'.toString()">
方案 2:使用equals方法(推荐)
<if test="status != null and status.equals('ACTIVE')">
<!-- 或使用Java 7+的语法糖 -->
<if test='"ACTIVE".equals(status)'>
方案 3:自定义判断方法
// 在工具类中定义方法
public static boolean isActive(String status) {
return "ACTIVE".equals(status);
}
<if test="@com.example.util.MyBatisUtils@isActive(status)">
七、动态 SQL 实战案例:多条件分页查询
7.1 步骤 1:引入分页插件依赖(以 Maven 为例)
PageHelper 是一个流行的 MyBatis 分页插件,它能简化分页查询的实现过程。在项目的 pom.xml 文件中添加以下依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
注意事项:
- 确保版本号与你的 Spring Boot 版本兼容
- 对于非 Spring Boot 项目,可以使用
pagehelper核心依赖 - 该依赖会自动配置拦截器,无需额外配置
7.2 步骤 2:定义查询条件实体类
创建一个数据传输对象(DTO)来封装查询条件和分页参数:
public class UserQueryDTO {
// 分页参数
private Integer pageNum = 1; // 默认第一页
private Integer pageSize = 10; // 默认每页10条
// 查询条件
private String username; // 用户名模糊查询
private Integer age; // 精确年龄查询
private String email; // 邮箱模糊查询
// 省略getter和setter方法
// 实际开发中建议使用Lombok的@Data注解
}
设计说明:
- 分页参数设置默认值可提高接口健壮性
- 查询条件字段可根据业务需求扩展
- 建议使用Java Bean验证注解(@NotNull, @Size等)进行参数校验
7.3 步骤 3:编写 Mapper 接口和 XML 映射文件
Mapper 接口定义
public interface UserMapper {
/**
* 多条件分页查询用户列表
* @param queryDTO 查询条件对象
* @return 用户列表(不包含分页信息)
*/
List<User> selectUserByPage(UserQueryDTO queryDTO);
}
XML 映射文件实现
<select id="selectUserByPage" parameterType="com.example.dto.UserQueryDTO"
resultType="com.example.entity.User">
SELECT id, username, age, email
FROM user
<where>
<!-- 用户名模糊查询(当username不为空时生效) -->
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<!-- 精确年龄查询 -->
<if test="age != null">
AND age = #{age}
</if>
<!-- 邮箱模糊查询 -->
<if test="email != null and email != ''">
AND email LIKE CONCAT('%', #{email}, '%')
</if>
</where>
<!-- 注意:这里不需要手动添加LIMIT语句,分页插件会自动处理 -->
</select>
动态SQL特点:
<where>标签会自动处理WHERE关键字和去除多余的AND<if>标签实现条件动态拼接- CONCAT函数确保模糊查询的正确格式
7.4 步骤 4:编写 Service 层代码(集成分页插件)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 分页查询用户列表
* @param queryDTO 查询条件
* @return 包含分页信息的查询结果
*/
public PageInfo<User> selectUserByPage(UserQueryDTO queryDTO) {
// 开启分页(必须放在查询方法前)
PageHelper.startPage(queryDTO.getPageNum(), queryDTO.getPageSize());
// 执行查询(此时SQL已被分页插件拦截并修改)
List<User> userList = userMapper.selectUserByPage(queryDTO);
// 封装分页结果(包含总条数、总页数、当前页数据等)
return new PageInfo<>(userList);
}
}
关键点说明:
PageHelper.startPage()必须放在查询方法前- 分页插件通过线程绑定方式传递分页参数
PageInfo提供丰富的分页信息:- 当前页码(pageNum)
- 每页条数(pageSize)
- 总页数(pages)
- 总记录数(total)
- 当前页数据(list)
7.5 步骤 5:测试分页查询
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testSelectUserByPage() {
// 构建查询条件
UserQueryDTO queryDTO = new UserQueryDTO();
queryDTO.setPageNum(1); // 查询第1页
queryDTO.setPageSize(10); // 每页10条记录
queryDTO.setUsername("zhang"); // 用户名包含"zhang"
// 执行分页查询
PageInfo<User> pageInfo = userService.selectUserByPage(queryDTO);
// 验证结果
System.out.println("总条数:" + pageInfo.getTotal());
System.out.println("总页数:" + pageInfo.getPages());
System.out.println("当前页数据量:" + pageInfo.getList().size());
System.out.println("当前页数据:" + pageInfo.getList());
// 可选择断言验证
assertTrue(pageInfo.getList().size() <= 10);
}
}
测试要点:
- 验证分页参数是否正确应用
- 检查条件查询是否生效
- 确认分页信息计算是否正确
- 边界测试:第一页、最后一页、空结果等情况
1490

被折叠的 条评论
为什么被折叠?



