Mybatis动态SQL深度解析与实战:让你的SQL“活”起来
动态SQL是Mybatis最强大、最富有魅力的功能之一。如果说Mybatis的基础功能是让你告别了繁琐的JDBC,那么动态SQL就是让你在面对复杂多变的业务需求时,能够写出优雅、灵活且极易维护的SQL的“神兵利兵”。
本教程将带你领略动态SQL的魔力,从简单的if判断到复杂的循环,让你彻底掌握这项企业级开发必备的核心技能。
Mybatis动态SQL深度解析与实战:让你的SQL“活”起来
引子:当SQL遇到“选择困难症”
想象一下,你正在开发一个电商网站的商品搜索功能。产品经理提出了这样的需求:
- 用户可以只输入商品名称进行模糊搜索。
- 用户可以只选择一个价格区间进行筛选。
- 用户可以只选择一个商品分类。
- 当然,用户也可以同时输入商品名称、选择价格区间和分类进行组合搜索!
- 哦对了,用户还可以选择按价格升序或降序排序...
如果为每一种组合都写一个独立的SQL查询方法,你可能会写出findByName、findByPrice、findByNameAndPrice、findByNameAndPriceAndCategory... 你的代码会瞬间爆炸,维护起来简直是噩梦。
这时,你多么希望SQL能像Java代码一样,拥有if-else、switch、for这样的逻辑判断能力,根据不同的输入,智能地“组装”出自己想要的样子。
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 -
代码准备
-
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; } -
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 <= #{maxPrice} <!-- 注意: XML中 < 要转义为 < --> </if> </select> </mapper> -
痛点分析:上面的写法有个小问题。如果所有
if条件都不满足,SQL会变成SELECT * FROM product WHERE 1=1,虽然能运行,但WHERE 1=1有点多余。如果第一个if条件满足,SQL会变成WHERE AND ...,这会导致语法错误。为了解决这个问题,where标签登场了。
3. where标签:智能处理WHERE和AND
where标签会自动判断其内部是否有SQL片段被生成。
-
如果有,它会自动添加一个
WHERE关键字,并且智能地去掉第一个AND或OR。 -
如果没有,它什么也不做。
-
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 <= #{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的艺术。熟练掌握它,你就能在数据访问层的开发中游刃有余,无往不利。

浙公网安备 33010602011771号