likes
comments
collection
share

MyBatis映射器文件解析:sql元素与静态SQL语句

作者站长头像
站长
· 阅读数 16

今天我们接着来学习 MyBatis 应用程序初始化过程中的源码,本文主要的内容是 sql 元素的解析和 MyBatis 映射器中静态 SQL 语句的解析。

Tips:在 MyBatis 中我们可以将不含有 trim 元素,where 元素,set 元素,foreach 元素,if 元素,choose 元素,when 元素等元素的 SQL 语句认为是静态 SQL 语句。

解析 sql 元素

首先我们来看解析 sql 元素的部分,在 XMLMapperBuilder 中,解析 sql 元素调用的是 XMLMapperBuilder#sqlElement 方法,源码如下:

private void sqlElement(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    // 获取 databaseId
    String databaseId = context.getStringAttribute("databaseId");
    // 获取 sql 元素的 id
    String id = context.getStringAttribute("id");
    // 将 sql 元素的 id 与namesapce 组装起来
    id = builderAssistant.applyCurrentNamespace(id, false);
    // 检测 sql 元素的 datebaseid 与 Configuration 实例中的 databaseId 是否一致
    if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
	  // 将 sql 元素的解析结果保存到 XMLMapperBuilder 实例的 sqlFragments 中
      sqlFragments.put(id, context);
    }
  }
}

XMLMapperBuilder#sqlElement 方法的整体逻辑非常简单,主要的功能是解析出 sql 元素之间的内容,并存储到 Configuration 实例中

不过有一点需要注意下,在 XMLMapperBuilder#sqlElement 方法中,sql 元素解析后的结果看似是只保存到了 XMLMapperBuilder 实例的 sqlFragments 字段中,但实际上也是保存到了 Configuration 实例的 sqlFragments 字段中。

这是因为在创 XMLMapperBuilder 实例时,传入了 Configuration 实例的 sqlFragments 字段,并将 XMLMapperBuilder 实例的 sqlFragments 字段指向了 Configuration 实例的 sqlFragments 字段,因此无论是 Configuration 实例的 sqlFragments 字段还是 XMLMapperBuilder 实例的 sqlFragments 字段指向的都是同一个 Map 实例(sqlFragments 是 Map 类型)。

创建 XMLMapperBuilder 实例的 逻辑是在 XMLConfigBuilder#mappersElement 方法中,源码如下:

private void mappersElement(XNode context) throws Exception {
	// 省略
	XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
	// 省略
}

XMLMapperBuilder 的构造方法:

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()), configuration, resource, sqlFragments);
}

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  super(configuration);
  this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
  this.parser = parser;
  this.sqlFragments = sqlFragments;
  this.resource = resource;
}

XMLMapperBuilder#buildStatementFromContext 方法分析

MyBatis 应用程序解析 SQL 语句(由 select 元素,insert 元素,update 元素和 delete 元素定义)调用的是 XMLMapperBuilder#buildStatementFromContext 方法,源码如下:

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  // 遍历 SQL 语句
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

与 MyBatis 映射器中其它元素的解析过程不同的是,SQL 语句的解析不再是由 XMLMapperBuilder 实例完成,而是通过构建 XMLStatementBuilder 实例,并调用 XMLStatementBuilder#parseStatementNode 方法解析。

第 3 行代码,循环 MyBatis 映射器中的所有 SQL 语句。

第 6 行代码,调用 XMLStatementBuilder#parseStatementNode 方法解析 SQL 语句,这是我们今天的重点内容之一,下面我们详细聊。

XMLStatementBuilder#parseStatementNode 方法分析

XMLStatementBuilder#parseStatementNode 方法是所有类型的 SQL 语句(静态 SQL 语句和动态 SQL 语句)的解析入口,该方法很长,不过内容并不复杂,我这里稍微做了一些修改并添加了注释,修改后源码如下:

