【Mybatis】 1.执行流程

159 阅读8分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

版本

mybatis-3.5.3

正文

在执行中,实际上我们调用的,是被代理的类。具体可trace的点,在MapperProxy类中:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else if (method.isDefault()) {
      if (privateLookupInMethod == null) {
        return invokeDefaultMethodJava8(proxy, method, args);
      } else {
        return invokeDefaultMethodJava9(proxy, method, args);
      }
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

一般来说,我们会走下面的这个cachedMapperMethod和execute方法:

private MapperMethod cachedMapperMethod(Method method) {
  return methodCache.computeIfAbsent(method,
      k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

注意一下这里的mapperMethod,实际上包含了需要的大部分信息,同时这也留了一个问题:

  • 这些参数哪里来的,包含了这么多信息?

这里的method,实际上就是我们在mapper中调用的方法的名称了。

我们可以看到,这里的代理实际上做的工作是:

  • 缓存并生成mapperMethod对象
  • 使用这个对象并执行

我们先顺着流程往下看执行的部分(mapperMethod.execute).

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional()
            && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

这里需要注意:

  • command是对象中包含的预设值,在这里实际上是固定的。

  • 观察这个switch,实际上

    insert,update,delete【1】

    都是一样的套路,我们后续再看,先看看select:

    • 这里我们没有配置resultHandler,并且查询的是list,因此我们进入到了executeForMany方法。
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);
  }
  // issue #510 Collections & arrays support
  if (!method.getReturnType().isAssignableFrom(result.getClass())) {
    if (method.getReturnType().isArray()) {
      return convertToArray(result);
    } else {
      return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
    }
  }
  return result;
}

我们发现这里还和sql啊执行啊什么的一点没关系,使用的是我们的sqlSession,进行一个list的select。

我们这里的是SqlSession的子类SqlSessionTemplate(实际上这个都不在mybatis的包下了,而是在org.mybatis.spring,这里后面需要注意【2】),代码如下:

public <E> List<E> selectList(String statement, Object parameter) {
  return this.sqlSessionProxy.selectList(statement, parameter);
}

这里的代理是DefaultSqlSession,兜兜转转来到了这里:

@Override
public <E> List<E> selectList(String statement, Object parameter) {
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
​
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

这里又引入了我们不懂的东西了,这里的:

  • configuration【3】
  • executor【4】

是啥?哪来的?

按照惯例我们先掖着,这里我们看到了我们想看到的:

executor.query

到这里跟查询算有关系了,我们接着往下开始看:

  • 这里的executor是一个接口,包括

    • BaseExecutor【5】
    • CachingExecutor

    两个实现类,这里我们进去的是cachingExecutor。

我们找到的方法,它长这样:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
​
//上面调用的的query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    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);
  }
  • 这里的delegate也是个executor对象【6】,这里很明显用的是装饰器的设计模式。
  • tcmTransactionalCacheManager【7】对象,这里的执行大概可以看到,这个东西我们用来存取缓存的。

实际上这个方法定义了我们执行的一个套路,可以看到缓存在这里是会做更新的,我们大概可以知道:

  • 这里的缓存指的是数据的缓存了,之前的缓存是执行器的缓存。
  • 那么多培训机构说的几级几级缓存,指的都是啥?
  • 这里的缓存明显有个大问题,因为在分布式的场景下这里的缓存很有可能缓存的数据是过时而不自知的,实际上这里的ms.getCache() 得到的是一个null,这也就避免了过期数据被查出来,我们是在哪里配置这个救命稻草的【8】?

这里的问题都按下不表,我们顺着

delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

接着往下看。

  • 这里我们能看到executor包的全貌【9】,包括:

    • baseExecutor
    • batchExecutor
    • cachingExecutor
    • reuseExecutor
    • simpleExecutor

后续我们需要统一来看看,这些都有什么门道。

这里我们调用的是simpleExecutor,实际上继承了的是baseExecutor抽象类,我们调用的是baseExecutor中下述的方法:

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();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

