MyBatis动态SQL实战:告别SQL拼接,高效构建复杂查询
你是不是也曾在写MyBatis的Mapper XML时,面对一堆if、choose、foreach标签感到头疼?或者为了一个多条件查询,不得不写好几个几乎一样的SQL语句,导致Mapper文件臃肿不堪,维护起来像在走迷宫?
更现实的问题是,当你临近毕业,面对一个需要大量复杂查询的毕设项目时,是不是发现一半的时间都花在了拼接SQL字符串上?手动拼接不仅容易出错,还带来了SQL注入的安全隐患,代码行数轻松突破上千行,调试起来苦不堪言。
这篇文章要解决的核心痛点,就是如何用MyBatis的动态SQL,将你从这种重复、易错、低效的“SQL拼接地狱”中解救出来。我的核心判断是:动态SQL不是MyBatis的一个“锦上添花”的特性,而是其解决复杂业务查询问题的“核心武器”。掌握它,你不仅能少写大量冗余代码,更能构建出清晰、安全、易于维护的数据访问层。
很多人对动态SQL的理解停留在“写几个<if>标签”的层面,这大大低估了它的价值。本文将带你深入MyBatis动态SQL的肌理,从“为什么需要”到“怎么用好”,再到“如何避坑”。读完本文,你将能:
- 彻底理解动态SQL的设计哲学和核心标签。
- 亲手构建一个支持多条件、分页、排序的通用查询模块。
- 掌握在实际项目中(尤其是毕设这类中小型项目)应用动态SQL的最佳实践,显著减少DAO层代码量。
- 避开常见的性能陷阱和设计误区。
我们直接从最让你头疼的场景开始。
1. 动态SQL要解决的真实问题:告别“SQL拼接地狱”
想象一下这个经典场景:你需要为一个“用户管理”模块实现一个搜索功能。搜索条件可能包括:用户名(模糊匹配)、状态(精确匹配)、创建时间范围、所属角色(多选)。如果用最原始的JDBC或者简单的MyBatis,你可能会写出下面这样的代码:
反面教材:手动拼接的灾难
public List<User> searchUsers(String username, Integer status, Date startTime, Date endTime, List<Integer> roleIds) { StringBuilder sql = new StringBuilder("SELECT * FROM user WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (username != null && !username.trim().isEmpty()) { sql.append("AND username LIKE ? "); params.add("%" + username + "%"); } if (status != null) { sql.append("AND status = ? "); params.add(status); } if (startTime != null) { sql.append("AND create_time >= ? "); params.add(startTime); } if (endTime != null) { sql.append("AND create_time <= ? "); params.add(endTime); } if (roleIds != null && !roleIds.isEmpty()) { sql.append("AND role_id IN ("); for (int i = 0; i < roleIds.size(); i++) { sql.append("?"); if (i != roleIds.size() - 1) { sql.append(", "); } } sql.append(") "); params.addAll(roleIds); } sql.append("ORDER BY create_time DESC"); // 然后需要自己创建PreparedStatement,设置参数,执行查询,处理结果集... // 此处省略冗长的JDBC模板代码 }这段代码的问题一目了然:
- 极易出错:字符串拼接稍有不慎就会导致SQL语法错误(比如多余的
AND或WHERE)。 - SQL注入风险:虽然用了
?占位符,但复杂的逻辑下仍可能出错,直接拼接字符串则是高危行为。 - 难以维护:业务逻辑和SQL字符串耦合在一起,可读性差。增加一个条件就要动好几处代码。
- 代码臃肿:一个方法动辄几十行,项目中如果有十个这样的查询,代码量可想而知。
动态SQL的解决思路:MyBatis的动态SQL功能,允许你在XML映射文件中,使用一组特定的标签(如<if>,<choose>,<foreach>等)来声明式地构建SQL语句。MyBatis框架会根据传入参数的实际值,在运行时智能地组装出最终的SQL,完全避免了手动拼接。
上面的搜索功能,用动态SQL实现,Mapper XML会变得清晰、安全且专注:
<!-- UserMapper.xml --> <select id="searchUsers" resultType="User"> SELECT * FROM user <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="startTime != null"> AND create_time >= #{startTime} </if> <if test="endTime != null"> AND create_time <= #{endTime} </if> <if test="roleIds != null and roleIds.size() > 0"> AND role_id IN <foreach collection="roleIds" item="roleId" open="(" separator="," close=")"> #{roleId} </foreach> </if> </where> ORDER BY create_time DESC </select>对应的Java接口方法极其简洁:
List<User> searchUsers(@Param("username") String username, @Param("status") Integer status, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("roleIds") List<Integer> roleIds);两相对比,高下立判。动态SQL将你从繁琐、易错的字符串操作中解放出来,让你能更专注于业务逻辑本身。接下来,我们系统性地掌握这套“武器库”。
2. 核心标签全解析:从<if>到<script>
MyBatis动态SQL的核心是一组在Mapper XML中使用的标签。理解每个标签的用途和细节,是高效使用它们的关键。
2.1<if>:条件判断的基石
<if>标签用于简单的条件判断。test属性内的表达式为true时,其包含的SQL片段会被包含。
<if test="title != null"> AND title = #{title} </if>关键点:
test表达式使用的是OGNL(Object-Graph Navigation Language),可以访问参数对象的属性,调用简单方法。- 常见的判断:
!= null,== ‘value’,>,<,and,or,not。 - 注意:判断字符串是否为空,推荐使用
name != null and name != ‘’,或者@org.apache.commons.lang3.StringUtils@isNotBlank(name)(如果引入了commons-lang3)。
2.2<choose>,<when>,<otherwise>:Java中的switch-case
用于实现多分支选择逻辑,类似Java的switch。
<choose> <when test="status == 1"> AND status = ‘ACTIVE’ </when> <when test="status == 2"> AND status = ‘INACTIVE’ </when> <otherwise> AND status IS NOT NULL </otherwise> </choose>2.3<trim>,<where>,<set>:智能处理SQL关键字
这是动态SQL中最能体现“智能”的部分,用于解决拼接SQL时多余或缺失的关键字问题。
<where>标签:它会自动处理其内部片段开头的AND或OR。如果标签内有任何内容,它会插入WHERE关键字,并去掉第一个条件前的AND/OR。这完美解决了WHERE 1=1这种不优雅的写法。<select id="findActiveUser" resultType="User"> SELECT * FROM user <where> <if test="id != null"> AND id = #{id} </if> <if test="name != null"> AND name like #{name} </if> </where> </select>如果
id和name都为空,<where>标签内的内容为空,则不会生成WHERE子句。如果只有name不为空,生成的SQL会是SELECT * FROM user WHERE name like ?,开头的AND被去掉了。<set>标签:用于UPDATE语句,会自动处理结尾的逗号。它会动态地在行首插入SET关键字,并去掉更新字段列表末尾多余的逗号。<update id="updateUser"> UPDATE user <set> <if test="username != null">username = #{username},</if> <if test="email != null">email = #{email},</if> <if test="status != null">status = #{status},</if> </set> WHERE id = #{id} </update>即使最后一个
<if>条件不成立,<set>也会确保SET关键字后的列表没有尾随逗号。<trim>标签:功能更强大的通用标签,可以自定义前缀、后缀以及要覆盖(去除)的字符串。<where>和<set>本质上是<trim>的特定实现。- 实现
<where>功能:<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim> - 实现
<set>功能:<trim prefix="SET" suffixOverrides=","> ... </trim>
- 实现
2.4<foreach>:遍历集合的利器
这是处理IN查询和批量操作的神器。
<!-- 1. IN查询 --> <select id="selectUsersByIds" resultType="User"> SELECT * FROM user WHERE id IN <foreach collection="idList" item="id" index="index" open="(" separator="," close=")"> #{id} </foreach> </select> <!-- 2. 批量插入 (MySQL) --> <insert id="batchInsertUsers"> INSERT INTO user (username, email) VALUES <foreach collection="userList" item="user" separator=","> (#{user.username}, #{user.email}) </foreach> </insert>参数详解:
collection:要遍历的集合参数名。如果参数是List,通常写list或@Param注解指定的名字;如果是数组,写array。item:遍历时每个元素的别名。index:遍历的索引(可选)。open/close:遍历结果的整体前缀/后缀,如IN (和)。separator:元素间的分隔符,如,。
2.5<bind>:创建变量并在OGNL中使用
有时需要在test表达式中进行复杂的字符串处理或重复使用一个值,<bind>就派上用场了。
<select id="selectByExample" resultType="User"> <!-- 将模糊查询的字符串绑定到一个新变量 --> <bind name="pattern" value="'%' + username + '%'" /> SELECT * FROM user <where> <if test="username != null"> AND name LIKE #{pattern} </if> </where> </select>这在数据库方言不一致时(如CONCAT函数)或简化复杂表达式时很有用。
2.6<sql>和<include>:代码复用
用于提取公共的SQL片段,避免重复。
<!-- 定义可重用的列列表 --> <sql id="userColumns">id, username, email, create_time</sql> <select id="selectAllUsers" resultType="User"> SELECT <include refid="userColumns"/> FROM user </select> <select id="selectUserById" resultType="User"> SELECT <include refid="userColumns"/> FROM user WHERE id = #{id} </select><include>还可以传递属性:
<sql id="someColumns"> ${alias}.id, ${alias}.username </sql> <select id="select" resultType="map"> SELECT <include refid="someColumns"> <property name="alias" value="u"/> </include> FROM user u </select>3. 环境准备与项目搭建
在深入实战前,我们快速搭建一个标准的Spring Boot + MyBatis项目环境。这是绝大多数Java毕设项目的技术选型。
1. 使用Spring Initializr创建项目访问 start.spring.io ,选择:
- Project: Maven
- Language: Java
- Spring Boot: 选择最新的稳定版(如3.x)
- Dependencies: 添加
Spring Web,MyBatis Framework,MySQL Driver(或你使用的数据库驱动)
点击生成并下载项目。
2. 项目结构解压后,标准的项目结构如下:
src/main/java/com/example/demo/ ├── DemoApplication.java // 启动类 ├── controller/ // 控制层(可选,本文聚焦Mapper) ├── service/ // 业务层 ├── mapper/ // Mapper接口层 └── entity/ // 实体类 src/main/resources/ ├── application.properties // 配置文件 └── mapper/ // Mapper XML文件目录3. 关键依赖 (pom.xml)确保你的pom.xml中包含以下核心依赖(版本号请根据Spring Boot版本自动协调):
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> <!-- 请使用与Spring Boot兼容的最新版本 --> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 可选,用于简化实体类 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>4. 数据库配置 (application.properties)
# 数据库连接 spring.datasource.url=jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # MyBatis 配置 # 指定Mapper XML文件的位置 mybatis.mapper-locations=classpath:mapper/*.xml # 开启驼峰命名自动映射(数据库user_name -> 实体类userName) mybatis.configuration.map-underscore-to-camel-case=true # 打印SQL日志到控制台(开发环境非常有用) logging.level.com.example.demo.mapper=debug5. 创建实体类和Mapper接口
// src/main/java/com/example/demo/entity/User.java package com.example.demo.entity; import lombok.Data; import java.util.Date; @Data public class User { private Long id; private String username; private String email; private Integer status; // 0-禁用,1-启用 private Date createTime; private Date updateTime; }// src/main/java/com/example/demo/mapper/UserMapper.java package com.example.demo.mapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.Date; import java.util.List; @Mapper // Spring Boot中必须的注解 public interface UserMapper { // 我们将在XML中实现这个复杂的动态查询方法 List<User> searchUsers(@Param("username") String username, @Param("status") Integer status, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("roleIds") List<Integer> roleIds); }环境搭建完毕。接下来,我们进入最核心的实战环节。
4. 实战:构建一个通用的动态查询模块
毕设中经常需要实现各种带过滤条件的列表查询。我们将设计一个相对通用的UserMapper.xml,并实现一个支持多条件、分页、排序的searchUsers方法。
4.1 基础动态查询实现
首先,在resources/mapper/目录下创建UserMapper.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.demo.mapper.UserMapper"> <!-- 定义可复用的查询列和条件片段 --> <sql id="Base_Column_List"> id, username, email, status, create_time, update_time </sql> <sql id="Base_Where_Condition"> <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="startTime != null"> AND create_time >= #{startTime} </if> <if test="endTime != null"> AND create_time <= #{endTime} </if> <!-- 假设通过关联表查询角色 --> <if test="roleIds != null and roleIds.size() > 0"> AND id IN ( SELECT user_id FROM user_role WHERE role_id IN <foreach collection="roleIds" item="roleId" open="(" separator="," close=")"> #{roleId} </foreach> ) </if> </where> </sql> <!-- 核心动态查询方法 --> <select id="searchUsers" resultType="com.example.demo.entity.User"> SELECT <include refid="Base_Column_List"/> FROM user <include refid="Base_Where_Condition"/> <!-- 默认按创建时间倒序 --> ORDER BY create_time DESC </select> </mapper>这个XML已经具备了强大的动态查询能力。但还不够,我们还需要分页。
4.2 集成分页插件(MyBatis PageHelper)
手动写LIMIT语句很麻烦,而且分页逻辑和业务逻辑耦合。使用PageHelper插件可以极大简化。
1. 添加依赖
<!-- pom.xml --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>2.1.0</version> <!-- 使用最新稳定版 --> </dependency>2. 配置PageHelper (application.properties)
# PageHelper 配置 pagehelper.helper-dialect=mysql pagehelper.reasonable=true pagehelper.support-methods-arguments=true pagehelper.params=count=countSql3. 在Service层使用分页
// src/main/java/com/example/demo/service/UserService.java package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; @Service public class UserService { @Autowired private UserMapper userMapper; public PageInfo<User> searchUsersPage(String username, Integer status, Date startTime, Date endTime, List<Integer> roleIds, Integer pageNum, Integer pageSize) { // 关键:在查询方法前调用PageHelper.startPage PageHelper.startPage(pageNum, pageSize); // 紧接着的第一次MyBatis查询会被自动分页 List<User> userList = userMapper.searchUsers(username, status, startTime, endTime, roleIds); // 用PageInfo包装结果,包含分页信息(总条数、总页数等) return new PageInfo<>(userList); } }现在,你的动态查询已经支持了分页。调用searchUsersPage方法,传入页码和大小,返回的PageInfo对象包含了当前页的数据和完整的分页导航信息。
4.3 支持动态排序
有时前端需要根据不同的列进行排序。我们可以进一步改造XML,支持动态排序字段和排序方式。 首先,修改Mapper接口,增加排序参数:
// UserMapper.java List<User> searchUsers(@Param("username") String username, @Param("status") Integer status, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("roleIds") List<Integer> roleIds, @Param("orderBy") String orderBy, // 例如 "create_time DESC" @Param("orderByClause") String orderByClause); // 或更结构化的方式但更优雅和安全的方式是避免直接将字符串拼接到ORDER BY后(有SQL注入风险)。我们可以使用<choose>标签或<bind>标签来限制可排序的字段。
安全动态排序实现:
<!-- 在UserMapper.xml的searchUsers的SELECT语句后,ORDER BY之前 --> <choose> <!-- 安全字段白名单判断 --> <when test="orderBy != null and orderBy != ''"> ORDER BY <choose> <when test="orderBy == 'createTime'">create_time</when> <when test="orderBy == 'updateTime'">update_time</when> <when test="orderBy == 'username'">username</when> <otherwise>create_time</otherwise> <!-- 默认排序字段 --> </choose> <choose> <!-- 排序方式白名单 --> <when test="orderDirection != null and orderDirection == 'desc'">DESC</when> <otherwise>ASC</otherwise> </choose> </when> <otherwise> ORDER BY create_time DESC <!-- 默认排序 --> </otherwise> </choose>对应的Mapper接口参数可以改为@Param("orderBy") String orderBy, @Param("orderDirection") String orderDirection。这种方式通过白名单机制,彻底杜绝了SQL注入的可能。
5. 高级技巧与最佳实践
掌握了基础用法后,了解以下高级技巧和最佳实践,能让你的动态SQL写得更加专业和高效。
5.1 使用<script>标签在注解中使用动态SQL
MyBatis也支持在接口方法上使用@Select、@Update等注解直接编写SQL。对于简单的动态SQL,可以使用<script>标签。
@Select("<script>" + "SELECT * FROM user " + "<where>" + " <if test='username != null'> AND username LIKE CONCAT('%', #{username}, '%')</if>" + " <if test='status != null'> AND status = #{status}</if>" + "</where>" + "</script>") List<User> selectByCondition(@Param("username") String username, @Param("status") Integer status);但请注意:对于复杂的动态SQL,强烈建议写在XML中,可读性和可维护性远高于注解方式。
5.2 避免WHERE 1=1,善用<where>和<trim>
如前所述,<where>标签是处理动态WHERE子句的首选。它比WHERE 1=1更语义化,且生成的SQL更干净。只有在需要自定义更复杂的前缀/后缀逻辑时,才使用<trim>。
5.3 批量操作的正确姿势
<foreach>标签是批量操作的灵魂。除了IN查询,批量插入和更新也经常用到。
批量插入(MySQL):
<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO user (username, email) VALUES <foreach collection="list" item="item" separator=","> (#{item.username}, #{item.email}) </foreach> </insert>注意:MySQL对单条SQL的长度有限制(max_allowed_packet)。当列表非常大时(例如超过1000条),建议在Service层进行分批插入。
批量更新(Case When): MyBatis没有提供直接的批量更新标签,但可以通过<foreach>结合SQL的CASE WHEN语句实现。
<update id="batchUpdateStatus"> UPDATE user SET status = <foreach collection="list" item="item" open="CASE id" close="END"> WHEN #{item.id} THEN #{item.status} </foreach> WHERE id IN <foreach collection="list" item="item" open="(" separator="," close=")"> #{item.id} </foreach> </update>5.4 动态SQL中的OGNL表达式技巧
- 调用静态方法:
@java.util.UUID@randomUUID(),@org.apache.commons.lang3.StringUtils@isNotBlank(name)。 - 三目运算符:
test="status != null ? status == 1 : false"。但尽量保持表达式简单。 - 判断集合/数组非空:
test="list != null and list.size() > 0"或test="array != null and array.length > 0"。
5.5 性能考量:<if>测试的代价
动态SQL在运行时构建,<if>标签的test属性表达式会被频繁计算。虽然通常开销很小,但在极高并发或极端复杂的动态SQL中,也应考虑:
- 保持
test表达式简单。 - 对于固定模式但参数多变的查询,可以考虑使用MyBatis的
Provider注解(如@SelectProvider)来用Java代码动态生成SQL,这在逻辑极其复杂时可能更灵活。
6. 常见问题与排查指南 (Q&A)
在实际使用中,你肯定会遇到一些问题。下面是一些最常见的问题及其解决方案。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 生成的SQL语句缺少WHERE关键字或有多余的AND | 1. 未使用<where>标签,且条件全为空。2. 使用了 <where>,但条件内的<if>标签都未满足。 | 查看MyBatis打印的SQL日志(logging.level.com.xxx.mapper=DEBUG)。 | 1. 使用<where>标签包裹动态条件。2. 如果确实需要无条件查询,考虑写两个不同的方法,或在 <where>内加一个1=1的<if test=”true”>。 |
<foreach>标签报错:Parameter ‘list‘ not found | 1. 在Mapper接口方法中,集合参数未使用@Param注解指定名称。2. 在XML中, collection属性值写错。 | 检查Mapper接口方法参数和XML中collection的值。 | 1. 如果接口方法参数是List<User> userList,则XML中应为collection=”userList”。2. 如果参数是单个 List且无@Param,则必须使用collection=”list”(小写)。 |
模糊查询LIKE语句不生效或报错 | 1. 在XML中直接写LIKE ‘%#{name}%’,参数被错误地加了引号。2. 不同数据库的字符串连接函数不同。 | 查看生成的SQL日志,确认参数值是否正确嵌入。 | 1. 使用LIKE CONCAT(‘%’, #{name}, ‘%’)(MySQL)。2. 或使用 <bind>标签:<bind name=”pattern” value=”‘%’ + name + ‘%’”/>,然后LIKE #{pattern}。 |
| 分页插件PageHelper不生效 | 1.PageHelper.startPage()调用位置不对,必须在查询之前。2. 依赖冲突或配置错误。 3. 查询语句被多个线程干扰。 | 1. 确认startPage紧邻MyBatis查询方法。2. 检查依赖树是否有其他分页插件冲突。 | 1. 确保调用顺序:startPage()->mapper.query()。2. 使用 PageHelper.clearPage()清理线程变量,或在finally块中清理。3. 检查 application.properties中的PageHelper配置。 |
动态SQL中的<、>等符号导致XML解析错误 | XML将<和>视为标签的开始/结束符号。 | 查看启动或解析时的错误日志。 | 使用XML转义符:<代替<,>代替>,&代替&。或者将SQL片段放入<![CDATA[ ... ]]>中。 |
test表达式中的布尔判断出错 | OGNL表达式对布尔类型和字符串的判等容易混淆。 | 仔细检查test表达式逻辑。 | 对于布尔类型字段active,使用test=”active == true”或test=”active”。对于字符串,使用test=’status == “A”‘(注意单双引号嵌套)。 |
| 动态SQL性能感觉慢 | 1. 条件组合太多,导致数据库无法有效使用索引。 2. <if>表达式过于复杂。 | 1. 分析最终生成的SQL,用EXPLAIN查看执行计划。2. 检查是否有全表扫描。 | 1. 为常用查询条件建立数据库索引。 2. 考虑将一些固定的查询模式拆分成多个Mapper方法,而不是一个极度动态的方法。 3. 简化 test表达式。 |
7. 在毕设中应用动态SQL的工程建议
对于毕业设计这类项目,目标不仅是实现功能,更要展示良好的工程能力和代码质量。以下建议能让你更好地运用动态SQL:
- 规划Mapper层结构:不要把所有查询都塞进一个庞大的
UserMapper.xml。可以按模块或功能拆分,例如UserMapper.xml、OrderMapper.xml。每个XML文件内,使用<sql>片段复用公共部分。 - 使用DTO/QueryObject封装查询参数:当查询条件非常多时,不要把所有参数都列在Mapper方法中。创建一个专门的查询参数类(如
UserQueryDTO)。
Mapper接口方法简化为:@Data public class UserQueryDTO { private String username; private Integer status; private Date startTime; private Date endTime; private List<Integer> roleIds; private Integer pageNum; private Integer pageSize; private String orderBy; private String orderDirection; }List<User> searchUsers(UserQueryDTO query);。在XML中,test表达式直接使用query的属性,如test=”query.username != null”。 - 编写单元测试:为你的动态SQL方法编写单元测试,覆盖各种参数组合(全为空、部分为空、边界值等)。这能确保动态SQL在各种场景下生成正确的SQL,也是毕设答辩的加分项。
- 关注SQL注入:永远不要将未经验证的用户输入直接用于
test表达式或通过${}方式拼接到SQL中(${}是直接文本替换,而#{}是预编译参数绑定)。动态排序、动态表名等场景如果必须使用${},务必在前端或Service层进行严格的白名单校验。 - 日志与调试:务必开启MyBatis的SQL日志(
logging.level.com.xxx.mapper=DEBUG)。在开发阶段,这是调试动态SQL生成结果最直接有效的方式。 - 代码生成器的利用:许多代码生成器(如MyBatis Generator, MyBatis-Plus的代码生成器)都能生成包含基础CRUD的Mapper和实体。你可以在生成的基础上,专注于编写复杂的、业务相关的动态SQL方法,而不是重复劳动。
回到文章的标题“2分钟搞定MyBatis动态SQL,毕设少写500行代码”。这并非夸张。当你熟练运用<where>、<if>、<foreach>等标签后,过去需要数十行Java代码拼接的复杂查询,现在只需要一个清晰简洁的XML片段。一个中等规模的毕设项目,数据访问层往往有几十个查询方法,每个方法节省10行代码,总量减少数百行是完全可能的。更重要的是,减少的是那些容易出错的、难以维护的“胶水代码”,换来的是更清晰的结构和更高的可靠性。
动态SQL是MyBatis的精髓之一,它很好地平衡了SQL的灵活性和Java代码的简洁性。希望本文的梳理和实战示例,能帮助你在接下来的项目中,更自信、更高效地使用这一强大工具。建议你将文中的示例代码保存下来,作为你自己的动态SQL代码片段库,在需要时快速复用和调整。