public void parseStatementNode() {
  // 通过 context 解析 keyProperty 属性,keyColumn 属性,
  // resultSets 属性,parameterMap 属性,fetchSize 属性,
  // timeout 属性,dirtySelect 属性,resultOrdered 属性,
  // 实现非常简单,这里只展示解析 keyProperty 属性的过程
  String keyProperty = context.getStringAttribute("keyProperty");

  // 解析 id 属性和 databaseId 属性
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  // 获取 SQL 语句的类型(select 语句,insert 语句,update 语句和 delete 语句等)
  String nodeName = context.getNode().getNodeName();
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);

  // 解析 SQL 语句中通过 include 元素引入的 sql 元素定义的 SQL 语句片段
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  // 解析 parameterType 属性,lang 属性,selectKey 元素,略过
  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);
  processSelectKeyNodes(id, parameterTypeClass, langDriver);

  // 解析 insert 语句中的 useGeneratedKeys 属性,略过
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }

  // 创建 SqlSource 实例
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

  // 获取 statementType 属性
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));

  // 解析 resultType 属性
  String resultType = context.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  // 解析 reusltMap 属性
  String resultMap = context.getStringAttribute("resultMap");
  if (resultTypeClass == null && resultMap == null) {
    resultTypeClass = MapperAnnotationBuilder.getMethodReturnType(builderAssistant.getCurrentNamespace(), id);
  }
  // 解析 resultSetType 属性
  String resultSetType = context.getStringAttribute("resultSetType");
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
  }

  // 调用 MapperBuilderAssistant#addMappedStatement 创建 MappedStatement 实例并添加到 Configuration 实例中  
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout,  
                                      parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache,  
                                      resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}

注释中标记为略过的部分是在日常工作中基本不会使用到的配置,这部分感兴趣的可以自行阅读源码。

第 11 行代码,这里的条件语句用于判断当前 SQL 语句的 databaseId 是否与默认的数据库环境的 databaseId 一致,如果一致则继续进行解析,如果不一致则跳过。

第 18 行代码,标记当前 SQL 语句是否为 select 语句,通常情况下,select 类型的 SQL 语句只需要使用缓存,而不需要刷新缓存,因此在第 19 行代码使用 !isSelect 作为 flushCache 的默认值,第 20 行代码使用 isSelect 作为 useCache 的默认值。

第 24 行代码,将通过 include 元素引入的内容拼装到 SQL 语句中。

第 30 行代码,获取 LanguageDriver 实例,没有做任何配置的场景下 MyBatis 提供了两个默认的选项:XMLLanguageDriver 和 RawLanguageDriver:

  • XMLLanguageDriver 提供了通过 XML 元素结合 OGNL 表达式实现的动态 SQL 语句能力;
  • RawLanguageDriver 仅支持静态 SQL 语句配置,不支持动态 SQL 语句。

第 44 行代码,创建了 SqlSource 实例,下面我们会详细聊 SqlSource 的结构和 XMLLanguageDriver#createSqlSource 方法。

第 47 行代码,获取 SQL 语句的 StatementType,默认值为 StatementType.PREPARED,即 MyBatis 会为该 SQL 创建 PreparedStatement 实例。

第 65 行代码,调用 MapperBuilderAssistant#addMappedStatement 方法创建 MappedStatement 实例并添加到 Configuration 实例中。关于 MapperBuilderAssistant#addMappedStatement 方法和 MappedStatement 的结构,我们下面再来详细聊。

到这里,我们已经从整体上了解到了 XMLStatementBuilder#parseStatementNode 方法,该方法的主要功能就是解析 MyBatis 映射器中的 SQL 语句的配置,并将其生成为 MyBatis 应用程序内部可以使用的 MappedStatement 实例添加到 Configuration 实例中

SqlSource 的结构

SqlSource 是 MyBatiis 中用于表示 SQL 语句的接口,该接口的定义如下:

public interface SqlSource {
  BoundSql getBoundSql(Object parameterObject);
}

SqlSource 只提供了一个方法,该方法用于根据传入的参数,返回 BoundSql 实例。BoundSql 实例中存储着可用于执行的 SQL 语句,和相关的参数信息,关于 BoundSql 我们后面再聊。

接着来看 SqlSource 接口,它有 4 个实现类:

MyBatis映射器文件解析:sql元素与静态SQL语句

如果你是通过 MyBatis 的源码程序来看 SqlSource 的实现体系,你还可以发现另一个 SqlSource 的实现类 VelocitySqlSource,不过它只是 MyBatis 中的一个测试案例,并不是真正的实现。

  • DynamicSqlSource 负责处理动态 SQL 语句,即含有 if 元素,where 元素和 foreach 元素等元素的 SQL 语句;
  • RawSqlSource 负责处理静态 SQL 语句;
  • ProviderSqlSource 负责处理通过注解方式定义的 SQL 语句;
  • StaticSqlSource 用于存储 SQL 语句,RawSqlSource 会委派 StaticSqlSource 存储 SQL 语句(委派模式)。

由于今天我们只讲静态 SQL 语句的部分,因此只会涉及到 RawSqlSource 和 StaticSqlSource。

Tips:因为一旦要分析 DynamicSqlSource,就势必要讲 SqlNode 和 XMLScriptBuilder 的内部类 NodeHandler,而这块的内容也是非常多的,真不是我偷懒~~

XMLLanguageDriver#createSqlSource 方法分析

我们回到创建 SqlSource 实例的 XMLLanguageDriver#createSqlSource 方法中,源码如下:

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  return builder.parseScriptNode();
}

