news 2026/1/9 23:46:52

【源码解读之 Mybatis】【核心篇】-- 第10篇:动态SQL实现原理详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【源码解读之 Mybatis】【核心篇】-- 第10篇:动态SQL实现原理详解

第10篇:动态SQL实现原理详解

前言

在前面的章节中,我们学习了 MyBatis 的核心执行流程,特别是第9篇中详细介绍了MappedStatementSqlSource的作用。你可能注意到,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 如何生成最终的可执行 SQL

1. 学习目标确认

1.0 第9篇思考题回顾

💡说明:第9篇思考题的详细解答请见文末附录A

核心要点回顾

  • MappedStatement是单条 SQL 映射语句的完整描述
  • SqlSource负责提供 SQL,分为静态和动态两大类
  • BoundSql包含最终可执行的 SQL 和参数映射
  • additionalParameters用于存储动态生成的临时参数
  • 缓存策略影响查询性能

1.1 本篇学习目标

  1. 深入理解动态SQL的核心组件:SqlNodeSqlSourceDynamicContext
  2. 掌握常用动态标签的实现原理:<if><where><foreach><choose>
  3. 理解 OGNL 表达式在动态SQL中的作用
  4. 掌握动态SQL的执行流程和性能优化
  5. 学会调试和排查动态SQL相关问题

2. 动态SQL核心架构

2.1 核心组件关系图

持有
使用
«interface»
SqlSource
+getBoundSql(Object) : BoundSql
DynamicSqlSource
-SqlNode rootSqlNode
-Configuration configuration
+getBoundSql(Object) : BoundSql
«interface»
SqlNode
+apply(DynamicContext) : boolean
DynamicContext
-Map<String,Object> bindings
-StringBuilder sqlBuilder
+appendSql(String)
+getBindings() : Map
+getSql() : String
MixedSqlNode
-List<SqlNode> contents
+apply(DynamicContext) : boolean
IfSqlNode
-ExpressionEvaluator evaluator
-String test
-SqlNode contents
+apply(DynamicContext) : boolean
ForEachSqlNode
-String collection
-String item
-String index
-SqlNode contents
+apply(DynamicContext) : boolean

2.2 核心组件职责

组件职责关键方法
SqlSourceSQL来源接口getBoundSql(Object)
DynamicSqlSource动态SQL实现运行期生成SQL
SqlNodeSQL节点接口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树SqlSourceBuilderBoundSql

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 断点调试位置

推荐断点位置

  1. DynamicSqlSource.getBoundSql(Object)- 动态SQL生成入口
  2. DynamicContext构造函数 - 查看初始参数绑定
  3. IfSqlNode.apply(DynamicContext)- 条件判断逻辑
  4. ForEachSqlNode.apply(DynamicContext)- 循环遍历逻辑
  5. SqlSourceBuilder.parse(...)-#{}占位符解析
  6. 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设计原则

  1. 简单优先:能用静态SQL就不用动态SQL
  2. 条件前置:把最可能过滤数据的条件放在前面
  3. 合理嵌套:避免过深的嵌套结构(建议<= 3层)
  4. 参数验证:使用@Param明确参数名
  5. 集合判空:遍历前先判断集合非空
  6. 性能测试:对复杂动态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. 小结

核心知识点

  1. 动态SQL体系SqlNodeDynamicContextDynamicSqlSourceBoundSql
  2. 常用标签<if><where><foreach><choose><bind>
  3. OGNL表达式:用于条件判断和变量绑定
  4. 额外参数机制<foreach><bind>生成的临时参数
  5. 性能优化:优先使用静态SQL,避免过度动态化

设计亮点

  • ✅ 灵活强大的SQL拼接能力
  • ✅ 清晰的节点树结构
  • ✅ 完善的表达式求值
  • ✅ 高效的参数绑定机制

