MyBatis 动态 SQL 原理?

2025年 阅读约 12 分钟 面试指南 · MyBatis

深入解析MyBatis动态SQL原理:if/choose/where/foreach/trim标签使用、SqlSource体系(DynamicSqlSource vs RawSqlSource)、OGNL表达式引擎、动态SQL安全注意事项,附面试模拟问答。

一句话总结

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 可以从编译期避免字段名硬编码问题。