MyBatis动态SQL核心标签详解与Spring Boot集成实战
在实际 Java Web 项目中,持久层框架 MyBatis 因其灵活性和对 SQL 的直接掌控能力而被广泛使用。然而,当业务逻辑变得复杂,需要根据不同条件拼接 SQL 时,传统的字符串拼接方式不仅代码冗长、难以维护,还极易引入 SQL 注入的安全风险。MyBatis 提供的动态 SQL 功能正是为了解决这一问题而生,它允许你在 XML 映射文件中,使用一组强大的标签来构建条件化的 SQL 语句。掌握动态 SQL,意味着你能用更少的代码实现更复杂的查询逻辑,显著提升开发效率和代码可读性。本文将带你从零开始,深入理解 MyBatis 动态 SQL 的核心标签及其应用场景,并通过一个完整的 Spring Boot 集成案例,展示如何在实际项目中运用这些标签来简化代码,最终形成一套可复用的查询构建模式。
1. 理解 MyBatis 动态 SQL 的核心价值与工作机制
1.1 为什么需要动态 SQL?
在数据库操作中,我们经常遇到根据前端传入的不同参数组合进行查询的场景。例如,一个用户查询接口,可能支持按姓名、年龄、状态等多个字段进行筛选,且这些条件都是可选的。如果使用静态 SQL,你需要为每一种可能的条件组合都编写一个独立的查询方法,这会导致 Mapper 接口和 XML 文件急剧膨胀,产生大量重复代码。更糟糕的是,使用 Java 代码进行字符串拼接来动态生成 SQL,不仅可读性差,而且一旦忘记处理参数中的特殊字符(如单引号),就会为 SQL 注入攻击打开大门。
MyBatis 动态 SQL 的本质,是在 XML 映射文件中,通过一组预定义的标签(如<if>,<choose>,<where>,<set>,<foreach>等),在 SQL 语句被发送到数据库执行之前,根据传入参数的值,智能地组装出最终的 SQL 字符串。这个过程由 MyBatis 框架在运行时完成,开发者无需关心字符串拼接的细节,只需声明逻辑规则。
1.2 动态 SQL 的执行流程与底层原理
当 MyBatis 执行一条映射语句时,它会经历一个完整的生命周期:从解析 Mapper XML 文件开始,到创建 SqlSession,最终执行 JDBC 操作。动态 SQL 的处理发生在 SQL 语句的构建阶段。
- 解析阶段:MyBatis 启动时会加载所有
*Mapper.xml文件,并将其中的 SQL 语句解析成一颗包含动态标签的抽象语法树(AST)。 - 参数绑定:当调用 Mapper 方法时,传入的参数对象会被封装到一个
ParameterHandler中。 - 动态处理:
SqlSource接口的实现类(通常是DynamicSqlSource)会根据传入的参数,遍历之前解析好的 AST。它会评估每个动态标签中的 OGNL 表达式(如test=“name != null”),根据表达式的结果(true/false)来决定是否包含该标签内的 SQL 片段,并处理标签本身的逻辑(如<where>标签会自动去除首个AND或OR)。 - SQL 生成:经过动态处理后,一颗静态的 SQL 树被生成,并转换为一个
BoundSql对象,其中包含了最终要发送到数据库的、带有?占位符的 SQL 字符串,以及对应的参数映射关系。 - 执行:
PreparedStatement使用这个BoundSql对象来设置参数并执行。
理解这个流程有助于你在遇到动态 SQL 拼接错误时,能够从原理层面进行排查,而不是盲目尝试。
2. 环境准备与项目基础搭建
在开始编写动态 SQL 之前,我们需要一个可以运行的基础项目。这里以 Spring Boot 集成 MyBatis 为例。
2.1 技术栈与依赖配置
确保你的开发环境已安装 JDK 8+、Maven 或 Gradle 以及一个 IDE(如 IntelliJ IDEA 或 Eclipse)。我们将使用以下技术栈:
- Spring Boot: 2.7.x (或 3.x,注意依赖包名变化)
- MyBatis Spring Boot Starter: 用于快速集成
- MySQL: 作为示例数据库
- Lombok: 简化实体类编写(可选)
在项目的pom.xml文件中添加关键依赖:
<dependencies> <!-- Spring Boot Web Starter (如果构建Web应用) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis Spring Boot Starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> <!-- 请使用与Spring Boot版本匹配的版本 --> </dependency> <!-- MySQL Connector --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>2.2 数据库与实体类准备
假设我们有一个user表,用于演示复杂的条件查询和批量操作。
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', `status` tinyint(4) DEFAULT '1' COMMENT '状态:0-禁用,1-启用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';对应的 Java 实体类User.java:
package com.example.demo.entity; import lombok.Data; import java.time.LocalDateTime; @Data public class User { private Long id; private String name; private Integer age; private String email; private Integer status; // 0:禁用, 1:启用 private LocalDateTime createTime; }2.3 MyBatis 基础配置
在application.yml或application.properties中配置数据源和 MyBatis:
# application.yml spring: datasource: url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: your_username password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: # 指定 mapper.xml 文件的位置,如果放在 resources 下对应目录 mapper-locations: classpath:mapper/*.xml # 配置实体类所在的包,方便在 XML 中使用短类名 type-aliases-package: com.example.demo.entity configuration: # 开启驼峰命名自动映射(数据库 user_name -> 实体类 userName) map-underscore-to-camel-case: true # 在控制台打印执行的 SQL,便于调试 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl3. 核心动态 SQL 标签详解与实战应用
接下来,我们逐一剖析最常用的动态 SQL 标签,并通过具体的 Mapper 接口和 XML 配置来演示其用法。我们将创建一个UserMapper.java接口及其对应的UserMapper.xml。
3.1<if>标签:基础条件判断
<if>标签用于简单的条件判断。其test属性中填写 OGNL 表达式,表达式结果为true时,标签内的 SQL 片段会被包含。
场景:实现一个多条件分页查询用户的功能,所有查询条件均为可选。
首先,创建一个查询参数封装类UserQuery.java:
package com.example.demo.dto; import lombok.Data; @Data public class UserQuery { private String name; private Integer minAge; private Integer maxAge; private Integer status; // 分页参数 private Integer pageNum; private Integer pageSize; }在UserMapper.java接口中定义方法:
package com.example.demo.mapper; import com.example.demo.entity.User; import com.example.demo.dto.UserQuery; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface UserMapper { List<User> selectByCondition(UserQuery query); }在resources/mapper/UserMapper.xml中编写动态 SQL:
<?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.demo.mapper.UserMapper"> <select id="selectByCondition" resultType="User" parameterType="UserQuery"> SELECT id, name, age, email, status, create_time FROM user WHERE 1=1 <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="minAge != null"> AND age >= #{minAge} </if> <if test="maxAge != null"> AND age <= #{maxAge} </if> <if test="status != null"> AND status = #{status} </if> ORDER BY create_time DESC </select> </mapper>关键解释:
test表达式:name != null and name != ''是典型的非空且非空字符串判断。对于字符串,两者通常需要同时检查。WHERE 1=1:这是一个小技巧,用于避免所有<if>条件都不满足时,SQL 变成SELECT ... FROM user WHERE导致语法错误。但这不是最佳实践,后面会用<where>标签优化。#{name}:MyBatis 的参数占位符,能有效防止 SQL 注入。
3.2<where>,<set>,<trim>标签:智能处理关键字
直接使用WHERE 1=1虽然简单,但不够优雅,且在某些数据库优化器中可能影响性能。<where>和<set>标签可以智能地处理WHERE和SET子句。
<where>标签:会自动去除其内容开头多余的AND或OR,并且如果标签内没有任何内容,则不会生成WHERE关键字。
优化上面的查询 SQL:
<select id="selectByCondition" resultType="User" parameterType="UserQuery"> SELECT id, name, age, email, status, create_time FROM user <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="minAge != null"> AND age >= #{minAge} </if> <if test="maxAge != null"> AND age <= #{maxAge} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>现在,即使所有<if>条件都不满足,<where>标签也不会输出WHERE,SQL 语句完全正确。
<set>标签:常用于 UPDATE 语句,会自动去除末尾多余的逗号。
// Mapper 接口 int updateUserSelective(User user);<update id="updateUserSelective" parameterType="User"> UPDATE user <set> <if test="name != null">name = #{name},</if> <if test="age != null">age = #{age},</if> <if test="email != null">email = #{email},</if> <if test="status != null">status = #{status},</if> </set> WHERE id = #{id} </update>如果只有name和age不为空,生成的 SQL 会是UPDATE user SET name=?, age=? WHERE id=?,<set>标签自动去掉了age=后面的逗号。
<trim>标签:是<where>和<set>的底层实现,功能更强大,可以自定义前缀、后缀以及要覆盖的多余字符串。
prefix:添加前缀。prefixOverrides:去掉第一个指定的前缀(如AND|OR)。suffix:添加后缀。suffixOverrides:去掉最后一个指定的后缀(如,)。
用<trim>实现<where>功能:
<trim prefix="WHERE" prefixOverrides="AND |OR "> <!-- 条件... --> </trim>用<trim>实现<set>功能:
<trim prefix="SET" suffixOverrides=","> <!-- 赋值... --> </trim>3.3<choose>,<when>,<otherwise>标签:多路选择
类似于 Java 中的switch-case语句,用于实现多选一的逻辑。
场景:查询用户,优先按姓名精确匹配,若姓名为空则按邮箱模糊匹配,若邮箱也为空则查询所有启用状态的用户。
<select id="selectByPriority" resultType="User" parameterType="map"> SELECT * FROM user <where> <choose> <when test="name != null and name != ''"> name = #{name} </when> <when test="email != null and email != ''"> email LIKE CONCAT('%', #{email}, '%') </when> <otherwise> status = 1 </otherwise> </choose> </where> </select>3.4<foreach>标签:遍历集合
这是动态 SQL 中最强大的标签之一,用于处理IN查询和批量操作。
场景1:IN 查询。根据一组 ID 列表查询用户。
// Mapper 接口 List<User> selectByIdList(@Param("idList") List<Long> idList);<select id="selectByIdList" resultType="User"> SELECT * FROM user WHERE id IN <foreach collection="idList" item="id" index="index" open="(" separator="," close=")"> #{id} </foreach> </select>collection: 指定要遍历的集合参数名。如果参数是单集合且未用@Param指定,则可用list或array。item: 遍历过程中每个元素的别名。index: 遍历的索引(可选)。open/close: 遍历开始和结束时添加的字符串。separator: 每次遍历之间添加的分隔符。
最终生成的 SQL 类似:SELECT * FROM user WHERE id IN (1, 2, 3, 4)
场景2:批量插入。这是<foreach>标签的经典应用,能极大提升数据初始化效率。
// Mapper 接口 int batchInsert(@Param("userList") List<User> userList);<insert id="batchInsert"> INSERT INTO user (name, age, email, status) VALUES <foreach collection="userList" item="user" separator=","> (#{user.name}, #{user.age}, #{user.email}, #{user.status}) </foreach> </insert>生成的 SQL 类似:INSERT INTO user (...) VALUES (...), (...), (...)
注意:虽然批量插入高效,但单条 SQL 语句的长度和参数数量受数据库配置限制(如
max_allowed_packet)。如果列表过大,需要分批处理。
3.5<sql>与<include>标签:代码复用
用于提取公共的 SQL 片段,避免重复编码。
<!-- 定义可复用的列名列表 --> <sql id="Base_Column_List"> id, name, age, email, status, create_time </sql> <!-- 定义可复用的查询条件片段 --> <sql id="Base_Where_Condition"> <where> <if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if> <if test="status != null">AND status = #{status}</if> </where> </sql> <!-- 在查询语句中引用 --> <select id="selectByCondition" resultType="User" parameterType="UserQuery"> SELECT <include refid="Base_Column_List"/> FROM user <include refid="Base_Where_Condition"/> ORDER BY create_time DESC </select>4. 构建一个完整的多条件查询服务
现在,我们将上述标签组合起来,实现一个完整的、可复用的用户查询服务,并处理分页。
4.1 完善 Mapper 与 Service
首先,在UserMapper.xml中整合一个功能强大的查询:
<select id="selectByConditionWithPage" resultType="User" parameterType="UserQuery"> SELECT <include refid="Base_Column_List"/> FROM user <where> <!-- 姓名模糊查询 --> <if test="name != null and name != ''"> <bind name="namePattern" value="'%' + name + '%'"/> AND name LIKE #{namePattern} </if> <!-- 年龄范围查询 --> <if test="minAge != null"> AND age >= #{minAge} </if> <if test="maxAge != null"> AND age <= #{maxAge} </if> <!-- 状态精确查询 --> <if test="status != null"> AND status = #{status} </if> <!-- 复杂条件:邮箱不为空或状态为启用 --> <if test="emailNotNull == true"> AND email IS NOT NULL </if> <choose> <when test="statusList != null and statusList.size() > 0"> AND status IN <foreach collection="statusList" item="s" open="(" separator="," close=")"> #{s} </foreach> </when> <when test="defaultActive == true"> AND status = 1 </when> </choose> </where> ORDER BY create_time DESC </select>新知识点:
<bind>标签:创建一个新的变量并将其绑定到当前上下文。这里用于构建模糊查询的模式字符串,使得 SQL 更清晰,也便于在多个地方复用该模式。- 在
<choose>中嵌套使用<foreach>,实现了更复杂的逻辑:优先按状态列表查询,否则按默认启用状态查询。
创建对应的 Mapper 接口方法和 Service:
// UserMapper.java List<User> selectByConditionWithPage(UserQuery query); // UserService.java @Service public class UserService { @Autowired private UserMapper userMapper; public List<User> queryUsers(UserQuery query) { // 在实际项目中,这里可以添加业务逻辑,如参数校验、数据转换等 return userMapper.selectByConditionWithPage(query); } }4.2 编写控制器进行测试
创建一个简单的 REST 控制器来测试我们的动态查询:
@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @GetMapping("/list") public ResponseEntity<List<User>> getUserList(UserQuery query) { // 示例:查询姓名包含“张”,年龄在20到30之间,状态为启用的用户 // 请求URL: /api/user/list?name=张&minAge=20&maxAge=30&status=1 List<User> users = userService.queryUsers(query); return ResponseEntity.ok(users); } }启动 Spring Boot 应用,访问上述接口,观察控制台打印的 SQL。你会看到,根据传入参数的不同,MyBatis 动态生成了完全不同的 SQL 语句,这正是动态 SQL 的魅力所在。
5. 动态 SQL 的常见问题与深度排查
即使掌握了语法,在实际开发中依然会遇到各种问题。下面是一些典型场景的排查思路。
5.1 SQL 拼接错误与语法问题
| 问题现象 | 可能原因 | 检查与解决 |
|---|---|---|
控制台报错:SQL 语法错误,例如多出AND、WHERE后无条件等。 | 1. 未使用<where>标签,且所有<if>条件都不满足,导致WHERE关键字后无内容。2. 在 <where>标签内的条件前手动添加了WHERE。3. <set>标签内条件都不满足,导致SET后无内容。 | 1.始终使用<where>和<set>标签来包裹动态条件,避免手动处理关键字。2. 检查生成的 SQL 日志,对比预期与实际语句。 |
IN查询时,传入空列表导致 SQL 为IN (),语法错误。 | 在<foreach>标签外未对集合进行判空。 | 在<foreach>外层使用<if>判断集合是否为空或非空。例如:<if test=“idList != null and idList.size() > 0”>。 |
模糊查询LIKE参数未正确拼接,导致查询不到数据。 | 在test表达式中直接拼接字符串,如LIKE ‘%#{name}%’,这会导致参数被当作字符串字面量而非预编译参数。 | 使用CONCAT函数:LIKE CONCAT(‘%’, #{name}, ‘%’),或使用<bind>标签提前构建模式字符串。 |
5.2 参数绑定与类型处理问题
| 问题现象 | 可能原因 | 检查与解决 |
|---|---|---|
报错:There is no getter for property ‘XXX’ in class ‘java.lang.Integer’ | 在test表达式中,直接使用了基本类型或包装类型的属性,如test=“status == 1”。MyBatis 的 OGNL 表达式在处理基本类型时,会尝试将其作为对象获取属性。 | 对于简单类型的参数,在test中直接使用参数名进行比较即可,或者使用_parameter关键字。更推荐的做法是将查询条件封装成对象传入。 |
传入的布尔类型参数在test表达式中判断失效。 | MyBatis OGNL 对于布尔值的判断比较严格。 | 在test中使用== true或== false进行显式判断,例如test=“isAdmin == true”。 |
| 日期类型参数在比较时出错。 | 在 SQL 中直接比较java.util.Date或LocalDateTime对象与数据库日期字段。 | 确保传入的参数类型与数据库字段类型兼容。对于范围查询,可以考虑在 Java 层将日期转换为字符串(格式与数据库匹配),或者在 SQL 中使用数据库函数(如DATE())进行处理。 |
5.3 性能与最佳实践考量
- 避免过度使用动态 SQL:过于复杂的动态 SQL 会降低 SQL 的可读性和可维护性,也可能影响数据库的查询计划缓存。如果条件组合爆炸式增长,可以考虑使用更专业的查询构建器(如 MyBatis-Plus 的
QueryWrapper)或将部分逻辑移到业务层。 <foreach>的批量大小:批量操作时,单次INSERT的 VALUES 列表不宜过长。建议在 Service 层进行分批,每批处理 500-1000 条数据。- 索引失效风险:动态 SQL 可能导致某些查询用不上索引。例如,
WHERE status = #{status},当status为null时,条件不拼接,可能导致全表扫描。需要结合数据库表的具体索引设计来评估。 - 使用
<sql>片段:将常用的列列表、条件片段提取出来,不仅能减少代码量,还能统一修改点。
6. 进阶:与 MyBatis 其他特性结合
6.1 动态 SQL 与 ResultMap 结合
ResultMap用于解决数据库字段名与 Java 对象属性名不一致的映射问题。动态 SQL 的查询结果可以映射到复杂的ResultMap。
<resultMap id="UserDetailMap" type="User"> <id property="id" column="id"/> <result property="name" column="user_name"/> <!-- 字段别名映射 --> <result property="createTime" column="create_time"/> <!-- 可以包含 association, collection 等复杂映射 --> </resultMap> <select id="selectComplexUser" resultMap="UserDetailMap" parameterType="UserQuery"> SELECT id, name as user_name, age, create_time FROM user <where> <!-- 动态条件... --> </where> </select>6.2 关于 MyBatis 的二级缓存
在搜索热词中提到了“二级缓存”。二级缓存是 MyBatis 的会话工厂(SqlSessionFactory)级别的缓存,可以跨 SqlSession 共享。但在使用动态 SQL 时,需要特别注意缓存的失效问题。
- 缓存键的生成:MyBatis 根据 Mapper 语句的 ID、传递给语句的参数、分页参数等生成一个唯一的缓存键。如果动态 SQL 根据不同的参数生成了不同的 SQL,它们会被视为不同的查询,缓存键也不同。
- 缓存失效:对一张表执行了 INSERT、UPDATE、DELETE 操作后,所有与该表相关的二级缓存区域都会被清空。在读写频繁的场景下,开启二级缓存可能弊大于利。
- 配置与使用:需要在 MyBatis 全局配置中开启二级缓存,并在具体的 Mapper XML 文件中使用
<cache/>标签声明。对于动态 SQL 多的查询,需要仔细评估数据一致性和性能提升的平衡。
<!-- 在 MyBatis 配置文件中 --> <settings> <setting name="cacheEnabled" value="true"/> </settings> <!-- 在具体的 Mapper.xml 中 --> <mapper namespace="com.example.demo.mapper.UserMapper"> <cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/> <!-- 你的动态 SQL 语句 --> </mapper>6.3 在 Spring Boot 中集成 PageHelper 分页
另一个热词是“springboot集成mybatis pagehelper”。PageHelper 是一个优秀的分页插件,它与动态 SQL 可以完美结合。
- 添加依赖:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version> </dependency> - 在 Service 层使用:
public PageInfo<User> queryUsersByPage(UserQuery query) { // 紧跟在查询方法前调用,传入页码和每页数量 PageHelper.startPage(query.getPageNum(), query.getPageSize()); // 接下来执行的第一条 MyBatis 查询方法会被自动分页 List<User> list = userMapper.selectByConditionWithPage(query); // 用 PageInfo 包装结果,包含总条数、总页数等信息 return new PageInfo<>(list); }
关键点:PageHelper.startPage()必须紧贴在执行数据库查询的代码之前。它通过一个本地线程变量(ThreadLocal)设置分页参数,然后由插件在运行时动态修改你的 SQL(添加LIMIT子句和计算总数的查询)。你的动态 SQL 完全不需要为分页做任何修改。
7. 总结与最佳实践清单
通过本文,你应该已经掌握了 MyBatis 动态 SQL 从基础到进阶的核心用法。最后,我们整理一份在项目中使用动态 SQL 的最佳实践清单,帮助你规避常见陷阱,写出更健壮的代码:
- 优先使用
<where>和<set>:永远不要手动拼接WHERE 1=1或处理SET子句末尾的逗号,让标签自动处理。 - 始终进行非空判断:在
<if>的test表达式中,对字符串判断null和'',对集合判断null和size()。 - 善用
<bind>处理复杂表达式:对于重复的字符串拼接(如模糊查询)或复杂的 OGNL 表达式,使用<bind>提升可读性。 - 控制
<foreach>的集合大小:批量操作时,在业务层控制每次传入的集合大小,避免单条 SQL 过长。 - 提取公共 SQL 片段:使用
<sql>和<include>维护列名、条件等公共部分,保证一致性并便于修改。 - 结合分页插件:对于列表查询,集成 PageHelper 等分页插件,让动态 SQL 专注于业务条件,分页逻辑由插件透明处理。
- 关注生成的 SQL:开发阶段开启 MyBatis SQL 日志 (
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl),仔细核对动态生成的 SQL 是否符合预期。 - 谨慎使用二级缓存:在包含大量动态 SQL 或数据更新频繁的场景下,评估二级缓存带来的收益和潜在的数据一致性问题。
- 参数封装:将多个查询条件封装成 QueryDTO 对象传入,避免 Mapper 接口出现过多参数,也便于在
test表达式中使用。 - 保持 XML 可读性:合理的缩进和注释对于复杂的动态 SQL 至关重要。如果一段动态 SQL 超过 50 行,考虑是否可以通过拆分或使用查询构建器来简化。
动态 SQL 是 MyBatis 的灵魂特性之一,它将 SQL 的灵活性与 Java 代码的结构化控制完美结合。从简单的<if>条件判断到复杂的<foreach>批量操作,理解并熟练运用这些标签,能让你在面对复杂多变的业务查询时游刃有余,真正实现“少写代码,多做事”的目标。在实际项目中,建议从简单的条件查询开始实践,逐步应用到更复杂的场景,并时刻关注生成的 SQL 语句,确保其正确性和性能。