适合零基础到进阶:解释每个动态标签的作用、使用场景、常见坑,并给出基于本项目的示例(User/Student/Teacher)。代码含详尽注释,便于直接参考。
1. 为什么需要动态 SQL?
- 业务条件多且可选(多条件查询、可选更新字段)。
- 需要批量操作(批量插入/删除)。
- 避免手写字符串拼接和 if/else 造成的代码混乱,同时提升安全性(使用
#{}预编译占位符,防 SQL 注入)。
2. 动态 SQL 的核心标签概览
<if>:条件成立则拼接片段。<where>:自动处理首个条件前的AND/OR并在无条件时去掉WHERE。<set>:更新语句中自动去掉末尾逗号。<trim>:自定义前缀/后缀及多余字符的裁剪(prefixOverrides、suffixOverrides)。<choose>/<when>/<otherwise>:相当于 SQL 里的 if/else if/else。<foreach>:循环,常用于IN (...)、批量插入。<bind>:定义局部变量,构造更灵活的条件(本文示例主要用前几个标签)。
3. 项目结构与前置
- 实体:
entity.User/Student/Teacher(字段:id, name, age, birthday, gender, phone)。 - Mapper 接口:
UserDao、StudentDao、TeacherDao。 - Mapper XML:
UserMapper.xml、StudentMapper.xml、TeacherMapper.xml。 - 测试:
UserTest、StudentTest、TeacherTest。
4. 基础示例:多条件查询(<where> + <if>)
示例(User):
xml
<select id="dtFindUser" parameterType="entity.User" resultType="entity.User">
select * from user
<where> <!-- 有条件时自动加 WHERE 并去掉首个 AND/OR -->
<if test="name != null">
and name like concat('%', #{name}, '%') <!-- 仅当 name 非空才拼 -->
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="phone != null">
and phone like concat('%', #{phone}, '%')
</if>
</where>
</select>
要点:
<where>会:- 在有条件时自动加上
WHERE - 自动去掉首个条件前多余的
AND/OR
- 在有条件时自动加上
#{}是预编译占位符,安全防注入。- 传入一个
User对象,非 null 的字段才会生效。
扩展到 Student/Teacher 只需改表名和类型,已在 StudentMapper.xml / TeacherMapper.xml 中实现。
5. 动态更新(<set> + <if>)
示例(User):
xml
<update id="dtUpdate" parameterType="entity.User">
update user
<set> <!-- 自动去掉最后一个逗号,避免 SQL 语法错误 -->
<if test="name != null"> name = #{name}, </if>
<if test="gender != null"> gender = #{gender}, </if>
<if test="phone != null"> phone = #{phone}, </if>
<if test="age != null"> age = #{age}, </if>
<if test="birthday != null"> birth_date = #{birthday}, </if>
</set>
where id = #{id} <!-- 必填,防止全表更新 -->
</update>
要点:
<set>自动去掉最后一个逗号,避免语法错误。- 仅更新非空字段,常用于"部分字段更新"。
Student/Teacher 对应方法:dtUpdateStudent、dtUpdateTeacher。
6. 灵活裁剪(<trim>)
6.1 动态 where + trim
xml
<select id="trFindUser" parameterType="entity.User" resultType="entity.User">
select * from user
<trim prefix="where" prefixOverrides="and | or"> <!-- 自定义 where,裁掉首个 AND/OR -->
<if test="name != null"> and name like concat('%', #{name}, '%') </if>
<if test="gender != null"> and gender = #{gender} </if>
<if test="phone != null"> and phone like concat('%', #{phone}, '%') </if>
</trim>
</select>
要点:
prefix="where":有条件时自动加WHERE。prefixOverrides="and | or":去掉首个多余的AND/OR。
6.2 动态 set + trim
xml
<update id="trUpdateUser" parameterType="entity.User">
update user
<trim prefix="set" suffixOverrides=","> <!-- 功能类似 set,但可自定义裁剪 -->
<if test="name != null"> name = #{name}, </if>
<if test="gender != null"> gender = #{gender}, </if>
<if test="phone != null"> phone = #{phone}, </if>
<if test="age != null"> age = #{age}, </if>
<if test="birthday != null"> birth_date = #{birthday}, </if>
</trim>
where id = #{id}
</update>
suffixOverrides=",":去掉最后的逗号,等效于<set>,但更自由(可自定义前后缀)。
Student/Teacher 已有同名的 trFind* / trUpdate* 版本。
7. 选择分支(<choose>/<when>/<otherwise>)
示例(User):
xml
<select id="selectUserByChoose" resultType="entity.User" parameterType="entity.User">
select * from user
<where>
<choose> <!-- 命中第一个 when 后就停止,适合按优先级选条件 -->
<when test="name != null"> name = #{name} </when>
<when test="gender != null"> gender = #{gender} </when>
<when test="phone != null"> phone = #{phone} </when>
<otherwise> id = #{id} </otherwise>
</choose>
</where>
</select>
要点:
- 类似 if / else if / else。
- 一旦命中某个
<when>,后续不会再执行。 - 常用于"优先级条件"场景,例如先按 name,没 name 再按 gender 等。
Student/Teacher 中有对应的 selectStudentByChoose / selectTeacherByChoose。
8. 批量操作(<foreach>)
8.1 批量删除
xml
<delete id="deleteMore">
delete from user where id in
<foreach item="id" collection="ids" index="index" open="(" close=")" separator=",">
#{id} <!-- 逐个绑定,防注入 -->
</foreach>
</delete>
要点:
collection="ids"对应接口参数名@Param("ids") List<Integer> ids。open/close/separator控制括号与分隔符。
8.2 批量插入
xml
<insert id="insertMore">
insert into user (name, gender, phone, age, birth_date) values
<foreach item="user" collection="users" index="index" separator=",">
(#{user.name}, #{user.gender}, #{user.phone}, #{user.age}, #{user.birthday}) <!-- 拼成多行 values -->
</foreach>
</insert>
要点:
collection="users"对应接口参数名@Param("users") List<User> users。- 每个元素生成一组
(...),用逗号分隔。
Student/Teacher 有同名的批量方法:deleteMore、insertMore。
9. 模糊查询与防注入
安全写法:
xml
<select id = "likeByName2" resultType="entity.User" parameterType="java.lang.String">
select * from user where name like concat('%', #{value}, '%')
</select>
#{}预编译占位符,安全。- 避免
${}直接拼接,除非做白名单校验的动态表/列名。
(本项目中 likeByName 使用 ${value} 的写法仅作对比示例,实际推荐 likeByName2。)
10. 回填主键(<selectKey>)
xml
<insert id="insertGetId" parameterType="entity.User">
insert into user (name, gender, phone, age, birth_date) values(#{name}, #{gender}, #{phone}, #{age}, #{birthday})
<selectKey keyProperty="id" resultType="java.lang.Integer" order="AFTER">
select last_insert_id()
</selectKey>
</insert>
要点:
order="AFTER":先插入,再查自增主键。keyProperty="id":把生成的主键回填到入参对象的id字段。- Student/Teacher 也有
insertGetId。
11. Java 测试用例示例(以 User 为例,Student/Teacher 类似)
java
@Test
public void dtFindUser(){
User user = new User();
user.setName("张"); // 只给 name,动态 where 会生成 name like '%张%'
List<User> users = mapper.dtFindUser(user);
users.forEach(System.out::println);
}
@Test
public void dtUpdate() {
User user = new User();
user.setId(13);
user.setName("张三"); // 只更新 name
int count = mapper.dtUpdate(user);
System.out.println(count);
}
@Test
public void deleteMore(){
List<Integer> ids = new ArrayList<>();
ids.add(1);
ids.add(2);
int count = mapper.deleteMore(ids);
System.out.println(count);
}
@Test
public void insertMore() {
List<User> users = new ArrayList<>();
users.add(new User("批量1", 18, new Date(), "男", "1111"));
users.add(new User("批量2", 19, new Date(), "女", "2222"));
int count = mapper.insertMore(users);
System.out.println(count);
}
运行说明:
- 确保
SqlMapConfig.xml已注册对应 Mapper。 - 用 JUnit 运行测试,控制台可看到 STDOUT_LOGGING 打印的 SQL。
- 事务在
@After里session.commit(),写操作会提交。
12. 常见坑与规避
- 参数名不一致 :接口参数名、XML 中的占位符/集合名必须一致;多个参数时可用
@Param。 ${}注入风险 :优先用#{};若必须用${}(动态列/表),先做白名单校验。- 空条件导致全表更新/删除 :动态 SQL 条件为空时要谨慎,可在 Java 侧校验,或在 XML 中加兜底(如
otherwise限制)。 - 末尾逗号 :
<set>或suffixOverrides=","处理;不要手写逗号。 - choose 优先级 :命中第一个
<when>后不会再看其他条件,顺序要慎重。
13. 常见面试题与简答
-
#{} 和 ${}区别?#{}预编译占位,防注入;${}字符串直拼,易注入,仅在动态表/列名等必须场景使用且要白名单。
-
<where>和<trim prefix="where">有何不同?<where>自动加WHERE并去掉首个 AND/OR;<trim>更灵活,可自定义前后缀与裁剪规则。
-
<set>与<trim prefix="set" suffixOverrides=",">的区别?- 效果类似,
<trim>可自定义更复杂的裁剪逻辑,<set>更简洁专用于 UPDATE。
- 效果类似,
-
<choose>适用场景?- 有优先级的条件选择,类似 if/else if/else,一次只命中一个分支。
-
批量操作用什么标签?
<foreach>,常见用于IN (...)和批量插入。
-
如何防止动态更新把所有字段清空?
- 在 Java 侧校验至少有一个可更新字段;或在 XML 用
<set>包裹<if>,确保where id = ...必填。
- 在 Java 侧校验至少有一个可更新字段;或在 XML 用
-
selectKey的作用?- 获取插入后的主键并回填到对象属性;
order可选 BEFORE/AFTER。
- 获取插入后的主键并回填到对象属性;
-
动态 SQL 是否影响性能?
- MyBatis 会生成最终 SQL 并预编译,性能主要取决于 SQL 本身和索引;关键是保持条件有索引、避免全表扫描。
14. 实践清单(照着做)
- 按需挑选标签:多条件用
<where> + <if>,优先级用<choose>,动态更新用<set>/<trim>,批量用<foreach>。 - 参数命名对齐:接口参数名 + XML 占位符一致,多个参数用
@Param。 - 防注入:能用
#{}不用${};动态表/列名必须白名单。 - 提交事务:写操作后记得
commit();或openSession(true)。 - 打日志:
<setting name="logImpl" value="STDOUT_LOGGING"/>便于排查。
15. 结语
掌握动态 SQL 的几个标签组合,就能写出可维护、可扩展的查询和更新语句。建议多练习:
- 将你的条件从 1 个扩展到 N 个,看 SQL 是否按预期生成;
- 尝试批量插入/删除,观察生成的 SQL;
- 用
choose做优先级控制,避免多重 if 嵌套。
祝学习顺利,面试稳稳拿下! 😊