【Mybatis】【配置文件解析】【四】Mybatis源码解析-mappers的解析三(sql、select、update、delete、insert)

1  前言

上一节我们分析了 resultMap 的解析,这节就该是我们的 sql 以及我们常写的 select、update、delete、insert。

贴一下我在调试中的XML哈,方便你们跟着调试:

<sql id="mySql">
  select * from ${tableName}
</sql>
<select id="getOne" resultMap="orderResultMap">
   <include refid="mySql">
     <property name="tableName" value="tc_order"/>
   </include>
   where order_no = 'xxx';
</select>

2  源码分析

2.1  解析 <sql> 节点

对于 sql 节点的信息比较简单我们直接看源码:

/**
 * 这个方法没做什么具体的操作,最后就是把sql节点的内容 放进了 sqlFragments 里
 * private final Map<String, XNode> sqlFragments;
 * @param list
 */
private void sqlElement(List<XNode> list) {
  // databaseId
  if (configuration.getDatabaseId() != null) {
    sqlElement(list, configuration.getDatabaseId());
  }
  sqlElement(list, null);
}
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    String databaseId = context.getStringAttribute("databaseId");
    String id = context.getStringAttribute("id");
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
      sqlFragments.put(id, context);
    }
  }
}

主要就是根据当前 configuration 的数据库厂商标志,匹配 sql 节点的厂商,能匹配到的话,就把 sql 节点的内容放进 sqlFragments 里保存了,就完事了,因为关于他的解析都在下边了哈,我们继续看。

2.2  解析 SQL 语句节点 buildStatementFromContext

接下来就到了我们平时用的最多的了,就是我们的增删改查的语句解析了,我们来看看究竟如何解析的,直接看:

private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    // 调用重载方法构建 Statement
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  // 调用重载方法构建 Statement,requiredDatabaseId 参数为空
  buildStatementFromContext(list, null);
}
 /**
  * 解析 statement
  * 这里就是 select 、update 、insert 、 delete 节点进来
  * 开始解析每个语句咯
  * @param list
  * @param requiredDatabaseId
  */
 private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
   for (XNode context : list) {
     // 创建解析对象
     final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
     try {
       // 开始解析
       statementParser.parseStatementNode();
     } catch (IncompleteElementException e) {
       /**
        * 报错的话把 statementParser 放进 configuration 的 incompleteStatements 里
        * protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
        */
       configuration.addIncompleteStatement(statementParser);
     }
   }
 }

上边都是调用重载,创建解析对象,调用解析方法那么重点就是这个解析方法了,我们看看:

/**
 * 看方法之前 先了解几个参数的来源 都代表着什么 来源是哪里
 * builderAssistant     创建XMLMapperBuilder是默认初始化的 new MapperBuilderAssistant(configuration, resource);
 * context              就是每个 增删改查节点的内容
 * requiredDatabaseId   configuration 上的数据库厂商
 */
public void parseStatementNode() {
  // 获取节点上的 id 、 databaseId
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");
  /**
   * databaseId 数据库厂商不匹配就不解析了
   * 比如你的是 mysql 可你的环境只配置了 pgsql 那么解析这个就没必要 因为就压根运行不到他 是不是
   */
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }
// nodeName 就是 select 、update 、insert 、 delete String nodeName = context.getNode().getNodeName(); // 包装成 SqlCommandType 类型,这是个枚举 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); // 判断当前是不是 select boolean isSelect = sqlCommandType == SqlCommandType.SELECT; // 获取 flushCache useCache resultOrdered 属性 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing // 处理 语句中包含 include 也就是引用了 sql 节点中的语句 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode());
// 获取参数类型 String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取 lang 属性管理语言的 // languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang);
// 解析 <selectKey> 节点 insert 和 update 有这个 processSelectKeyNodes(id, parameterTypeClass, langDriver); // 解析 SQL 语句 Parse the SQL (pre: <selectKey> and <include> were parsed and removed) KeyGenerator keyGenerator; // 命名 String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); // 获取 KeyGenerator 实例 if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; }
// 准备构建 MappedStatement 的属性值 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String resultType = context.getStringAttribute("resultType"); Class<?> resultTypeClass = resolveClass(resultType); String resultMap = context.getStringAttribute("resultMap"); String resultSetType = context.getStringAttribute("resultSetType"); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); if (resultSetTypeEnum == null) { resultSetTypeEnum = configuration.getDefaultResultSetType(); } String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); String resultSets = context.getStringAttribute("resultSets"); /* * 构建 MappedStatement 对象,并将该对象存储到 * Configuration 的 mappedStatements 集合中 */ builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }

