【文章较长,建议收藏】本文解析Mybatis动态sql源码以及动态sql的执行原理
Mybatis系列文章:
【深度好文】谈谈你对MyBatis的理解:MyBatis整体架构
【源码解析】谈谈你对 MyBatis工作原理 的理解 : MyBatis 工作流程源码解析
【源码解析】谈谈你对 MyBatis结果集映射和参数绑定 的理解:MyBatis结果集映射源码解析,详细分析了 handleRowValuesForSimpleResultMap() 等方法实现映射的核心步骤
目录
动态 SQL 语句解析
这部分我们要知道 Mybatis动态sql是做什么的?都有哪些动态sql?动态sql的执行原理?
Mybatis动态sql是做什么的?都有哪些动态sql?
Mybatis动态sql可以让我们在 xml 映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种 动态sql标签 trim、where、set、foreach、if、choose、when、otherwise、bind。
动态sql的执行原理
执行原理,MyBatis 会将 Mapper 映射文件中定义的 SQL 语句解析成 SqlSource 对象,其中的动态标签、SQL 语句文本等,会解析成对应类型的 SqlNode 对象。包含了 “${}”占位符的动态 SQL 片段;动态 SQL 语句中使用到了 OGNL 表达式,读写 JavaBean 属性值、执行 JavaBean 方法这两个基础功能。
OGNL 表达式语言
OGNL 表达式是相对完备的一门表达式语言,我们可以通过 “对象变量名称.方法名称(或属性名称)” 调用一个 JavaBean 对象的方法(或访问其属性),还可以通过 “@[类的完全限定名]@[静态方法(或静态字段)]” 调用一个 Java 类的静态方法(或访问静态字段)。
MyBatis 为了提高 OGNL 表达式的工作效率,添加了一层 OgnlCache 来缓存表达式编译之后的结果(不是表达式的执行结果),OgnlCache 通过一个 ConcurrentHashMap<String, Object> 类型的集合(expressionCache 字段,静态字段)来记录OGNL 表达式编译之后的结果。通过缓存拿到表达式编译的结果之后,OgnlCache 底层还会依赖上述示例中的 OGNL 工具类以及 OgnlContext 完成表达式的执行。
DynamicContext 源码解析
在 MyBatis 解析一条动态 SQL 语句的时候,整个流程非常长,其中涉及多层方法的调用、方法的递归、复杂的循环等,其中产生的中间结果需要有一个地方进行存储,那就是 DynamicContext 上下文对象。
DynamicContext 中有两个核心属性:一个是 sqlBuilder 字段(StringJoiner 类型),用来记录解析之后的 SQL 语句;另一个是 bindings 字段,用来记录上下文中的一些 KV 信息。
DynamicContext 定义了一个 ContextMap 内部类,ContextMap 用来记录运行时用户传入的、用来替换“#{}”占位符的实参。在 DynamicContext 构造方法中,会根据传入的实参类型决定如何创建对应的 ContextMap 对象,核心代码如下:
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
// 对于非Map类型的实参,会创建对应的MetaObject对象,并封装成ContextMap对象
MetaObject metaObject = configuration.newMetaObject(parameterObject);
boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
bindings = new ContextMap(metaObject, existsTypeHandler);
} else {
// 对于Map类型的实参,这里会创建一个空的ContextMap对象
bindings = new ContextMap(null, false);
}
// 这里实参对应的Key是_parameter
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
读取这个 ContextMap 的地方主要是在 OGNL 表达式中,也就是在 DynamicContext 中定义了一个静态代码块,指定了 OGNL 表达式读写 ContextMap 集合的逻辑,这部分读取逻辑封装在 ContextAccessor 中。
public class DynamicContext {
public static final String PARAMETER_OBJECT_KEY = "_parameter";
static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}
static class ContextAccessor implements PropertyAccessor {
@Override
public Object getProperty(Map context, Object target, Object name)
throws OgnlException {
Map map = (Map) target;
// getProperty() 方法会将传入的 target 参数(实际上就是 ContextMap)转换为 Map
// 并先尝试按照 Map 规则进行查找
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}
// 查找失败之后,会尝试获取“_parameter”对应的 parameterObject 对象
// 从 parameterObject 中获取指定的 Value 值。
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map)parameterObject).get(name);
}
return null;
}
}
SqlNode 源码解析
在 MyBatis 处理动态 SQL 语句的时候,会将动态 SQL 标签解析为 SqlNode 对象,多个 SqlNode 对象就是通过组合模式组成树形结构供上层使用的。
public interface SqlNode {
// apply()方法会根据用户传入的实参,解析该SqlNode所表示的动态SQL内容并
// 将解析之后的SQL片段追加到DynamicContext.sqlBuilder字段中暂存。
// 当SQL语句中全部的动态SQL片段都解析完成之后,就可以从DynamicContext.sqlBuilder字段中
// 得到一条完整的、可用的SQL语句了
boolean apply(DynamicContext context);
}
TrimSqlNode 对应 MyBatis 动态 SQL 语句中的 <trim> 标签;ChooseSqlNode在 MyBatis 的动态 SQL 语句中,我们可以使用 <choose>
、<when>
和 <otherwise>
三个标签来实现类似的效果;ForeachSqlNode 对应 <foreach>
标签对一个集合进行迭代;VarDeclSqlNode 抽象了 <bind>
标签;<choose>
标签会被 MyBatis 解析成 ChooseSqlNode 对象<otherwise>
标签会被解析成 MixedSqlNode 对象。StaticTextSqlNode 用于表示非动态的 SQL 片段;TextSqlNode 实现抽象了包含 “${}”占位符的动态 SQL 片段;IfSqlNode 实现类对应了动态 SQL 语句中的 <if>
、<when>
标签
本部分说明了动态 SQL 语句中使用的 OGNL 表达式语言,动态 SQL 语句中占位符的处理逻辑。分析了 DynamicContext 对象源码,其中维护了解析动态 SQL 语句上下文信息;随后我还分析了组合模式,因为它是 MyBatis 组合各动态 SQL 节点的设计思想。