MyBatis官方教程及源代码解析——mapper映射文件

Posted llguanli

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis官方教程及源代码解析——mapper映射文件相关的知识,希望对你有一定的参考价值。

缓存

1.官方文档

MyBatis 包括一个非常强大的查询缓存特性,它能够非常方便地配置和定制。

MyBatis 3 中的缓存实现的非常多改进都已经实现了,使得它更加强大并且易于配置。

默认情况下是没有开启缓存的,除了局部的session 缓存,能够增强变现并且处理循环 依赖也是必须的。要开启二级缓存,你须要在你的 SQL 映射文件里加入一行:

<cache/>

字面上看就是这样。

这个简单语句的效果例如以下:

·????????映射语句文件里的全部 select语句将会被缓存。

·????????映射语句文件里的全部 insert,update和 delete 语句会刷新缓存。

·????????缓存会使用 Least Recently Used(LRU,近期最少使用的)算法来收回。

·????????依据时间表(比方 no Flush Interval,没有刷新间隔), 缓存不会以不论什么时间顺序 来刷新。

·????????缓存会存储列表集合或对象(不管查询方法返回什么)的 1024 个引用。

·????????缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且能够安全地被调用者改动,而不干扰其它调用者或线程所做的潜在改动。

全部的这些属性都能够通过缓存元素的属性来改动。

比方:

<cache
? eviction="FIFO"
? flushInterval="60000"
? size="512"
? readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,并且返回的对象被觉得是仅仅读的,因此在不同线程中的调用者之间改动它们会 导致冲突。

可用的收回策略有:

·????????LRU?–近期最少使用的:移除最长时间不被使用的对象。

·????????FIFO?–先进先出:按对象进入缓存的顺序来移除它们。

·????????SOFT?–软引用:移除基于垃圾回收器状态和软引用规则的对象。

·????????WEAK?–弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

默认的是 LRU

flushInterval(刷新间隔)能够被设置为随意的正整数,并且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

size(引用数目)能够被设置为随意正整数,要记住你缓存的对象数目和你执行环境的 可用内存资源数目。

默认值是 1024。

readOnly(仅仅读)属性能够被设置为 true 或 false。

仅仅读的缓存会给全部调用者返回缓 存对象的同样实例。因此这些对象不能被改动。这提供了非常重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化) 。这会慢一些,可是安全,因此默认是 false。


2.源代码解析

缓存的解析比較简单。mybatis依据配置生成一个cache对象,并存入configuration。每一个映射配置文件仅仅能有一个cache,配置多个时仅仅有第一个生效

//XMLMapperBuilder类
private void cacheElement(XNode context) throws Exception {
if (context != null) {
      //获取配置
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<?

extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); //由builderAssistant对象生成 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } } //MapperBuilderAssistant类 public Cache useNewCache(Class<?

extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); //将cache存入configuration configuration.addCache(cache); //置为当前cache currentCache = cache; return cache; }


除了定义一个新的缓存,我们还能够直接使用其它映射文件配置的缓存,这就利用到了cache-ref

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

要注意的是<cache-ref>的解析在<cache>之前,所以currentCache 对象会选择后者

private void cacheRefElement(XNode context) {
if (context != null) {
      //依照两个映射文件的名空间存入configuration
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
//这里尽管定义了cacheRefResolver 对象,但终于调用的是MapperBuilderAssistant类的useCacheRef方法
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
  }
public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;
      Cache cache = configuration.getCache(namespace);
      //cache找不到的一种可能是相应的映射文件还未解析,这样的时候会抛出异常
      if (cache == null) {
        throw new IncompleteElementException("No cache for namespace ‘" + namespace + "‘ could be found.");
      }
      //假设找到相应cache则currentCache 会置为该对象
      currentCache = cache;
      unresolvedCacheRef = false;
      return cache;
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("No cache for namespace ‘" + namespace + "‘ could be found.", e);
    }
  }

Sql语句块

1.官方文档

这个元素能够被用来定义可重用的 SQL 代码段,能够包括在其它语句中。它能够被静态地(在载入參数) 參数化. 不同的属性值通过包括的实例变化. 比方:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

这个 SQL 片段能够被包括在其它语句中,比如:

<select id="selectUsers" resultType="map">
? select
??? <include refid="userColumns"><property name="alias" value="t1"/></include>,
??? <include refid="userColumns"><property name="alias" value="t2"/></include>
? from some_table t1
??? cross join some_table t2</select>

属性值能够用于包括的refid属性或者包括的字句里面的属性值,比如:

<sql id="sometable">
? ${prefix}Table</sql>
<sql id="someinclude">
? from
??? <include refid="${include_target}"/></sql>
<select id="select" resultType="map">
? select
??? field1, field2, field3
? <include refid="someinclude">
??? <property name="prefix" value="Some"/>
??? <property name="include_target" value="sometable"/>
? </include></select>

2.源代码解析

Sql语句块和映射语句在解析的时候会依据DatabaseId来进行区分,假设同一时候找到带有?databaseId?和不带?databaseId?的同样语句,则后者会被舍弃。利用这点,在配置语句的时候能够为不同数据库配置不同的语句。

//XMLMapperBuilder类中
private void sqlElement(List<XNode> list) throws Exception {
//这里的配置取自配置文件里的databaseIdProvider
    if (configuration.getDatabaseId() != null) {
      sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
  }

  private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
    for (XNode context : list) {
      String databaseId = context.getStringAttribute("databaseId");
      String id = context.getStringAttribute("id");
      id = builderAssistant.applyCurrentNamespace(id, false);
      //在此处对databaseId配置和当前数据库进行匹配
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
//终于依据ID将整个节点存入
        sqlFragments.put(id, context);
      }
    }
  }
  
  private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
