MyBatis(技术NeiMu):核心处理层(SQL映射配置文件解析:select、insert、update、delete标签的解析)

回顾

前面我们已经看了SQL映射配置文件上的一些核心标签是如何解析的了,同时也认识了二级缓存的构建过程,下面来看一下对于sql我们常常使用的四种标签是如何解析的

现在人然后回到我们的XMLMapperBuilder的configurationElement方法中
在这里插入图片描述
上一章,我们对cache、cache-ref、parameterMap、resultMap标签已经分析解析过程了,下面到我们常用的四种sql标签的解析

buildStatementFromContext

对应的方法为XMLMapperBuilder里面的buildStatementFromContext方法

源码如下
在这里插入图片描述
可以看到,其会调用重载方法,重载方法的源码如下

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    //遍历每个select、update、delete、insert节点
    //可以看到,在这里是对其一同处理的
    for (XNode context : list) {
        //select、update、delete、insert的标签解析是交由XMLStatementBuilder来完成的
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
          //使用XMLStatementBuilder来解析select、update、delete、insert标签
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
          //这里同样捕捉InCompleteElementException
          //因为存在先后节点先后加载顺序的影响,因此会先记录在configuration中,后续在处理
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

从方法上可以看到,emmmm,对于select、insert、update、delete标签的解析是交由XMLStatementBuilder来进行解析的,MyBatis该不会对于所有标签的解析都是采用建造者模式的吧。。。

不过在此之前,我们先认识一些概念

  • 4个节点都会被解析成一个个MappedStatement
  • MappedStatement使用SqlSource来存储描述的SQL

下面就先来认识一下SqlSource与MappedStatement

SqlSource

在这里插入图片描述

该接口很简单,仅仅只有一个获取绑定的SQL,该方法可以根据注解或配置文件中描述的SQL语句,以及传入的参数,返回可以执行的SQL
在这里插入图片描述

MappedStatement

一个MappedStatement就代表一个select、delete、update、insert节点的解析。。。

在这里插入图片描述
先认识一下里面的成员属性,不过太多了,我们只讲几个关键的属性

  • resources:节点中的id属性,我们常常用这个来对应绑定SQL映射接口文件的方法名,但这个id属性会加上命名空间前缀!
  • SqlSource:SqlSource对象,对应绑定的SQL语句
  • SqlCommandType:对应的SQL类型,其是一个枚举类,里面有6种类型
    • UNKNOWN
    • INSERT
    • UPDATE
    • DELETE
    • SELECT
    • FLUSH

MapperStatementBuilder的parseStatementNode方法

认识完了这两个类之后,回到我们的解析SQL方法中,我们知道,XMLMapperBuilder将解析sql节点的工作交由了XMLStatementBuilder来进行,并且一个XMLStatementBuilder只解析一个节点!对应的方法为parseStatementNode

该方法源码如下

   public void parseStatementNode() {
       //获取id属性
    String id = context.getStringAttribute("id");
       //获取databaseId属性
    String databaseId = context.getStringAttribute("databaseId");
	//检查databaseId属性与当前configuration的属性是否一致
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
	//获取节点名字
    String nodeName = context.getNode().getNodeName();
    //转化为对应的SqlCommandType,学到了,可以使用valueOf方法来根据名字来获取枚举类型实例
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //判断是不是select类型
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //判断需要需要刷新缓存,根据flushCache属性来判断,如果没有flushCache,则根据isSelect属性判断
    //可以看到,默认条件下,只要不是查询动作,都会进行刷新缓存!
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //判断需不需要使用缓存,根据属性useCache属性判断,如果没有useCache属性,则根据isSlect属性判断
    //可以看到,默认情况下,如果没有配置useCache属性,那么只有select语句会使用缓存
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //判断resultOrdered属性,默认为false
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
	//上面已经完成了sql节点里面常用属性的解析
    
    //在解析sql节点前,先解析里面的include标签节点
    //解析include标签是交由XMLIncludeTransformer进行的
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    //使用XMLIncludeTransformer去解析include节点
   	includeParser.applyIncludes(context.getNode());
	//解析完include节点之后,继续下面属性的解析
    //获取parameterType属性
    String parameterType = context.getStringAttribute("parameterType");
    //通过别名注册中心去找到parameterType属性对应的类
   	Class<?> parameterTypeClass = resolveClass(parameterType);
	//获取lang属性
    String lang = context.getStringAttribute("lang");
    //注册LanguageDriver,LanguageDriver用于生成SqlSource的!
    LanguageDriver langDriver = getLanguageDriver(lang);

    //解析selectKey节点
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 下面正式开始解析sql,后面再看这整个部分
    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 = 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");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

可以看到总体分为4部

  • 根据sql节点的类型和对应的属性去判断是否需要刷新缓存、使用缓存,对应的属性为flushCache和useCache
  • 解析include标签
  • 解析selectKey标签
  • 解析sql

刷新缓存与使用缓存

  • 刷新缓存根据flushCache属性来设置,如果没有flushCache属性,默认情况下,不是select类型的都会去刷线缓存
  • 使用缓存是根据useCache属性来设置,如果没有useCache属性,默认情况下,只有select类型的才会去使用缓存

很简单。。。

解析include标签

先说明一下include标签的作用

include标签是用来代替对应的sql标签里面的SQL片段的,说白了就是给select等标签引用了sql标签定义的sql片段,并将里面的$ {…}占位符替换成真实的参数,该占位符的作用其实就是引用properties标签里面的配置,所以会去替换成真实的参数,前面我们已经看过了,MyBatis如何解析properties标签的了,对于properties标签是在总配置文件里面的,但会被解析成Properties对象存放进Configuration中,然后在解析Mapper的时候也是可以使用的!!!

下面就来看看是如何解析的

在这里插入图片描述
可以看到,XMLStatementBuilder对于include标签的解析,是交由XMLIncludeTransformer来解析的,可以看到其在创建时注入了Configuration和XMLBuilderAssistant

解析的具体方法是applyIncludes

public void applyIncludes(Node source) {
    //获取Configuration中的properties,之前在解析总配置文件上
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    //如果configurationVariables不为空就添加进variablesContext中。。
    //为什么要这么麻烦呢??
    Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
    //调用重载的applyIncludes,false代表并不是嵌套进上一层的
    //因为此时是第一层,肯定不是嵌套的,后面递归的时候false会变为true,因为发生了嵌套
    applyIncludes(source, variablesContext, false);
  }
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    //判断标签类型
    if ("include".equals(source.getNodeName())) {
       //对于include标签的解析,通过refid属性去找到对应的sql片段
       //返回的是一个深拷贝的node节点,实际上是一个sql节点
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      //解析include标签下的property节点,该property可以用来注入进sql片段中!
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
        //递归去处理include节点。。。
        //include是可以嵌套的。。sql片段里面嵌入sql片段。。
        //因为在sql节点里面也可能使用了include节点。。。
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
        //将include节点替换成sql节点
      source.getParentNode().replaceChild(toInclude, source);
        //判断sql节点里面是不是有子节点
      while (toInclude.hasChildNodes()) {
          //将sql节点的子节点添加进sql节点前面。。
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
        //移除sql节点。。。。
      toInclude.getParentNode().removeChild(toInclude);
    }
    //如果节点类型是一个Element
    else if (source.getNodeType() == Node.ELEMENT_NODE) {
      if (included && !variablesContext.isEmpty()) {
        // 获取节点的所有属性
        NamedNodeMap attributes = source.getAttributes();
          //遍历属性
        for (int i = 0; i < attributes.getLength(); i++) {
          Node attr = attributes.item(i);
            //使用propertyParser去映射去property的值!
          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);
      }
    }
    //如果是嵌套处理,并且节点类型为Text或者SECTION
    else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
        && !variablesContext.isEmpty()) {
      // 使用propertyParser解析节点的值
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

总结一下include标签的解析

  • 判断标签的类型,如果是include的话做以下处理
    • 首先对property标签进行解析,因为在include标签里面是可以嵌套property标签的
    • 通过ref属性找到对应的sql节点,并且深拷贝一份过来
    • 对该sql节点继续解析,因为可能存在嵌套
  • 递归对后续sql节点的解析会利用propertyParser去进行解析,将${…}占位符替换成property中的实参!
  • 最后返回到解析include时,此时对于sql标签已经解析好了
  • 使用sql标签替代include标签,然后让sql标签的子标签都插入到sql标签前面,然后最后移除sql标签!

可以看到,对于include标签的解析,其实就是通过递归的方式来解析里面的sql标签,然后使用sql节点代替include节点,再将sql节点中的子节点插入到sql节点前面,最后移除掉sql节点,这里的移除是从select、update等节点对象中移除!

解析include标签还是比较难懂的。。。。。

解析selectKey标签

selectKey标签可以解决主键自增的问题,实现自定义主键自增,在insert、update节点中我们都可以使用selectKey节点来解决主键自增的问题!其实也不算是解决主键自增问题,更像是一种主键生成策略!因为有了自定义的主键生成策略,可以让在完成插入后获取到主键值,里面也是一句SQL!并且通常是一句查询SQL

在这里插入图片描述
对应的方法是processSelectKeyNodes方法

源码如下

private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    //获取所有的selectKey标签
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    //使用parseSlectKeyNodes方法对selectKey节点进行解析
    //需要用到select、update等节点中的parameterType属性和id属性
    if (configuration.getDatabaseId() != null) {
      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    //移除selectKey节点
    removeSelectKeyNodes(selectKeyNodes);
  }

源码如下

private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
    //遍历每一个selectKey节点进行处理
    for (XNode nodeToHandle : list) {
     //使用父标签的id属性和主键生成器的前缀拼接生成id,这里要去拼接前缀
        //否则当后续注册selectKey的mappedStatement会与上一层sql的mappedStatement发生冲突
      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        //获取selectKey的databaseId属性
      String databaseId = nodeToHandle.getStringAttribute("databaseId");
        //如果databaseId属性匹配configuration中的id
      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) {
    //获取seletctresultType属性,代表主键的类型!
    String resultType = nodeToHandle.getStringAttribute("resultType");
    //根据resultType属性找到对应的类,
    Class<?> resultTypeClass = resolveClass(resultType);
    //根据statementType属性找到对应的StatementType
    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //获取keyProperty属性,也就是主键值对应的JavaBean的属性名,会将计算出的主键值附上去
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    //获取keyColumn属性,也就是对应数据库的主键名
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    //首先获取selectKey中的order属性,如果没有默认为after!
    //也就是是先执行操作还是先选择主键设置keyProperty
    //设置KeyProperty其实就是执行keySelect标签里面的SQL
    //如果是before,则会先将插入数据的主键值改为sql的结果,并且keyProperty也会被设置成查出来的结果,后再执行SQL
    //如果是after,则会先插入,然后再执行sql查询,此时已经不能改动主键值了,只能设置keyProperty
    //一般对于自动增长类型,设为after才会取到正确的值,因此默认为after
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
	
    //默认的一些属性
    //不开启缓存
    boolean useCache = false;
    boolean resultOrdered = false;
    //主键值生成策略,默认为NoKeyGenerator
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;
	//前面一系列属性处理之后,使用LangDriver创建SqlSource对象
    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;
	//使用builderAssistant注册MappedStatement
    //keySelect也代表一个MappedStatement!!
    //其本身就是通过SQL查出或生成主键值!
    //如果是before则是查出和生成主键值
    //如果是after仅仅只是查出主键值
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    //对id属性拼接上mapper的命名空间
    //id是外层insert、select节点的id属性
    id = builderAssistant.applyCurrentNamespace(id, false);
	//从configuration中获取刚刚创建好的MappedStatement
    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    //往configuration中添加KeyGenerator,给该insert节点绑定了对应的SelectKeyGenerator
    //KeyGenerator会进行前、后处理,完成前面定义的主键策略
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
  }

其实整个过程分为

  • 解析SelectKey中的属性
    • keyColumn:对应数据库中的主键名
    • keyProperty:对应JavaBean中的属性名
    • resultType:主键的数据类型
    • order:After、Before,默认是After
      • After:仅仅只有查询主键的功能,也就是再完成插入之后,才会执行查询SQL,然后修改keyProperty对应的属性值
      • Before:先执行查询SQL,然后使用结果来当作此次插入的主键值,并且修改KeyProperty对应的属性值,再执行插入动作
    • 一些默认属性,比如是否使用缓存、是否清空缓存、对应的resultMap、parameterMap,这些属性全部关闭或者设为Null
  • 使用LangDriver生成SqlSource,也就是生成、查询主键的sql
  • 整合解析出的属性和SqlSource,创建出MappedStatement,然后交由builderAssistant完成注册,添加进Configuration中
  • 将对应的SelectKeyGenerator绑定节点的id属性,然后也注册进Configuration中

下面先来看看如何生成SqlSource的

在这里插入图片描述

LangDriver

LangDriver是一个接口,用来创建SqlSource和ParameterHandler的

  • SqlSource:SQL映射文件或者注解绑定的SQL语句的封装
  • ParameterHandler:将Java的参数转化为JDBC类型的

在这里插入图片描述
其实现关系如上

XMLLanguageDriver实现了LanguageDriver,然后RawLanaguageDriver继承了XMLLanguageDriver

我们可以在MyBatis的总配置文件上自定义XMLLanguageDriver

默认使用的是XMLLanaguageDriver进行解析

在这里插入图片描述
可以看到,对于SqlSource的解析是使用XMLScriptBuilder来进行解析的,Emmmm,对于SqlSouce其实也是使用建造者模式来创建的

源码如下

public SqlSource parseScriptNode() {
    //解析sql,判断是不是动态的,通过节点的类型来判断。。。。
    //如含有占位符或是含有动态SQL的相关节点,也就是where、set、if那些标签!
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    //判断SQL是不是动态的
    if (isDynamic) {
        //如果是动态的,则是DynamicSqlSource
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        //如果不是动态的,则是RawSqlSource
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

可以看到,XMLLangDriver会返回两种类型的SqlSource,一种是普通的RawSqlSource,一种是动态Sql的DynamicSqlSource

  • RawSqlSource:立刻就能使用
  • DynamicSqlSource:动态Sql,还需要进一步解析才能使用,判断动态Sql有两个条件
    • 有${}占位符的视为动态SQL
    • 含有动态SQL相关的节点标签视为动态SQL
    • 具体判断的细节在parseDynamicTags中,有兴趣可以去看

在这里插入图片描述
nodeHandlerMap里面就存储着一系列动态SQL相关标签的处理器,从中也可以知道哪些标签是根动态SQL相关的

  • trim标签
  • where标签
  • set标签
  • foreach标签
  • if标签
  • choose标签
  • when标签
  • otherwise标签
  • bind标签
NodeHandler

下面来分析一下NodeHandler,从前面可以看到有这么多动态标签的Handler,MyBatis将其都统一抽象成一个NodeHandler,并且是XMLScriptBuilder中的私有内部接口

在这里插入图片描述
其实对于上述遇到的各种NodeHandler,其本质上都是去创建对应的SqlNode然后添加进targetContents中

比如WhereHandler

在这里插入图片描述
生成对应的whereSqlNode然后添加进targetContents中

其他的也是一致的,就不再赘述

现在selectKey标签也完成解析了

解析SQL

完成include标签和selectKey标签的解析后,下面就到解析Select、insert、update等标签里面的SQL了

返回我们的代码

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    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);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
 // 从这里开始,解析sql
	//检测有没有对应的KeyGenerator!
    //也就是前面我们在解析SelectKey时,会有对应的KeyGenerator生成
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        //如果没有定义,则会根据usseGeneratedKeys属性和SqlCommandType属性来判断
        //如果是Insert类型,并且useGeneratedKeys为true会使用Jdbc3KeyGenerator
        //如果不是的话,使用NoKeyGenerator(啥都不做)
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
   
    // 创建对应的sqlSource,已经提到过
    // RawSqlSource或者DynamicSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //根据statementType属性来创建出StatementType
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //获取fetchSize属性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //获取timeout属性
    Integer timeout = context.getIntAttribute("timeout");
    //获取parameterMap属性
    String parameterMap = context.getStringAttribute("parameterMap");
    //获取resultType属性
    String resultType = context.getStringAttribute("resultType");
    //根据resultType属性,从别名注册中心找到对应的类
    Class<?> resultTypeClass = resolveClass(resultType);
    //获取resultMap属性
    String resultMap = context.getStringAttribute("resultMap");
    //获取resultSetType属性
    String resultSetType = context.getStringAttribute("resultSetType");
    //
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    //获取keyProperty属性
    String keyProperty = context.getStringAttribute("keyProperty");
    //获取keyColumn属性
    String keyColumn = context.getStringAttribute("keyColumn");
    //获取resultSets属性
    String resultSets = context.getStringAttribute("resultSets");
	//注册MappedStatement
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

步骤如下

  • 创建KeyGenerator,会先从Configuration找出前面解析SelectKey标签时创建的KeyGenerator,如果没有,则会根据属性和标签类型来进行选择

    • 如果是insert标签,并且useGenerator属性为true,则会使用Jdbc3KeyGenerator,只有后处理,没有前处理
    • 如果上面其中一个条件不满足,则是NoKeyGenertor,啥也不做的
  • 创建对应的SqlSouce

  • 获取对应的一系列属性,比如resultMap,resultType,keyProperty、keyColumn这些。。。

  • MapperBuilderAssistant使用SqlSource、KeyGenerator和一系列属性,创建并且注册MappedStatement(创建MappedStatement实际上是交由MappedBuilderAssistant里面的MappedStatementBuilder来负责的),底层的注册容器也是一个StrictMap。。

  • SQL节点的对应的MappedStatement会与SelectKey的MappedStatement进行区分,selectKey的MappedStatement对应的id是会在sql节点的id属性上拼接上前缀(!selectKey)

至此,对于select、insert、update、delete标签的解析也完成了

总结一下Mapper的解析过程

  • XMLMapperBuilder对于select、update、insert、delete标签的解析其实是交由MapperStatementBuilder负责的
  • 对于SQL,MyBatis统一抽象成SqlSource
  • 对于select、update、insert、delete等,MyBatis统一归类于MappedStatement
  • 对于解析4种标签,其实就是创建出对应的MappedStatement出来,然后注册进Configuration中
  • 解析Select、update、insert、delete大致分为4个部分
    • 解析一些缓存属性
    • 解析include标签:通过递归去将include标签对应的sql节点中的${}占位符进行替换,替换完成之后,将解析出来的字符串去替代include标签,此时就完成拼接了。。
    • 解析selectKey标签:解析selectKey标签,生成对应的KeyGenerator和MappedStatement,存储进Configuration中
    • 解析SQL:此时节点里面已经将include标签和selectKey标签都解析完并且删除了,里面仅仅剩下了SQL语句和一些动态SQL相关的标签,根据剩下的内容去创建出对应的SqlSource;取出前面解析SelectKey创建出的KeyGenerator;最后解析出剩余的属性,根据SqlSource、KeyGenerator和剩余属性,交由MapperBuilderAssistant完成创建MapperStatement并注册

绑定Mapper接口

回到我们的XMLMapperBuilder

在这里插入图片描述
现在我们已经完成了configurationElement步骤,解析完了总配置文件下的mapper标签节点,接下来就是bindMapperForNameSpace了,根据命名空间去绑定对应的接口

源码如下

 private void bindMapperForNamespace() {
     //获取当前命名空间
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
          //根据命名空间,命名空间其实也是接口的全限定类名
          //解析命名空间对应的类
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        // ignore, bound type is not required
      }
        //判断是不是已经绑定了
      if (boundType != null && !configuration.hasMapper(boundType)) {
        //如果还没有绑定
        //补充namespace前缀,添加到loadedResource集合中
        configuration.addLoadedResource("namespace:" + namespace);
        //注册boundType接口,注册进Configuration中的MapperRegistry中
        configuration.addMapper(boundType);
      }
    }
  }

绑定Mapper接口,其实就是去注册Mapper接口

  • 获取当前命名空间
  • 根据命名空间去找到对应的Mapper接口
  • 将namespace:命名空间添加进loadedResource中,代表该Mapper已经解析过了
  • configuration中注册Mapper接口

处理incomplete*集合

还记得之前我们解析各种标签的时候,有时候会标签之间会存在依赖关系,所以在解析某个标签时会因为依赖的标签还没解析从而失败,会被记录到对应的incomplete*集合中,现在已经完成MyBatis所有配置的解析了,接下来就要去处理这些未完成的集合

在这里插入图片描述

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
	//处理未完成的resultMaps
    parsePendingResultMaps();
    //处理未完成的Cache-ref
    parsePendingCacheRefs();
    //处理未完成的statement
    parsePendingStatements();
  }

里面其实都是遍历这些集合,然后对各个标签重新解析而已,调用对应的resolve方法
在这里插入图片描述
至此,MyBatis的初始化工作就完成了!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值