Mybatis动态SQL深度解析与实战:让你的SQL“活”起来

动态SQL是Mybatis最强大、最富有魅力的功能之一。如果说Mybatis的基础功能是让你告别了繁琐的JDBC,那么动态SQL就是让你在面对复杂多变的业务需求时,能够写出优雅、灵活且极易维护的SQL的“神兵利兵”。

本教程将带你领略动态SQL的魔力,从简单的if判断到复杂的循环,让你彻底掌握这项企业级开发必备的核心技能。


Mybatis动态SQL深度解析与实战:让你的SQL“活”起来

引子:当SQL遇到“选择困难症”

想象一下,你正在开发一个电商网站的商品搜索功能。产品经理提出了这样的需求:

  • 用户可以输入商品名称进行模糊搜索。
  • 用户可以选择一个价格区间进行筛选。
  • 用户可以选择一个商品分类。
  • 当然,用户也可以同时输入商品名称、选择价格区间和分类进行组合搜索!
  • 哦对了,用户还可以选择按价格升序或降序排序...

如果为每一种组合都写一个独立的SQL查询方法,你可能会写出findByNamefindByPricefindByNameAndPricefindByNameAndPriceAndCategory... 你的代码会瞬间爆炸,维护起来简直是噩梦。

这时,你多么希望SQL能像Java代码一样,拥有if-elseswitchfor这样的逻辑判断能力,根据不同的输入,智能地“组装”出自己想要的样子。

Mybatis的动态SQL,就是为了解决这个“选择困难症”而生的!它赋予了SQL逻辑判断和动态拼接的能力,让你的SQL从死板的字符串,变成一个能思考、会变形的“智能体”。


第一部分:核心动态SQL标签与基础实践

动态SQL的所有魔法都蕴藏在Mapper.xml文件中的几个核心标签里。我们逐一来看。

1. 项目结构与准备

我们将创建一个新的ProductMapper来演示商品搜索的场景。

  • 项目结构

    mybatis-dynamic-sql-demo/
    └── src/
        └── main/
            ├── java/
            │   └── com/
            │       └── example/
            │           ├── entity/
            │           │   └── Product.java      // 商品实体类
            │           ├── mapper/
            │           │   └── ProductMapper.java  // Mapper接口
            │           └── test/
            │               └── DynamicSqlTest.java // 动态SQL测试类
            └── resources/
                ├── mappers/
                │   └── ProductMapper.xml     // 【核心】动态SQL在这里
                └── mybatis-config.xml
    
  • 代码准备

    1. Product.java (实体类)

      // src/main/java/com/example/entity/Product.java
      package com.example.entity;
      import lombok.Data;
      import java.math.BigDecimal;
      
      @Data
      public class Product {
          private Integer id;
          private String name;
          private Integer categoryId;
          private BigDecimal price;
      }
      
    2. ProductMapper.java (接口)

      // src/main/java/com/example/mapper/ProductMapper.java
      package com.example.mapper;
      import com.example.entity.Product;
      import org.apache.ibatis.annotations.Param;
      import java.util.List;
      import java.math.BigDecimal;
      
      public interface ProductMapper {
          // 这是我们将要实现的动态查询方法
          List<Product> findProducts(
              @Param("name") String name,
              @Param("minPrice") BigDecimal minPrice,
              @Param("maxPrice") BigDecimal maxPrice
          );
      
          // 用于测试<set>标签
          int updateProduct(Product product);
      
          // 用于测试<foreach>标签
          List<Product> findProductsByIds(List<Integer> ids);
      }
      

2. if标签:最基础的条件判断

