【Mybatis源码学习】Sql执行

本文深入探讨Mybatis的源码,从动态代理生成mapper开始,详细解析SQL执行过程,包括结果集处理。通过跟踪getMapper方法,了解JDK动态代理在生成Mapper接口实例中的应用。接着,分析SQL执行,从MapperProxy的invoke方法切入,解释不同返回类型的处理逻辑。最后,简述结果集处理,为后续的详细分析埋下伏笔。
摘要由CSDN通过智能技术生成

前言

上一篇【Mybatis源码学习】Sql解析中,我主要梳理了sql的源码解析过程,本章我通过同样的一个例子,来仔细瞧瞧sql是怎样执行下来的。主要分为三个步骤:

  • 动态代理生成mapper
  • sql执行
  • 结果集处理

还是通过同样的例子来开始本章的源码sql执行过程跟踪。

@Test
// 程序员喜欢的风格
public void quickStart() throws IOException {
    // 2.获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 3.获取对应mapper
    TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
    // 4.执行查询语句并返回单条数据
    TUser user = mapper.selectByPrimaryKey(2);
    System.out.println(user);
}

一、动态代理生成mapper

我们直接从这句开始跟下源码:

TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);

断点调式开始
在这里插入图片描述
我们看到SqlSession实例是DefaultSqlSession,点进去看看getMapper:
在这里插入图片描述
进入的是“大管家”Configuration的getMapper.
在这里插入图片描述

而真正给出接口代理类的是MapperRegistry.在这里插入图片描述
点进去看看mapperProxyFactory.newInstance(sqlSession)具体生成实例的代码:

// MapperProxyFactory
// 生成一个 MapperProxy对象
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

发现,运用的正是JDK动态代理。
梳理下该过程的时序图,如下:
在这里插入图片描述

二、sql执行

通过一中的过程,我们可以得到一个接口的mapperProxy动态代理对象了。sql的执行过程就是从MapperProxy的invoke方法开始的。
在这里插入图片描述
在这里插入图片描述
先去缓存中看看是否已经有了现成的MapperMethod,没有才回去new一个出来。
接下来,调用 MapperMethod 中的execute方法。
在这里插入图片描述
本例中,执行的是一个查询语句,进入case SELECT分支


case SELECT: // 如果是查询语句
        if (method.returnsVoid() && method.hasResultHandler()) { // 返回为void,且有结果处理器
          // 使用结果处理器执行查询
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) { // 多条结果查询
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) { // map结果查询
          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;

我们可以看到,根据返回的结果类型,进入不同的处理逻辑,主要有

  • 返回为void
  • 返回多条结果
  • 返回map结果
  • 返回单条结果

返回值为空的情况下,直接返回 result = null。其余几种情况内部都调用了sqlSession 中的selectList 方法。这里我们以selectOne为例。
convertArgsToSqlCommandParam()前面我们在【Mybatis源码学习】参数解析做过说明,主要进行参数的顺序映射解析。这里不做赘述。直接看看DefaultSqlSession.selectOne方法。

@Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

如果selectList查询返回1条,则直接返回;如果返回多条则抛出异常,否则直接返回null。
点进去看看selectList方法。

/**
   * 查询结果列表
   * @param <E> 返回的列表元素的类型
   * @param statement SQL语句
   * @param parameter 参数对象
   * @param rowBounds  翻页限制条件
   * @return 结果对象列表
   */
  @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();
    }
  }

这里说明下,mybatis默认是开启了二级缓存的,所以这里executor其实是CachingExecutor.我们可以在创建sqlSession中看看Configuration中创建执行器时的代码:
在这里插入图片描述
要关闭二级缓存,我们可以在mybatis-config.xml中显示配置,如下:

<!-- 二级缓存开关,默认为开启-->
<setting name="cacheEnabled" value="false"/>

关闭二级缓存,我们回到selectList方法中,继续跟下这段代码的执行过程:

executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER)

executor这里为SimpleExecutor,其首先调用的是BaseExecutor的query方法。
在这里插入图片描述

/**
   * 查询数据库中的数据
   * @param ms 映射语句
   * @param parameter 参数对象
   * @param rowBounds 翻页限制条件
   * @param resultHandler 结果处理器
   * @param key 缓存的键
   * @param boundSql 查询语句
   * @param <E> 结果类型
   * @return 结果列表
   * @throws SQLException
   */
  @SuppressWarnings("unchecked")
  @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) {
        // 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上
        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;
  }

先从一级缓存中查询是否已有结果,没有则才去查询数据库。我们直接看查询数据库的部分,也就是方法queryFromDatabase().
在这里插入图片描述
进入doQuery()方法,发现这里是个模版方法,由于前面提到我们此处的执行器为SimpleExecutor,所以这个方法来到了SimpleExecutor的doQuery()方法.

在这里插入图片描述
如上,该方法主要有三步:

  • 新建一个StatementHandler
  • 获取Statement
  • StatementHandler.query(实际调用的是PreparedStatementHandler)获取查询结果。

我们看看prepareStatement方法。

  //SimpleExcutor
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
	//获取数据库连接
    Connection connection = getConnection(statementLog);
    //创建Statement
    stmt = handler.prepare(connection);
    //为Statement设置IN参数
    handler.parameterize(stmt);
    return stmt;
  }

经典的流程:获取数据库连接;创建Statement; 为Statement设置IN参数。再看第三步执行语句:

  //PreparedStatementHandler
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
	// 执行SQL
    ps.execute();
	// 处理执行结果
    return resultSetHandler.<E> handleResultSets(ps);
  }

查询完毕,总结一下这个过程的时序图,一目了然:
在这里插入图片描述

三、结果集处理

结果集的映射,这部分我会在下一篇详细的跟进。Game over.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值