上面的代码比较长,看起来有点复杂。大致的执行流程如下:

  1. 解析 <include> 节点
  2. 解析 <selectKey> 节点
  3. 解析 SQL,获取 SqlSource
  4. 构建 MappedStatement 实例

以上流程对应的代码比较复杂,每个步骤都能分析出一些东西来。下面我会每个步骤都进行分析,首先来分析 <include> 节点的解析过程。

2.2.1  解析 <include> 节点

<include> 节点的解析逻辑封装在 applyIncludes 中,该方法的代码如下:

public void applyIncludes(Node source) {
  Properties variablesContext = new Properties();
  // 把 configuration 中的变量值取出来赋值进去
  Properties configurationVariables = configuration.getVariables();
  Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
  applyIncludes(source, variablesContext, false);
}

可以看到 调用的重载的 applyIncludes 方法,那我们具体看下是如何解析 include 内容的:

/**
 * 分析 include 之前,先要知道 include 里都能有什么,我们拿个例子看
 * <sql id="mySql">
 *   select * from ${tableName}
 * </sql>
 * <select id="getOne" resultMap="orderResultMap">
 *    <include refid="mySql">
 *      <property name="tableName" value="tc_order"/>
 *    </include>
 *    where order_no = 'xxxx';
 * </select>
 * 这里挺有意思的
 * 这个我来画个图讲解一下这里的解析
 */
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
  // 如果是 include 节点
  if ("include".equals(source.getNodeName())) {
    /**
     * getStringAttribute(source, "refid") 就是获取 refid 的值
     * variablesContext 是 configuration 里的变量值
     * toInclude 就是已经获取到 include 的引用节点的 sql 的内容了
     */
    Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
    // 解析汇总变量值
    Properties toIncludeContext = getVariablesContext(source, variablesContext);
    /**
     * 递归调用
     * toInclude是你引用的 sql的内容
     * toIncludeContext 是变量值
     * sql 本身还能套 include 所以递归调
     * 那么这些都是在循环解析变量的值  什么时候覆盖呢?
     * 就在下边的 else 根据节点的类型递归赋值
     */
    applyIncludes(toInclude, toIncludeContext, true);
    /**
     * 如果 <sql> 和 <include> 节点不在一个文档中,
     * 则从其他文档中将 <sql> 节点引入到 <include> 所在文档中
     * 这个就不深究了都是 dom 的东西
     */
    if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
      toInclude = source.getOwnerDocument().importNode(toInclude, true);
    }
    /**
     * 下边这段主要就是节点内容的替换
     * 比如我们的 select 中的 include节点的内容就会被引用的 sql 替换掉
     */
    source.getParentNode().replaceChild(toInclude, source);
    while (toInclude.hasChildNodes()) {
      toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
    }
    toInclude.getParentNode().removeChild(toInclude);
  /**
   * 上边我们的示例,正常的 select 首先会来到这里
   *
   */
  } else if (source.getNodeType() == Node.ELEMENT_NODE) {
    /**
     * 第一次 included = false 的所以直接走下边的遍历每个子元素
     */
    if (included && !variablesContext.isEmpty()) {
      // replace variables in attribute values
      NamedNodeMap attributes = source.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++) {
        Node attr = attributes.item(i);
        attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
      }
    }
    NodeList children = source.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      applyIncludes(children.item(i), variablesContext, included);
    }
  } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
      && !variablesContext.isEmpty()) {
    // replace variables in text node
    source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
  }
}

根据节点中的 include 寻找合适的 sql 节点内容:

/**
 * 这个方法就是 针对你的 sql 里 include 引用的 sql 了
 * 根据你 include 上的 refid 值找到对应的sql
 * @param refid
 * @param variables
 * @return
 */