该方法的源码非常简单,我们直接来看 XMLScriptBuilder 和 XMLScriptBuilder#parseScriptNode 方法。

XMLScriptBuilder 的结构

XMLScriptBuilder 的声明与构造方法如下:

public class XMLScriptBuilder extends BaseBuilder {
  private final XNode context;
  private boolean isDynamic;
  private final Class<?> parameterType;
  private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

  public XMLScriptBuilder(Configuration configuration, XNode context) {
    this(configuration, context, null);
  }

  public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
    super(configuration);
    this.context = context;
    this.parameterType = parameterType;
    // 初始化用于处理动态 SQL 语句的 Map
    initNodeHandlerMap();
  }
}

可以看到 XMLScriptBuilder 依旧是继承 BaseBuilder,那么我们已经可以知道,XMLScriptBuilder 实例中会持有 Configuration 实例,以及别名注册器和类型处理器注册器。

另外,我们注意到 XMLScriptBuilder 中声明了 boolean 类型的变量 isDynamic,该变量用于标记 SQL 语句是否为动态 SQL 语句

XMLScriptBuilder#parseScriptNode 方法分析

接着我们来看 XMLScriptBuilder#parseScriptNode 方法,源码如下:

public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

第 2 行代码,调用 XMLScriptBuilder#parseDynamicTags 方法,该方法除了用于创建 MixedSqlNode 实例,还会标记该 SQL 语句是否为动态 SQL 语句,即修改 XMLScriptBuilder 的 isDynamic 变量,具体内容我们下文分析。

第 4 行代码,该条件语句根据 isDynamic 的取值,来决定生成 DynamicSqlSource 实例还是 RawSqlSource 实例。

XMLScriptBuilder#parseDynamicTags 方法分析

XMLScriptBuilder#parseDynamicTags 方法用于生成 MixedSqlNode 实例,同时修改 XMLScriptBuilder 的 isDynamic 变量,我这里简化下该方法的源码,如下:

protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<>();
  NodeList children = node.getNode().getChildNodes();
  for (int i = 0; i < children.getLength(); i++) {
    XNode child = node.newXNode(children.item(i));
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      // 处理静态 SQL 语句
      // 获取节点中的文本
      String data = child.getStringBody("");
      TextSqlNode textSqlNode = new TextSqlNode(data);
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {
        contents.add(new StaticTextSqlNode(data));
      }
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
      // 处理动态 SQL 语句
      String nodeName = child.getNode().getNodeName();  
      NodeHandler handler = nodeHandlerMap.get(nodeName);   
      handler.handleNode(child, contents);  
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}

第 6 行代码的条件语句中,判断当前 SQL 语句中的节点是否为动态 SQL 语句,这里是根据 java.xml.Node 节点的类型来判断的,我们可以统一认为 Node 节点的类型为 Node.CDATA_SECTION_NODE 或 Node.TEXT_NODE 时为静态 SQL 语句,类型为 Node.ELEMENT_NODE 时为动态 SQL 语句。

Tips:如果你不需要经常使用 java.xml,你就不要过度深入,否则会很痛苦。

但实际上是有例外的,在第 10 行到第 13 行的代码中,处理了一种特殊情况,当查询的列是动态组成时,会被认为是动态 SQL 语句,例如:

<select id="selectByUserId" resultMap="userMap">
  select ${column} from user where user_id = #{userId, jdbcType=INTEGER}
</select>

其中待查询的列由方法参数中传入,不过这种场景也相对少见。

第 9 行代码,获取节点中的文本,在第 15 行代码中创建 StaticTextSqlNode 实例存储节点中的文本,并将 StaticTextSqlNode 实例存储到 contents 中。

这里出现的 TextSqlNode 和 StaticTextSqlNode 虽然也是 SqlNode 的实现,但是整体结构都比较简单,如下:

public class TextSqlNode implements SqlNode {  
  private final String text;  
  private final Pattern injectionFilter;
}

public class StaticTextSqlNode implements SqlNode {  
  private final String text;
}

关于 TextSqlNode 和 StaticTextSqlNode 的具体实现我们放到下篇文章中一起分析。

第 25 行代码,使用存储 SqlNode 的容器 contents 构建 MixedSqlNode 实例,并返回。

构建 RawSqlSource 实例

上面我们分析了 XMLScriptBuilder#parseDynamicTags 方法,现在我们回到 XMLScriptBuilder#parseScriptNode 方法的第 7 行,构建 RawSqlSource 实例的部分。

RawSqlSource 的构造方法如下:

public class RawSqlSource implements SqlSource {

