MyBatis 源码解析(七)MyBatis如何解析配置 ?(七)

最后还剩sqlselect|insert|update|delete

先看sql

XMLMapperBuilder###sqlElement()

  private void sqlElement(List<XNode> list) {
    //首先看是否配置了database id 
    //如果配置了则将id传入,通过id进行选择  
    if (configuration.getDatabaseId() != null) {
      sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
  }

感觉这里完全可以改成

 sqlElement(list, configuration.getDatabaseId());

反正configuration.getDatabaseId()null就传入null


后期修改:

最近一直在后面的databaseIdMatchesCurrent()被卡住,实在没看懂这个方法。

现在才反应过来,这个是光if,没有else的。也就是逻辑上是首先筛选出和configuration.getDatabaseId()相同的,再筛选出sql中没有配置databaseId的语句,筛选了两次。

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");
       //为id添加上域名空间,使其变得唯一 
      id = builderAssistant.applyCurrentNamespace(id, false);
      //如果的当前需要的databaseId和驱动中的databaseId相一致,则将xnode存入sqlFragments中  
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        sqlFragments.put(id, context);
      }
    }
  }

可以看到sql只是简单的配置了一下databaseIdsql语句却原封不动的留下了

XMLMapperBuilder###databaseIdMatchesCurrent()

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    //如果需要匹配的id不为空,则检查两个id是否相等
    if (requiredDatabaseId != null) {
      return requiredDatabaseId.equals(databaseId);
    }
    //如果需要匹配的id为空,但是节点中配置的id不为空,那么一定为false
    if (databaseId != null) {
      return false;
    }
    //如果需要匹配的id为空,且节点中配置的id也为空,一般此时应该直接返回true了
    //但是要业务逻辑?
    //如果已经解析的sql节点中不包含此id才返回true
    if (!this.sqlFragments.containsKey(id)) {
      return true;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    //如果前面已经加载了一个相同的id,并且这个id不为null,则返回false
    XNode context = this.sqlFragments.get(id);
    return context.getStringAttribute("databaseId") == null;
  }

这个判断的逻辑看的有点懵。

根据官方文档说明:

如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。

直接if(databaseId==null || databaseId==configuration.getDatabseId())即可,为什么还有其他的语句呢?

其实这段代码要配合前面的代码看:

private void sqlElement(List<XNode> list) {
    //首先看是否配置了database id 
    //如果配置了则将id传入,通过id进行选择  
    if (configuration.getDatabaseId() != null) {
        sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
}

可以看到,这里首先将 configuration.getDatabaseId()传入进去进行解析,然后再传入了一个null,也就是说如果配置了databaseIdProvider,那么每个语句都会被解析两次。

  • 第一次是带着databaseId进行解析,如果相等,则加入到sqlFragments()里。
  • 第二个便是都为null的比较

这就是为什么后面会有一个判断是否在集合中的逻辑了,

虽然看懂了这段代码,但是这段代码还是挺乱,明明比较简单的需求,,

可能是后面改bug,强行这样改的。。。。(个人猜测)

XMLMapperBuilder###buildStatementFromContext()

  private void buildStatementFromContext(List<XNode> list) {
    //和sql相同,只是简单判断下databaseid  
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }
  private void buildStatementFromContext(List<XNode> list) {
    //和sql一样,只是简单判断下是否有databaseId
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

XMLMapperBuilder###buildStatementFromContext()


private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { //使用XMLStatementBuilder创建状态对象 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }

XMLStatementBuilder###XMLStatementBuilder()

//代码比较长,因为涉及到比较多的配置  
public void parseStatementNode() {
    //获取id
    String id = context.getStringAttribute("id");
    //获取配置的databaseId
    String databaseId = context.getStringAttribute("databaseId");

    //如果databaseId不符合,则直接返回(这里面的判断和前面的判断一样)
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    //获取nodeName  `select|insert|update|delete`
    String nodeName = context.getNode().getNodeName();
    //转换为枚举
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //是否为select语句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //是否需要刷新缓存,默认情况下除了查找,其他都会更新缓存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否将结果进行缓存
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //对select嵌套结果集的引用是否特殊处理,默认false
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    //使用XMLIncludeTransformer 找到所<include>的SQL
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
    //参数类型,已废弃
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //脚本语言,mybatis3.2的新功能
    //如果为没有配置则使用defaultDriverClass
    String lang = context.getStringAttribute("lang");
    //加载lang驱动
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
     //解析之前先解析自动生成id
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    //应该添加一个标志,表示这个id 的<selectKey>和<include>已经被处理了
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    //给id添加域名空间前缀
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    //先检查是否已经有现成的keyGenerator 如果有,就直接使用
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      //如果配置了useGeneratedKeys,则会使用Jdbc3KeyGenerator对象来获取数据管理的id,
      //一般用于mysql或则sql Server这种由数据库自己管理的自增key
      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 = 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");
    //指定resultSetType
    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);
  }

简单来说,就是获取各种属性,然后调用助手类进行构造

上面有个比较重要的类便是通过LangDriver生成的SqlSource

这个SqlSource大概就是根据XML包含的属性不同,而产生不同处理方式,比如如果包含<where> \ <set>等节点,那么需要在运行时根据代码处理这些节点,如果不需要任何的处理,那么可以直接生成对应的statement直接缓存起来,后续直接传入参数即可使用。等

关于SqlSource的具体工作,后续会详细分析。

MapperBuilderAssostant###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) {

    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 statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }

可以看到还是根据这些参数生成一个对象。


到这里,MyBatis的配置解析基本完毕,可以发现大概就是各种获取属性然后保存。这里也能看出来MyBatis把各个阶段也划分的很清楚,读取配置,解析配置,预处理一些配置。

同时我们可以回头看看解析配置文件涉及到的各个类已经各个类的职责。

  • 首先是Configuration,这个类算是一个大对象,它包含了MyBatis的整个配置
  • 然后是XMLConfigBuilder,这个类是用来解析MyBatisXML配置文件
  • 接下来XMLConfigBuilder会调用XPathParser来解析XML中各个节点
  • 在解析过程中会调用XMLMapperBuilder来解析MyBatis对应的Mapper配置
  • XMLMapperBuilder负责读取配置,组合配置以及将具体的配置写入到Configuration对象中由MapperBuilderAssistant完成
  • 解析XMLMapper过程中会有各种其他需用解析的,则会有各种Resolver,比如:CacheRefResolver,ResultMapResolver
  • 由于StatementBuilder的解析也比较复杂,因此在XMLMapperBuilder解析的过程中,会调用XMLStatementBuilder来解析select|insert|update|delete

以上大概就是MyBatis解析配置文件中各个类的划分,可以说功能划分的比较细,真正像是在向单一职责看齐。