实际开发中SQL语句往往需要根据不同条件动态生成(如多条件查询、动态排序、批量操作等),MyBatis的动态SQL通过标签化语法,实现了SQL的灵活拼接,避免了手动拼接SQL的繁琐和SQL注入风险。本文我将系统讲解MyBatis动态SQL的核心标签(if
、choose
、foreach
等),并结合实例解析其用法和最佳实践,带你掌握动态SQL的编写技巧。
动态SQL是MyBatis的核心特性之一,解决了传统JDBC手动拼接SQL的痛点:
// 传统方式:手动拼接SQL,繁琐且易出错
public List<User> queryUser(String username, Integer age) {
StringBuilder sql = new StringBuilder("SELECT * FROM user WHERE 1=1");
if (username != null) {
sql.append(" AND username = '" + username + "'"); // 存在SQL注入风险
}
if (age != null) {
sql.append(" AND age = " + age);
}
// 执行SQL...
}
问题:
WHERE
后的AND
/OR
拼接(如WHERE 1=1
的冗余条件);MyBatis通过XML标签自动拼接SQL,解决上述问题:
<select id="queryUser" resultType="User">
SELECT * FROM user
<where>
<if test="username != null">AND username = #{username}if>
<if test="age != null">AND age = #{age}if>
where>
select>
优势:
where
标签自动去除多余的AND
/OR
);#{}
传入,避免SQL注入;MyBatis提供了多种动态SQL标签,覆盖常见的条件判断、循环、拼接等场景。
if
标签:条件判断if
标签用于单条件判断,根据test
属性的表达式(OGNL表达式)决定是否拼接SQL片段。
<select id="queryUser" resultType="User">
SELECT * 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>
<if test="createTime != null">
AND create_time >= #{createTime}
if>
select>
test
表达式规则!=
、==
、&&
、||
等);username != null and username != ''
);age != null
(无需判断空字符串)。where
与trim
标签:条件拼接优化if
标签配合where
或trim
标签,可自动处理多余的AND
/OR
,替代冗余的WHERE 1=1
。
where
标签where
标签自动去除条件前的AND
/OR
,若没有条件则不生成WHERE
子句。
<select id="queryUser" resultType="User">
SELECT * FROM user
<where>
<if test="username != null">AND username = #{username}if>
<if test="age != null">AND age = #{age}if>
where>
select>
效果:
username
和age
都有值,生成WHERE username = ? AND age = ?
;age
有值,生成WHERE age = ?
(自动去除AND
);WHERE
子句(避免SELECT * FROM user WHERE
的语法错误)。trim
标签:自定义拼接规则trim
标签更灵活,可自定义前缀、后缀及需要去除的字符。
属性 | 作用 |
---|---|
prefix |
拼接前缀(如WHERE ) |
suffix |
拼接后缀(如; ) |
prefixOverrides |
需要去除的前缀字符(如AND 、OR ) |
suffixOverrides |
需要去除的后缀字符(如, ) |
<trim prefix="WHERE" prefixOverrides="AND | OR">
<if test="username != null">AND username = #{username}if>
trim>
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">username = #{username},if>
<if test="age != null">age = #{age},if>
trim>
choose
、when
、otherwise
标签:多条件分支choose
标签类似Java的switch
语句,用于多条件分支判断(仅执行第一个满足条件的when
)。
<select id="queryUserByCondition" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="id != null">AND id = #{id}when>
<when test="username != null">AND username = #{username}when>
<otherwise>AND age > 18otherwise>
choose>
where>
select>
逻辑:
id != null
,仅拼接AND id = #{id}
;id == null
但username != null
,拼接AND username = #{username}
;AND age > 18
。set
标签:更新语句拼接set
标签用于UPDATE
语句,自动去除多余的逗号,适合动态更新部分字段。
<update id="updateUser">
UPDATE user
<set>
<if test="username != null">username = #{username},if>
<if test="age != null">age = #{age},if>
<if test="createTime != null">create_time = #{createTime}if>
set>
WHERE id = #{id}
update>
效果:
username
和age
有值,生成UPDATE user SET username = ?, age = ? WHERE id = ?
(自动去除最后一个逗号);SET username = ?, age = ?
后的冗余逗号)。foreach
标签:循环遍历foreach
标签用于循环遍历集合(数组、List、Set等),常用于IN
查询、批量插入、批量更新等场景。
属性 | 作用 | 示例 |
---|---|---|
collection |
集合参数名(需与接口方法参数名一致) | list 、array 、ids |
item |
循环变量名(遍历出的元素) | item="id" |
index |
循环索引(可选) | index="i" |
open |
拼接前缀 | open="(" |
close |
拼接后缀 | close=")" |
separator |
元素分隔符 | separator="," |
IN
查询(遍历集合)// Mapper接口
List<User> queryByIds(@Param("ids") List<Integer> ids);
<select id="queryByIds" 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)
// Mapper接口
int batchInsert(@Param("users") List<User> users);
<insert id="batchInsert">
INSERT INTO user (username, age, create_time)
VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.age}, #{user.createTime})
foreach>
insert>
当users
包含2个用户时,生成SQL:
INSERT INTO user (username, age, create_time)
VALUES ('张三', 20, '2023-01-01'), ('李四', 22, '2023-01-02')
<update id="batchUpdate">
<foreach collection="users" item="user" separator=";">
UPDATE user
SET username = #{user.username}, age = #{user.age}
WHERE id = #{user.id}
foreach>
update>
生成多个UPDATE
语句(需数据库支持批量执行,如MySQL需添加allowMultiQueries=true
到JDBC URL)。
sql
与include
标签:SQL片段复用sql
标签用于定义可复用的SQL片段,include
标签用于引用,减少重复代码。
<sql id="baseColumn">id, username, age, create_timesql>
<select id="queryAll" resultType="User">
SELECT <include refid="baseColumn"/> FROM user
select>
<select id="queryById" resultType="User">
SELECT <include refid="baseColumn"/> FROM user WHERE id = #{id}
select>
进阶:通过property
传递参数,动态调整片段:
<sql id="dynamicColumn">
<if test="includeId">id,if>
username, age
sql>
<select id="query" resultType="User">
SELECT <include refid="dynamicColumn">
<property name="includeId" value="true"/>
include> FROM user
select>
结合多个标签实现复杂查询(多条件+排序+分页)。
List<User> queryUserByAdvanced(
@Param("username") String username,
@Param("age") Integer age,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge,
@Param("sortBy") String sortBy, // 排序字段
@Param("asc") Boolean asc, // 是否升序
@Param("offset") Integer offset, // 分页偏移量
@Param("limit") Integer limit // 分页大小
);
<select id="queryUserByAdvanced" resultType="User">
SELECT id, username, age, create_time
FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
if>
<if test="age != null">
AND age = #{age}
if>
<choose>
<when test="age == null">
<if test="minAge != null">AND age >= #{minAge}if>
<if test="maxAge != null">AND age <= #{maxAge}if>
when>
choose>
where>
<if test="sortBy != null and sortBy != ''">
ORDER BY ${sortBy}
<if test="asc != null">
<if test="asc">ASCif>
<if test="!asc">DESCif>
if>
<if test="asc == null">ASCif>
if>
<if test="offset != null and limit != null">
LIMIT #{offset}, #{limit}
if>
select>
age
(等于)优先级高于minAge
/maxAge
(范围),通过choose
标签控制;sortBy
使用${}
(因需作为字段名),需确保sortBy
为后端可控值(避免用户输入直接传入,防止SQL注入);offset
和limit
都存在时拼接分页条件。test
表达式空值判断错误问题:字符串空值判断遗漏null
或空字符串,导致条件不生效。
<if test="username != null">AND username = #{username}if>
当username
为空字符串(""
)时,条件不拼接(符合预期),但需明确逻辑。
正确写法:
test="username != null"
;test="username != null and username != ''"
。foreach
集合参数名错误问题:接口方法参数为List
时,collection
属性值错误。
// 接口(未用@Param注解)
List<User> queryByIds(List<Integer> ids);
<foreach collection="ids" ...>
解决方案:
@Param
:集合参数默认名为list
(List)或array
(数组);@Param
明确命名(可读性更好):List<User> queryByIds(@Param("ids") List<Integer> ids);
<foreach collection="ids" ...>
$
与#
的区别)MyBatis有两种参数占位符:
#{}
:预编译参数(安全,推荐),生成?
占位符;${}
:直接拼接SQL(不安全),用于表名、排序字段等无法预编译的场景。风险示例:
ORDER BY ${sortBy}
安全措施:
${}
的使用场景(仅用于后端可控参数);${}
参数进行白名单校验(如排序字段仅允许id
、username
等)。问题:复杂查询的XML标签嵌套过多,可读性差。
解决方案:
sql
+include
);总结
MyBatis动态SQL通过标签化语法解决了SQL拼接的痛点:
- 简化条件拼接:
if
、where
、set
等标签自动处理冗余的AND
、逗号等,避免语法错误;- 提升代码复用:
sql
+include
标签复用SQL片段,减少重复代码;- 支持复杂场景:
foreach
处理集合,choose
处理分支,满足批量操作、多条件查询等需求;- 安全可靠:
#{}
参数预编译,避免SQL注入(合理使用${}
)。
若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