//假设有配置databaseIdProvider,则两者必须一致
    if (requiredDatabaseId != null) {
      if (!requiredDatabaseId.equals(databaseId)) {
        return false;
      }
} else {
  //假设没有配置databaseIdProvider,则不须要配置databaseId 
      if (databaseId != null) {
        return false;
      }
      // 假设存在同样ID且databaseId不为空。则省略
      if (this.sqlFragments.containsKey(id)) {
        XNode context = this.sqlFragments.get(id);
        if (context.getStringAttribute("databaseId") != null) {
          return false;
        }
      }
    }
    return true;
  }

在最后sql代码块将整个节点都存入Map中,这样做是由于sql能够实现动态复用,因此每次都必须又一次解析sql代码块的值,这些在接下来映射语句的解析部分完毕。


映射语句

映射语句是Mapper配置中比較复杂的一部分。一方面能够嵌入sql语句块,还有一方面还有动态Sql。

//XMLMapperBuilder类中
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        //这里又由XMLStatementBuilder类来进行解析
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }
//XMLStatementBuilder类中
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

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

    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//这里推断是不是查询语句。影响到后面flushCache和userCache的默认值
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    //1.先处理sql代码块(include)
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    //2.再处理selectKey并移除
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    //3.最后解析SQL语句
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    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))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

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

先来看<include>的解析。在官方文档里面能够看到<include>中包括了<propery>子节点。

用户能够为<propery>配置不同的值来实现动态的复用,假设没有对<propery>进行配置,Mybatis会从XML配置文件里面寻找

public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
//获取XML配置文件里的property值
    Properties configurationVariables = configuration.getVariables();
    if (configurationVariables != null) {
      variablesContext.putAll(configurationVariables);
    }
    applyIncludes(source, variablesContext);
  }
  private void applyIncludes(Node source, final Properties variablesContext) {
//针对<include>进行解析
    if (source.getNodeName().equals("include")) {
      //这里新定义fullContext对象,保证在一个解析过程中使用同一套值      
Properties fullContext;
      String refid = getStringAttribute(source, "refid");
      //对refid进行解析(比如refid="${include_target}"的形式)
      refid = PropertyParser.parse(refid, variablesContext);
      Node toInclude = findSqlFragment(refid);
      //这里对<property>进行解析并返回结果
      Properties newVariablesContext = getVariablesContext(source, variablesContext);
      //依据解析结果使用不同的值
      if (!newVariablesContext.isEmpty()) {
        // merge contexts
        fullContext = new Properties();
        fullContext.putAll(variablesContext);
        fullContext.putAll(newVariablesContext);
      } else {
        // no new context - use inherited fully
        fullContext = variablesContext;
      }
      //针对Sql代码块解析,toInclude是<sql>节点
      applyIncludes(toInclude, fullContext);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      //将<include>替换成相应<sql>
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
//插入<sql>节点解析后的sql语句
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      //最后移除<sql>节点
      toInclude.getParentNode().removeChild(toInclude);
}else if (source.getNodeType() == Node.ELEMENT_NODE) {
      NodeList children = source.getChildNodes();
      for (int i=0; i<children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext);
      }
    } else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {
      // replace variables in all attribute values
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    } else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
      // replace variables ins all text nodes
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
  }
以上过程终于将<include>替换成相应的sql语句。


接下来是<selectKey>的解析。我们能够将其视为一种特殊的映射语句。终于结果保存在configuration的keyGenerators中
private void processSelectKeyNodes(String id, Class<?

> parameterTypeClass, LanguageDriver langDriver) { List<XNode> selectKeyNodes = context.evalNodes("selectKey"); //也须要推断databaseId if (configuration.getDatabaseId() != null) { parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId()); } parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null); //解析后删除节点 removeSelectKeyNodes(selectKeyNodes); } private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) { for (XNode nodeToHandle : list) { String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX; String databaseId = nodeToHandle.getStringAttribute("databaseId"); if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) { 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 = new NoKeyGenerator(); Integer fetchSize = null; Integer timeout = null; boolean flushCache = false; String parameterMap = null; String resultMap = null; ResultSetType resultSetTypeEnum = null; SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass); //这里将<selectKey>当做一种select语句 SqlCommandType sqlCommandType = SqlCommandType.SELECT; //和映射语句一样,<selectKey>解析成MappedStatement对象并保存, builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null); id = builderAssistant.applyCurrentNamespace(id, false); //这里将解析结果保存在configuration中 MappedStatement keyStatement = configuration.getMappedStatement(id, false); configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); }


最后是映射语句的解析

public void parseStatementNode() {
    //....省略
    
//3.最后解析SQL语句
//先是生产SqlSource对象。保存解析后的Sql语句
//该对象由langDriver产生,这部分主要是和动态Sql有关,临时省略
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
//下面生成KeyGenerator 
KeyGenerator keyGenerator;
//keyStatementId 和<SelectKey>的id解析方式一样,这就保证能取到前面<SelectKey>解析的结果
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))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

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

从以上代码能够看出,映射语句终于会由MapperBuilderAssistant解析成MappedStatement对象。最后看看该过程怎样实现

//MapperBuilderAssistant类
 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) {
//必须cache-ref解析完毕后才干继续
    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//最后由statementBuilder构建
    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)
        .resulSets(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官方教程及源代码解析——mapper映射文件的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis框架中Mapper映射配置的使用及原理解析 MapperProxy,MapperProxyFactory

MyBatis框架中Mapper映射配置的使用及原理解析 配置篇 SqlSessionFactoryBuilder,XMLConfigBuilder

Mybatis中@Mapper与@MapperScan配置及注入原理解析

mybatis源码配置文件解析之五:解析mappers标签

Springboot+Mybatis

Springboot+Mybatis