if标签是最常用的动态SQL标签,它的作用是:如果满足test属性中的条件,就将标签内的SQL片段包含进来。

  • ProductMapper.xml

    <!-- src/main/resources/mappers/ProductMapper.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.mapper.ProductMapper">
        <select id="findProducts" resultType="com.example.entity.Product">
            SELECT * FROM product
            WHERE 1=1
            <if test="name != null and name != ''">
                AND name LIKE CONCAT('%', #{name}, '%')
            </if>
            <if test="minPrice != null">
                AND price >= #{minPrice}
            </if>
            <if test="maxPrice != null">
                AND price &lt;= #{maxPrice}  <!-- 注意: XML中 < 要转义为 &lt; -->
            </if>
        </select>
    </mapper>
    
  • 痛点分析:上面的写法有个小问题。如果所有if条件都不满足,SQL会变成SELECT * FROM product WHERE 1=1,虽然能运行,但WHERE 1=1有点多余。如果第一个if条件满足,SQL会变成WHERE AND ...,这会导致语法错误。为了解决这个问题,where标签登场了。

3. where标签:智能处理WHEREAND

where标签会自动判断其内部是否有SQL片段被生成。

  • 如果有,它会自动添加一个WHERE关键字,并且智能地去掉第一个ANDOR

  • 如果没有,它什么也不做。

  • ProductMapper.xml (优化版)

    <select id="findProducts" resultType="com.example.entity.Product">
        SELECT * FROM product
        <where>
            <if test="name != null and name != ''">
                AND name LIKE CONCAT('%', #{name}, '%')
            </if>
            <if test="minPrice != null">
                AND price >= #{minPrice}
            </if>
            <if test="maxPrice != null">
                AND price &lt;= #{maxPrice}
            </if>
        </where>
    </select>
    

    现在,代码既安全又优雅!

4. choose, when, otherwise:多选一的switch

这组标签类似于Java的switch-case-default,用于实现“多选一”的逻辑。

企业级例子:复杂的排序需求
"如果用户指定了排序方式,就按他说的办;否则,如果商品名称不为空,就按相关度(模拟)排;再否则,就默认按ID排。"

  • ProductMapper.xml (添加choose逻辑)

    <!-- 修改findProducts方法,增加一个orderBy参数 -->
    <select id="findProducts" resultType="com.example.entity.Product">
        SELECT * FROM product
        <where>
            <!-- ... 省略之前的if条件 ... -->
        </where>
        <choose>
            <when test="orderBy != null and orderBy != ''">
                ORDER BY ${orderBy} <!-- orderBy是列名,只能用$,要确保传入的值安全! -->
            </when>
            <when test="name != null and name != ''">
                ORDER BY name ASC <!-- 模拟按相关度排序 -->
            </when>
            <otherwise>
                ORDER BY id DESC
            </otherwise>
        </choose>
    </select>
    

第二部分:进阶动态SQL标签与企业级实践

1. set标签:智能处理UPDATE语句

UPDATE语句中,我们常常只更新传入对象中不为空的字段。如果手动拼接逗号,,会非常麻烦。set标签就是为了解决这个问题。

  • 它会自动添加SET关键字。
  • 它会自动去掉最后一个多余的逗号

企业级例子:动态更新商品信息

  • ProductMapper.xml

    <update id="updateProduct">
        UPDATE product
        <set>
            <if test="name != null and name != ''">
                name = #{name},
            </if>
            <if test="categoryId != null">
                category_id = #{categoryId},
            </if>
            <if test="price != null">
                price = #{price},
            </if>
        </set>
        WHERE id = #{id}
    </update>
    

    无论你传入的product对象有几个字段不为空,生成的SQL都会是完美的UPDATE语句,没有多余的逗号。

2. foreach标签:循环处理集合

当需要对一个集合(如List, Set, Array)进行循环,并将其用于IN查询或批量插入时,foreach是你的不二之选。

企业级例子:根据ID列表批量查询商品

  • ProductMapper.xml

    <select id="findProductsByIds" resultType="com.example.entity.Product">
        SELECT * FROM product
        WHERE id IN
        <foreach item="productId" collection="list" open="(" separator="," close=")">
            #{productId}
        </foreach>
    </select>
    
  • foreach标签属性详解

    • collection: 要迭代的集合。如果参数是List,默认值是list;如果是Array,默认是array。如果用@Param注解命名了,就用注解的名字。
    • item: 每次迭代出的元素的变量名。
    • open: 在循环开始前拼接的字符串。
    • close: 在循环结束后拼接的字符串。
    • separator: 每次迭代之间拼接的分隔符。

    上面的配置会生成类似 WHERE id IN (1, 2, 3) 这样的SQL。

3. <sql><include>:重用SQL片段

当多个查询有共同的列或条件时,可以把它们抽取成一个可重用的SQL片段。

  • <sql>: 用于定义一个SQL片段。

  • <include>: 用于在其他地方引用这个SQL片段。

  • ProductMapper.xml

    <!-- 1. 定义一个可重用的列列表 -->
    <sql id="base_column_list">
        id, name, category_id, price
    </sql>
    
    <!-- 2. 在查询中引用它 -->
    <select id="findProducts" resultType="com.example.entity.Product">
        SELECT <include refid="base_column_list"/>
        FROM product
        <where>
            <!-- ... -->
        </where>
    </select>
    
    <select id="findProductsByIds" resultType="com.example.entity.Product">
        SELECT <include refid="base_column_list"/>
        FROM product
        WHERE id IN
        <!-- ... -->
    </select>
    

    这样做极大地提高了代码的复用性和可维护性。当表结构变化需要增减字段时,只需修改一处<sql>片段即可。


第三部分:动手测试

现在,我们来编写测试代码,亲手验证动态SQL的威力。

  • DynamicSqlTest.java (测试类)

    // src/main/java/com/example/test/DynamicSqlTest.java
    package com.example.test;
    
    import com.example.entity.Product;
    import com.example.mapper.ProductMapper;
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    import java.io.IOException;
    import java.io.InputStream;
    import java.math.BigDecimal;
    import java.util.Arrays;
    import java.util.List;
    
    public class DynamicSqlTest {
        public static void main(String[] args) throws IOException {
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(
                Resources.getResourceAsStream("mybatis-config.xml")
            );
    
            try (SqlSession session = sqlSessionFactory.openSession(true)) {
                ProductMapper mapper = session.getMapper(ProductMapper.class);
    
                System.out.println("--- 测试 <if> 和 <where> ---");
                System.out.println("1. 只按名称搜索:");
                List<Product> products1 = mapper.findProducts("Laptop", null, null);
                products1.forEach(System.out::println);
    
                System.out.println("\n2. 只按价格区间搜索:");
                List<Product> products2 = mapper.findProducts(null, new BigDecimal("1000"), new BigDecimal("5000"));
                products2.forEach(System.out::println);
    
                System.out.println("\n3. 组合搜索:");
                List<Product> products3 = mapper.findProducts("Phone", new BigDecimal("3000"), null);
                products3.forEach(System.out::println);
    
                System.out.println("\n--- 测试 <set> ---");
                Product productToUpdate = new Product();
                productToUpdate.setId(1); // 假设更新ID为1的商品
                productToUpdate.setPrice(new BigDecimal("8999.99")); // 只更新价格
                int updatedRows = mapper.updateProduct(productToUpdate);
                System.out.println("更新了 " + updatedRows + " 行数据。");
    
                System.out.println("\n--- 测试 <foreach> ---");
                List<Integer> ids = Arrays.asList(1, 3, 5);
                List<Product> productsByIds = mapper.findProductsByIds(ids);
                System.out.println("根据ID列表查询到的商品:");
                productsByIds.forEach(System.out::println);
            }
        }
    }
    

总结:动态SQL是思想,而非仅仅是标签

掌握动态SQL,不仅仅是记住几个标签的用法,更重要的是建立一种“构建SQL”的思想。当你面对一个复杂的查询需求时,你应该思考:

  • 这个SQL的哪些部分是固定不变的?(如 SELECT * FROM table
  • 哪些部分是根据条件可选的?(如 AND name = ?,用 <if>
  • 哪些部分是多选一的?(如排序,用 <choose>
  • 哪些部分需要循环生成?(如 IN 查询,用 <foreach>
  • 哪些部分可以被重用?(如列列表,用 <sql>

将需求拆解成这些可组合的SQL片段,再用Mybatis的动态标签将它们优雅地组织起来,这就是动态SQL的艺术。熟练掌握它,你就能在数据访问层的开发中游刃有余,无往不利。

posted @ 2025-07-02 17:18  清明雨上~  阅读(54)  评论(0)    收藏  举报