MyBatis 处理动态 SQL 的方式非常巧妙,它并没有直接在运行时去解析 XML 字符串,而是在初始化阶段就将动态 SQL 解析成了一个对象树结构,然后在运行时根据传入的参数去解释这个树结构,最终生成可执行的 SQL。
这个过程主要涉及以下几个关键组件和步骤:
-
解析阶段 (MyBatis 初始化时):
- 当 MyBatis 解析 Mapper XML 文件时,遇到包含动态标签(如
<if>
,<foreach>
,<where>
,<set>
,<trim>
,<choose>
等)的 SQL 语句时,它不会将整个 SQL 视为一个简单的字符串。 - 取而代之,MyBatis 会使用一系列的
SqlNode
实现类来解析这些动态标签和静态 SQL 片段。 - 每个动态标签和静态文本块都会被解析成一个对应的
SqlNode
对象(例如:<if>
对应IfSqlNode
,静态文本对应StaticTextSqlNode
,<foreach>
对应ForEachSqlNode
等)。 - 这些
SqlNode
对象会按照它们在 XML 中出现的顺序和嵌套关系,组成一个树形结构(或一个MixedSqlNode
包含的列表)。 - 这个最终的
SqlNode
树(或根节点)会被封装到一个DynamicSqlSource
对象中。 DynamicSqlSource
对象最终被设置到MappedStatement
中。
- 当 MyBatis 解析 Mapper XML 文件时,遇到包含动态标签(如
-
运行阶段 (执行 Mapper 方法时):
- 当调用 Mapper 接口的方法,需要执行对应的动态 SQL 时,MyBatis 会从
MappedStatement
中获取到DynamicSqlSource
。 - 调用
DynamicSqlSource
的getBoundSql(parameterObject)
方法。 getBoundSql
方法内部会创建一个DynamicContext
对象。这个DynamicContext
主要用于:- 存储传入的参数对象。
- 提供一个StringBuilder (
sqlBuilder
) 来逐步构建最终的 SQL 字符串。 - 管理参数绑定(例如,在
<foreach>
中生成的临时变量)。
- 接着,会调用根
SqlNode
的apply(DynamicContext context)
方法。 apply
方法是SqlNode
接口的核心。每个具体的SqlNode
实现类会根据自己的逻辑来执行apply
:StaticTextSqlNode
: 直接将它代表的静态 SQL 文本追加到DynamicContext
的sqlBuilder
中。IfSqlNode
: 使用 OGNL (Object-Graph Navigation Language) 表达式引擎,根据DynamicContext
中的参数对象来评估test
属性中的条件。如果条件为真,则递归调用其内部包含的子SqlNode
的apply
方法。ForEachSqlNode
: 同样使用 OGNL 获取要迭代的集合,然后循环遍历集合。在每次迭代中,将当前项和索引(如果需要)设置到DynamicContext
中,并递归调用其内部包含的子SqlNode
的apply
方法。它还会处理open
,close
,separator
属性。WhereSqlNode
,SetSqlNode
,TrimSqlNode
: 这些节点比较特殊,它们会先调用其内部子节点的apply
方法,然后对DynamicContext
中已经生成的 SQL 进行后处理,例如智能地添加WHERE
或SET
关键字,并移除多余的AND
,OR
或逗号。MixedSqlNode
: 依次调用其包含的所有子SqlNode
的apply
方法。
- 当根
SqlNode
的apply
方法执行完毕后,DynamicContext
中的sqlBuilder
就包含了根据传入参数动态生成的最终 SQL 字符串。 DynamicSqlSource
最后将生成的 SQL 字符串和对应的参数映射信息封装成一个BoundSql
对象返回。
- 当调用 Mapper 接口的方法,需要执行对应的动态 SQL 时,MyBatis 会从
用到的主要设计模式:
MyBatis 在处理动态 SQL 时,巧妙地运用了以下几种设计模式:
-
组合模式 (Composite Pattern):
- 这是最核心的模式。
SqlNode
接口及其各种实现类(StaticTextSqlNode
,IfSqlNode
,MixedSqlNode
等)构成了典型的组合模式。 SqlNode
定义了一个统一的操作接口apply(DynamicContext context)
。- 叶子节点(如
StaticTextSqlNode
)实现了这个接口,执行具体的操作(追加文本)。 - 容器节点(如
MixedSqlNode
,IfSqlNode
,ForEachSqlNode
)也实现了这个接口,但它们的实现通常是递归地调用其子节点的apply
方法。 - 这使得 MyBatis 可以一致地处理单个 SQL 片段和复杂的、嵌套的动态 SQL 结构。客户端(
DynamicSqlSource
)只需要调用根节点的apply
方法,整个树结构就会被处理。
- 这是最核心的模式。
-
解释器模式 (Interpreter Pattern):
- 动态 SQL 标签(
<if>
,<foreach>
等)可以看作是一种领域特定语言 (DSL),用于定义如何根据参数生成 SQL。 SqlNode
的实现类可以看作是这个 DSL 的解释器。每个节点类都知道如何解释(apply
)它所代表的特定语法元素(标签或文本)。DynamicContext
在解释过程中传递上下文信息(参数、正在构建的 SQL)。- 整个
SqlNode
树的apply
过程就是对这个动态 SQL 语言进行解释执行的过程。
- 动态 SQL 标签(
-
构建器模式 (Builder Pattern):
- 虽然运行时 SQL 的构建主要是通过
DynamicContext
中的StringBuilder
累加完成的,但在 MyBatis 解析 XML 并创建MappedStatement
的过程中,大量使用了构建器模式。例如,MappedStatement.Builder
,SqlSourceBuilder
等,它们用于逐步构建复杂的配置对象。 - 在动态 SQL 处理的解析阶段,构建器模式帮助将 XML 配置信息逐步构建成
SqlNode
树和DynamicSqlSource
对象。
- 虽然运行时 SQL 的构建主要是通过
-
责任链模式 (Chain of Responsibility Pattern) (某种程度上):
- 虽然不是严格的责任链模式,但像
WhereSqlNode
,SetSqlNode
,TrimSqlNode
这样的节点,它们在子节点处理完(调用完子节点的apply
)之后,会对自己和子节点生成的 SQL 进行后处理。这有点类似于责任链中的节点在处理请求后,还可以对结果进行修改或传递。它们负责处理 SQL 拼接中的特定问题(如多余的连接符)。
- 虽然不是严格的责任链模式,但像
-
上下文对象模式 (Context Object Pattern):
DynamicContext
对象就是典型的上下文对象。它封装了在SqlNode
树的apply
方法调用链中需要共享的状态和数据(参数、SQL 构建器、参数绑定),避免了在方法间传递大量参数。
总结:
MyBatis 通过组合模式将动态 SQL 解析成 SqlNode
树,然后利用解释器模式在运行时根据 DynamicContext
(上下文对象)来解释这棵树,动态地生成最终的 SQL。构建器模式则在初始解析阶段发挥作用。这种设计使得动态 SQL 的处理逻辑清晰、结构化且易于扩展。