本文基于mybatis-spring 1.3.1和mybatis 3.4.4版本

本文分析一下Mybatis如何执行SQL查询。


文章目录

  • 一、调用Mapper接口代理对象
  • 二、SqlSessionTemplate
  • 三、创建SqlSession对象
  • 四、执行SQL查询
  • 五、总结


一、调用Mapper接口代理对象

mybatis启动时将MapperProxy类作为InvocationHandler对所有的mapper接口创建了代理,我们在程序中使用的对象都是代理,那么调用代理对象的方法都会调用MapperProxy的invoke方法。

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 (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //上面两个if判断表示如果是Object的方法或者是有方法体的实例方法,直接调用即可
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

MapperMethod会对调用的方法进行判断,只有调用接口的方法才会进入到mybatis中。
下面是MapperMethod的execute方法,execute方法内容涉及的比较多,本文只关注查询,因此下面代码把增删改的内容删除了:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          //如果返回是void并且入参有ResultHandler
          //下面方法里面会调用SqlSessionTemplate.select()
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          //如果返回值是集合
          //下面方法里面会调用SqlSessionTemplate.selectList()
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          //如果返回对象是Map并且配置了MapKey注解
          //下面方法里面会调用SqlSessionTemplate.selectMap()
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          //如果返回对象是Cursor
          //下面方法里面会调用SqlSessionTemplate.selectCursor()
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          //sqlSession其实是SqlSessionTemplate对象
          result = sqlSession.selectOne(command.getName(), param);
        }
        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;
  }

上面每个方法会再调用SqlSessionTemplate的selectXXX方法。下面来详细看一下SqlSessionTemplate类。

二、SqlSessionTemplate

MybatisAutoConfiguration作为mybatis自动配置类,它创建了SqlSessionTemplate。在SqlSessionTemplate的构造方法里面会创建SqlSession代理:

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {
      //代码有删减
    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;//异常翻译器,当数据库抛出异常,可以对异常统一做转码,屏蔽数据库底层
    //创建SqlSession代理
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }

在mybatis中,SqlSession并不直接对外暴露,所有对SqlSession的调用都通过SqlSessionTemplate调用代理对象sqlSessionProxy完成。大家可以看一下SqlSessionTemplate的代码,所有对SqlSessionTemplate的调用都委托给了sqlSessionProxy。所以MapperMethod.execute里面调用SqlSessionTemplate,其实都委托给了sqlSessionProxy。
在创建代理对象的时候,使用到了SqlSessionInterceptor,调用sqlSessionProxy时,需要执行SqlSessionInterceptor 的invoke方法。

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      //获得一个SqlSession对象
      //getSqlSession方法下面有介绍
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        //调用SqlSession对象的方法,比如selectOne,selectList
        //下面这一步调用会访问数据库,所以result是数据库的执行结果
        Object result = method.invoke(sqlSession, args);
        //判断当前事务是否由spring控制
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          //如果当前没有事务控制,那么直接调用commit方法提交事务
          //commit方法后文分析
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

下面是getSqlSession和closeSqlSession的代码:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
	//判断当前事务是否已经注册过SqlSession,或者说当前事务是否已经创建过SqlSession对象
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);[1]
	//如果当前事务已有SqlSession,那么便从holder中取出SqlSession对象,
	//并对SqlSession的引用计数+1
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }
	//如果当前不在事务中或者是事务执行的第一个SQL语句,
	//那么通过SqlSessionFactory创建一个SqlSession
    session = sessionFactory.openSession(executorType);
	//下面这个方法创建SqlSessionHolder,它持有上面创建的SqlSession对象,
	//之后将SqlSessionHolder对象注册到TransactionSynchronizationManager
	//这样当在一个事务中时,在[1]处,mybatis就可以取到SqlSession对象
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
    return session;
  }
	
  private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) {
    SqlSession session = null;
    if (holder != null && holder.isSynchronizedWithTransaction()) {
      if (holder.getExecutorType() != executorType) {
        throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
      }
	  //对SqlSession的引用计数+1
      holder.requested();

      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
      }
	  //从holder中取出SqlSession对象
      session = holder.getSqlSession();
    }
    return session;
  }

  public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
    notNull(session, NO_SQL_SESSION_SPECIFIED);
    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    if ((holder != null) && (holder.getSqlSession() == session)) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Releasing transactional SqlSession [" + session + "]");
      }
      //对SqlSession的引用计数-1
      holder.released();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Closing non transactional SqlSession [" + session + "]");
      }
      //如果当前不在事务中,则将SqlSession关闭,关闭了SqlSession也就关闭了数据库连接
      session.close();
    }
  }

