动态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
@Param("name") String name,
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice
);
// 用于测试
int updateProduct(Product product);
// 用于测试
List
}
2. if标签:最基础的条件判断
if标签是最常用的动态SQL标签,它的作用是:如果满足test属性中的条件,就将标签内的SQL片段包含进来。
ProductMapper.xml
SELECT * FROM product
WHERE 1=1
AND name LIKE CONCAT('%', #{name}, '%')
AND price >= #{minPrice}
AND price <= #{maxPrice}
痛点分析:上面的写法有个小问题。如果所有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 * FROM product
AND name LIKE CONCAT('%', #{name}, '%')
AND price >= #{minPrice}
AND price <= #{maxPrice}
现在,代码既安全又优雅!
4. choose, when, otherwise:多选一的switch
这组标签类似于Java的switch-case-default,用于实现“多选一”的逻辑。
企业级例子:复杂的排序需求
"如果用户指定了排序方式,就按他说的办;否则,如果商品名称不为空,就按相关度(模拟)排;再否则,就默认按ID排。"
ProductMapper.xml (添加choose逻辑)
SELECT * FROM product
ORDER BY ${orderBy}
ORDER BY name ASC
ORDER BY id DESC
第二部分:进阶动态SQL标签与企业级实践
1. set标签:智能处理UPDATE语句
在UPDATE语句中,我们常常只更新传入对象中不为空的字段。如果手动拼接逗号,,会非常麻烦。set标签就是为了解决这个问题。
它会自动添加SET关键字。
它会自动去掉最后一个多余的逗号。
企业级例子:动态更新商品信息
ProductMapper.xml
UPDATE product
name = #{name},
category_id = #{categoryId},
price = #{price},
WHERE id = #{id}
无论你传入的product对象有几个字段不为空,生成的SQL都会是完美的UPDATE语句,没有多余的逗号。
2. foreach标签:循环处理集合
当需要对一个集合(如List, Set, Array)进行循环,并将其用于IN查询或批量插入时,foreach是你的不二之选。
企业级例子:根据ID列表批量查询商品
ProductMapper.xml
SELECT * FROM product
WHERE id IN
#{productId}
foreach标签属性详解:
collection: 要迭代的集合。如果参数是List,默认值是list;如果是Array,默认是array。如果用@Param注解命名了,就用注解的名字。
item: 每次迭代出的元素的变量名。
open: 在循环开始前拼接的字符串。
close: 在循环结束后拼接的字符串。
separator: 每次迭代之间拼接的分隔符。
上面的配置会生成类似 WHERE id IN (1, 2, 3) 这样的SQL。
3.
当多个查询有共同的列或条件时,可以把它们抽取成一个可重用的SQL片段。
ProductMapper.xml
id, name, category_id, price
SELECT
FROM product
SELECT
FROM product
WHERE id IN
这样做极大地提高了代码的复用性和可维护性。当表结构变化需要增减字段时,只需修改一处
第三部分:动手测试
现在,我们来编写测试代码,亲手验证动态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("--- 测试
System.out.println("1. 只按名称搜索:");
List
products1.forEach(System.out::println);
System.out.println("\n2. 只按价格区间搜索:");
List
products2.forEach(System.out::println);
System.out.println("\n3. 组合搜索:");
List
products3.forEach(System.out::println);
System.out.println("\n--- 测试
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--- 测试
List
List
System.out.println("根据ID列表查询到的商品:");
productsByIds.forEach(System.out::println);
}
}
}
总结:动态SQL是思想,而非仅仅是标签
掌握动态SQL,不仅仅是记住几个标签的用法,更重要的是建立一种“构建SQL”的思想。当你面对一个复杂的查询需求时,你应该思考:
这个SQL的哪些部分是固定不变的?(如 SELECT * FROM table)
哪些部分是根据条件可选的?(如 AND name = ?,用
哪些部分是多选一的?(如排序,用
哪些部分需要循环生成?(如 IN 查询,用
哪些部分可以被重用?(如列列表,用
将需求拆解成这些可组合的SQL片段,再用Mybatis的动态标签将它们优雅地组织起来,这就是动态SQL的艺术。熟练掌握它,你就能在数据访问层的开发中游刃有余,无往不利。