注意事项

  • ⚠️ 动态SQL有性能开销,谨慎使用
  • ⚠️ 注意SQL注入风险(${}vs#{}
  • ⚠️ 集合遍历前要判空
  • ⚠️ 复杂动态SQL要充分测试

思考题

  1. 为什么 MyBatis 要设计SqlNode树状结构,而不是简单的模板字符串替换?
  2. <foreach>生成的额外参数(如__frch_id_0)为什么要用这种命名方式?直接用原参数有什么问题?
  3. 在什么场景下应该使用<bind>,而不是在 Java 代码中处理参数?
  4. 如何设计一个插件来缓存动态SQL的解析结果,以提升性能?
  5. 对于包含大量<if>条件的复杂动态SQL,如何进行性能优化?

💬 交流与讨论

感谢您阅读本篇文章!希望这篇深入解析能帮助您更好地理解 MyBatis 的动态SQL实现原理。

🤝 期待您的参与

在学习和实践过程中,您可能会遇到各种问题或有独特的见解,欢迎在评论区分享

💡 分享您的经验

  • 在实际项目中使用动态SQL的经验和技巧
  • 遇到的性能优化案例
  • 有趣的问题排查过程
  • 对动态SQL设计的理解

附录A:第9篇思考题详细解答

本附录提供第9篇《MappedStatement映射语句解析》思考题的详细解答。

思考题1:在读多写少的场景下,如何组合使用useCacheflushCacheRequired达到最优的缓存收益?

最佳实践

  1. 查询语句
<!-- ✅ 开启二级缓存 --><selectid="findById"resultType="User"useCache="true"flushCacheRequired="false">SELECT * FROM user WHERE id = #{id}</select>
  1. 更新语句
<!-- ✅ 更新后刷新缓存 --><updateid="updateUser"flushCacheRequired="true">UPDATE user SET name = #{name} WHERE id = #{id}</update>
  1. 统计查询
<!-- ✅ 实时统计不使用缓存 --><selectid="countUsers"resultType="long"useCache="false">SELECT COUNT(*) FROM user</select>

策略组合

场景useCacheflushCacheRequired说明
基础查询truefalse使用缓存,提升性能
更新操作falsetrue不缓存,更新后刷新命名空间缓存
实时查询falsefalse不缓存,保证实时性
统计查询视情况false高频统计可缓存,实时统计不缓存

思考题2:什么时候应该优先选择RawSqlSource而避免动态SQL?有哪些折中方案?

选择 RawSqlSource 的场景

  1. ✅ SQL结构固定,只有参数值变化
  2. ✅ 高并发热点查询
  3. ✅ 对性能要求极高的场景
  4. ✅ 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在哪些场景会出现?它们的取值优先级如何影响参数绑定?

出现场景

  1. <foreach>遍历
<foreachcollection="ids"item="id">#{id}</foreach><!-- 生成: __frch_id_0, __frch_id_1, ... -->
  1. <bind>变量
<bindname="pattern"value="'%'+ name +'%'"/><!-- 生成: pattern = '%张三%' -->
  1. 嵌套查询传参
<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;END

MyBatis配置

<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);}}

影响范围

  1. 构建期影响

    • 改变SQL的解析方式(XML/注解/JSON/其他格式)
    • 自定义SqlNode的生成逻辑
    • 控制SqlSource的类型选择
  2. 运行期影响

    • 影响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篇思考题的详细分析和代码示例。更多深入讨论和源码分析,请参考主文档或相关章节。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/5 13:44:39

传统修复 vs AI修复:DirectX问题处理效率提升300%

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个DirectX修复效率对比工具&#xff1a;1.传统修复流程模拟(手动下载、安装等) 2.AI修复流程实现 3.自动记录各步骤耗时 4.生成对比图表 5.支持导出测试报告。要求使用Python…

作者头像 李华
网站建设 2025/12/23 19:48:39

用HBuilderX快速原型设计:1小时打造产品Demo

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个电商APP的快速原型&#xff0c;包含商品列表、商品详情和购物车功能。使用HBuilderX和uni-app框架&#xff0c;要求界面美观&#xff0c;有基本的交互效果&#xff08;如点…

作者头像 李华
网站建设 2025/12/25 0:06:23

解锁Git高阶技能:Rebase、Stash与子模块的奇妙之旅

引言在当今软件开发的世界里&#xff0c;版本控制系统是开发者不可或缺的工具&#xff0c;而 Git 无疑是其中的佼佼者。它以强大的功能、高效的分布式特性以及丰富的命令集&#xff0c;成为了全球开发者首选的版本管理工具。无论是个人开发者在小型项目中的代码管理&#xff0c…

作者头像 李华
网站建设 2025/12/25 0:07:47

告别IDEA卡顿!全方位性能调优秘籍大放送

一、引言在软件开发的世界里&#xff0c;IntelliJ IDEA 凭借其强大的功能和丰富的插件生态&#xff0c;成为了众多开发者的首选集成开发环境。然而&#xff0c;随着项目规模的不断扩大以及对代码质量要求的日益提高&#xff0c;不少开发者都遭遇过 IDEA 卡顿的困扰。想象一下&a…

作者头像 李华
网站建设 2025/12/24 21:16:03

一文吃透!HTTPS之SSL/TLS握手全流程剖析

引言&#xff1a;为啥要懂 HTTPS 和 SSL/TLS 握手 在当今数字化浪潮席卷全球的时代&#xff0c;网络已然成为人们生活、工作和娱乐不可或缺的部分 。无论是日常使用的社交软件分享生活点滴&#xff0c;还是电商平台进行购物消费&#xff0c;亦或是企业通过网络开展业务、传输重…

作者头像 李华