ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MyBatis 动态 SQL 完全指南:让 SQL 灵活起来

MyBatis 动态 SQL 完全指南:让 SQL 灵活起来 在实际开发中我们经常需要根据不同的条件拼接不同的 SQL 语句。比如查询用户时可能按姓名查、按年龄查、按邮箱查也可能组合多个条件更新数据时只更新变化的字段批量操作时需要遍历集合生成IN子句。如果手动用字符串拼接代码不仅冗长还容易出错更要命的是可能引入 SQL 注入漏洞。MyBatis 动态 SQL正是为解决这些问题而生。它通过一系列 XML 标签if、choose、where、set、foreach等让你能够像写 Java 代码一样在 SQL 中实现条件判断、循环和逻辑分支。本文将系统讲解 MyBatis 动态 SQL 的所有核心标签、使用技巧和最佳实践。学习建议本文以 MyBatis 3.x 为基础建议边学边练配合 MySQL 数据库实操。如果你还不熟悉 MyBatis 基础建议先阅读 MyBatis 入门文章。一、为什么需要动态 SQL1.1 传统 JDBC 拼接的痛点java// 手动拼接 SQL 的典型代码 StringBuilder sql new StringBuilder(SELECT * FROM users WHERE 11); ListObject params new ArrayList(); if (username ! null !username.isEmpty()) { sql.append( AND username LIKE ?); params.add(% username %); } if (age ! null) { sql.append( AND age ?); params.add(age); } if (email ! null !email.isEmpty()) { sql.append( AND email ?); params.add(email); } // 问题 // 1. 字符串拼接繁琐容易漏空格、多 AND // 2. 参数顺序容易出错 // 3. 大量重复代码 // 4. 维护困难1.2 动态 SQL 的优势xml!-- MyBatis 动态 SQL声明式清晰安全 -- select idselectByCondition resultTypeUser SELECT * FROM users where if testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /if if testage ! null AND age #{age} /if if testemail ! null and email ! AND email #{email} /if /where /select声明式用标签描述逻辑而非手动拼接字符串。安全使用#{}预编译参数防止 SQL 注入。可维护SQL 结构清晰修改方便。功能强大支持条件、循环、变量绑定等复杂逻辑。二、if条件判断if是最基础也最常用的动态 SQL 标签用于根据条件决定是否包含某段 SQL。2.1 基本语法xmlselect idselectByCondition resultTypeUser SELECT * FROM users WHERE 11 if testusername ! null and username ! AND username #{username} /if if testage ! null AND age #{age} /if /selecttest属性是一个OGNL 表达式返回布尔值。表达式为true时标签内的 SQL 会被包含。2.2test表达式的常用写法场景写法说明判断 nulltestname ! null不为 null判断空字符串testname ! 不为空串同时判断testname ! null and name ! 常用组合判断数字testage ! null and age 0数值比较判断布尔testisActive true布尔值判断集合testlist ! null and list.size() 0集合非空判断字符串相等testtype admin注意单引号包裹字符串判断字符testtype A.charAt(0)字符比较需特殊处理⚠️ 常见陷阱xml!-- ❌ 错误字符串比较用双引号会与外层冲突 -- if testtype admin !-- 语法错误 -- !-- ✅ 正确字符串用单引号 -- if testtype admin !-- ❌ 错误数字与字符串比较 -- if testage 18 !-- 可能不生效 -- !-- ✅ 正确数字直接比较 -- if testage 18 !-- ⚠️ 注意单个字符比较 -- if testtype A !-- A 可能被解析为 char与 String 比较会失败 -- !-- 解决方案使用 toString() -- if testtype.toString() A2.3 使用11的经典模式在where标签出现之前开发者常用WHERE 11来避免第一个条件缺少AND的问题xmlselect idselectByCondition resultTypeUser SELECT * FROM users WHERE 11 if testusername ! null and username ! AND username #{username} /if if testage ! null AND age #{age} /if /select虽然能工作但11不够优雅且当所有条件都不满足时会生成WHERE 11的无效查询。推荐使用where标签。三、where智能 WHERE 子句where标签会自动处理WHERE关键字和多余的AND/OR。3.1 基本用法xmlselect idselectByCondition resultTypeUser SELECT * FROM users where if testusername ! null and username ! AND username #{username} /if if testage ! null AND age #{age} /if if testemail ! null and email ! AND email #{email} /if /where /select3.2where的智能行为如果所有if条件都不满足不会生成WHERE关键字。如果至少一个条件满足会插入WHERE。自动去除第一个条件多余的AND或OR。xml!-- 当 username 和 age 都满足时生成 -- SELECT * FROM users WHERE username ? AND age ? !-- 当只有 age 满足时生成 -- SELECT * FROM users WHERE age ? !-- 当都不满足时生成 -- SELECT * FROM users3.3where的等价写法where本质上是trim的快捷方式xml!-- where 等价于 -- trim prefixWHERE prefixOverridesAND |OR if test...AND .../if /trim四、choose/when/otherwise多分支选择类似于 Java 的switch-case在多个条件中只选择一个。4.1 基本语法xmlselect idselectByCondition resultTypeUser SELECT * FROM users where choose when testid ! null AND id #{id} /when when testusername ! null and username ! AND username #{username} /when when testemail ! null and email ! AND email #{email} /when otherwise AND age 18 /otherwise /choose /where /select执行逻辑按顺序检查每个when第一个满足的条件生效。如果所有when都不满足则执行otherwise。otherwise可选不写则什么都不执行。4.2 与if的区别特性ifchoose逻辑独立的可多个同时生效互斥的只有一个生效类比if语句switch-case适用场景组合条件查询优先级选择、单一条件4.3 实战场景搜索优先级xml!-- 优先按 ID 查其次按用户名最后按邮箱 -- select idsearchUser resultTypeUser SELECT * FROM users where choose when testid ! null id #{id} /when when testusername ! null and username ! username LIKE CONCAT(%, #{username}, %) /when when testemail ! null and email ! email #{email} /when otherwise 10 !-- 不返回任何结果 -- /otherwise /choose /where /select五、set智能更新语句set标签用于UPDATE语句自动处理SET关键字和多余的逗号。5.1 基本用法xmlupdate idupdateSelective UPDATE users set if testusername ! null and username ! username #{username}, /if if testemail ! null and email ! email #{email}, /if if testage ! null age #{age}, /if /set WHERE id #{id} /update5.2set的智能行为自动插入SET关键字。自动去除最后一个多余的逗号。xml!-- 当 username 和 email 都满足时生成 -- UPDATE users SET username ?, email ? WHERE id ? !-- 当只有 age 满足时生成 -- UPDATE users SET age ? WHERE id ?5.3set的等价写法xml!-- set 等价于 -- trim prefixSET suffixOverrides, if test....../if /trim5.4 注意事项如果所有if都不满足会生成UPDATE users WHERE id ?这是无效 SQL会报错。解决方案确保至少有一个字段会更新或在业务层校验。xml!-- 安全写法使用 set 并确保 id 不在 set 内 -- update idupdateSelective UPDATE users set if testusername ! nullusername #{username},/if if testemail ! nullemail #{email},/if /set WHERE id #{id} /update六、trim自定义修剪trim是最灵活的标签可以自定义前缀、后缀以及需要去除的内容。6.1 属性说明属性说明prefix如果标签内有内容添加的前缀suffix如果标签内有内容添加的后缀prefixOverrides去除内容开头的指定字符串多个用|分隔suffixOverrides去除内容结尾的指定字符串6.2 用trim实现wherexmltrim prefixWHERE prefixOverridesAND |OR if testusername ! nullAND username #{username}/if if testage ! nullAND age #{age}/if /trim6.3 用trim实现setxmltrim prefixSET suffixOverrides, if testusername ! nullusername #{username},/if if testage ! nullage #{age},/if /trim6.4 其他用法动态添加查询条件xmlselect idselectByCondition resultTypeUser SELECT * FROM users trim prefixWHERE prefixOverridesAND |OR if testusername ! null AND username #{username} /if if testage ! null OR age #{age} /if /trim /selectprefixOverridesAND |OR 会去除第一个出现的AND或OR。七、foreach循环遍历foreach用于遍历集合生成IN子句、批量插入、批量更新等。7.1 属性说明属性说明collection要遍历的集合必填item集合中每个元素的变量名index索引变量名List 为序号Map 为 keyopen循环开始时的字符串close循环结束时的字符串separator每次迭代之间的分隔符7.2 遍历 ListIN 查询xmlselect idselectByIds resultTypeUser SELECT * FROM users WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /select生成结果SELECT * FROM users WHERE id IN (?, ?, ?)Java 接口javaListUser selectByIds(Param(ids) ListLong ids);7.3 遍历数组xmlselect idselectByIds resultTypeUser SELECT * FROM users WHERE id IN foreach collectionarray itemid open( separator, close) #{id} /foreach /selectjavaListUser selectByIds(Long[] ids);注意数组的默认collection名称是array。7.4 遍历 Mapxmlselect idselectByMap resultTypeUser SELECT * FROM users WHERE foreach collectionmap indexkey itemvalue separator AND ${key} #{value} /foreach /selectjavaListUser selectByMap(Param(map) MapString, Object map);7.5 批量插入xmlinsert idbatchInsert INSERT INTO users (username, email, age) VALUES foreach collectionusers itemuser separator, (#{user.username}, #{user.email}, #{user.age}) /foreach /insertjavaint batchInsert(Param(users) ListUser users);生成结果INSERT INTO users (username, email, age) VALUES (?,?,?), (?,?,?), (?,?,?)7.6 批量更新CASE WHENxmlupdate idbatchUpdate UPDATE users set foreach collectionusers itemuser openusername CASE id closeEND, WHEN #{user.id} THEN #{user.username} /foreach foreach collectionusers itemuser openage CASE id closeEND, WHEN #{user.id} THEN #{user.age} /foreach /set WHERE id IN foreach collectionusers itemuser open( separator, close) #{user.id} /foreach /update7.7foreach的空集合问题当集合为空时IN ()会导致 SQL 语法错误。xml!-- ❌ 危险集合为空时生成 IN () -- select idselectByIds resultTypeUser SELECT * FROM users WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /select !-- ✅ 安全添加空集合判断 -- select idselectByIds resultTypeUser SELECT * FROM users where if testids ! null and ids.size() 0 id IN foreach collectionids itemid open( separator, close) #{id} /foreach /if if testids null or ids.size() 0 10 !-- 返回空结果 -- /if /where /select7.8collection属性的命名规则参数类型collection值Listlist或Param指定的名称数组array或Param指定的名称Mapmap或Param指定的名称单个对象中的集合属性属性名最佳实践始终使用Param注解明确命名避免混淆。java// 推荐 ListUser selectByIds(Param(ids) ListLong ids);xmlforeach collectionids itemid ...八、bind变量绑定bind用于创建一个变量可以在后续的 SQL 中引用。常用于模糊查询的%拼接。8.1 基本用法xmlselect idselectByUsername resultTypeUser bind namepattern value% username % / SELECT * FROM users WHERE username LIKE #{pattern} /select8.2 模糊查询的三种写法xml!-- 方式一使用 CONCAT推荐数据库无关性较好 -- select idselectByUsername resultTypeUser SELECT * FROM users WHERE username LIKE CONCAT(%, #{username}, %) /select !-- 方式二使用 bind -- select idselectByUsername resultTypeUser bind namepattern value% username % / SELECT * FROM users WHERE username LIKE #{pattern} /select !-- 方式三在 Java 层拼接不推荐耦合业务逻辑 -- !-- username % username % --8.3bind的其他用途xml!-- 动态表名注意 SQL 注入风险 -- select idselectFromTable resultTypeMap bind nametableName valueuser_ suffix / SELECT * FROM ${tableName} /select !-- 计算字段 -- select idselectWithDiscount resultTypeProduct bind namediscountedPrice valueprice * 0.8 / SELECT *, #{discountedPrice} AS discount_price FROM products /select九、sql与includeSQL 片段复用当多个查询使用相同的列或条件时可以将它们提取为 SQL 片段。9.1 定义片段xmlsql iduserColumns id, username, email, age, created_at /sql sql iduserCondition where if testusername ! null and username ! AND username #{username} /if if testage ! null AND age #{age} /if /where /sql9.2 引用片段xmlselect idselectAll resultTypeUser SELECT include refiduserColumns/ FROM users /select select idselectByCondition resultTypeUser SELECT include refiduserColumns/ FROM users include refiduserCondition/ /select9.3 带参数的includexmlsql iduserColumns ${alias}.id, ${alias}.username, ${alias}.email /sql select idselectWithAlias resultTypeUser SELECT include refiduserColumns property namealias valueu/ /include FROM users u /select9.4 跨文件引用xml!-- 在 UserMapper.xml 中 -- sql idbaseColumnsid, username, email/sql !-- 在其他 Mapper 中引用需要完整命名空间 -- include refidcom.example.mapper.UserMapper.baseColumns/十、综合实战动态查询构建器下面是一个完整的用户查询示例综合运用了多种动态 SQL 标签。10.1 Java 接口javapublic interface UserMapper { ListUser searchUsers(UserQuery query); int batchInsert(Param(users) ListUser users); int updateSelective(User user); int deleteByIds(Param(ids) ListLong ids); }10.2 查询对象javapublic class UserQuery { private Long id; private String username; private String email; private Integer minAge; private Integer maxAge; private ListLong ids; private String orderBy; private String orderDirection; private Integer offset; private Integer limit; // getter/setter 省略 }10.3 Mapper XMLxmlmapper namespacecom.example.mapper.UserMapper !-- 可复用片段 -- sql iduserColumns id, username, email, age, created_at /sql !-- 动态查询 -- select idsearchUsers resultTypeUser SELECT include refiduserColumns/ FROM users where !-- 精确 ID 查询 -- if testid ! null AND id #{id} /if !-- 用户名模糊查询 -- if testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /if !-- 邮箱精确查询 -- if testemail ! null and email ! AND email #{email} /if !-- 年龄范围 -- if testminAge ! null AND age gt; #{minAge} /if if testmaxAge ! null AND age lt; #{maxAge} /if !-- ID 集合查询 -- if testids ! null and ids.size() 0 AND id IN foreach collectionids itemid open( separator, close) #{id} /foreach /if !-- 优先级选择 -- choose when testemail ! null and email ! AND email IS NOT NULL /when otherwise AND age 0 /otherwise /choose /where !-- 动态排序使用 ${} 需注意 SQL 注入 -- if testorderBy ! null and orderBy ! ORDER BY ${orderBy} if testorderDirection ! null and orderDirection ! ${orderDirection} /if /if !-- 分页 -- if testoffset ! null and limit ! null LIMIT #{offset}, #{limit} /if /select !-- 批量插入 -- insert idbatchInsert INSERT INTO users (username, email, age, created_at) VALUES foreach collectionusers itemuser separator, (#{user.username}, #{user.email}, #{user.age}, NOW()) /foreach /insert !-- 选择性更新 -- update idupdateSelective UPDATE users set if testusername ! null and username ! username #{username}, /if if testemail ! null and email ! email #{email}, /if if testage ! null age #{age}, /if /set WHERE id #{id} /update !-- 批量删除 -- delete iddeleteByIds DELETE FROM users WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /delete /mapper10.4 使用示例java// 动态查询 UserQuery query new UserQuery(); query.setUsername(张); query.setMinAge(18); query.setMaxAge(60); query.setOrderBy(created_at); query.setOrderDirection(DESC); query.setOffset(0); query.setLimit(10); ListUser users userMapper.searchUsers(query);生成的 SQLsqlSELECT id, username, email, age, created_at FROM users WHERE username LIKE CONCAT(%, ?, %) AND age ? AND age ? ORDER BY created_at DESC LIMIT ?, ?十一、最佳实践与常见陷阱11.1 最佳实践实践说明优先使用where和set避免手动处理WHERE 11和多余逗号使用Param明确命名避免集合参数命名混淆#{}优先于${}防止 SQL 注入${}仅用于动态表名/列名空集合要判断避免IN ()语法错误SQL 片段复用将公共列和条件提取为sql避免过长的动态 SQL复杂逻辑可拆分为多个查询方法测试所有分支确保每个条件组合都能正常执行11.2 常见陷阱陷阱问题解决方案if中字符串用双引号OGNL 解析失败字符串用单引号admin字符比较失败A被解析为 char使用toString()或直接与字符串比较set所有条件为空生成无效 SQL业务层校验或使用trimforeach集合为空IN ()报错添加size() 0判断${}导致 SQL 注入用户输入直接拼接严格校验或使用#{}collection名称错误找不到集合参数使用Param明确命名where中 OR 条件第一个 OR 被保留使用trim自定义嵌套foreach性能问题、SQL 过长拆分为多次查询bind表达式错误OGNL 语法问题使用CONCAT替代11.3 安全警告${}的使用xml!-- ❌ 危险直接拼接用户输入 -- select idsearch resultTypeUser SELECT * FROM users ORDER BY ${userInput} /select !-- ✅ 安全使用白名单校验 -- select idsearch resultTypeUser SELECT * FROM users choose when testorderBy ageORDER BY age/when when testorderBy nameORDER BY username/when otherwiseORDER BY id/otherwise /choose /select十二、总结速查表标签用途核心属性典型场景if条件判断test动态添加查询条件choose/when/otherwise多分支选择test优先级选择where智能 WHERE自动去除多余 AND/OR组合查询条件set智能 SET自动去除多余逗号选择性更新trim自定义修剪prefix、suffix、prefixOverrides、suffixOverrides自定义 SQL 结构foreach循环遍历collection、item、open、close、separatorIN 查询、批量插入bind变量绑定name、value模糊查询、计算字段sql/includeSQL 片段复用refid公共列、公共条件一句话记忆MyBatis 动态 SQL 用if做判断choose做选择where和set智能处理关键字foreach遍历集合bind绑定变量sql复用片段——让你用声明式的方式构建灵活的 SQL既安全又优雅。动态 SQL 是 MyBatis 最强大的特性之一掌握它能让你在面对复杂查询需求时游刃有余。建议在实际项目中多实践从简单的条件查询开始逐步尝试批量操作、动态排序等复杂场景最终你将能写出既灵活又安全的持久层代码。
返回列表