MyBatis之动态SQL编写指南

MyBatis之动态SQL编写指南

    • 一、动态SQL的核心价值
      • 传统JDBC的SQL拼接问题
      • MyBatis动态SQL的优势
    • 二、核心动态SQL标签详解
      • 2.1 `if`标签:条件判断
        • 基本用法
        • `test`表达式规则
      • 2.2 `where`与`trim`标签:条件拼接优化
        • 2.2.1 `where`标签
        • 2.2.2 `trim`标签:自定义拼接规则
      • 2.3 `choose`、`when`、`otherwise`标签:多条件分支
      • 2.4 `set`标签:更新语句拼接
      • 2.5 `foreach`标签:循环遍历
        • 2.5.1 基本属性
        • 2.5.2 常见用法
          • 用法1:`IN`查询(遍历集合)
          • 用法2:批量插入
          • 用法3:批量更新(MySQL)
      • 2.6 `sql`与`include`标签:SQL片段复用
    • 三、实战案例:复杂查询场景
      • 3.1 Mapper接口
      • 3.2 XML映射器
      • 3.3 关键逻辑说明
    • 四、常见问题与避坑指南
      • 4.1 `test`表达式空值判断错误
      • 4.2 `foreach`集合参数名错误
      • 4.3 SQL注入风险(`$`与`#`的区别)
      • 4.4 条件过多导致SQL冗长

实际开发中SQL语句往往需要根据不同条件动态生成(如多条件查询、动态排序、批量操作等),MyBatis的动态SQL通过标签化语法,实现了SQL的灵活拼接,避免了手动拼接SQL的繁琐和SQL注入风险。本文我将系统讲解MyBatis动态SQL的核心标签(ifchooseforeach等),并结合实例解析其用法和最佳实践,带你掌握动态SQL的编写技巧。

一、动态SQL的核心价值

动态SQL是MyBatis的核心特性之一,解决了传统JDBC手动拼接SQL的痛点:

传统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的冗余条件);
  • 存在SQL注入风险(直接拼接用户输入);
  • 代码可读性差,维护困难。

MyBatis动态SQL的优势

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注入;
  • 标签化语法清晰,易于维护。

二、核心动态SQL标签详解

MyBatis提供了多种动态SQL标签,覆盖常见的条件判断、循环、拼接等场景。

2.1 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(无需判断空字符串)。

2.2 wheretrim标签:条件拼接优化

if标签配合wheretrim标签,可自动处理多余的AND/OR,替代冗余的WHERE 1=1

2.2.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>

效果

  • usernameage都有值,生成WHERE username = ? AND age = ?
  • 若仅age有值,生成WHERE age = ?(自动去除AND);
  • 若没有条件,不生成WHERE子句(避免SELECT * FROM user WHERE的语法错误)。
2.2.2 trim标签:自定义拼接规则

trim标签更灵活,可自定义前缀、后缀及需要去除的字符。

属性 作用
prefix 拼接前缀(如WHERE
suffix 拼接后缀(如;
prefixOverrides 需要去除的前缀字符(如ANDOR
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>

2.3 choosewhenotherwise标签:多条件分支

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 == nullusername != null,拼接AND username = #{username}
  • 若前两个条件都不满足,拼接AND age > 18

2.4 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>

效果

  • usernameage有值,生成UPDATE user SET username = ?, age = ? WHERE id = ?(自动去除最后一个逗号);
  • 避免手动处理逗号拼接(如SET username = ?, age = ?后的冗余逗号)。

2.5 foreach标签:循环遍历

foreach标签用于循环遍历集合(数组、List、Set等),常用于IN查询、批量插入、批量更新等场景。

2.5.1 基本属性
属性 作用 示例
collection 集合参数名(需与接口方法参数名一致) listarrayids
item 循环变量名(遍历出的元素) item="id"
index 循环索引(可选) index="i"
open 拼接前缀 open="("
close 拼接后缀 close=")"
separator 元素分隔符 separator=","
2.5.2 常见用法
用法1: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)
用法2:批量插入
// 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')
用法3:批量更新(MySQL)
<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)。

2.6 sqlinclude标签: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>

三、实战案例:复杂查询场景

结合多个标签实现复杂查询(多条件+排序+分页)。

3.1 Mapper接口

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 // 分页大小
);

3.2 XML映射器

<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>

3.3 关键逻辑说明

  • 条件优先级age(等于)优先级高于minAge/maxAge(范围),通过choose标签控制;
  • 排序安全sortBy使用${}(因需作为字段名),需确保sortBy为后端可控值(避免用户输入直接传入,防止SQL注入);
  • 分页灵活:仅当offsetlimit都存在时拼接分页条件。

四、常见问题与避坑指南

4.1 test表达式空值判断错误

问题:字符串空值判断遗漏null或空字符串,导致条件不生效。


<if test="username != null">AND username = #{username}if>

username为空字符串("")时,条件不拼接(符合预期),但需明确逻辑。

正确写法

  • 允许空字符串作为查询条件:test="username != null"
  • 不允许空字符串:test="username != null and username != ''"

4.2 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" ...>

4.3 SQL注入风险($#的区别)

MyBatis有两种参数占位符:

  • #{}:预编译参数(安全,推荐),生成?占位符;
  • ${}:直接拼接SQL(不安全),用于表名、排序字段等无法预编译的场景。

风险示例


ORDER BY ${sortBy}

安全措施

  • 限制${}的使用场景(仅用于后端可控参数);
  • ${}参数进行白名单校验(如排序字段仅允许idusername等)。

4.4 条件过多导致SQL冗长

问题:复杂查询的XML标签嵌套过多,可读性差。

解决方案

  • 拆分SQL片段(sql+include);
  • 合理使用注释;
  • 必要时考虑MyBatis-Plus等增强框架(自动生成动态SQL)。

总结
MyBatis动态SQL通过标签化语法解决了SQL拼接的痛点:

  1. 简化条件拼接ifwhereset等标签自动处理冗余的AND、逗号等,避免语法错误;
  2. 提升代码复用sql+include标签复用SQL片段,减少重复代码;
  3. 支持复杂场景foreach处理集合,choose处理分支,满足批量操作、多条件查询等需求;
  4. 安全可靠#{}参数预编译,避免SQL注入(合理使用${})。

若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ

你可能感兴趣的:(mybatis,mybatis)