网站建设 电子商务网站开发玉林做绿化苗木网站的是哪个单位
网站建设 电子商务网站开发,玉林做绿化苗木网站的是哪个单位,网站cms系统 开源框架,网站修改标题有影响吗第10篇#xff1a;动态SQL实现原理详解
前言
在前面的章节中#xff0c;我们学习了 MyBatis 的核心执行流程#xff0c;特别是第9篇中详细介绍了 MappedStatement 和 SqlSource 的作用。你可能注意到#xff0c;MyBatis 能够根据不同的条件动态生成 SQL#xff0c;这正是 …第10篇动态SQL实现原理详解前言在前面的章节中我们学习了 MyBatis 的核心执行流程特别是第9篇中详细介绍了MappedStatement和SqlSource的作用。你可能注意到MyBatis 能够根据不同的条件动态生成 SQL这正是 MyBatis 最强大的特性之一 ——动态SQL。动态SQL 允许我们根据不同的条件、参数来拼接不同的 SQL 语句避免了在 Java 代码中进行繁琐的字符串拼接。理解动态SQL的实现原理是掌握 MyBatis 高级用法的关键。本篇在整体架构中的位置配置解析第2篇MappedStatement第9篇动态SQL解析本篇核心SqlSource生成BoundSql获取StatementHandler第6篇执行SQL与前序章节的关联第2篇(配置系统)学习了 XML 解析本篇将深入了解动态标签如何被解析第9篇(MappedStatement)学习了SqlSource的类型本篇将详细分析DynamicSqlSource的实现第6篇(StatementHandler)学习了如何执行 SQL本篇将了解动态 SQL 如何生成最终的可执行 SQL1. 学习目标确认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 核心组件关系图持有使用«interface»SqlSourcegetBoundSql(Object) : BoundSqlDynamicSqlSource-SqlNode rootSqlNode-Configuration configurationgetBoundSql(Object) : BoundSql«interface»SqlNodeapply(DynamicContext) : booleanDynamicContext-MapString,Object bindings-StringBuilder sqlBuilderappendSql(String)getBindings() : MapgetSql() : StringMixedSqlNode-ListSqlNode contentsapply(DynamicContext) : booleanIfSqlNode-ExpressionEvaluator evaluator-String test-SqlNode contentsapply(DynamicContext) : booleanForEachSqlNode-String collection-String item-String index-SqlNode contentsapply(DynamicContext) : boolean2.2 核心组件职责组件职责关键方法SqlSourceSQL来源接口getBoundSql(Object)DynamicSqlSource动态SQL实现运行期生成SQLSqlNodeSQL节点接口apply(DynamicContext)DynamicContext动态上下文收集SQL片段和参数ExpressionEvaluator表达式求值器使用OGNL求值SqlSourceBuilderSQL源构建器解析#{}占位符2.3 执行流程时序图MappedStatementDynamicSqlSourceDynamicContextSqlNode树SqlSourceBuilderBoundSqlgetBoundSql(parameterObject)new DynamicContext(config, param)rootSqlNode.apply(context)求值表达式(OGNL)appendSql(片段)bind(临时变量)loop[遍历SqlNode树]SQL片段收集完成parse(sql, paramType, bindings)解析StaticSqlSourcesqlSource.getBoundSql(param)复制 additionalParametersBoundSqlMappedStatementDynamicSqlSourceDynamicContextSqlNode树SqlSourceBuilderBoundSql3. 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 userMixedSqlNode混合节点包含多个子节点select的子内容IfSqlNodeif条件判断if testname ! nullTrimSqlNodetrim去除前后缀trim prefixWHEREWhereSqlNodewhereWHERE子句处理whereSetSqlNodesetSET子句处理setForEachSqlNodeforeach遍历集合foreach collectionidsChooseSqlNodechoose多分支选择choosewhenVarDeclSqlNodebind绑定变量bind namepattern value% name %/3.3 StaticTextSqlNode最简单的实现/** * 静态文本SQL节点 * 直接输出文本内容不做任何处理 * * 源码位置: org.apache.ibatis.scripting.xmltags.StaticTextSqlNode */publicclassStaticTextSqlNodeimplementsSqlNode{privatefinalStringtext;publicStaticTextSqlNode(Stringtext){this.texttext;}Overridepublicbooleanapply(DynamicContextcontext){// 直接将文本添加到上下文context.appendSql(text);returntrue;}}示例selectidfindAllresultTypeUserSELECT * FROM user/select解析后会生成一个StaticTextSqlNode包含文本SELECT * FROM user。3.4 MixedSqlNode组合多个子节点/** * 混合SQL节点 * 包含多个子节点按顺序应用 * * 源码位置: org.apache.ibatis.scripting.xmltags.MixedSqlNode */publicclassMixedSqlNodeimplementsSqlNode{privatefinalListSqlNodecontents;publicMixedSqlNode(ListSqlNodecontents){this.contentscontents;}Overridepublicbooleanapply(DynamicContextcontext){// 遍历所有子节点依次应用contents.forEach(node-node.apply(context));returntrue;}}示例selectidfindByNameresultTypeUserSELECT * FROM userwhereiftestname ! nullAND 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.testtest;this.contentscontents;this.evaluatornewExpressionEvaluator();}Overridepublicbooleanapply(DynamicContextcontext){// 1. 使用OGNL求值表达式if(evaluator.evaluateBoolean(test,context.getBindings())){// 2. 条件为true应用子节点contents.apply(context);returntrue;}returnfalse;}}使用示例selectidfindUsersresultTypeUserSELECT * FROM user WHERE 11iftestname ! null and name !AND name #{name}/ififtestage ! nullAND age #{age}/if/select运行期行为// 场景1: 只传 nameMapString,ObjectparamsnewHashMap();params.put(name,张三);// 生成SQL: SELECT * FROM user WHERE 11 AND name ?// 场景2: 传 name 和 ageparams.put(name,张三);params.put(age,18);// 生成SQL: SELECT * FROM user WHERE 11 AND name ? AND age ?4.2 ChooseSqlNode多分支选择/** * CHOOSE多分支SQL节点 * 类似Java的 switch-case * * 源码位置: org.apache.ibatis.scripting.xmltags.ChooseSqlNode */publicclassChooseSqlNodeimplementsSqlNode{privatefinalSqlNodedefaultSqlNode;// otherwiseprivatefinalListSqlNodeifSqlNodes;// when 列表publicChooseSqlNode(ListSqlNodeifSqlNodes,SqlNodedefaultSqlNode){this.ifSqlNodesifSqlNodes;this.defaultSqlNodedefaultSqlNode;}Overridepublicbooleanapply(DynamicContextcontext){// 1. 遍历 when 节点找到第一个满足条件的for(SqlNodesqlNode:ifSqlNodes){if(sqlNode.apply(context)){returntrue;}}// 2. 如果都不满足应用 otherwiseif(defaultSqlNode!null){defaultSqlNode.apply(context);returntrue;}returnfalse;}}使用示例selectidfindUsersresultTypeUserSELECT * FROM userwherechoosewhentestname ! nullAND name #{name}/whenwhentestemail ! nullAND email #{email}/whenotherwiseAND status ACTIVE/otherwise/choose/where/select5. WHERE/SET 子句处理5.1 TrimSqlNode通用的前后缀处理/** * TRIM修剪SQL节点 * 可以去除前后缀和多余的分隔符 * * 源码位置: org.apache.ibatis.scripting.xmltags.TrimSqlNode */publicclassTrimSqlNodeimplementsSqlNode{privatefinalSqlNodecontents;privatefinalStringprefix;// 要添加的前缀privatefinalStringsuffix;// 要添加的后缀privatefinalListStringprefixesToOverride;// 要去除的前缀列表privatefinalListStringsuffixesToOverride;// 要去除的后缀列表privatefinalConfigurationconfiguration;Overridepublicbooleanapply(DynamicContextcontext){// 1. 创建过滤上下文FilteredDynamicContextfilteredDynamicContextnewFilteredDynamicContext(context);// 2. 应用子节点收集SQL片段booleanresultcontents.apply(filteredDynamicContext);// 3. 处理前后缀filteredDynamicContext.applyAll();returnresult;}/** * 过滤动态上下文 * 负责去除多余的前后缀 */privateclassFilteredDynamicContextextendsDynamicContext{privatefinalStringBuildersqlBuffer;publicvoidapplyAll(){StringtrimmedSqlsqlBuffer.toString().trim();if(trimmedSql.length()0){// 去除要移除的前缀trimmedSqlapplyPrefixes(trimmedSql);// 去除要移除的后缀trimmedSqlapplySuffixes(trimmedSql);// 添加新前缀if(prefix!null){trimmedSqlprefixtrimmedSql;}// 添加新后缀if(suffix!null){trimmedSqltrimmedSqlsuffix;}delegate.appendSql(trimmedSql);}}privateStringapplyPrefixes(Stringsql){StringupperSqlsql.toUpperCase(Locale.ENGLISH);for(StringtoRemove:prefixesToOverride){if(upperSql.startsWith(toRemove.toUpperCase(Locale.ENGLISH))){returnsql.substring(toRemove.length());}}returnsql;}privateStringapplySuffixes(Stringsql){StringupperSqlsql.toUpperCase(Locale.ENGLISH);for(StringtoRemove:suffixesToOverride){if(upperSql.endsWith(toRemove.toUpperCase(Locale.ENGLISH))){returnsql.substring(0,sql.length()-toRemove.length());}}returnsql;}}}使用示例selectidfindUsersresultTypeUserSELECT * FROM usertrimprefixWHEREprefixOverridesAND |ORiftestname ! nullAND name #{name}/ififtestage ! nullAND 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 WhereSqlNodeWHERE 子句简化/** * WHERE子句SQL节点 * 本质是 TrimSqlNode 的特例 * * 源码位置: org.apache.ibatis.scripting.xmltags.WhereSqlNode */publicclassWhereSqlNodeextendsTrimSqlNode{privatestaticListStringprefixListArrays.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 --whereiftestname ! nullAND name #{name}/if/where!-- 等价于 trim --trimprefixWHEREprefixOverridesAND |ORiftestname ! nullAND name #{name}/if/trim5.3 SetSqlNodeSET 子句简化/** * SET子句SQL节点 * 用于UPDATE语句自动去除最后的逗号 * * 源码位置: org.apache.ibatis.scripting.xmltags.SetSqlNode */publicclassSetSqlNodeextendsTrimSqlNode{privatestaticfinalListStringCOMMACollections.singletonList(,);publicSetSqlNode(Configurationconfiguration,SqlNodecontents){super(configuration,contents,SET,COMMA,null,COMMA);}}使用示例updateidupdateUserparameterTypeUserUPDATE usersetiftestname ! nullname #{name},/ififtestemail ! nullemail #{email},/ififtestage ! nullage #{age},/if/setWHERE 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){MapString,Objectbindingscontext.getBindings();// 1. 求值集合表达式finalIterable?iterableevaluator.evaluateIterable(collectionExpression,bindings,Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));if(iterablenull||!iterable.iterator().hasNext()){returntrue;}booleanfirsttrue;// 2. 添加开始字符applyOpen(context);inti0;for(Objecto:iterable){DynamicContextoldContextcontext;// 3. 添加分隔符if(first){first!separator.isEmpty();}else{applySeparator(context);}intuniqueNumbercontext.getUniqueNumber();// 4. 绑定迭代变量if(oinstanceofMap.Entry){SuppressWarnings(unchecked)Map.EntryObject,ObjectmapEntry(Map.EntryObject,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){contextoldContext;}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_PREFIXitem_i;}/** * 过滤动态上下文 * 将 #{item} 替换为 #{__frch_item_0} */privatestaticclassFilteredDynamicContextextendsDynamicContext{privatefinalDynamicContextdelegate;privatefinalintindex;privatefinalStringitemIndex;privatefinalStringitem;OverridepublicvoidappendSql(Stringsql){GenericTokenParserparsernewGenericTokenParser(#{,},content-{StringnewContentcontent.replaceFirst(^\\s*item(?![^.,:\\s]),itemizeItem(item,index));if(itemIndex!nullnewContent.equals(content)){newContentcontent.replaceFirst(^\\s*itemIndex(?![^.,:\\s]),itemizeItem(itemIndex,index));}return#{newContent};});delegate.appendSql(parser.parse(sql));}}}6.2 ForEach 使用示例示例1IN 子句selectidfindByIdsresultTypeUserSELECT * FROM user WHERE id INforeachcollectionlistitemidopen(separator,close)#{id}/foreach/select执行过程// 输入参数ListLongidsArrays.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批量插入insertidbatchInsertparameterTypelistINSERT INTO user (name, email, age) VALUESforeachcollectionlistitemuserseparator,(#{user.name}, #{user.email}, #{user.age})/foreach/insert执行过程// 输入参数ListUserusersArrays.asList(newUser(张三,zhangexample.com,25),newUser(李四,liexample.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.namevar;this.expressionexp;}Overridepublicbooleanapply(DynamicContextcontext){// 1. 使用OGNL求值表达式ObjectvalueOgnlCache.getValue(expression,context.getBindings());// 2. 绑定到上下文context.bind(name,value);returntrue;}}7.2 使用示例selectidfindByNameresultTypeUser!-- 绑定模糊查询模式 --bindnamepatternvalue% name %/SELECT * FROM user WHERE name LIKE #{pattern}/select执行过程// 输入参数MapString,ObjectparamsnewHashMap();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收集器privateintuniqueNumber0;// 唯一编号生成器/** * 构造函数 */publicDynamicContext(Configurationconfiguration,ObjectparameterObject){// 1. 处理参数对象if(parameterObject!null!(parameterObjectinstanceofMap)){MetaObjectmetaObjectconfiguration.newMetaObject(parameterObject);booleanexistsTypeHandlerconfiguration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());bindingsnewContextMap(metaObject,existsTypeHandler);}else{bindingsnewContextMap(null,false);}// 2. 绑定内置参数bindings.put(PARAMETER_OBJECT_KEY,parameterObject);bindings.put(DATABASE_ID_KEY,configuration.getDatabaseId());this.sqlBuildernewStringBuilder();}/** * 获取绑定参数 */publicMapString,ObjectgetBindings(){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访问属性 */staticclassContextMapextendsHashMapString,Object{privatefinalMetaObjectparameterMetaObject;privatefinalbooleanfallbackParameterObject;publicContextMap(MetaObjectparameterMetaObject,booleanfallbackParameterObject){this.parameterMetaObjectparameterMetaObject;this.fallbackParameterObjectfallbackParameterObject;}OverridepublicObjectget(Objectkey){StringstrKey(String)key;// 1. 先从Map中查找if(super.containsKey(strKey)){returnsuper.get(strKey);}// 2. 从参数对象中获取属性值if(parameterMetaObjectnull){returnnull;}if(fallbackParameterObject!parameterMetaObject.hasGetter(strKey)){returnparameterMetaObject.getOriginalObject();}else{returnparameterMetaObject.getValue(strKey);}}}}8.2 内置参数参数名说明使用场景_parameter原始参数对象访问整个参数对象_databaseId数据库厂商标识多数据库适配使用示例selectidfindUsersresultTypeUserSELECT * FROM userwhere!-- 使用 _databaseId 做数据库兼容 --iftest_databaseId mysqlAND created_at DATE_SUB(NOW(), INTERVAL 1 YEAR)/ififtest_databaseId oracleAND created_at ADD_MONTHS(SYSDATE, -12)/if!-- 使用 _parameter 访问整个参数对象 --iftest_parameter ! nullAND status ACTIVE/if/where/select9. OGNL 表达式求值9.1 ExpressionEvaluator 实现/** * 表达式求值器 * 使用OGNL进行表达式求值 * * 源码位置: org.apache.ibatis.scripting.xmltags.ExpressionEvaluator */publicclassExpressionEvaluator{/** * 求值布尔表达式 */publicbooleanevaluateBoolean(Stringexpression,ObjectparameterObject){ObjectvalueOgnlCache.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){ObjectvalueOgnlCache.getValue(expression,parameterObject);if(valuenull){if(nullable){returnnull;}else{thrownewBuilderException(The expression expression evaluated to a null value.);}}if(valueinstanceofIterable){return(Iterable?)value;}if(value.getClass().isArray()){// 数组转ListintsizeArray.getLength(value);ListObjectanswernewArrayList(size);for(inti0;isize;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 testname ! nullname ! null and name ! 非空非空串if testname ! null and name ! age 18数值比较if testage 18list ! null and list.size() 0集合非空if testlist ! null and list.size() 0user.name ! null嵌套属性if testuser.name ! nulljava.lang.Mathmax(a, b)静态方法调用bind namemax valuejava.lang.Mathmax(a, b)/10. DynamicSqlSource 完整流程10.1 源码实现/** * 动态SQL源 * 运行期根据参数动态生成SQL * * 源码位置: org.apache.ibatis.scripting.xmltags.DynamicSqlSource */publicclassDynamicSqlSourceimplementsSqlSource{privatefinalConfigurationconfiguration;privatefinalSqlNoderootSqlNode;publicDynamicSqlSource(Configurationconfiguration,SqlNoderootSqlNode){this.configurationconfiguration;this.rootSqlNoderootSqlNode;}OverridepublicBoundSqlgetBoundSql(ObjectparameterObject){// 1. 创建动态上下文DynamicContextcontextnewDynamicContext(configuration,parameterObject);// 2. 应用SqlNode树生成SQLrootSqlNode.apply(context);// 3. 使用SqlSourceBuilder解析 #{} 占位符SqlSourceBuildersqlSourceParsernewSqlSourceBuilder(configuration);Class?parameterTypeparameterObjectnull?Object.class:parameterObject.getClass();SqlSourcesqlSourcesqlSourceParser.parse(context.getSql(),parameterType,context.getBindings());// 4. 获取BoundSqlBoundSqlboundSqlsqlSource.getBoundSql(parameterObject);// 5. 复制额外参数context.getBindings().forEach((key,value)-{boundSql.setAdditionalParameter(key,value);});returnboundSql;}}10.2 完整执行示例publicclassDynamicSqlDemo{publicstaticvoidmain(String[]args)throwsException{// 1. 初始化MyBatisSqlSessionFactoryfactorynewSqlSessionFactoryBuilder().build(Resources.getResourceAsStream(mybatis-config.xml));Configurationconfigurationfactory.getConfiguration();// 2. 获取MappedStatementStringstatementIdcom.example.UserMapper.findUsers;MappedStatementmsconfiguration.getMappedStatement(statementId);// 3. 准备参数MapString,ObjectparamsnewHashMap();params.put(name,张三);params.put(age,25);params.put(ids,Arrays.asList(1L,2L,3L));System.out.println( 动态SQL生成过程 \n);// 4. 获取BoundSql触发动态SQL生成BoundSqlboundSqlms.getBoundSql(params);// 5. 打印结果System.out.println(生成的SQL:);System.out.println(boundSql.getSql());System.out.println(\n参数映射:);ListParameterMappingparameterMappingsboundSql.getParameterMappings();for(inti0;iparameterMappings.size();i){ParameterMappingpmparameterMappings.get(i);Stringpropertypm.getProperty();Objectvalue;if(boundSql.hasAdditionalParameter(property)){valueboundSql.getAdditionalParameter(property);System.out.println( [i] property value (额外参数));}else{valueparams.get(property);System.out.println( [i] property value (普通参数));}}// 6. 实际执行System.out.println(\n 执行查询 \n);try(SqlSessionsessionfactory.openSession()){ListObjectresultssession.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{id1, name张三, age25} User{id2, name张三, age26}11. 性能优化11.1 避免不必要的动态SQL!-- ❌ 不推荐简单查询使用动态SQL --selectidfindByIdresultTypeUserSELECT * FROM userwhereiftestid ! nullid #{id}/if/where/select!-- ✅ 推荐静态SQL --selectidfindByIdresultTypeUserSELECT * FROM user WHERE id #{id}/select11.2 合理使用where和trim!-- ❌ 不推荐手动处理WHERE --selectidfindUsersresultTypeUserSELECT * FROM user WHERE 11iftestname ! nullAND name #{name}/ififtestage ! nullAND age #{age}/if/select!-- ✅ 推荐使用 where 自动处理 --selectidfindUsersresultTypeUserSELECT * FROM userwhereiftestname ! nullAND name #{name}/ififtestage ! nullAND age #{age}/if/where/select11.3 大集合遍历优化!-- ❌ 不推荐单条SQL处理大量数据 --selectidfindByIdsresultTypeUserSELECT * FROM user WHERE id INforeachcollectionidsitemidopen(separator,close)#{id}/foreach/select!-- ✅ 推荐分批处理 --// Java代码分批publicListUserfindByIds(ListLongids){intbatchSize1000;ListUserresultsnewArrayList();for(inti0;iids.size();ibatchSize){intendMath.min(ibatchSize,ids.size());ListLongbatchids.subList(i,end);results.addAll(userMapper.findByIdsBatch(batch));}returnresults;}11.4 缓存动态SQL解析结果MyBatis 内部已经对SqlSource进行了缓存但要注意RawSqlSource构建期解析一次后续直接复用性能最优DynamicSqlSource每次执行都要重新解析性能较低性能对比// 测试代码BenchmarkMode(Mode.Throughput)Warmup(iterations3)Measurement(iterations5)publicclassSqlSourceBenchmark{BenchmarkpublicBoundSqltestRawSqlSource(){// 静态SQLSELECT * FROM user WHERE id #{id}returnrawMs.getBoundSql(Collections.singletonMap(id,1L));}BenchmarkpublicBoundSqltestDynamicSqlSource(){// 动态SQLif testid ! nullid #{id}/ifreturndynamicMs.getBoundSql(Collections.singletonMap(id,1L));}}/** * 测试结果ops/sec * * testRawSqlSource: 12,000,000 (快) * testDynamicSqlSource: 500,000 (慢24倍) */12. 调试与排查12.1 开启SQL日志settings!-- 开启标准输出日志 --settingnamelogImplvalueSTDOUT_LOGGING//settings12.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());// 获取BoundSqlBoundSqlboundSqlms.getBoundSql(parameterObject);// 打印生成的SQLSystem.out.println(\n生成的SQL:);System.out.println(boundSql.getSql());// 打印参数映射System.out.println(\n参数映射:);ListParameterMappingparameterMappingsboundSql.getParameterMappings();for(inti0;iparameterMappings.size();i){ParameterMappingpmparameterMappings.get(i);Stringpropertypm.getProperty();ObjectvaluegetParameterValue(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-{ObjectvalueboundSql.getAdditionalParameter(property);System.out.println( property value);});System.out.println(\n);}privatestaticObjectgetParameterValue(BoundSqlboundSql,ObjectparameterObject,Stringproperty){if(boundSql.hasAdditionalParameter(property)){returnboundSql.getAdditionalParameter(property);}if(parameterObjectnull){returnnull;}if(parameterObjectinstanceofMap){return((Map?,?)parameterObject).get(property);}MetaObjectmetaObjectMetaObject.forObject(parameterObject,newDefaultObjectFactory(),newDefaultObjectWrapperFactory(),newDefaultReflectorFactory());returnmetaObject.getValue(property);}}使用示例// 调试动态SQLDynamicSqlDebugger.debug(ms,params);12.4 常见问题排查问题1条件不生效!-- 错误字符串比较使用 --ifteststatus ACTIVEAND status #{status}/if!-- 正确字符串比较使用 equals 或单引号 --ifteststatus ACTIVEAND status #{status}/if!-- 或 --ifteststatus ! null and status.equals(ACTIVE)AND status #{status}/if问题2集合为空报错!-- 错误未判断集合是否为空 --foreachcollectionidsitemid#{id}/foreach!-- 正确添加判空条件 --iftestids ! null and ids.size() 0foreachcollectionidsitemidopen(separator,close)#{id}/foreach/if问题3参数名错误// 错误未使用 Param 注解ListUserfindUsers(Stringname,Integerage);// 参数名会是 param1, param2 或 arg0, arg1// 正确使用 Param 指定参数名ListUserfindUsers(Param(name)Stringname,Param(age)Integerage);13. 最佳实践13.1 动态SQL设计原则✅简单优先能用静态SQL就不用动态SQL✅条件前置把最可能过滤数据的条件放在前面✅合理嵌套避免过深的嵌套结构建议 3层✅参数验证使用Param明确参数名✅集合判空遍历前先判断集合非空✅性能测试对复杂动态SQL进行压测13.2 可维护性建议!-- ✅ 推荐使用 sql 片段复用 --sqliduserColumnsid, name, email, age, status, created_at, updated_at/sqlsqliduserWherewhereiftestname ! nullAND name #{name}/ififtestage ! nullAND age #{age}/ififteststatus ! nullAND status #{status}/if/where/sqlselectidfindUsersresultTypeUserSELECTincluderefiduserColumns/FROM userincluderefiduserWhere//selectselectidcountUsersresultTypelongSELECT COUNT(*) FROM userincluderefiduserWhere//select13.3 安全注意事项!-- ❌ 危险使用 ${} 可能导致SQL注入 --selectidfindUsersresultTypeUserSELECT * FROM user ORDER BY ${orderBy}/select!-- ✅ 安全使用枚举或白名单验证 --// Java代码验证publicListUserfindUsers(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、bindOGNL表达式用于条件判断和变量绑定额外参数机制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达到最优的缓存收益最佳实践查询语句!-- ✅ 开启二级缓存 --selectidfindByIdresultTypeUseruseCachetrueflushCacheRequiredfalseSELECT * FROM user WHERE id #{id}/select更新语句!-- ✅ 更新后刷新缓存 --updateidupdateUserflushCacheRequiredtrueUPDATE user SET name #{name} WHERE id #{id}/update统计查询!-- ✅ 实时统计不使用缓存 --selectidcountUsersresultTypelonguseCachefalseSELECT COUNT(*) FROM user/select策略组合场景useCacheflushCacheRequired说明基础查询truefalse使用缓存提升性能更新操作falsetrue不缓存更新后刷新命名空间缓存实时查询falsefalse不缓存保证实时性统计查询视情况false高频统计可缓存实时统计不缓存思考题2什么时候应该优先选择RawSqlSource而避免动态SQL有哪些折中方案选择 RawSqlSource 的场景✅ SQL结构固定只有参数值变化✅ 高并发热点查询✅ 对性能要求极高的场景✅ SQL逻辑简单不需要条件判断折中方案方案1拆分多个静态SQL!-- 代替复杂的动态SQL --selectidfindByNameresultTypeUserSELECT * FROM user WHERE name #{name}/selectselectidfindByAgeresultTypeUserSELECT * FROM user WHERE age #{age}/selectselectidfindByNameAndAgeresultTypeUserSELECT * FROM user WHERE name #{name} AND age #{age}/select方案2使用 SelectProvider 编程式构建publicclassUserSqlProvider{publicStringfindUsers(MapString,Objectparams){// 可以缓存构建结果StringcacheKeygenerateCacheKey(params);returnsqlCache.computeIfAbsent(cacheKey,k-buildSql(params));}}方案3限制动态范围!-- 只在必要的部分使用动态SQL --selectidfindUsersresultTypeUserSELECT * FROM user WHERE status ACTIVEiftestname ! nullAND name #{name}/if/select思考题3BoundSql.additionalParameters在哪些场景会出现它们的取值优先级如何影响参数绑定出现场景foreach遍历foreachcollectionidsitemid#{id}/foreach!-- 生成: __frch_id_0, __frch_id_1, ... --bind变量bindnamepatternvalue% name %/!-- 生成: pattern %张三% --嵌套查询传参associationpropertyusercolumnuser_idselectfindUserById/!-- column值作为额外参数传递 --取值优先级// 优先级额外参数 原始参数Objectvalue;StringpropertyparameterMapping.getProperty();if(boundSql.hasAdditionalParameter(property)){// 优先级1额外参数valueboundSql.getAdditionalParameter(property);}elseif(parameterObjectnull){// 优先级2空值valuenull;}elseif(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())){// 优先级3基本类型valueparameterObject;}else{// 优先级4复杂对象属性MetaObjectmetaObjectconfiguration.newMetaObject(parameterObject);valuemetaObject.getValue(property);}思考题4如何利用resultSets正确处理存储过程返回的多个结果集示例存储过程CREATEPROCEDUREgetUserAndOrders(INuserIdBIGINT)BEGIN-- 第一个结果集用户信息SELECT*FROMuserWHEREiduserId;-- 第二个结果集订单信息SELECT*FROMorderWHEREuser_iduserId;ENDMyBatis配置selectidgetUserAndOrdersstatementTypeCALLABLEresultSetsusers,orders{call getUserAndOrders(#{userId})}/selectResultMap配置!-- 第一个结果集 --resultMapiduserMaptypeUseridpropertyidcolumnid/resultpropertynamecolumnname//resultMap!-- 第二个结果集 --resultMapidorderMaptypeOrderidpropertyidcolumnid/resultpropertyorderNocolumnorder_no//resultMap处理逻辑// ResultSetHandler按照resultSets顺序处理ListObjectresultsnewArrayList();// 1. 处理第一个结果集 (users)ResultSetrs1stmt.getResultSet();ListUserusershandleResultSet(rs1,userMap);results.add(users);// 2. 移动到下一个结果集stmt.getMoreResults();// 3. 处理第二个结果集 (orders)ResultSetrs2stmt.getResultSet();ListOrderordershandleResultSet(rs2,orderMap);results.add(orders);思考题5如果要自定义LanguageDriver它会如何影响MappedStatement的构建与运行期行为自定义示例/** * 自定义语言驱动 * 支持JSON格式的动态SQL */publicclassJsonLanguageDriverimplementsLanguageDriver{OverridepublicSqlSourcecreateSqlSource(Configurationconfiguration,Stringscript,Class?parameterType){// 1. 解析JSON格式的SQL定义JSONObjectjsonJSON.parseObject(script);// 2. 构建自定义的SqlNode树SqlNoderootNodeparseJsonToSqlNode(json);// 3. 返回自定义SqlSourcereturnnewJsonDynamicSqlSource(configuration,rootNode);}privateSqlNodeparseJsonToSqlNode(JSONObjectjson){// 解析逻辑...returnnewMixedSqlNode(nodes);}}影响范围构建期影响改变SQL的解析方式XML/注解/JSON/其他格式自定义SqlNode的生成逻辑控制SqlSource的类型选择运行期影响影响BoundSql的生成方式改变参数绑定策略自定义占位符解析规则配置方式!-- 全局配置 --settingssettingnamedefaultScriptingLanguagevaluecom.example.JsonLanguageDriver//settings!-- 或单个statement指定 --selectidfindUserslangjsonresultTypeUser{ select: SELECT * FROM user, where: { conditions: [ {test: name ! null, sql: AND name #{name}}, {test: age ! null, sql: AND age #{age}} ] } }/select说明以上解答提供了第9篇思考题的详细分析和代码示例。更多深入讨论和源码分析请参考主文档或相关章节。