前面知道了SqlSession 初始化过程,那么下一步就看看Mybatis具体的增删改查逻辑。

本文以以下几个问题开展:

  1. Mybatis Mapper 代理对象获取流程
  2. 动态sql查询时,if,foreach等节点是怎么处理的?
  3. Mybatis动态Sql对如何读取参数的?
  4. @Param注解什么作用,什么情况没有@Param注解会读取不到参数?
  5. 对于结果集,Mybatis是如何处理的?
  6. Mybatis 一级二级缓存如何使用?

Mybatis代理对象

当使用 Mybatis是,我们只需要定义接口,而后定义对应 xml文件,就可以完成增删改查,接口对象怎么创建呢?

  1. 通过SqlSession 的 getMapper
public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
  }
  1. Configuration中 的getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

MapperRegistry 中保存了来自 xml配置下所有Mappers,并放到一个 Map<Class<?>, MapperProxyFactory<?>> 保存所有接口文件对应的 Mapper 构造器。

  1. 最后在 MapperRegistry 的 通过 在初始化中构造好的 MapperProxyFactorynewInstance,生成一个 由MapperProxy 包装的动态对象。
public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

而后通过JDK 反射获取对应接口:

protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

以上返回了一个动态代理类,该类实现了 mapperInterface 接口,并且以 MapperProxy 作为默认包装代理。
所以当使用 执行代理类方法,首先会进入 MapperProxyinvoke方法。

Select 查询分析

Mybatis 整体流程是比较好理解的,好理解的前提起始是我们都忽略了 其强大的XML解析配置,以及对动态SQL的强大支持。下面看看查询:

  1. 执行对应查询方法,例如 mapper.listAllActivedUsers(actived, list);
  2. 进入MapperProxyinvoke方法:期间会创建一个 MapperMethod 类,用于构建 代表增删改查的 SqlComand;以及方法签名包装类 MethodSignature,里面放着其返回值,是否返回list,是否返回的Cursor 等。
  3. MapperMethod 中,主要是判断 SqlComand 类型,增删改还是查,最后返回查询初的结果。
  4. 当Mybatis 判断为查询多个时候,则会进入 MapperMethodexcuteForMany 方法,这里面重要的代码是 Object param = method.convertArgsToSqlCommandParam(args);,param是解析 参数后的 Map。
    它会将传入参数以<key,value> 存储,以 @Param 的值作为key,并且还会固定以参数顺序存储一份 param1, param2, param3等固定名字参数,所以param中容量是实际传入的一倍
  5. 判断是否有传入 RowBounds分页参数,有则加载进入。
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
    ...
  1. DefaultSqlSession 对其进行 查询,其中会如果参数是collection或者数组,则会进行封装一层。
  2. 动态sql 拼装逻辑看下一节
  3. 查询时,首先会执行 CachingExecutorquery,这一层主要对 一级缓存进行相应逻辑判断,一级缓存默认开启:
@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      // 判断 MappedStatement 是否有缓存
    Cache cache = ms.getCache();
    if (cache != null) {
     // 有cache 判断是否要删除缓存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 从 TransactionalCacheManager 加载缓存
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
        // 查询后加入缓存
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  1. 而后拿着句柄就往下执行 BaseExecutorquery 方法, 在 里面有个 对 ErrorContext 进行的设定,关于 ErrorContext 可以看下下节。
  2. 如果第一次 查询,则会清除 localCachelocalOutputParameterCache 缓存,而下面对该次查询进行了重入性判断,当再次查询时则会直接从 localCache 获取。
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  3. 下一步即从数据库中查询,默认使用 SimpleExecutor进行 操作,应此Statement 也是一次性使用
@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 获取一个 StatementHandler, 也包括其中对过滤器链的操作
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 执行 prepare,即预检查
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

prepareStatement:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }
  1. 最后就是在 PreparedStatementHandler执行 sql,执行完后 使用 DefaultResultSetHandler.handleResultSets 处理结果。
  2. 处理查询结果主要将 查出结果 ResultSet 封装成结果返回,中途还会 处理 自定义 ResultSet 以及Lazy Loading 相关。下面基于 DefaultResultSetHandlerhandleResultSets
@Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
  // 设置当前动作
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    final List<Object> multipleResults = new ArrayList<>();
	// 设置检索
    int resultSetCount = 0;
    // 从 Statment 中获取 ResultSet 并封装
    ResultSetWrapper rsw = getFirstResultSet(stmt);
  // 获取ResultMap
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    // 计算resultMaps 数量
    int resultMapCount = resultMaps.size();
    // 简单校验result map 数量
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
    // 获取resultMap
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 处理结果成 resultMap形式,并放入multipleResult
      handleResultSet(rsw, resultMap, multipleResults, null);
      // 下一个resultSetWrapper
      rsw = getNextResultSet(stmt);
      // 将当前nestedResultObjects 内容
      cleanUpAfterHandlingResultSet();
      // 如果有则下一个
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
    // 嵌套查询
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }
	// 封装成list返回
    return collapseSingleResultList(multipleResults);
  }

动态SQL解析

下一个关键点就在 Mybatis的ongl表达式解析了,或许会有这样一个问题,为什么传入的参数mybatis可以理解,并且可以以动态sql形式解析,支持 if 和 foreach 等逻辑?
MappedStatementgetBoundSql 可以获取一个BoundSqlBoundSql 对象是一个从 SqlSource 中分析之后获取的 动态对象。最终 能从 xml 对象中将 动态sql解析出含有 和 参数的字段。

  1. 从 sqlSource中获取对象:
