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 的处理逻辑清晰、结构化且易于扩展。