private Node findSqlFragment(String refid, Properties variables) {
  // refid 可能是变量比如 ${name} 需要解析出值
  refid = PropertyParser.parse(refid, variables);
  // builderAssistant.applyCurrentNamespace 这个方法就是给你的名字拼上 namespace 的信息
  refid = builderAssistant.applyCurrentNamespace(refid, true);
  try {
    /**
     * 上一个方法我们的 sql 节点的信息 是不是都存在 sqlFragments 这个里边了
     * 这里就是把它取出来 进行一个深拷贝 就是不影响原始的值
     * 很好 很懂数据的保护性哈
     */
    XNode nodeToInclude = configuration.getSqlFragments().get(refid);
    return nodeToInclude.getNode().cloneNode(true);
  } catch (IllegalArgumentException e) {
    throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
  }
}

解析 sql 节点中的变量:

/**
 * 就是解析当前 sql 中都有哪些变量,变量的值又是多少
 * 进行汇总并返回
 * 如果某个有变量表达式,没有配置值的话会报错
 * @param node node 还是你的增删改查的sql
 * @param inheritedVariablesContext
 * @return
 */
private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) {
  Map<String, String> declaredProperties = null;
  NodeList children = node.getChildNodes();
  // 遍历每个子节点
  for (int i = 0; i < children.getLength(); i++) {
    Node n = children.item(i);
    // 如果是子节点
    if (n.getNodeType() == Node.ELEMENT_NODE) {
      // 获取节点上的 name 属性值
      String name = getStringAttribute(n, "name");
      // 解析节点上 value 的值,如果是变量表达式就根据变量表得到值
      String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext);
      if (declaredProperties == null) {
        declaredProperties = new HashMap<>();
      }
      // 好写法如果当前的 value 没有值就报错
      if (declaredProperties.put(name, value) != null) {
        throw new BuilderException("Variable " + name + " defined twice in the same include definition");
      }
    }
  }
  /**
   * 如果 declaredProperties 是空的 就说明没有新的变量
   * 有的话 进行合并并返回
   */
  if (declaredProperties == null) {
    return inheritedVariablesContext;
  } else {
    Properties newProperties = new Properties();
    newProperties.putAll(inheritedVariablesContext);
    newProperties.putAll(declaredProperties);
    return newProperties;
  }
}

这个 applyIncludes 方法的代码看着不多,但是递归很多,所以要花些时间还有一定要调试,你才会发现每次递归的都是什么,什么时候给变量赋值,什么时候替换节点内容的,以及节点的类型

编号

子节点

类型

描述

1

SELECT * FROM

TEXT_NODE

文本节点

2

<include refid="mySql"/>

ELEMENT_NODE

普通节点

3

WHERE order_no='xxx'

TEXT_NODE

文本节点

这里是我在调试的时候一个过程,希望能帮助你理解哈。

2.2.2  解析 <selectKey> 节点

这个节点 selectKey 的作用就是辅助帮我们查询主键的值的,比如我们插入的时候可能是根据主键自增的,这个标签就可以辅助你完成主键的赋值。下面我们来看一下 <selectKey> 节点的解析过程。

private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
  // 得到 selectKey 节点信息
  List<XNode> selectKeyNodes = context.evalNodes("selectKey");
  if (configuration.getDatabaseId() != null) {
    // 解析 <selectKey> 节点,databaseId 不为空
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
  }
  // 解析 <selectKey> 节点,databaseId 为空
  parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
  // 将 <selectKey> 节点从 dom 树中移除
  removeSelectKeyNodes(selectKeyNodes);
}

从上面的代码中可以看出,<selectKey> 节点在解析完成后,会被从 dom 树中移除。这样后续可以更专注的解析 <insert> 或 <update> 节点中的 SQL,无需再额外处理 <selectKey> 节点。继续往下看。

