一句话总结
MyBatis 动态 SQL 通过 OGNL 表达式 + XML 标签实现运行时 SQL 拼接。核心标签:<if>(条件判断)、<choose>/<when>/<otherwise>(多分支选择)、<foreach>(集合遍历)、<trim>/<where>/<set>(前缀后缀处理)。底层 SqlSource 体系:动态 SQL 使用 DynamicSqlSource(每次执行解析),静态 SQL 使用 RawSqlSource(初始化时解析一次)。#{} 防注入,${} 不防注入,动态表名/排序必须用 ${} 但要白名单校验。
初级理解
常用动态 SQL 标签
# 1. <if> 条件判断
<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>
</select>
# 2. <choose> <when> <otherwise> 多分支
<select id="selectByPriority" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="name != null">
name = #{name}
</when>
<otherwise>
1 = 1
</otherwise>
</choose>
</where>
</select>
# 3. <foreach> 集合遍历
<select id="selectByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="list" item="id"
open="(" separator="," close=")">
#{id}
</foreach>
</select>
# 批量插入
<insert id="batchInsert">
INSERT INTO user(name, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
<where> <set> <trim> 标签
# <where> 自动处理 WHERE 和多余的 AND/OR
<select id="selectByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">
AND name = #{name} <!-- 自动去掉第一个 AND -->
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
# <set> 自动处理 SET 和多余的逗号
<update id="updateSelective">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</set>
WHERE id = #{id}
</update>
# <trim> 自定义前缀后缀处理(最灵活)
# 等价于 <where>
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
</trim>
# 等价于 <set>
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
</trim>
中级深入
SqlSource 体系
# SqlSource 继承体系
SqlSource (接口)
├── DynamicSqlSource # 动态 SQL(含 ${} 或动态标签)
├── RawSqlSource # 静态 SQL(纯 #{} 无动态标签)
├── StaticSqlSource # 完全静态 SQL
└── ProviderSqlSource # @SelectProvider 等注解方式
# 判断逻辑(XMLScriptBuilder):
# 1. 解析 XML 中的动态标签(if/where/foreach 等)
# 2. 如果有动态标签 → DynamicSqlSource
# 3. 如果只有 #{} 无动态标签 → RawSqlSource
# 4. 如果含 ${} → DynamicSqlSource(因为 ${} 是运行时替换)
# DynamicSqlSource.getBoundSql() 每次执行都解析:
# 1. 创建 DynamicContext(参数上下文)
# 2. 依次执行每个 SqlNode.apply(context)
# 3. <if> → IfSqlNode:OGNL 表达式判断
# 4. <foreach> → ForEachSqlNode:遍历集合
# 5. 最终生成 BoundSql(含 SQL + 参数映射)
# RawSqlSource.getBoundSql() 初始化时解析一次:
# 1. 解析 #{} 占位符 → 替换为 ?
# 2. 构建 ParameterMapping 列表
# 3. 每次执行直接使用缓存的 SQL
OGNL 表达式
# MyBatis 动态 SQL 使用 OGNL 表达式判断 test 属性
# 常用 OGNL 表达式:
# 空值判断
<if test="name != null and name != ''"> # 不为空
<if test="list != null and list.size() > 0"> # 集合非空
# 比较运算
<if test="age > 18">
<if test="status == 1">
# 字符串判断
<if test="type == 'A'.toString()"> # 单字符需转String
# 逻辑运算
<if test="name != null and (age > 18 or vip == true)">
# 方法调用
<if test="name.contains('admin')">
<if test="list.contains(1)">
# 注意:OGNL 中字符串比较要用双引号
# 正确:test='type == "A"' (外层单引号,内层双引号)
# 错误:test="type == 'A'" (OGNL 中 'A' 是字符不是字符串)
高级进阶
动态 SQL 安全问题
# 危险用法1:动态排序用 ${} 无校验
# 错误示例:
<select id="selectByOrder" resultType="User">
SELECT * FROM user ORDER BY ${orderColumn} ${sortDirection}
</select>
# 攻击者传入 orderColumn="id; DROP TABLE user--"
# 导致 SQL 注入!
# 正确做法:白名单校验
private static final Set<String> ALLOWED_COLUMNS =
Set.of("id", "name", "age", "create_time");
private static final Set<String> ALLOWED_DIRECTIONS =
Set.of("ASC", "DESC");
public List<User> selectByOrder(String orderColumn, String direction) {
if (orderColumn == null || !ALLOWED_COLUMNS.contains(orderColumn)) {
throw new IllegalArgumentException("非法列名: " + orderColumn);
}
if (direction == null || !ALLOWED_DIRECTIONS.contains(
direction.toUpperCase())) {
throw new IllegalArgumentException("非法排序: " + direction);
}
return mapper.selectByOrder(orderColumn, direction.toUpperCase());
}
# 危险用法2:foreach 拼接过多导致 SQL 过长
# IN 查询超过 1000 个 ID 时,Oracle 报错
# 解决方案:分批查询
List<List<Long>> partitions = Lists.partition(ids, 500);
for (List<Long> batch : partitions) {
result.addAll(mapper.selectByIds(batch));
}
@SelectProvider 动态 SQL
# 注解方式动态 SQL(复杂场景用 XML,简单场景用注解)
public class UserSqlProvider {
public String selectByCondition(Map<String, Object> params) {
return new SQL() {{
SELECT("*");
FROM("user");
if (params.get("name") != null) {
WHERE("name = #{name}");
}
if (params.get("age") != null) {
WHERE("age = #{age}");
}
ORDER_BY("id DESC");
}}.toString();
}
}
@SelectProvider(type = UserSqlProvider.class,
method = "selectByCondition")
List<User> selectByCondition(Map<String, Object> params);
# 四种 Provider 注解:
# @SelectProvider → 查询
# @InsertProvider → 插入
# @UpdateProvider → 更新
# @DeleteProvider → 删除
MyBatis-Plus 动态 SQL
# MyBatis-Plus 条件构造器(Lambda 方式,类型安全)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三") // name = '张三'
.gt(User::getAge, 18) // age > 18
.like(User::getEmail, "@qq.com") // email LIKE '%@qq.com%'
.orderByDesc(User::getCreateTime);
List<User> users = userMapper.selectList(wrapper);
# 生成的 SQL:
# SELECT * FROM user
# WHERE name = '张三' AND age > 18
# AND email LIKE '%@qq.com%'
# ORDER BY create_time DESC
# 优势:
# 1. Lambda 方式避免字段名硬编码
# 2. 类型安全,编译期检查
# 3. 链式调用,代码简洁
实战场景
# 场景1:多条件动态查询(通用写法)
<select id="selectByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="status"
open="(" separator="," close=")">
#{status}
</foreach>
</if>
</where>
ORDER BY create_time DESC
</select>
# 场景2:批量更新(CASE WHEN)
<update id="batchUpdate">
UPDATE user
<trim prefix="SET" suffixOverrides=",">
<trim prefix="name = CASE" suffix="END,">
<foreach collection="list" item="user">
WHEN id = #{user.id} THEN #{user.name}
</foreach>
</trim>
<trim prefix="age = CASE" suffix="END,">
<foreach collection="list" item="user">
WHEN id = #{user.id} THEN #{user.age}
</foreach>
</trim>
</trim>
WHERE id IN
<foreach collection="list" item="user"
open="(" separator="," close=")">
#{user.id}
</foreach>
</update>
面试模拟
面试官:MyBatis 动态 SQL 有哪些标签?原理是什么?
你:常用标签有 if、choose/when/otherwise、foreach、trim/where/set。底层通过 SqlSource 体系实现:动态 SQL 使用 DynamicSqlSource,每次执行时通过 OGNL 表达式解析标签生成最终 SQL;静态 SQL 使用 RawSqlSource,初始化时解析一次。每个标签对应一个 SqlNode 实现类,如 IfSqlNode、ForEachSqlNode。
面试官:动态 SQL 中如何防止 SQL 注入?
你:WHERE 条件值必须用 #{}(预编译占位符),不能用 ${}。动态表名/列名/排序必须用 ${} 时,要在 Java 代码层做白名单校验,不允许用户输入直接拼到 SQL 中。MyBatis-Plus 的 LambdaQueryWrapper 可以从编译期避免字段名硬编码问题。