1. MyBatis动态SQL核心语法精要
作为Java生态中最受欢迎的ORM框架之一,MyBatis的动态SQL功能在实际开发中使用频率极高。不同于Hibernate的HQL,MyBatis通过XML标签实现灵活的SQL拼接,这种设计既保留了SQL的直观性,又提供了程序化的控制能力。以下是经过多年实战验证的黄金模板集合:
1.1 条件分支控制标签
xml复制<!-- if标签:基础条件判断 -->
<select id="findUsers" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</select>
<!-- choose-when-otherwise:多条件分支 -->
<select id="findActiveUser" resultType="User">
SELECT * FROM users
WHERE
<choose>
<when test="status == 'active'">
status = 1
</when>
<when test="status == 'inactive'">
status = 0
</when>
<otherwise>
status IN (0,1)
</otherwise>
</choose>
</select>
关键经验:if标签的test属性使用OGNL表达式,注意字符串判断要同时检查null和空字符串。choose-when结构相当于Java的switch-case,适合处理枚举类条件。
1.2 循环遍历标签
xml复制<!-- foreach:集合遍历 -->
<update id="batchUpdateStatus">
UPDATE users SET status = #{status}
WHERE id IN
<foreach item="id" collection="ids"
open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 批量插入最佳实践 -->
<insert id="batchInsert">
INSERT INTO users (name, age) VALUES
<foreach item="user" collection="list"
separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
实战技巧:
- collection属性根据参数类型不同取值:
- 单参数List:直接写"list"
- 单参数数组:写"array"
- Map或@Param注解指定:写对应key
- 大数据量批量操作时,建议每1000条拆分一次SQL
2. 动态SQL高级应用场景
2.1 动态表名与列名
xml复制<!-- 安全动态表名(使用${}要谨慎) -->
<select id="findByTable" resultType="map">
SELECT * FROM ${tableName}
WHERE
<if test="columns != null">
<foreach item="col" collection="columns" separator=" OR ">
${col} = #{value}
</foreach>
</if>
</select>
安全警告:${}会直接拼接SQL,存在SQL注入风险。必须确保参数来源可信,或提前校验白名单。建议关键业务使用#{}配合固定选项模式。
2.2 动态排序与分页
xml复制<!-- 安全动态排序 -->
<select id="findWithOrder" resultType="User">
SELECT * FROM users
ORDER BY
<choose>
<when test="orderBy == 'name'">name</when>
<when test="orderBy == 'age'">age</when>
<otherwise>id</otherwise>
</choose>
<if test="orderDir != null and orderDir == 'desc'">DESC</if>
</select>
<!-- 分页查询模板 -->
<select id="findByPage" resultType="User">
SELECT * FROM users
<where>
<include refid="commonConditions"/>
</where>
LIMIT #{offset}, #{pageSize}
</select>
性能优化建议:
- 排序字段建议建立索引
- 大数据量分页使用"延迟关联"优化:
sql复制SELECT * FROM users u1 JOIN (SELECT id FROM users LIMIT 100000,10) u2 ON u1.id = u2.id
3. 防御性编程与最佳实践
3.1 SQL注入防护方案
xml复制<!-- 使用OGNL函数过滤 -->
<select id="safeQuery" resultType="User">
SELECT * FROM users
WHERE name = #{name, javaType=String, jdbcType=VARCHAR}
<if test="@org.apache.commons.lang3.StringUtils@isNotBlank(orderBy)">
ORDER BY ${@com.example.util.SQLFilter@filter(orderBy)}
</if>
</select>
安全措施清单:
- 所有用户输入必须使用#{}预编译
- 必须使用${}的场景要:
- 实现白名单校验
- 使用工具类过滤特殊字符
- 审计日志记录原始参数
3.2 复杂查询的维护技巧
xml复制<!-- SQL片段复用 -->
<sql id="userBaseColumn">
id, name, age, status
</sql>
<sql id="commonConditions">
<if test="name != null">
AND name LIKE CONCAT('%',#{name},'%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
</sql>
<select id="searchUsers" resultType="User">
SELECT <include refid="userBaseColumn"/>
FROM users
<where>
<include refid="commonConditions"/>
</where>
</select>
可维护性建议:
- 将重复使用的列定义提取为
片段 - 复杂条件使用
标签自动处理AND/OR - 每个XML文件不超过500行,过大时按功能拆分
4. 性能优化与疑难排查
4.1 批量操作性能对比
| 操作类型 | 示例代码 | 数据量 | 耗时(ms) | 建议阈值 |
|---|---|---|---|---|
| 单条循环insert | 在Java中循环执行单条insert | 1000 | 1200 | <100 |
| foreach批量 | 使用foreach拼接values | 1000 | 150 | <5000 |
| BatchExecutor | 开启JDBC批处理模式 | 1000 | 80 | >10000 |
实测发现:
- 小批量(100内):foreach拼接最优
- 中批量(100-5000):BatchExecutor更优
- 大批量(5000+):考虑使用LOAD DATA INFILE
4.2 常见错误排查指南
问题1: 动态SQL未生效
- 检查点:
- test表达式语法(使用==而不是equals)
- 参数是否为null(基本类型永远不为null)
- 参数名是否匹配(注意@Param注解)
问题2: 出现Your version of cursor is no longer supported
- 解决方案:
xml复制<!-- 添加fetchSize配置 --> <select id="largeQuery" fetchSize="1000" resultType="User"> SELECT * FROM large_table </select>
问题3: 日志显示SQL正确但结果不对
- 调试技巧:
- 开启MyBatis完整日志:
properties复制logging.level.org.mybatis=DEBUG - 使用MyBatisX插件可视化SQL
- 检查参数类型处理器是否匹配
- 开启MyBatis完整日志:
5. 现代MyBatis开发模式
5.1 注解与XML混合开发
java复制public interface UserMapper {
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUserByName")
User getByName(@Param("name") String name);
// XML中实现复杂查询
List<User> searchByCondition(UserCondition condition);
}
public class UserSqlBuilder {
public String buildGetUserByName(String name) {
return new SQL()
.SELECT("*")
.FROM("users")
.WHERE("name = #{name}")
.toString();
}
}
架构建议:
- 简单CRUD使用注解
- 复杂动态SQL使用XML
- 动态SQL构建考虑SQL Builder模式
5.2 MyBatis-Plus扩展
java复制// 条件构造器动态查询
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StringUtils.isNotBlank(name), User::getName, name)
.gt(age != null, User::getAge, age);
// 存在更新,不存在插入
userService.saveOrUpdate(user);
升级建议:
- 新项目直接使用MyBatis-Plus
- 老项目逐步引入Wrapper简化代码
- 注意避开AR(ActiveRecord)模式过度使用
在长期使用MyBatis的过程中,我发现动态SQL最易出错的是参数处理边界情况。建议团队建立SQL模板库,对高频操作如分页查询、批量更新等场景进行标准化封装。同时要特别注意,在微服务架构下,复杂SQL应考虑拆分为多次查询,避免过度依赖数据库计算能力。
