MyBatis 动态SQL学习

MyBatis 动态SQL详解

MyBatis 的动态SQL是其核心特性之一,允许基于条件动态构建SQL语句,避免了大量重复的SQL代码。

一、核心动态SQL标签

1. <if> 标签 - 条件判断

xml 复制代码
<select id="findUsers" resultType="User">
  SELECT * FROM users 
  WHERE 1=1
  <if test="name != null and name != ''">
    AND name = #{name}
  </if>
  <if test="age != null">
    AND age = #{age}
  </if>
  <if test="email != null">
    AND email LIKE CONCAT('%', #{email}, '%')
  </if>
</select>

2. <choose>, <when>, <otherwise> - 多条件选择

xml 复制代码
<select id="findActiveUsers" resultType="User">
  SELECT * FROM users 
  WHERE 
  <choose>
    <when test="status == 'active'">
      status = 'ACTIVE'
    </when>
    <when test="status == 'inactive'">
      status = 'INACTIVE'
    </when>
    <otherwise>
      status = 'PENDING'
    </otherwise>
  </choose>
</select>

3. <where> 标签 - 智能处理WHERE子句

xml 复制代码
<select id="searchUsers" resultType="User">
  SELECT * FROM users 
  <where>
    <if test="name != null">
      AND name = #{name}
    </if>
    <if test="age != null">
      AND age = #{age}
    </if>
    <if test="email != null">
      AND email LIKE #{email}
    </if>
  </where>
  ORDER BY id
</select>

特点:自动删除第一个多余的AND或OR,如果所有条件都不满足,则不生成WHERE子句。

4. <set> 标签 - 更新语句

xml 复制代码
<update id="updateUser">
  UPDATE users
  <set>
    <if test="name != null">name = #{name},</if>
    <if test="age != null">age = #{age},</if>
    <if test="email != null">email = #{email},</if>
    <if test="updateTime != null">update_time = #{updateTime},</if>
  </set>
  WHERE id = #{id}
</update>

特点:自动删除末尾多余的逗号。

5. <trim> 标签 - 自定义截取

xml 复制代码
<!-- 替代<where> -->
<select id="findUsers" resultType="User">
  SELECT * FROM users 
  <trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="name != null">AND name = #{name}</if>
    <if test="age != null">OR age = #{age}</if>
  </trim>
</select>

<!-- 替代<set> -->
<update id="updateUser">
  UPDATE users
  <trim prefix="SET" suffixOverrides=",">
    <if test="name != null">name = #{name},</if>
    <if test="age != null">age = #{age},</if>
  </trim>
  WHERE id = #{id}
</update>

6. <foreach> 标签 - 循环遍历

xml 复制代码
<!-- IN查询 -->
<select id="findUsersByIds" resultType="User">
  SELECT * FROM users 
  WHERE id IN 
  <foreach item="id" index="index" collection="ids"
      open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

<!-- 批量插入 -->
<insert id="batchInsertUsers">
  INSERT INTO users (name, age, email) VALUES 
  <foreach item="user" collection="list" separator=",">
    (#{user.name}, #{user.age}, #{user.email})
  </foreach>
</insert>

<!-- 批量更新(MySQL) -->
<update id="batchUpdateUsers">
  <foreach item="user" collection="list" separator=";">
    UPDATE users 
    SET name = #{user.name}, age = #{user.age}
    WHERE id = #{user.id}
  </foreach>
</update>

7. <bind> 标签 - 变量绑定

xml 复制代码
<select id="searchUsers" resultType="User">
  <!-- 创建变量并在OGNL表达式中使用 -->
  <bind name="pattern" value="'%' + name + '%'" />
  <bind name="emailPattern" value="email + '%'" />
  
  SELECT * FROM users 
  WHERE 
    name LIKE #{pattern}
    AND email LIKE #{emailPattern}
</select>

二、OGNL表达式

MyBatis动态SQL中使用的表达式语言:

xml 复制代码
<!-- 复杂条件判断 -->
<if test="(name != null and name != '') or (age != null and age > 0)">
  AND status = 'ACTIVE'
</if>

<!-- 调用Java方法 -->
<if test="@com.example.StringUtils@isNotEmpty(email)">
  AND email = #{email}
</if>

<!-- 集合判断 -->
<if test="ids != null and ids.size() > 0">
  AND id IN 
  <foreach collection="ids" item="id" open="(" close=")" separator=",">
    #{id}
  </foreach>
</if>

三、实战应用示例

1. 复杂搜索查询

xml 复制代码
<select id="searchWithPagination" resultType="User">
  SELECT * FROM users 
  <where>
    <!-- 姓名模糊搜索 -->
    <if test="name != null and name != ''">
      <bind name="namePattern" value="'%' + name + '%'" />
      AND (first_name LIKE #{namePattern} OR last_name LIKE #{namePattern})
    </if>
    
    <!-- 年龄范围 -->
    <if test="minAge != null">
      AND age >= #{minAge}
    </if>
    <if test="maxAge != null">
      AND age &lt;= #{maxAge}
    </if>
    
    <!-- 状态多选 -->
    <if test="statusList != null and statusList.size() > 0">
      AND status IN 
      <foreach collection="statusList" item="status" open="(" close=")" separator=",">
        #{status}
      </foreach>
    </if>
    
    <!-- 注册时间范围 -->
    <if test="startDate != null and endDate != null">
      AND register_time BETWEEN #{startDate} AND #{endDate}
    </if>
  </where>
  
  <!-- 动态排序 -->
  <choose>
    <when test="orderBy != null and orderBy != ''">
      ORDER BY ${orderBy}
      <if test="orderDirection != null and orderDirection != ''">
        ${orderDirection}
      </if>
    </when>
    <otherwise>
      ORDER BY id DESC
    </otherwise>
  </choose>
  
  <!-- 分页(MySQL) -->
  <if test="offset != null and limit != null">
    LIMIT #{offset}, #{limit}
  </if>
</select>

2. 批量操作优化

xml 复制代码
<!-- 批量插入(MySQL的ON DUPLICATE KEY UPDATE) -->
<insert id="batchInsertOrUpdate">
  INSERT INTO products (id, name, price, stock) VALUES 
  <foreach item="product" collection="list" separator=",">
    (#{product.id}, #{product.name}, #{product.price}, #{product.stock})
  </foreach>
  ON DUPLICATE KEY UPDATE
  name = VALUES(name),
  price = VALUES(price),
  stock = stock + VALUES(stock)
</insert>

<!-- 批量删除 -->
<delete id="batchDelete">
  DELETE FROM users 
  WHERE id IN 
  <foreach item="id" collection="ids" open="(" close=")" separator=",">
    #{id}
  </foreach>
  AND status = 'INACTIVE'
</delete>

3. 动态表名和列名

xml 复制代码
<sql id="dynamicTable">
  <!-- 根据年份分表 -->
  <choose>
    <when test="year == 2023">orders_2023</when>
    <when test="year == 2024">orders_2024</when>
    <otherwise>orders</otherwise>
  </choose>
</sql>

<select id="findOrdersByYear" resultType="Order">
  SELECT * FROM 
  <include refid="dynamicTable"/>
  WHERE user_id = #{userId}
</select>

四、注解方式动态SQL

java 复制代码
// 使用@SelectProvider注解
@SelectProvider(type = UserSqlProvider.class, method = "buildSearchSql")
List<User> searchUsers(@Param("name") String name, 
                       @Param("status") String status);

// SQL提供者类
public class UserSqlProvider {
    public String buildSearchSql(Map<String, Object> params) {
        return new SQL() {{
            SELECT("*");
            FROM("users");
            
            if (params.get("name") != null) {
                WHERE("name = #{name}");
            }
            
            if (params.get("status") != null) {
                WHERE("status = #{status}");
            }
            
            ORDER_BY("id DESC");
        }}.toString();
    }
}

// 或者使用字符串拼接方式
public class UserSqlProvider {
    public String buildDynamicSql(Map<String, Object> params) {
        String sql = "SELECT * FROM users WHERE 1=1";
        
        if (params.get("name") != null) {
            sql += " AND name = #{name}";
        }
        
        if (params.get("minAge") != null) {
            sql += " AND age >= #{minAge}";
        }
        
        return sql;
    }
}

五、高级技巧

1. 使用<script>包裹动态SQL

xml 复制代码
<!-- 在注解中使用动态SQL -->
@Update("<script>" +
        "UPDATE users " +
        "<set>" +
        "  <if test='name != null'>name = #{name},</if>" +
        "  <if test='age != null'>age = #{age},</if>" +
        "</set>" +
        "WHERE id = #{id}" +
        "</script>")
void updateUserSelective(User user);

2. 动态include SQL片段

xml 复制代码
<!-- 定义可重用的SQL片段 -->
<sql id="userColumns">
  id, name, age, email, 
  <if test="includeSensitive == true">
    phone, address
  </if>
</sql>

<!-- 使用 -->
<select id="getUser" resultType="map">
  SELECT 
  <include refid="userColumns">
    <property name="includeSensitive" value="true"/>
  </include>
  FROM users WHERE id = #{id}
</select>

3. 条件中的类型处理

xml 复制代码
<select id="findByConditions" resultType="User">
  SELECT * FROM users
  <where>
    <!-- 字符串判断 -->
    <if test='name != null and name != ""'>
      AND name = #{name}
    </if>
    
    <!-- 数字判断 -->
    <if test="age != null">
      AND age = #{age}
    </if>
    
    <!-- 布尔判断 -->
    <if test="active != null">
      AND is_active = #{active}
    </if>
    
    <!-- 集合/数组判断 -->
    <if test="ids != null and ids.length > 0">
      AND id IN 
      <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
      </foreach>
    </if>
  </where>
</select>

六、性能优化建议

  1. 避免过度使用动态SQL:简单的条件判断可使用数据库函数
  2. 合理使用索引:确保动态生成的SQL能利用索引
  3. 分页优化:大数据量时使用延迟加载
  4. 缓存策略:合理配置MyBatis二级缓存
  5. 批量操作 :使用<foreach>进行批量操作,减少数据库交互

七、常见问题解决

1. 参数为0时判断失效

xml 复制代码
<!-- 错误 -->
<if test="status != null">
  <!-- 当status=0时,条件不生效 -->
</if>

<!-- 正确 -->
<if test="status != null or status == 0">
  AND status = #{status}
</if>

2. 特殊字符转义

xml 复制代码
<!-- 使用CDATA或转义 -->
<if test="type != null and type != ''">
  AND type = #{type}
  <!-- 小于号要转义 -->
  AND age &lt; 18
</if>

<!-- 或者使用CDATA -->
<if test="value != null">
  AND amount <![CDATA[ < ]]> #{maxValue}
</if>

3. 动态排序安全问题

xml 复制代码
<!-- 不安全:直接使用${} -->
ORDER BY ${orderBy}

<!-- 安全方案:白名单验证 -->
ORDER BY 
<choose>
  <when test="orderBy == 'name'">name</when>
  <when test="orderBy == 'age'">age</when>
  <when test="orderBy == 'create_time'">create_time</when>
  <otherwise>id</otherwise>
</choose>

总结

MyBatis动态SQL提供了强大而灵活的SQL构建能力,通过合理使用各种标签,可以:

  • 减少大量重复SQL代码
  • 提高代码可维护性
  • 实现复杂的业务查询逻辑
  • 保证SQL注入安全(正确使用时)

关键是要掌握各种标签的使用场景和组合方式,同时注意性能优化和安全问题。

相关推荐
devlei11 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
努力的小郑12 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor35613 小时前
MongoDB(87)如何使用GridFS?
后端
Victor35613 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁13 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp13 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴15 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友15 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒16 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan17 小时前
Go 内存回收-GC 源码1-触发与阶段
后端