第10篇:动态SQL实现原理详解
前言
在前面的章节中,我们学习了 MyBatis 的核心执行流程,特别是第9篇中详细介绍了MappedStatement和SqlSource的作用。你可能注意到,MyBatis 能够根据不同的条件动态生成 SQL,这正是 MyBatis 最强大的特性之一 ——动态SQL。
动态SQL 允许我们根据不同的条件、参数来拼接不同的 SQL 语句,避免了在 Java 代码中进行繁琐的字符串拼接。理解动态SQL的实现原理,是掌握 MyBatis 高级用法的关键。
本篇在整体架构中的位置
与前序章节的关联
- 第2篇(配置系统):学习了 XML 解析,本篇将深入了解动态标签如何被解析
- 第9篇(MappedStatement):学习了
SqlSource的类型,本篇将详细分析DynamicSqlSource的实现 - 第6篇(StatementHandler):学习了如何执行 SQL,本篇将了解动态 SQL 如何生成最终的可执行 SQL
1. 学习目标确认
1.0 第9篇思考题回顾
💡说明:第9篇思考题的详细解答请见文末附录A。
核心要点回顾:
MappedStatement是单条 SQL 映射语句的完整描述SqlSource负责提供 SQL,分为静态和动态两大类BoundSql包含最终可执行的 SQL 和参数映射additionalParameters用于存储动态生成的临时参数- 缓存策略影响查询性能
1.1 本篇学习目标
- 深入理解动态SQL的核心组件:
SqlNode、SqlSource、DynamicContext - 掌握常用动态标签的实现原理:
<if>、<where>、<foreach>、<choose>等 - 理解 OGNL 表达式在动态SQL中的作用
- 掌握动态SQL的执行流程和性能优化
- 学会调试和排查动态SQL相关问题
2. 动态SQL核心架构
2.1 核心组件关系图
2.2 核心组件职责
| 组件 | 职责 | 关键方法 |
|---|---|---|
| SqlSource | SQL来源接口 | getBoundSql(Object) |
| DynamicSqlSource | 动态SQL实现 | 运行期生成SQL |
| SqlNode | SQL节点接口 | apply(DynamicContext) |
| DynamicContext | 动态上下文 | 收集SQL片段和参数 |
| ExpressionEvaluator | 表达式求值器 | 使用OGNL求值 |
| SqlSourceBuilder | SQL源构建器 | 解析#{}占位符 |
2.3 执行流程时序图
3. SqlNode 体系详解
3.1 SqlNode 接口定义
/** * SQL节点接口 * 所有动态SQL标签都实现此接口 * * 源码位置: org.apache.ibatis.scripting.xmltags.SqlNode */publicinterfaceSqlNode{/** * 应用当前节点,将SQL片段添加到上下文 * * @param context 动态上下文 * @return 是否应用成功 */booleanapply(DynamicContextcontext);}3.2 SqlNode 实现类全景
| SqlNode 实现类 | 对应XML标签 | 作用 | 示例 |
|---|---|---|---|
| StaticTextSqlNode | 静态文本 | 原样输出SQL片段 | SELECT * FROM user |
| MixedSqlNode | 混合节点 | 包含多个子节点 | <select>的子内容 |
| IfSqlNode | <if> | 条件判断 | <if test="name != null"> |
| TrimSqlNode | <trim> | 去除前后缀 | <trim prefix="WHERE"> |
| WhereSqlNode | <where> | WHERE子句处理 | <where> |
| SetSqlNode | <set> | SET子句处理 | <set> |
| ForEachSqlNode | <foreach> | 遍历集合 | <foreach collection="ids"> |
| ChooseSqlNode | <choose> | 多分支选择 | <choose><when> |
| VarDeclSqlNode | <bind> | 绑定变量 | <bind name="pattern" value="'%' + name + '%'"/> |
3.3 StaticTextSqlNode:最简单的实现
/** * 静态文本SQL节点 * 直接输出文本内容,不做任何处理 * * 源码位置: org.apache.ibatis.scripting.xmltags.StaticTextSqlNode */publicclassStaticTextSqlNodeimplementsSqlNode{privatefinalStringtext;publicStaticTextSqlNode(Stringtext){this.text=text;}@Overridepublicbooleanapply(DynamicContextcontext){// 直接将文本添加到上下文context.appendSql(text);returntrue;}}示例:
<selectid="findAll"resultType="User">SELECT * FROM user</select>解析后会生成一个StaticTextSqlNode,包含文本"SELECT * FROM user"。
3.4 MixedSqlNode:组合多个子节点
/** * 混合SQL节点 * 包含多个子节点,按顺序应用 * * 源码位置: org.apache.ibatis.scripting.xmltags.MixedSqlNode */publicclassMixedSqlNodeimplementsSqlNode{privatefinalList<SqlNode>contents;publicMixedSqlNode(List<SqlNode>contents){this.contents=contents;}@Overridepublicbooleanapply(DynamicContextcontext){// 遍历所有子节点,依次应用contents.forEach(node->node.apply(context));returntrue;}}示例:
<selectid="findByName"resultType="User">SELECT * FROM user<where><iftest="name != null">AND name = #{name}</if></where></select>这会生成一个MixedSqlNode,包含多个子节点。
4. 条件判断标签
4.1 IfSqlNode:条件判断
/** * IF条件SQL节点 * 根据OGNL表达式决定是否包含SQL片段 * * 源码位置: org.apache.ibatis.scripting.xmltags.IfSqlNode */publicclassIfSqlNodeimplementsSqlNode{privatefinalExpressionEvaluatorevaluator;privatefinalStringtest;// OGNL表达式privatefinalSqlNodecontents;// 子节点publicIfSqlNode(SqlNodecontents,Stringtest){this.test=test;this.contents=contents;this.evaluator=newExpressionEvaluator();}@Overridepublicbooleanapply(DynamicContextcontext){// 1. 使用OGNL求值表达式if(evaluator.evaluateBoolean(test,context.getBindings())){// 2. 条件为true,应用子节点contents.apply(context);returntrue;}returnfalse;}}使用示例:
<selectid="findUsers"resultType="User">SELECT * FROM user WHERE 1=1<iftest="name != null and name !=''">AND name = #{name}</if><iftest="age != null">AND age > #{age}</if></select>运行期行为:
// 场景1: 只传 nameMap<String,Object>params=newHashMap<>();params.put("name","张三");// 生成SQL: SELECT * FROM user WHERE 1=1 AND name = ?// 场景2: 传 name 和 ageparams.put("name","张三");params.put("age",18);// 生成SQL: SELECT * FROM user WHERE 1=1 AND name = ? AND age > ?4.2 ChooseSqlNode:多分支选择
/** * CHOOSE多分支SQL节点 * 类似Java的 switch-case * * 源码位置: org.apache.ibatis.scripting.xmltags.ChooseSqlNode */publicclassChooseSqlNodeimplementsSqlNode{privatefinalSqlNodedefaultSqlNode;// <otherwise>privatefinalList<SqlNode>ifSqlNodes;// <when> 列表publicChooseSqlNode(List<SqlNode>ifSqlNodes,SqlNodedefaultSqlNode){this.ifSqlNodes=ifSqlNodes;this.defaultSqlNode=defaultSqlNode;}@Overridepublicbooleanapply(DynamicContextcontext){// 1. 遍历 <when> 节点,找到第一个满足条件的for(SqlNodesqlNode:ifSqlNodes){if(sqlNode.apply(context)){returntrue;}}// 2. 如果都不满足,应用 <otherwise>if(defaultSqlNode!=null){defaultSqlNode.apply(context);returntrue;}returnfalse;}}使用示例:
<selectid="findUsers"resultType="User">SELECT * FROM user<where><choose><whentest="name != null">AND name = #{name}</when><whentest="email != null">AND email = #{email}</when><otherwise>AND status = 'ACTIVE'</otherwise></choose></where></select>5. WHERE/SET 子句处理
5.1 TrimSqlNode:通用的前后缀处理
/** * TRIM修剪SQL节点 * 可以去除前后缀和多余的分隔符 * * 源码位置: org.apache.ibatis.scripting.xmltags.TrimSqlNode */publicclassTrimSqlNodeimplementsSqlNode{privatefinalSqlNodecontents;privatefinalStringprefix;// 要添加的前缀privatefinalStringsuffix;// 要添加的后缀privatefinalList<String>prefixesToOverride;// 要去除的前缀列表privatefinalList<String>suffixesToOverride;// 要去除的后缀列表privatefinalConfigurationconfiguration;@Overridepublicbooleanapply(DynamicContextcontext){// 1. 创建过滤上下文FilteredDynamicContextfilteredDynamicContext=newFilteredDynamicContext(context);// 2. 应用子节点,收集SQL片段booleanresult=contents.apply(filteredDynamicContext);// 3. 处理前后缀filteredDynamicContext.applyAll();returnresult;}/** * 过滤动态上下文 * 负责去除多余的前后缀 */privateclassFilteredDynamicContextextendsDynamicContext{privatefinalStringBuildersqlBuffer;publicvoidapplyAll(){StringtrimmedSql=sqlBuffer.toString().trim();if(trimmedSql.length()>0){// 去除要移除的前缀trimmedSql=applyPrefixes(trimmedSql);// 去除要移除的后缀trimmedSql=applySuffixes(trimmedSql);// 添加新前缀if(prefix!=null){trimmedSql=prefix+trimmedSql;}// 添加新后缀if(suffix!=null){trimmedSql=trimmedSql+suffix;}delegate.appendSql(trimmedSql);}}privateStringapplyPrefixes(Stringsql){StringupperSql=sql.toUpperCase(Locale.ENGLISH);for(StringtoRemove:prefixesToOverride){if(upperSql.startsWith(toRemove.toUpperCase(Locale.ENGLISH))){returnsql.substring(toRemove.length());}}returnsql;}privateStringapplySuffixes(Stringsql){StringupperSql=sql.toUpperCase(Locale.ENGLISH);for(StringtoRemove:suffixesToOverride){if(upperSql.endsWith(toRemove.toUpperCase(Locale.ENGLISH))){returnsql.substring(0,sql.length()-toRemove.length());}}returnsql;}}}使用示例:
<selectid="findUsers"resultType="User">SELECT * FROM user<trimprefix="WHERE"prefixOverrides="AND |OR"><iftest="name != null">AND name = #{name}</if><iftest="age != null">AND age > #{age}</if></trim></select>处理过程:
// 场景1: 只有 name 参数// 子节点生成: " AND name = #{name}"// trim处理后: "WHERE name = #{name}" (去除了开头的 AND)// 场景2: name 和 age 都有// 子节点生成: " AND name = #{name} AND age > #{age}"// trim处理后: "WHERE name = #{name} AND age > #{age}"5.2 WhereSqlNode:WHERE 子句简化
/** * WHERE子句SQL节点 * 本质是 TrimSqlNode 的特例 * * 源码位置: org.apache.ibatis.scripting.xmltags.WhereSqlNode */publicclassWhereSqlNodeextendsTrimSqlNode{privatestaticList<String>prefixList=Arrays.asList("AND ","OR ","AND\n","OR\n","AND\r","OR\r","AND\t","OR\t");publicWhereSqlNode(Configurationconfiguration,SqlNodecontents){super(configuration,contents,"WHERE",prefixList,null,null);}}等价关系:
<!-- 使用 <where> --><where><iftest="name != null">AND name = #{name}</if></where><!-- 等价于 <trim> --><trimprefix="WHERE"prefixOverrides="AND |OR"><iftest="name != null">AND name = #{name}</if></trim>5.3 SetSqlNode:SET 子句简化
/** * SET子句SQL节点 * 用于UPDATE语句,自动去除最后的逗号 * * 源码位置: org.apache.ibatis.scripting.xmltags.SetSqlNode */publicclassSetSqlNodeextendsTrimSqlNode{privatestaticfinalList<String>COMMA=Collections.singletonList(",");publicSetSqlNode(Configurationconfiguration,SqlNodecontents){super(configuration,contents,"SET",COMMA,null,COMMA);}}使用示例:
<updateid="updateUser"parameterType="User">UPDATE user<set><iftest="name != null">name = #{name},</if><iftest="email != null">email = #{email},</if><iftest="age != null">age = #{age},</if></set>WHERE id = #{id}</update>处理过程:
// 场景: 只更新 name 和 email// 子节点生成: "name = #{name}, email = #{email},"// set处理后: "SET name = #{name}, email = #{email}" (去除最后的逗号)6. ForEach 循环标签
6.1 ForEachSqlNode 实现
/** * FOREACH循环SQL节点 * 用于遍历集合生成IN子句等 * * 源码位置: org.apache.ibatis.scripting.xmltags.ForEachSqlNode */publicclassForEachSqlNodeimplementsSqlNode{publicstaticfinalStringITEM_PREFIX="__frch_";privatefinalExpressionEvaluatorevaluator;privatefinalStringcollectionExpression;// collection属性privatefinalBooleannullable;privatefinalSqlNodecontents;// 子节点privatefinalStringopen;// 开始字符privatefinalStringclose;// 结束字符privatefinalStringseparator;// 分隔符privatefinalStringitem;// 当前元素变量名privatefinalStringindex;// 索引变量名privatefinalConfigurationconfiguration;@Overridepublicbooleanapply(DynamicContextcontext){Map<String,Object>bindings=context.getBindings();// 1. 求值集合表达式finalIterable<?>iterable=evaluator.evaluateIterable(collectionExpression,bindings,Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));if(iterable==null||!iterable.iterator().hasNext()){returntrue;}booleanfirst=true;// 2. 添加开始字符applyOpen(context);inti=0;for(Objecto:iterable){DynamicContextoldContext=context;// 3. 添加分隔符if(first){first=!separator.isEmpty();}else{applySeparator(context);}intuniqueNumber=context.getUniqueNumber();// 4. 绑定迭代变量if(oinstanceofMap.Entry){@SuppressWarnings("unchecked")Map.Entry<Object,Object>mapEntry=(Map.Entry<Object,Object>)o;applyIndex(context,mapEntry.getKey(),uniqueNumber);applyItem(context,mapEntry.getValue(),uniqueNumber);}else{applyIndex(context,i,uniqueNumber);applyItem(context,o,uniqueNumber);}// 5. 应用子节点(使用过滤上下文)contents.apply(newFilteredDynamicContext(configuration,context,index,item,uniqueNumber));if(context!=oldContext){context=oldContext;}i++;}// 6. 添加结束字符applyClose(context);// 7. 移除迭代变量context.getBindings().remove(item);context.getBindings().remove(index);returntrue;}/** * 绑定当前元素 */privatevoidapplyItem(DynamicContextcontext,Objecto,inti){if(item!=null){context.bind(item,o);context.bind(itemizeItem(item,i),o);}}/** * 绑定当前索引 */privatevoidapplyIndex(DynamicContextcontext,Objecto,inti){if(index!=null){context.bind(index,o);context.bind(itemizeItem(index,i),o);}}/** * 生成唯一的参数名 * 例如: __frch_id_0, __frch_id_1, ... */privatestaticStringitemizeItem(Stringitem,inti){returnITEM_PREFIX+item+"_"+i;}/** * 过滤动态上下文 * 将 #{item} 替换为 #{__frch_item_0} */privatestaticclassFilteredDynamicContextextendsDynamicContext{privatefinalDynamicContextdelegate;privatefinalintindex;privatefinalStringitemIndex;privatefinalStringitem;@OverridepublicvoidappendSql(Stringsql){GenericTokenParserparser=newGenericTokenParser("#{","}",content->{StringnewContent=content.replaceFirst("^\\s*"+item+"(?![^.,:\\s])",itemizeItem(item,index));if(itemIndex!=null&&newContent.equals(content)){newContent=content.replaceFirst("^\\s*"+itemIndex+"(?![^.,:\\s])",itemizeItem(itemIndex,index));}return"#{"+newContent+"}";});delegate.appendSql(parser.parse(sql));}}}6.2 ForEach 使用示例
示例1:IN 子句
<selectid="findByIds"resultType="User">SELECT * FROM user WHERE id IN<foreachcollection="list"item="id"open="("separator=","close=")">#{id}</foreach></select>执行过程:
// 输入参数List<Long>ids=Arrays.asList(1L,2L,3L);// 生成SQL// SELECT * FROM user WHERE id IN (?, ?, ?)// ParameterMappings// [__frch_id_0, __frch_id_1, __frch_id_2]// AdditionalParameters// {// __frch_id_0 = 1,// __frch_id_1 = 2,// __frch_id_2 = 3// }示例2:批量插入
<insertid="batchInsert"parameterType="list">INSERT INTO user (name, email, age) VALUES<foreachcollection="list"item="user"separator=",">(#{user.name}, #{user.email}, #{user.age})</foreach></insert>执行过程:
// 输入参数List<User>users=Arrays.asList(newUser("张三","zhang@example.com",25),newUser("李四","li@example.com",30));// 生成SQL// INSERT INTO user (name, email, age)// VALUES (?, ?, ?), (?, ?, ?)// AdditionalParameters// {// __frch_user_0 = User(name=张三, ...),// __frch_user_1 = User(name=李四, ...)// }7. Bind 变量绑定
7.1 VarDeclSqlNode 实现
/** * 变量声明SQL节点 * 对应 <bind> 标签 * * 源码位置: org.apache.ibatis.scripting.xmltags.VarDeclSqlNode */publicclassVarDeclSqlNodeimplementsSqlNode{privatefinalStringname;// 变量名privatefinalStringexpression;// OGNL表达式publicVarDeclSqlNode(Stringvar,Stringexp){this.name=var;this.expression=exp;}@Overridepublicbooleanapply(DynamicContextcontext){// 1. 使用OGNL求值表达式Objectvalue=OgnlCache.getValue(expression,context.getBindings());// 2. 绑定到上下文context.bind(name,value);returntrue;}}7.2 使用示例
<selectid="findByName"resultType="User"><!-- 绑定模糊查询模式 --><bindname="pattern"value="'%'+ name +'%'"/>SELECT * FROM user WHERE name LIKE #{pattern}</select>执行过程:
// 输入参数Map<String,Object>params=newHashMap<>();params.put("name","张三");// bind 处理// pattern = '%' + '张三' + '%' = '%张三%'// 生成SQL// SELECT * FROM user WHERE name LIKE ?// 参数绑定// pattern = '%张三%'8. DynamicContext 动态上下文
8.1 核心实现
/** * 动态上下文 * 用于收集SQL片段和绑定参数 * * 源码位置: org.apache.ibatis.scripting.xmltags.DynamicContext */publicclassDynamicContext{publicstaticfinalStringPARAMETER_OBJECT_KEY="_parameter";publicstaticfinalStringDATABASE_ID_KEY="_databaseId";privatefinalContextMapbindings;// 参数绑定MapprivatefinalStringBuildersqlBuilder;// SQL收集器privateintuniqueNumber=0;// 唯一编号生成器/** * 构造函数 */publicDynamicContext(Configurationconfiguration,ObjectparameterObject){// 1. 处理参数对象if(parameterObject!=null&&!(parameterObjectinstanceofMap)){MetaObjectmetaObject=configuration.newMetaObject(parameterObject);booleanexistsTypeHandler=configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());bindings=newContextMap(metaObject,existsTypeHandler);}else{bindings=newContextMap(null,false);}// 2. 绑定内置参数bindings.put(PARAMETER_OBJECT_KEY,parameterObject);bindings.put(DATABASE_ID_KEY,configuration.getDatabaseId());this.sqlBuilder=newStringBuilder();}/** * 获取绑定参数 */publicMap<String,Object>getBindings(){returnbindings;}/** * 绑定参数 */publicvoidbind(Stringname,Objectvalue){bindings.put(name,value);}/** * 追加SQL片段 */publicvoidappendSql(Stringsql){sqlBuilder.append(sql);sqlBuilder.append(" ");}/** * 获取生成的SQL */publicStringgetSql(){returnsqlBuilder.toString().trim();}/** * 获取唯一编号 */publicintgetUniqueNumber(){returnuniqueNumber++;}/** * 上下文Map * 特殊的Map,支持通过MetaObject访问属性 */staticclassContextMapextendsHashMap<String,Object>{privatefinalMetaObjectparameterMetaObject;privatefinalbooleanfallbackParameterObject;publicContextMap(MetaObjectparameterMetaObject,booleanfallbackParameterObject){this.parameterMetaObject=parameterMetaObject;this.fallbackParameterObject=fallbackParameterObject;}@OverridepublicObjectget(Objectkey){StringstrKey=(String)key;// 1. 先从Map中查找if(super.containsKey(strKey)){returnsuper.get(strKey);}// 2. 从参数对象中获取属性值if(parameterMetaObject==null){returnnull;}if(fallbackParameterObject&&!parameterMetaObject.hasGetter(strKey)){returnparameterMetaObject.getOriginalObject();}else{returnparameterMetaObject.getValue(strKey);}}}}8.2 内置参数
| 参数名 | 说明 | 使用场景 |
|---|---|---|
_parameter | 原始参数对象 | 访问整个参数对象 |
_databaseId | 数据库厂商标识 | 多数据库适配 |
使用示例:
<selectid="findUsers"resultType="User">SELECT * FROM user<where><!-- 使用 _databaseId 做数据库兼容 --><iftest="_databaseId =='mysql'">AND created_at > DATE_SUB(NOW(), INTERVAL 1 YEAR)</if><iftest="_databaseId =='oracle'">AND created_at > ADD_MONTHS(SYSDATE, -12)</if><!-- 使用 _parameter 访问整个参数对象 --><iftest="_parameter != null">AND status = 'ACTIVE'</if></where></select>9. OGNL 表达式求值
9.1 ExpressionEvaluator 实现
/** * 表达式求值器 * 使用OGNL进行表达式求值 * * 源码位置: org.apache.ibatis.scripting.xmltags.ExpressionEvaluator */publicclassExpressionEvaluator{/** * 求值布尔表达式 */publicbooleanevaluateBoolean(Stringexpression,ObjectparameterObject){Objectvalue=OgnlCache.getValue(expression,parameterObject);if(valueinstanceofBoolean){return(Boolean)value;}if(valueinstanceofNumber){return!newBigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);}returnvalue!=null;}/** * 求值可迭代表达式 */publicIterable<?>evaluateIterable(Stringexpression,ObjectparameterObject,booleannullable){Objectvalue=OgnlCache.getValue(expression,parameterObject);if(value==null){if(nullable){returnnull;}else{thrownewBuilderException("The expression '"+expression+"' evaluated to a null value.");}}if(valueinstanceofIterable){return(Iterable<?>)value;}if(value.getClass().isArray()){// 数组转Listintsize=Array.getLength(value);List<Object>answer=newArrayList<>(size);for(inti=0;i<size;i++){answer.add(Array.get(value,i));}returnanswer;}if(valueinstanceofMap){return((Map<?,?>)value).entrySet();}thrownewBuilderException("Error evaluating expression '"+expression+"'. Return value ("+value+") was not iterable.");}}9.2 常用OGNL表达式
| 表达式 | 说明 | 示例 |
|---|---|---|
name != null | 非空判断 | <if test="name != null"> |
name != null and name != '' | 非空非空串 | <if test="name != null and name != ''"> |
age > 18 | 数值比较 | <if test="age > 18"> |
list != null and list.size() > 0 | 集合非空 | <if test="list != null and list.size() > 0"> |
user.name != null | 嵌套属性 | <if test="user.name != null"> |
@java.lang.Math@max(a, b) | 静态方法调用 | <bind name="max" value="@java.lang.Math@max(a, b)"/> |
10. DynamicSqlSource 完整流程
10.1 源码实现
/** * 动态SQL源 * 运行期根据参数动态生成SQL * * 源码位置: org.apache.ibatis.scripting.xmltags.DynamicSqlSource */publicclassDynamicSqlSourceimplementsSqlSource{privatefinalConfigurationconfiguration;privatefinalSqlNoderootSqlNode;publicDynamicSqlSource(Configurationconfiguration,SqlNoderootSqlNode){this.configuration=configuration;this.rootSqlNode=rootSqlNode;}@OverridepublicBoundSqlgetBoundSql(ObjectparameterObject){// 1. 创建动态上下文DynamicContextcontext=newDynamicContext(configuration,parameterObject);// 2. 应用SqlNode树,生成SQLrootSqlNode.apply(context);// 3. 使用SqlSourceBuilder解析 #{} 占位符SqlSourceBuildersqlSourceParser=newSqlSourceBuilder(configuration);Class<?>parameterType=parameterObject==null?Object.class:parameterObject.getClass();SqlSourcesqlSource=sqlSourceParser.parse(context.getSql(),parameterType,context.getBindings());// 4. 获取BoundSqlBoundSqlboundSql=sqlSource.getBoundSql(parameterObject);// 5. 复制额外参数context.getBindings().forEach((key,value)->{boundSql.setAdditionalParameter(key,value);});returnboundSql;}}10.2 完整执行示例
publicclassDynamicSqlDemo{publicstaticvoidmain(String[]args)throwsException{// 1. 初始化MyBatisSqlSessionFactoryfactory=newSqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));Configurationconfiguration=factory.getConfiguration();// 2. 获取MappedStatementStringstatementId="com.example.UserMapper.findUsers";MappedStatementms=configuration.getMappedStatement(statementId);// 3. 准备参数Map<String,Object>params=newHashMap<>();params.put("name","张三");params.put("age",25);params.put("ids",Arrays.asList(1L,2L,3L));System.out.println("========== 动态SQL生成过程 ==========\n");// 4. 获取BoundSql(触发动态SQL生成)BoundSqlboundSql=ms.getBoundSql(params);// 5. 打印结果System.out.println("生成的SQL:");System.out.println(boundSql.getSql());System.out.println("\n参数映射:");List<ParameterMapping>parameterMappings=boundSql.getParameterMappings();for(inti=0;i<parameterMappings.size();i++){ParameterMappingpm=parameterMappings.get(i);Stringproperty=pm.getProperty();Objectvalue;if(boundSql.hasAdditionalParameter(property)){value=boundSql.getAdditionalParameter(property);System.out.println(" ["+i+"] "+property+" = "+value+" (额外参数)");}else{value=params.get(property);System.out.println(" ["+i+"] "+property+" = "+value+" (普通参数)");}}// 6. 实际执行System.out.println("\n========== 执行查询 ==========\n");try(SqlSessionsession=factory.openSession()){List<Object>results=session.selectList(statementId,params);System.out.println("查询结果数: "+results.size());results.forEach(System.out::println);}}}输出示例:
========== 动态SQL生成过程 ========== 生成的SQL: SELECT * FROM user WHERE name = ? AND age > ? AND id IN ( ? , ? , ? ) 参数映射: [0] name = 张三 (普通参数) [1] age = 25 (普通参数) [2] __frch_id_0 = 1 (额外参数) [3] __frch_id_1 = 2 (额外参数) [4] __frch_id_2 = 3 (额外参数) ========== 执行查询 ========== 查询结果数: 2 User{id=1, name='张三', age=25} User{id=2, name='张三', age=26}11. 性能优化
11.1 避免不必要的动态SQL
<!-- ❌ 不推荐:简单查询使用动态SQL --><selectid="findById"resultType="User">SELECT * FROM user<where><iftest="id != null">id = #{id}</if></where></select><!-- ✅ 推荐:静态SQL --><selectid="findById"resultType="User">SELECT * FROM user WHERE id = #{id}</select>11.2 合理使用<where>和<trim>
<!-- ❌ 不推荐:手动处理WHERE --><selectid="findUsers"resultType="User">SELECT * FROM user WHERE 1=1<iftest="name != null">AND name = #{name}</if><iftest="age != null">AND age > #{age}</if></select><!-- ✅ 推荐:使用 <where> 自动处理 --><selectid="findUsers"resultType="User">SELECT * FROM user<where><iftest="name != null">AND name = #{name}</if><iftest="age != null">AND age > #{age}</if></where></select>11.3 大集合遍历优化
<!-- ❌ 不推荐:单条SQL处理大量数据 --><selectid="findByIds"resultType="User">SELECT * FROM user WHERE id IN<foreachcollection="ids"item="id"open="("separator=","close=")">#{id}</foreach></select><!-- ✅ 推荐:分批处理 -->// Java代码分批publicList<User>findByIds(List<Long>ids){intbatchSize=1000;List<User>results=newArrayList<>();for(inti=0;i<ids.size();i+=batchSize){intend=Math.min(i+batchSize,ids.size());List<Long>batch=ids.subList(i,end);results.addAll(userMapper.findByIdsBatch(batch));}returnresults;}11.4 缓存动态SQL解析结果
MyBatis 内部已经对SqlSource进行了缓存,但要注意:
- RawSqlSource:构建期解析一次,后续直接复用(性能最优)
- DynamicSqlSource:每次执行都要重新解析(性能较低)
性能对比:
// 测试代码@BenchmarkMode(Mode.Throughput)@Warmup(iterations=3)@Measurement(iterations=5)publicclassSqlSourceBenchmark{@BenchmarkpublicBoundSqltestRawSqlSource(){// 静态SQL:SELECT * FROM user WHERE id = #{id}returnrawMs.getBoundSql(Collections.singletonMap("id",1L));}@BenchmarkpublicBoundSqltestDynamicSqlSource(){// 动态SQL:<if test="id != null">id = #{id}</if>returndynamicMs.getBoundSql(Collections.singletonMap("id",1L));}}/** * 测试结果(ops/sec): * * testRawSqlSource: 12,000,000 (快) * testDynamicSqlSource: 500,000 (慢24倍) */12. 调试与排查
12.1 开启SQL日志
<settings><!-- 开启标准输出日志 --><settingname="logImpl"value="STDOUT_LOGGING"/></settings>12.2 断点调试位置
推荐断点位置:
DynamicSqlSource.getBoundSql(Object)- 动态SQL生成入口DynamicContext构造函数 - 查看初始参数绑定IfSqlNode.apply(DynamicContext)- 条件判断逻辑ForEachSqlNode.apply(DynamicContext)- 循环遍历逻辑SqlSourceBuilder.parse(...)-#{}占位符解析BoundSql构造函数 - 查看最终SQL和参数
12.3 打印动态SQL生成过程
/** * 动态SQL调试工具类 */publicclassDynamicSqlDebugger{/** * 打印动态SQL生成详情 */publicstaticvoiddebug(MappedStatementms,ObjectparameterObject){System.out.println("========== 动态SQL调试信息 ==========");System.out.println("MappedStatement ID: "+ms.getId());System.out.println("SqlSource类型: "+ms.getSqlSource().getClass().getSimpleName());// 获取BoundSqlBoundSqlboundSql=ms.getBoundSql(parameterObject);// 打印生成的SQLSystem.out.println("\n生成的SQL:");System.out.println(boundSql.getSql());// 打印参数映射System.out.println("\n参数映射:");List<ParameterMapping>parameterMappings=boundSql.getParameterMappings();for(inti=0;i<parameterMappings.size();i++){ParameterMappingpm=parameterMappings.get(i);Stringproperty=pm.getProperty();Objectvalue=getParameterValue(boundSql,parameterObject,property);System.out.printf(" [%d] %s = %s (jdbcType=%s, javaType=%s)%n",i,property,value,pm.getJdbcType(),pm.getJavaType().getSimpleName());}// 打印额外参数System.out.println("\n额外参数:");parameterMappings.stream().map(ParameterMapping::getProperty).filter(boundSql::hasAdditionalParameter).forEach(property->{Objectvalue=boundSql.getAdditionalParameter(property);System.out.println(" "+property+" = "+value);});System.out.println("====================================\n");}privatestaticObjectgetParameterValue(BoundSqlboundSql,ObjectparameterObject,Stringproperty){if(boundSql.hasAdditionalParameter(property)){returnboundSql.getAdditionalParameter(property);}if(parameterObject==null){returnnull;}if(parameterObjectinstanceofMap){return((Map<?,?>)parameterObject).get(property);}MetaObjectmetaObject=MetaObject.forObject(parameterObject,newDefaultObjectFactory(),newDefaultObjectWrapperFactory(),newDefaultReflectorFactory());returnmetaObject.getValue(property);}}使用示例:
// 调试动态SQLDynamicSqlDebugger.debug(ms,params);12.4 常见问题排查
问题1:条件不生效
<!-- 错误:字符串比较使用 == --><iftest="status =='ACTIVE'">AND status = #{status}</if><!-- 正确:字符串比较使用 equals 或单引号 --><iftest='status =="ACTIVE"'>AND status = #{status}</if><!-- 或 --><iftest="status != null and status.equals('ACTIVE')">AND status = #{status}</if>问题2:集合为空报错
<!-- 错误:未判断集合是否为空 --><foreachcollection="ids"item="id">#{id}</foreach><!-- 正确:添加判空条件 --><iftest="ids != null and ids.size() > 0"><foreachcollection="ids"item="id"open="("separator=","close=")">#{id}</foreach></if>问题3:参数名错误
// 错误:未使用 @Param 注解List<User>findUsers(Stringname,Integerage);// 参数名会是 param1, param2 或 arg0, arg1// 正确:使用 @Param 指定参数名List<User>findUsers(@Param("name")Stringname,@Param("age")Integerage);13. 最佳实践
13.1 动态SQL设计原则
- ✅简单优先:能用静态SQL就不用动态SQL
- ✅条件前置:把最可能过滤数据的条件放在前面
- ✅合理嵌套:避免过深的嵌套结构(建议<= 3层)
- ✅参数验证:使用
@Param明确参数名 - ✅集合判空:遍历前先判断集合非空
- ✅性能测试:对复杂动态SQL进行压测
13.2 可维护性建议
<!-- ✅ 推荐:使用 <sql> 片段复用 --><sqlid="userColumns">id, name, email, age, status, created_at, updated_at</sql><sqlid="userWhere"><where><iftest="name != null">AND name = #{name}</if><iftest="age != null">AND age > #{age}</if><iftest="status != null">AND status = #{status}</if></where></sql><selectid="findUsers"resultType="User">SELECT<includerefid="userColumns"/>FROM user<includerefid="userWhere"/></select><selectid="countUsers"resultType="long">SELECT COUNT(*) FROM user<includerefid="userWhere"/></select>13.3 安全注意事项
<!-- ❌ 危险:使用 ${} 可能导致SQL注入 --><selectid="findUsers"resultType="User">SELECT * FROM user ORDER BY ${orderBy}</select><!-- ✅ 安全:使用枚举或白名单验证 -->// Java代码验证publicList<User>findUsers(StringorderBy){// 白名单验证if(!Arrays.asList("name","age","created_at").contains(orderBy)){thrownewIllegalArgumentException("Invalid orderBy: "+orderBy);}returnuserMapper.findUsersOrderBy(orderBy);}14. 小结
核心知识点:
- 动态SQL体系:
SqlNode→DynamicContext→DynamicSqlSource→BoundSql - 常用标签:
<if>、<where>、<foreach>、<choose>、<bind> - OGNL表达式:用于条件判断和变量绑定
- 额外参数机制:
<foreach>和<bind>生成的临时参数 - 性能优化:优先使用静态SQL,避免过度动态化
设计亮点:
- ✅ 灵活强大的SQL拼接能力
- ✅ 清晰的节点树结构
- ✅ 完善的表达式求值
- ✅ 高效的参数绑定机制
注意事项:
- ⚠️ 动态SQL有性能开销,谨慎使用
- ⚠️ 注意SQL注入风险(
${}vs#{}) - ⚠️ 集合遍历前要判空
- ⚠️ 复杂动态SQL要充分测试
思考题
- 为什么 MyBatis 要设计
SqlNode树状结构,而不是简单的模板字符串替换? <foreach>生成的额外参数(如__frch_id_0)为什么要用这种命名方式?直接用原参数有什么问题?- 在什么场景下应该使用
<bind>,而不是在 Java 代码中处理参数? - 如何设计一个插件来缓存动态SQL的解析结果,以提升性能?
- 对于包含大量
<if>条件的复杂动态SQL,如何进行性能优化?
💬 交流与讨论
感谢您阅读本篇文章!希望这篇深入解析能帮助您更好地理解 MyBatis 的动态SQL实现原理。
🤝 期待您的参与
在学习和实践过程中,您可能会遇到各种问题或有独特的见解,欢迎在评论区分享:
💡 分享您的经验
- 在实际项目中使用动态SQL的经验和技巧
- 遇到的性能优化案例
- 有趣的问题排查过程
- 对动态SQL设计的理解
附录A:第9篇思考题详细解答
本附录提供第9篇《MappedStatement映射语句解析》思考题的详细解答。
思考题1:在读多写少的场景下,如何组合使用useCache与flushCacheRequired达到最优的缓存收益?
最佳实践:
- 查询语句:
<!-- ✅ 开启二级缓存 --><selectid="findById"resultType="User"useCache="true"flushCacheRequired="false">SELECT * FROM user WHERE id = #{id}</select>- 更新语句:
<!-- ✅ 更新后刷新缓存 --><updateid="updateUser"flushCacheRequired="true">UPDATE user SET name = #{name} WHERE id = #{id}</update>- 统计查询:
<!-- ✅ 实时统计不使用缓存 --><selectid="countUsers"resultType="long"useCache="false">SELECT COUNT(*) FROM user</select>策略组合:
| 场景 | useCache | flushCacheRequired | 说明 |
|---|---|---|---|
| 基础查询 | true | false | 使用缓存,提升性能 |
| 更新操作 | false | true | 不缓存,更新后刷新命名空间缓存 |
| 实时查询 | false | false | 不缓存,保证实时性 |
| 统计查询 | 视情况 | false | 高频统计可缓存,实时统计不缓存 |
思考题2:什么时候应该优先选择RawSqlSource而避免动态SQL?有哪些折中方案?
选择 RawSqlSource 的场景:
- ✅ SQL结构固定,只有参数值变化
- ✅ 高并发热点查询
- ✅ 对性能要求极高的场景
- ✅ SQL逻辑简单,不需要条件判断
折中方案:
方案1:拆分多个静态SQL
<!-- 代替复杂的动态SQL --><selectid="findByName"resultType="User">SELECT * FROM user WHERE name = #{name}</select><selectid="findByAge"resultType="User">SELECT * FROM user WHERE age > #{age}</select><selectid="findByNameAndAge"resultType="User">SELECT * FROM user WHERE name = #{name} AND age > #{age}</select>方案2:使用 @SelectProvider 编程式构建
publicclassUserSqlProvider{publicStringfindUsers(Map<String,Object>params){// 可以缓存构建结果StringcacheKey=generateCacheKey(params);returnsqlCache.computeIfAbsent(cacheKey,k->buildSql(params));}}方案3:限制动态范围
<!-- 只在必要的部分使用动态SQL --><selectid="findUsers"resultType="User">SELECT * FROM user WHERE status = 'ACTIVE'<iftest="name != null">AND name = #{name}</if></select>思考题3:BoundSql.additionalParameters在哪些场景会出现?它们的取值优先级如何影响参数绑定?
出现场景:
<foreach>遍历:
<foreachcollection="ids"item="id">#{id}</foreach><!-- 生成: __frch_id_0, __frch_id_1, ... --><bind>变量:
<bindname="pattern"value="'%'+ name +'%'"/><!-- 生成: pattern = '%张三%' -->- 嵌套查询传参:
<associationproperty="user"column="user_id"select="findUserById"/><!-- column值作为额外参数传递 -->取值优先级:
// 优先级:额外参数 > 原始参数Objectvalue;Stringproperty=parameterMapping.getProperty();if(boundSql.hasAdditionalParameter(property)){// 优先级1:额外参数value=boundSql.getAdditionalParameter(property);}elseif(parameterObject==null){// 优先级2:空值value=null;}elseif(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())){// 优先级3:基本类型value=parameterObject;}else{// 优先级4:复杂对象属性MetaObjectmetaObject=configuration.newMetaObject(parameterObject);value=metaObject.getValue(property);}思考题4:如何利用resultSets正确处理存储过程返回的多个结果集?
示例存储过程:
CREATEPROCEDUREgetUserAndOrders(INuserIdBIGINT)BEGIN-- 第一个结果集:用户信息SELECT*FROMuserWHEREid=userId;-- 第二个结果集:订单信息SELECT*FROM`order`WHEREuser_id=userId;ENDMyBatis配置:
<selectid="getUserAndOrders"statementType="CALLABLE"resultSets="users,orders">{call getUserAndOrders(#{userId})}</select>ResultMap配置:
<!-- 第一个结果集 --><resultMapid="userMap"type="User"><idproperty="id"column="id"/><resultproperty="name"column="name"/></resultMap><!-- 第二个结果集 --><resultMapid="orderMap"type="Order"><idproperty="id"column="id"/><resultproperty="orderNo"column="order_no"/></resultMap>处理逻辑:
// ResultSetHandler按照resultSets顺序处理List<Object>results=newArrayList<>();// 1. 处理第一个结果集 (users)ResultSetrs1=stmt.getResultSet();List<User>users=handleResultSet(rs1,userMap);results.add(users);// 2. 移动到下一个结果集stmt.getMoreResults();// 3. 处理第二个结果集 (orders)ResultSetrs2=stmt.getResultSet();List<Order>orders=handleResultSet(rs2,orderMap);results.add(orders);思考题5:如果要自定义LanguageDriver,它会如何影响MappedStatement的构建与运行期行为?
自定义示例:
/** * 自定义语言驱动 * 支持JSON格式的动态SQL */publicclassJsonLanguageDriverimplementsLanguageDriver{@OverridepublicSqlSourcecreateSqlSource(Configurationconfiguration,Stringscript,Class<?>parameterType){// 1. 解析JSON格式的SQL定义JSONObjectjson=JSON.parseObject(script);// 2. 构建自定义的SqlNode树SqlNoderootNode=parseJsonToSqlNode(json);// 3. 返回自定义SqlSourcereturnnewJsonDynamicSqlSource(configuration,rootNode);}privateSqlNodeparseJsonToSqlNode(JSONObjectjson){// 解析逻辑...returnnewMixedSqlNode(nodes);}}影响范围:
构建期影响:
- 改变SQL的解析方式(XML/注解/JSON/其他格式)
- 自定义SqlNode的生成逻辑
- 控制SqlSource的类型选择
运行期影响:
- 影响BoundSql的生成方式
- 改变参数绑定策略
- 自定义占位符解析规则
配置方式:
<!-- 全局配置 --><settings><settingname="defaultScriptingLanguage"value="com.example.JsonLanguageDriver"/></settings><!-- 或单个statement指定 --><selectid="findUsers"lang="json"resultType="User">{ "select": "SELECT * FROM user", "where": { "conditions": [ {"test": "name != null", "sql": "AND name = #{name}"}, {"test": "age != null", "sql": "AND age > #{age}"} ] } }</select>说明:以上解答提供了第9篇思考题的详细分析和代码示例。更多深入讨论和源码分析,请参考主文档或相关章节。