下面对SqlSessionInterceptor的作用做一下总结:

  1. 对所有调用SqlSession的请求进行拦截;
  2. 通过DefaultSqlSessionFactory创建一个SqlSession对象;
  3. 如果当前在事务中,那么mybatis会将SqlSession对象注册到spring事务管理器中,事务纳入spring管理,如果不在事务中,mybatis对连接自主提交或者回滚。

三、创建SqlSession对象

下面分析一下DefaultSqlSessionFactory如何创建DefaultSqlSession。
创建DefaultSqlSession是在DefaultSqlSessionFactory的openSession方法中完成的:

public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
  }
  //入参execType是执行器类型,level表示隔离级别,
  //如果使用spring管理事务,level值不起作用,使用spring设置的隔离级别
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      //获得事务工厂,默认是SpringManagedTransactionFactory
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //根据事务工厂创建一个事务对象,默认是SpringManagedTransaction
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //创建一个执行器,执行器中包含了事务对象Transaction 
      final Executor executor = configuration.newExecutor(tx, execType);
      //创建DefaultSqlSession对象
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在openSessionFromDataSource中使用configuration配置对象、执行器、是否自动提交三个参数创建了DefaultSqlSession。这段代码相对还是比较简单的。

从openSessionFromDataSource可以看到如下三者之间的关系:

Java mybatis select 游标 mybatis游标查询原理_spring boot


再来说一下Transaction接口,该接口表示事务,如果当前是spring事务管理,那么接口实现类是SpringManagedTransaction,如果当前使用原生的数据库连接管理事务,那么实现类是JdbcTransaction,事务对象的作用是获得数据库连接、对事务提交或者回滚,也就是mybatis对连接的获取、事务的提交或者回滚都是通过Transaction接口完成的;

从上面的代码分析中可以看到,SqlSession对象、事务、数据库连接三者之间是一一对应关系。创建一个SqlSession对象意味着打开了一个数据库连接,开启了一个数据库事务。

关于SqlSession的提交和回滚文章后面再介绍。

四、执行SQL查询

SqlSession对象创建完之后,就要执行对象对应的查询方法了。
后面的流程比较繁琐,不详细介绍,只介绍大概流程。
DefaultSqlSession会将查询请求委托给Executor执行。比如DefaultSqlSession的方法select():

public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      //得到MappedStatement对象,该对象里面包含了sql语句、参数
      //每个@Select注解的内容都会放入MappedStatement对象
      MappedStatement ms = configuration.getMappedStatement(statement);
      //调用执行器执行查询
      executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

Mybatis提供了多种Executor实现:

Java mybatis select 游标 mybatis游标查询原理_mybatis_02


每个执行器的作用后面文章在做分析。下面以SimpleExecutor为例介绍后面的执行逻辑。

  1. 检查缓存是否存在要查询的数据,这里的缓存是一级缓存;
  2. 获取数据库连接,如果事务是spring管理,且在一个事务中,那么每次获得的连接都是同一个;
  3. 设置Statement对象的参数,比如fetchSize、超时时间等;
  4. 如果使用PreparedStatement对象,还要设置参数;
  5. 执行查询;
  6. 对查询结果使用ResultHandler对象处理。ResultHandler对查询结果做转换,比如转换为指定对象、Map对象。
  7. 将执行结果存入一级缓存中。将结果返回调用方。

五、总结

最后对整个的查询流程做一个总结:

Java mybatis select 游标 mybatis游标查询原理_sql_03