惯例我们先抛出我们这里不打算解决的问题:

  • 这里的close何时配置,何时通知?【10】

  • 这里的resultHandler市什么。干啥用的,啥时候配的?【11】

  • 这里记这个栈(queryStack)干啥?看起来是要把这个缓存还是啥东西清了,是要干啥?【12】

    • 从层级上来说,这个装饰对象和修饰的对象中都有缓存,为什么要搞一层又一层,到底有几层缓存,存了啥,那几层可以关掉?【13】

因为我们没有缓存,因此我们执行了这个方法:

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
​
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
  • 我们可以看到这里是把数据写到缓存里面了。

实际上这里的doQuery是交付到子类上进行的,这次到SimpleExecutor里面了:

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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

这里:

  • 我们对statement进行了一些前期工作,这里的handler大概是用来处理的,根据名字大概是处理连接的【14】?
  • 我们大概知道这里的stmt对象是连接之类的东西【15】,按照惯例先放着
  • 在这里我们可以看到,boundSql这个对象被缝到handler里面了,也就是说在这里我们完成了SQL拼装的工作。
  • 这里注意try-finally关闭资源 接下来看
handler.query(stmt, resultHandler)

我们这里的handler是RoutingStatementHandler,点击去看看:

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  return delegate.query(statement, resultHandler);
}

上当了,还是装饰器,装饰器这里是PreparedStatementHandler:

  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }
  • 这里的ps实际上就直接连接到底层跟数据库的连接了【16】,我们是在 SimpleExecutor里面获得这个对象的,也就是说在这里被偷梁换柱了。

在这里的ps我们大概知道是把我们的SQL吭哧吭哧丢到数据库上进行执行了,然后就开始处理我们的结果集了。

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;
  ResultSetWrapper rsw = getFirstResultSet(stmt);
​
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  validateResultMapsCount(rsw, resultMapCount);
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    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++;
    }
  }
​
  return collapseSingleResultList(multipleResults);
}

在看代码之前有一个很有趣的结果集的知识(或者说常识吧):

  • 我们可以通过在结果集(就是mapper里面配一大堆resultMap啊之类的东西)里,配置 <id .../> 和...,来做到一条SQL拉出来一个list的复合对象(也就是说一个对象里包含着若干个多个其他对象的集合,如果需要还可以往下接着套娃)

接下来我们来看代码。

  • 这里有一步操作是getFirstResultSet(stmt) ,作用是将查出来的数据移到正确的位置,源码里是这么说的:
     // move forward to get the first resultset in case the driver
          // doesn't return the resultset as the first result (HSQLDB 2.1)

也就是说这里也算是一个Transactional script式的代码了,好在加了注解看懂了~

  • 这里我们可以看到:

    • while (rsw != null && resultMapCount > resultSetCount){
      ResultMap resultMap = resultMaps.get(resultSetCount);
      }
      

也就是说,我们查出来的数并不是固定的。代码结构上可以看出来第一个循环内,最重要的执行是以下这个方法:

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }
​
​
//上面方法的核心
  public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

在这里我们很清楚看到我们上面题外话是在哪里构建的了,就是handleRowValuesForNestedResultMap这个方法,显然我们那种套娃式的resultMap有个官方名称NestedResultMaps,在这里会把这些东西处理成数据【17】。

对应的下面的也是处理数据了,至此为止我们大致的将我们如何调用SQL,从头到尾梳理了一边。

小结

上面可能讲的层次较为模糊,总结一下如图:

可能打不开,链接复制一下应该能打开。

看完了上面大致的顺序,我们对mybatis查询主流程中涉及的一些组件有了初步的了解:

  • mapperProxy:我们调用的起点

  • mapperMethod:我们mapper方法中对应的配置以及内容属性

  • sqlSession:mapper到数据库的一些准备工作

    • 注意这里有两层缓存
  • statementHandler:到数据库执行SQL,并封装返回

    • 数据处理:resultHandler
  • 还有一个更底层的statement,是jdbc提供的一个接口规范,这部分我们在上述看完之后,可以考虑去看看我们实际上使用的Druid连接池是如何提供的。

在阅读代码的过程中,可以注意到有两个关键的参数对象:

  • sqlSession
  • configuration

组件基本上都是围绕这两个东西展开的,下一期我们就先以这两个对象,开始探究遗留的问题。