public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  1. 而后,在DynamicSqlSource中的 getBoundSql 进行进一步解析,在 DynamicSqlSource 中有变量 SqlNode, 动态sql主要组装逻辑就在 SqlNode 子类中。

    上面 SqlNode大致作用如下:
  • StaticTextSqlNode: 里面有个String 类型 text,主要是匹配xml中静态sql
  • MixedSqlNode : 里面有 List<SqlNode> ,主要是装载多种混合类型SqlNode,即可以为多种 SqlNode 的组合
  • TextSqlNode:用于存储解析 包含 ${} 占位符的动态SQL节点。将动态SQL,带 ${} 解析完成SQL语句的解析其,即将${}占位符替换成实际的变量值。
  • ForEachSqlNode:用于存储解析 <foreach> 节点值。
  • IfSqlNode : 用于解析 <if> 节点值,由于<if> 下面可能又是一个节点,所以里面还有一个 SqlNode contents
  • VarDeclSqlNode :处理动态xml标签<bind>的SqlNode类。
  • TrimSqlNode: 用于处理 <trim> 节点 的 SqlNode。
  • ChooseSqlNode: 用于处理 <choose> 标签的工具类。
    DynamicSqlSource 的 getBoundSql:
@Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 递归的处理动态sql节点
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

上面代码中,使用 rootSqlNode.apply(context); 解析动态sql节点,而后使用 SqlSourceBuilder,将 # 开头的占位符,全部使用 ? 替代,而后将解析出来参数设置到上下文 DynamicContext 中。
另外,对于处理 <foreach> 的逻辑,是将所有参数都以 ? 填充,而增加相应个数参数从而可以适配循环参数。

ErrorContext 作用

当使用Mybatis时候,是否会发现这样的感觉,如果哪个环节出错,能够迅速的找出问题,或者sql少写了一个点,或者 哪个 <if> 标签判断出错导致参数错误,或者是参数或者结果解析出问题,都能马上定位到问题。
这不是你厉害,而是Mybatis告诉你哪里出错了>-<

Mybatis就是使用ErrorContext 来帮你记录你sql运行的过程。
例如 : ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());

private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();

  private ErrorContext stored;
  private String resource;
  private String activity;
  private String object;
  private String message;
  private String sql;
  private Throwable cause;

看到 staticThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>(); 是一个很典型的 ThreadLocal 用法,每一次与数据库交互的查询,都能够被当前上下文保存下来。
ErrorContext 的构造方法是私有的,使用 instance 创建而成的 单例模式。

  1. resource:说明是加载哪一个资源,主要为 xml的Mapper文件
  2. activity:说明什么动作,例如查询就是 executing a query
  3. object:说明是执行mapper文件中,哪个语句,例如 anla.learn.mybatis.interceptor.dao.UserMapper.listAllActivedUsers
  4. message: 用于保存出错信息,调用点为 ExceptionFactory,而传入的message主要为 特定错误类型:
  5. sql:存入 boundSql中的sql,也就是通过动态sql解析过后的sql。在 BaseStatementHandler 中的 prepare 调用,所以所有与数据库交互语句都会在这里初始化 sql。
  6. cause : 主要存储错误栈,配合 上面 message 使用。

在每次执行完并且没有错,都会将当前 ErrorContext 清除。

一级缓存和二级缓存

其实一级缓存二级缓存命名比较别扭,因为事实上,如果开启了二级缓存,则最终缓存调用顺序为:
二级缓存->一级缓存->数据库。因为二级默认不开启,所以一般是一级缓存,如果一级缓存中没有,则到数据库。
一二级缓存这样区分,主要是Mybatis上有相关注释说明了 TransactionalCacheManager 为二级缓存。

  • 一级缓存:MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
  • 二级缓存
    二级缓存存储位置是 CachingExecutorTransactionalCacheManager 中,默认不开启。

下面简单分析下一二级缓存及其原理:

一级缓存

一级缓存在BaseExecutorPerpetualCache 中,默认开启。作用域默认是 SESSION,可以选择为STATEMENT。如果为SESSION,则在整个SqlSession中都会共享该缓存,而如果为Statement,则本次查询结束,则会清除该缓存。

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  // 定义当前上下文操作
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 看是否需要清空缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 尝试从缓存中读取
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      // 直接查询数据库
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      // 如果是STATEMENT 域缓存,则用完直接清空
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

一级缓存实现原理较为简单, PerpetualCache里面主要 定义一个 Map<Object, Object> cache 类型全局变量,实现了缓存操作。
总结:

  1. 一级缓存默认开启,默认为 SESSION级别
  2. Mybatis 一级缓存声明周期和 SqlSession一致,数据只在 SqlSession内部(配置为SESSION时)
  3. Mybatis一级缓存设计为一个没有容量的HashMap。
  4. 以及缓存范围为SqlSession,在多个SqlSession或者分布式下,会引起脏数据,建议设定为 STATEMENT。
二级缓存

二级缓存主要使用实际上在一级缓存之前就会调用,在 CachingExecutorquery 中,但是需要设置Setting:

<setting name="cacheEnabled" value="true"/>

实际在 3.5.2 中, 这个默认是开启。
第二个是需要在xml中增加 <cache/> 标签。并配置好namespace

从而在 读取配置时,会初始化 MappedStatement 中Cache,在 CachingExecutor 中会进行引用:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      // 如果配置了 二级缓存以及相应namespace,则会有Cache
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 没有则需要进行查询
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。