/**
 * @param parentId 也就是增删改查上的id
 * @param list     selectKey 节点列表  按理我觉得每个语句也就最多一个 selectKey 吧 不知道为啥用的集合
 * @param parameterTypeClass   增删改查节点上的参数类型
 * @param langDriver     langDriver
 * @param skRequiredDatabaseId  configuration 数据库厂商
 */
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
  for (XNode nodeToHandle : list) {
    // id = parentId + !selectKey,比如 saveUser!selectKey
    String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    // 获取 <selectKey> 节点的 databaseId 属性
    String databaseId = nodeToHandle.getStringAttribute("databaseId");
    // 匹配 databaseId
    if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
      // 解析 <selectKey> 节点
      parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
    }
  }
}
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
  // 获取节点属性
  String resultType = nodeToHandle.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
  String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
  boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
  // defaults 设置默认值
  boolean useCache = false;
  boolean resultOrdered = false;
  KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
  Integer fetchSize = null;
  Integer timeout = null;
  boolean flushCache = false;
  String parameterMap = null;
  String resultMap = null;
  ResultSetType resultSetTypeEnum = null;
  // 创建 SqlSource
  SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
  /*
   * <selectKey> 节点中只能配置 SELECT 查询语句,
   * 因此 sqlCommandType 为 SqlCommandType.SELECT
   */
  SqlCommandType sqlCommandType = SqlCommandType.SELECT;
  /*
   * 构建 MappedStatement,并将 MappedStatement
   * 添加到 Configuration 的 mappedStatements map 中
   */
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
  // id = namespace + "." + id
  id = builderAssistant.applyCurrentNamespace(id, false);
  MappedStatement keyStatement = configuration.getMappedStatement(id, false);
  // 创建 SelectKeyGenerator,并添加到 keyGenerators map 中
  configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

可以看到解析的几个关键步骤:

  1. 创建 sqlSource
  2. 构建 MappedStatement,并放进 configuration
  3. 创建 SelectKeyGenerator,并放进 configuration

那么我们看下这几个步骤。

2.2.2.1  创建 sqlSource

sqlSource是来干啥的,前面分析了 <include> 和 <selectKey> 节点的解析过程,这两个节点解析完成后,都会以不同的方式从 dom 树中消失。所以目前的 SQL 语句节点由一些文本节点和普通节点组成,比如 <if>、<where> 等。那下面我们来看一下移除掉 <include> 和 <selectKey> 节点后的 SQL 语句节点是如何解析的。

我们可以看到创建 sqlSource 是用 langDriver 来创建的,那它是怎么来的呢,是由 configuration 来的, configuration 里的  langDriver 怎么来的呢,其实是在 configuration 实例化的时候,默认值 XMLLanguageDriver。

languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);

那我们紧接着看看 XMLLanguageDriver如何解析的:

@Override
// XMLLanguageDriver
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  // 创建解析器
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  // 解析
  return builder.parseScriptNode();
}
// XMLScriptBuilder
public SqlSource parseScriptNode() {
  // 解析 SQL 语句节点
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  /**
   * 根据 isDynamic 状态创建不同的 SqlSource
   * 动态的判断依据就是 ${}看清楚奥是$
   */
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    /**
     * 不是动态的话  这个里边会解析你的 sql 文本
     * 比如#{id} #{orderNo} 就会解析出你这个 sql 需要 id,orderNo 两个参数
     */
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

如上,SQL 语句的解析逻辑被封装在了 XMLScriptBuilder 类的 parseScriptNode 方法中。该方法首先会调用 parseDynamicTags 解析 SQL 语句节点,在解析过程中,会判断节点是是否包含一些动态标记,比如 ${} 占位符以及动态 SQL 节点等。若包含动态标记,则会将 isDynamic 设为 true。后续可根据 isDynamic 创建不同的 SqlSource。

这里动态的话不会去解析,就是简单的创建 DynamicSqlSource 对象出来,非动态的话比如我们 sql 里的 #{} 这种,会解析这个 sql 都有哪些变量,我们来看看:

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);
  // 没有传参类型的话 默认Object
  Class<?> clazz = parameterType == null ? Object.class : parameterType;
  // 解析
  sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  /**
   * 分词器对象 + handler
   * 暂时不深入研究 总之解析出你 sql 中#{}中的参数
   */
  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);
  }
  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