  private final SqlSource sqlSource;

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {  
    this(configuration, getSql(configuration, rootSqlNode), parameterType);  
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {  
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);  
    Class<?> clazz = parameterType == null ? Object.class : parameterType;  
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());  
  }
}

第 2 行代码中,调用了 RawSqlSource#getSql 方法,该方法遍历 XMLScriptBuilder#parseDynamicTags 方法创建的 MixedSqlNode 实例,并拼装 String 类型的 SQL 语句。

第 6 行代码创建了 SqlSourceBuilder 实例,SqlSourceBuilder 也是继承自 BaseBuilder,并且它本身并没有增加额外的成员变量,这里我们就不过多赘述了。

第 8 行代码,调用 SqlSourceBuilder#parse 方法创建 SqlSource 实例。

SqlSourceBuilder#parse 方法的源码如下:

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
  // 创建用于解析 ${} 的解析器
  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
  String sql;
  if (configuration.isShrinkWhitespacesInSql()) {
    sql = parser.parse(removeExtraWhitespaces(originalSql));
  } else {
   // 解析 ${},将 ${} 替换为占位符 ?
    sql = parser.parse(originalSql);
  }
  // 构建 StaticSqlSource 实例
  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

我们再结合 RawSqlSource 构造方法,可以看到,RawSqlSource 内部持有了 StaticSqlSource 用于存储 SQL 语句。

到这里我们就把静态 SQL 语句创建 SqlSource 实例的过程全部都分析完了,这部分内容整体上难度并不高。

MappedStatement 的结构

MappedStatement 的类型声明,源码如下:

public final class MappedStatement {
  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
  private boolean dirtySelect;
}

可以看到 MappedStatement 中的成员变量,与我们在 XMLStatementBuilder#parseStatementNode 方法解析和创建的变量是一致的。除此之外,MappedStatement 内部还声明了内部类 Builder,用于构建 MappedStatement 实例,又是典型的构造者模式,代码的部分比较简单,我们就直接跳过了。

MapperBuilderAssistant#addMappedStatement 方法分析

最后我们来看 MapperBuilderAssistant#addMappedStatement 方法,不分源码如下:

public MappedStatement addMappedStatement(String id,
                                          SqlSource sqlSource,
                                          StatementType statementType,
                                          SqlCommandType sqlCommandType,
                                          Integer fetchSize, 
                                          Integer timeout,
                                          String parameterMap,
                                          Class<?> parameterType,
                                          String resultMap, Class<?> resultType, 
                                          ResultSetType resultSetType,
                                          boolean flushCache,
                                          boolean useCache,
                                          boolean resultOrdered,
                                          KeyGenerator keyGenerator,
                                          String keyProperty,
                                          String keyColumn,
                                          String databaseId,
                                          LanguageDriver lang,
                                          String resultSets,
                                          boolean dirtySelect) {

  id = applyCurrentNamespace(id, false);
  // 创建 MappedStatement.Builder 实例
  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
    .resource(resource)
    .fetchSize(fetchSize)
    .timeout(timeout)
    .statementType(statementType)
    .keyGenerator(keyGenerator)
    .keyProperty(keyProperty)
    .keyColumn(keyColumn)
    .databaseId(databaseId)
    .lang(lang)
    .resultOrdered(resultOrdered)
    .resultSets(resultSets)
    .resultMaps(getStatementResultMaps(resultMap, resultType, id))
    .resultSetType(resultSetType)
    .flushCacheRequired(flushCache)
    .useCache(useCache)
    .cache(currentCache)
    .dirtySelect(dirtySelect);

  // 使用 MappedStatement.Builder 实例创建 MappedStatement 实例
  MappedStatement statement = statementBuilder.build();
  // 将 MappedStatement.Builder 实例添加到 Configuration 实例中
  configuration.addMappedStatement(statement);
  return statement;
}

该方法的逻辑非常简单,只是创建了 MappedStatement.Builder 实例,并使用 MappedStatement.Builder 实例创建 MappedStatement 实例,最后将 MappedStatement 实例添加到 Configuration 实例的 mappedStatements 字段中。

而 Configuration 实例的 mappedStatements 字段是 Map 类型,所以 Configuration#addMappedStatement 方法的实现也只是调用了 Map#put 方法而已。

到这里,关于 MyBatis 应用程序初始化过程中解析静态 SQL 语句所涉及到的关键方法我们就全部分析完毕了。下一篇文章中我们将会分析 XMLScriptBuilder#parseDynamicTags 方法中遗留的处理动态 SQL 语句的部分,并会详细介绍 SqlSource 和 SqlNode 的体系。


MyBatis映射器文件解析:sql元素与静态SQL语句

转载自:https://juejin.cn/post/7396931529638756361
评论
请登录