MyBatis 动态 SQL 全攻略

适合零基础到进阶:解释每个动态标签的作用、使用场景、常见坑,并给出基于本项目的示例(User/Student/Teacher)。代码含详尽注释,便于直接参考。


1. 为什么需要动态 SQL?

  • 业务条件多且可选(多条件查询、可选更新字段)。
  • 需要批量操作(批量插入/删除)。
  • 避免手写字符串拼接和 if/else 造成的代码混乱,同时提升安全性(使用 #{} 预编译占位符,防 SQL 注入)。

2. 动态 SQL 的核心标签概览

  • <if>:条件成立则拼接片段。
  • <where>:自动处理首个条件前的 AND/OR 并在无条件时去掉 WHERE
  • <set>:更新语句中自动去掉末尾逗号。
  • <trim>:自定义前缀/后缀及多余字符的裁剪(prefixOverridessuffixOverrides)。
  • <choose>/<when>/<otherwise>:相当于 SQL 里的 if/else if/else。
  • <foreach>:循环,常用于 IN (...)、批量插入。
  • <bind>:定义局部变量,构造更灵活的条件(本文示例主要用前几个标签)。

3. 项目结构与前置

  • 实体:entity.User / Student / Teacher(字段:id, name, age, birthday, gender, phone)。
  • Mapper 接口:UserDaoStudentDaoTeacherDao
  • Mapper XML:UserMapper.xmlStudentMapper.xmlTeacherMapper.xml
  • 测试:UserTestStudentTestTeacherTest

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> 会:
    1. 在有条件时自动加上 WHERE
    2. 自动去掉首个条件前多余的 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 对应方法:dtUpdateStudentdtUpdateTeacher


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 有同名的批量方法:deleteMoreinsertMore


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);
}

运行说明:

  1. 确保 SqlMapConfig.xml 已注册对应 Mapper。
  2. 用 JUnit 运行测试,控制台可看到 STDOUT_LOGGING 打印的 SQL。
  3. 事务在 @Aftersession.commit(),写操作会提交。

12. 常见坑与规避

  • 参数名不一致 :接口参数名、XML 中的占位符/集合名必须一致;多个参数时可用 @Param
  • ${} 注入风险 :优先用 #{};若必须用 ${}(动态列/表),先做白名单校验。
  • 空条件导致全表更新/删除 :动态 SQL 条件为空时要谨慎,可在 Java 侧校验,或在 XML 中加兜底(如 otherwise 限制)。
  • 末尾逗号<set>suffixOverrides="," 处理;不要手写逗号。
  • choose 优先级 :命中第一个 <when> 后不会再看其他条件,顺序要慎重。

13. 常见面试题与简答

  1. #{} 和 ${} 区别?

    • #{} 预编译占位,防注入;${} 字符串直拼,易注入,仅在动态表/列名等必须场景使用且要白名单。
  2. <where><trim prefix="where"> 有何不同?

    • <where> 自动加 WHERE 并去掉首个 AND/OR;<trim> 更灵活,可自定义前后缀与裁剪规则。
  3. <set><trim prefix="set" suffixOverrides=","> 的区别?

    • 效果类似,<trim> 可自定义更复杂的裁剪逻辑,<set> 更简洁专用于 UPDATE。
  4. <choose> 适用场景?

    • 有优先级的条件选择,类似 if/else if/else,一次只命中一个分支。
  5. 批量操作用什么标签?

    • <foreach>,常见用于 IN (...) 和批量插入。
  6. 如何防止动态更新把所有字段清空?

    • 在 Java 侧校验至少有一个可更新字段;或在 XML 用 <set> 包裹 <if>,确保 where id = ... 必填。
  7. selectKey 的作用?

    • 获取插入后的主键并回填到对象属性;order 可选 BEFORE/AFTER。
  8. 动态 SQL 是否影响性能?

    • MyBatis 会生成最终 SQL 并预编译,性能主要取决于 SQL 本身和索引;关键是保持条件有索引、避免全表扫描。

14. 实践清单(照着做)

  1. 按需挑选标签:多条件用 <where> + <if>,优先级用 <choose>,动态更新用 <set>/<trim>,批量用 <foreach>
  2. 参数命名对齐:接口参数名 + XML 占位符一致,多个参数用 @Param
  3. 防注入:能用 #{} 不用 ${};动态表/列名必须白名单。
  4. 提交事务:写操作后记得 commit();或 openSession(true)
  5. 打日志:<setting name="logImpl" value="STDOUT_LOGGING"/> 便于排查。

15. 结语

掌握动态 SQL 的几个标签组合,就能写出可维护、可扩展的查询和更新语句。建议多练习:

  • 将你的条件从 1 个扩展到 N 个,看 SQL 是否按预期生成;
  • 尝试批量插入/删除,观察生成的 SQL;
  • choose 做优先级控制,避免多重 if 嵌套。

祝学习顺利,面试稳稳拿下! 😊

相关推荐
枫叶丹42 小时前
【Qt开发】Qt事件(二)-> QKeyEvent 按键事件
c语言·开发语言·数据库·c++·qt·microsoft
llxxyy卢4 小时前
JWT安全&预编译CASE注入
数据库·sql·安全
大布布将军11 小时前
⚡️ 深入数据之海:SQL 基础与 ORM 的应用
前端·数据库·经验分享·sql·程序人生·面试·改行学it
唐人街都是苦瓜脸11 小时前
踩坑记:ORA-01722 无效数字错误排查与解决(附实战案例)
sql·oracle
JIngJaneIL12 小时前
基于java+ vue农产投入线上管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
源代码•宸12 小时前
goframe框架签到系统项目(BITFIELD 命令详解、Redis Key 设计、goframe 框架教程、安装MySQL)
开发语言·数据库·经验分享·redis·后端·mysql·golang
川贝枇杷膏cbppg12 小时前
Redis 的 AOF
java·数据库·redis
TG:@yunlaoda360 云老大12 小时前
如何在华为云国际站代理商控制台进行SFS Turbo的性能与容量核查?
服务器·网络·数据库·华为云
ytttr87313 小时前
MATLAB基于LDA的人脸识别算法实现(ORL数据库)
数据库·算法·matlab