下面,我们来看一下 parseDynamicTags 方法的源码:

private void initNodeHandlerMap() {
  nodeHandlerMap.put("trim", new TrimHandler());
  nodeHandlerMap.put("where", new WhereHandler());
  nodeHandlerMap.put("set", new SetHandler());
  nodeHandlerMap.put("foreach", new ForEachHandler());
  nodeHandlerMap.put("if", new IfHandler());
  nodeHandlerMap.put("choose", new ChooseHandler());
  nodeHandlerMap.put("when", new IfHandler());
  nodeHandlerMap.put("otherwise", new OtherwiseHandler());
  nodeHandlerMap.put("bind", new BindHandler());
}
/**
 * @param node selectKey 节点信息
 * @return
 */
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) {
      // 获取文本内容
      String data = child.getStringBody("");
      // 创建个 TextSqlNode 对象把文本包装起来
      TextSqlNode textSqlNode = new TextSqlNode(data);
      // 他是不是动态的 取决于他的 sql 是不是有 ${}
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {
        // 不是动态的 拿静态的对象包起来
        contents.add(new StaticTextSqlNode(data));
      }
    // 如果节点类型是 比如 where foreach 节点型的
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
      // 就是根据你不同的标签 从 map 中获取对应的 handler
      String nodeName = child.getNode().getNodeName();
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      // 没有的话直接报错
      if (handler == null) {
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
      // 调用不同的 handler 进行节点解析
      handler.handleNode(child, contents);
      // 设置 isDynamic 为 true
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}

上面方法的逻辑我前面已经说过,主要是用来判断节点是否包含一些动态标记,比如 ${} 占位符以及动态 SQL 节点等。

这里,不管是动态 SQL 节点还是静态 SQL 节点,我们都可以把它们看成是 SQL 片段,一个 SQL 语句由多个 SQL 片段组成。在解析过程中,这些 SQL 片段被存储在 contents 集合中。

最后,该集合会被传给 MixedSqlNode 构造方法,用于创建 MixedSqlNode 实例。从 MixedSqlNode 类名上可知,它会存储多种类型的 SqlNode。除了上面代码中已出现的几种 SqlNode 实现类,还有一些 SqlNode 实现类未出现在上面的代码中。但它们也参与了 SQL 语句节点的解析过程,这里我们来看一下这些幕后的 SqlNode 类。

好到这里为止我们 SqlSource 就创建完了。

2.2.2.2  构建 MappedStatement

我们的增删改查的语句其实最后都会被归化为 MappedStatement 对象,并放进我们的 configuration 里,创建比较简单,我们简单来看下。

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) {
  if (unresolvedCacheRef) {
    throw new IncompleteElementException("Cache-ref not yet resolved");
  }
  id = applyCurrentNamespace(id, false);
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  // 通过建造器创建 并设置我们的属性值
  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(valueOrDefault(flushCache, !isSelect))
      .useCache(valueOrDefault(useCache, isSelect))
      .cache(currentCache);
  // 获取或创建 ParameterMap
  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  if (statementParameterMap != null) {
    statementBuilder.parameterMap(statementParameterMap);
  }
  // 构建 MappedStatement 做一些基础校验
  MappedStatement statement = statementBuilder.build();
  // 添加进 configuration 完事
  configuration.addMappedStatement(statement);
  return statement;
}

紧接着我们的 selectKey 创建 SelectKeyGenerator对象并放进 configuration 里,我们的 selectKey 节点就完事了。

2.2.3  createSqlSource

 我们上边在解析 selectKey 节点的时候已经看过这个方法了,都是同一个方法,会把我们的增删改查根据是否动态封装成 sqlSource。

2.2.4  构建 MappedStatement

也就是构建我们的增删改查封装成 MappedStatement,也看过了哈,就不再看了。

3  小结

本节我们主要解析了 mapper 的 sql节点以及我们熟悉的增删改查,内容还是比较多的,记住边调试边看哈,还可以结合官网的相关讲解。有理解不对的地方,欢迎指正哈。

posted @ 2023-02-28 20:52  酷酷-  阅读(203)  评论(0编辑  收藏  举报