转载

Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?

Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?

如果拿一次数据库请求操作做比喻,那么前面3篇文章就是在做请求准备,真正执行操作的是本篇文章要讲述的内容。正如标题一样,本篇文章最最核心的要点就是 SqlSession实现数据库操作的源码解析。但按照惯例,我这边依然列出如下的问题:

  • 1、 SqlSession 是如何被创建的? 每次的数据库操作都会创建一个新的SqlSession么?(也许有很多同学会说SqlSession是通过 SqlSessionFactory.openSession() 创建,但这个答案按照10分制顶多给5分)
  • 2、 SqlSession与事务(Transaction)之间的关系? 在同一个方法中,Mybatis多次请求数据库,是否要创建多个SqlSession?
  • 3、 SqlSession是如何实现数据库操作的?

本章内容就是围绕着上面三个问题进行解析,那么带着问题去看源码吧!

一、SqlSession 的创建

在学习Mybatis时,我们常常看到的 SqlSession 创建方式是 SqlSessionFactory.openSession() ,那么我们就拿此作为切入点,先来看看 SqlSessionFactory.openSession() 的方法源码(注意 是 DefaultSqlSessionFactory ),其内部是调用 openSessionFromDataSource() 方法,那么我们来下这个方法内部源码:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      // 创建了一个 TransactionFactory 事务工厂( 如果有仔细看过 SqlSessionFactoryBean.buildSqlSessionFactory() 过程的同学,应该能够看到是 SpringManagedTransactionFactory )
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 通过 TransactionFactory 获取了一个 事务 Transaction 
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 根据 execType(默认是 SIMPLE ) 获取了一个 Executor (真正执行数据库操作的对象)
      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();
    }
  }

整个SqlSession 的创建分 3个步骤:

  • 1、 获取到 TransactionFactory 事务工厂对象 ( 如果有仔细看过 SqlSessionFactoryBean.buildSqlSessionFactory() 过程的同学,应该能够看到是 SpringManagedTransactionFactory )
  • 2、 通过 TransactionFactory 获取了一个 事务 Transaction
  • 3、 根据 execType(默认是 SIMPLE ) 获取了一个 Executor (真正执行数据库操作的对象)
  • 4、 创建并返回 DefaultSqlSession 对象

通过源码我们知道每次 SqlSession(准确地说是 DefaultSqlSession )的创建都会 有一个 Transaction(在Mybatis-Spring 中 是 SpringManagedTransaction ) 事务对象 的生成。也就是说:

  • 1、 一个事务 Transaction 对象与一个 SqlSession 对象 是一一对应的关系。
  • 2、 同一个SqlSession 不管执行多少次数据库操作。只要没有执行close,那么整个操作都是在同一个 Transaction 中执行的。

看过之前的文章的同学应该有疑问,之前不管是创建MapperProxy 的 SqlSession 还是 MapperMethod中调用的SqlSession其实都是 SqlSessionTemplate ,与这里的 DefaultSqlSession 不是同一个SqlSession对象。那么我们就来简单分析下这2者之间的区别与职责吧!

SqlSessionTemplate与 DefaultSqlSession 之间不可告人的秘密

在之前的文章中,我们讲到过 每创建一个 MapperFactoryBean 就会创建一个 SqlSessionTemplate 对象,而 MapperFactoryBean 在获取 MapperProxy 时会将 SqlSessionTemplate 传递到 MapperProxy中。 也就是说 SqlSessionTemplate 的生命周期是与 MapperProxy 的生命周期是一致的。( 注意: MapperProxy 是被注入到Spring容器中的,所以结果不难想象)

在之前的文章中,我们简单的描述过 SqlSessionTemplate 内部维护了一个 sqlSessionProxy ,而 sqlSessionProxy 是通过动态代理创建的一个 SqlSession 对象, SqlSessionTemplate 的 数据库操作方法 insert/update 等等都是委托 sqlSessionProxy 来执行的。那么我们想看下这个 sqlSessionProxy 的创建:

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    // 通过 Proxy.newProxyInstance() 动态代理创建的 
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }

在创建 sqlSessionProxy 的代码中,我们可以发现其指定的代理对象是 SqlSessionInterceptor (SqlSession拦截器?),那么关键代码肯定在这个 SqlSessionInterceptor 中,查看 SqlSessionInterceptor发现其是一个内部类,其源码如下:

private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 根据条件获取一个SqlSession(注意此时的SqlSession 是 DefaultSqlSession ),此时的SqlSession 可能是新创建的,也可能是上一次的请求的SqlSession。
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        // 反射执行 SqlSession 方法
        Object result = method.invoke(sqlSession, args);
        // 判断当前的 SqlSession 是有事务,如果有则不commit
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        // 判断如果是PersistenceExceptionTranslator且不为空,那么就关闭当前会话,并且将sqlSession置为空防止finally重复关闭
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          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);
        }
      }
    }
  }

整个 invoke 分5个步骤:

  • 1、 根据条件获取一个SqlSession(注意此时的SqlSession 是 DefaultSqlSession ),此时的SqlSession 可能是新创建的,也可能是上一次的请求的SqlSession。
  • 2、 反射执行 SqlSession 方法
  • 3、 判断当前的 SqlSession 是否由事务所管控,如果是则不commit
  • 4、 判断如果是PersistenceExceptionTranslator且不为空,那么就关闭当前会话,并且将sqlSession置为空防止finally重复关闭
  • 5、 只要当前会话不为空, 那么就会关闭当前会话操作,关闭当前会话操作又会根据当前会话是否有事务来决定会话是释放还是直接关闭。

其中步骤1 是本次讨论的核心,其内部流程如下:

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

    notNull(sessionFactory, "No SqlSessionFactory specified");
    notNull(executorType, "No ExecutorType specified");
    
    // 通过 TransactionSynchronizationManager (Spring 的一个事务同步管理器) 获取到一个 SqlSessionHolder (从字面意思就应该明白其内部维护有 SqlSession)
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    // 判断SqlSessionHolder是否为空、是否与事务同步
    if (holder != null && holder.isSynchronizedWithTransaction()) {
      if (holder.getExecutorType() != executorType) {
        throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
      }
      // 将引用计数增加1
      holder.requested();
      // 返回持有的 SqLSession
      return holder.getSqlSession();
    }

    // 如果从 事务同步管理器 没能获取到 一个 SqlSessionHolder 则 调用  sessionFactory.openSession() 新建一个SqlSession 
    SqlSession session = sessionFactory.openSession(executorType);
    
    // 判断当前是否有事务,有则 根据 SqlSession 创建一个 SqlSessionHolder 并将其注册进入到 TransactionSynchronizationManager 中,以供当前事务中的下次使用
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      Environment environment = sessionFactory.getConfiguration().getEnvironment();

      if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
        holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
        TransactionSynchronizationManager.bindResource(sessionFactory, holder);
        TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
        holder.setSynchronizedWithTransaction(true);
        holder.requested();
      } else {
        if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
          if (logger.isDebugEnabled()) {
            logger.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
          }
        } else {
          throw new TransientDataAccessResourceException(
              "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
        }
      }
    } else {
      if (logger.isDebugEnabled()) {
        logger.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
      }
    }

    return session;
  }

整个流程步骤分一下几步:

  • 1、 通过 TransactionSynchronizationManager (Spring 的一个事务同步管理器) 获取到一个 SqlSessionHolder (从字面意思就应该明白其内部维护有 SqlSession)
  • 2、 判断SqlSessionHolder是否为空、是否与事务同步,是则返回持有的 SqLSession
  • 3、 如果从 事务同步管理器 没能获取到 一个 SqlSessionHolder 则 调用 sessionFactory.openSession() 新建一个SqlSession
  • 4、 判断当前是否有事务,有则 根据 SqlSession 创建一个 SqlSessionHolder 并将其注册进入到 TransactionSynchronizationManager 中,以供当前事务中的下次使用

从上面的步骤中,我们发现整个获取SqlSession都与 事务 有极大的关联关系,并且从上面的流程中,我们能够得到几个关键点信息:

  • 1、 同一事务中 不管调用多少次 mapper里的方法 ,最终都是用得同一个 sqlSession,即 一个事务中使用的是同一个sqlSession。
  • 2、 如果没有开启事务,调用一次mapper里的方法将会新建一个 sqlSession 来执行方法。

如果细心的同学会发现这个 TransactionSynchronizationManager 事务同步管理器 是由Spring 持有的,也就是说这里完美的应证了 Mybatis-Spring 中对该子项目的描述:

MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。 它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean中

DEBUG测试发现的问题

作者在进行测试DEBUG时发现,只要是开启了事务,最终都会通过 TransactionSynchronizationManager.getResource(sessionFactory) 获取到 SqlSessionHolder ,即便是重启程序后第一次请求也是一样会获取到,并且作者 在 SqlSessionHolder 和 DefaultSqlSession 的 构造函数中打上断点 也并未走到这里,因此作者实在百思不得其解,猜测 TransactionSynchronizationManager.getResource(sessionFactory) 该方法 内部有通过反射动态的创建了SqlSession,但看源码确实没找到,目前实力不被允许!!

成功获取到 SqlSession(准确的说是 DefaultSqlSession ) 后 通过 method.invoke() 反射调用到具体的 DefaultSqlSession 方法。方法调用完成后,判断当前SqlSession是否被事务所管控,如果是则不commit,最后再调用 closeSqlSession() 方法进行SqlSession “关闭”。这里为什么要打引号呢?原因是该方法内部处理时判断了当前SqlSession是否被事务所管控,是的话仅仅将引用计数器减一,并未真正将SqlSession 关闭(这也是为了下次能够使用同一个SqlSession),如果不被事务管控则执行正在的 session.close() 操作。

至此,我们明白了 一个 SqlSession(DefaultSqlSession) 的创建不仅仅是调用一下 sessionFactory.openSession() 那么简单,这其中关联到了 SqlSessionTemplateDefaultSqlSessionSqlSessionInterceptor 以及 Spring 事务(Transaction)(TransactionSynchronizationManager) 。 所以一定要明白这几个对象之间的联系和作用。

二、SqlSession实现数据库操作

从上面的分析我们知道,任何的Mapper接口方法请求最终都会请求到 DefaultSqlSession ,即 DefaultSqlSession 内部封装了数据库操作,其他 SqlSession 子类最终都得依靠它来操作数据库。那么我们就拿 DefaultSqlSession 内部的 selectList() 方法开始讲述其如何封装了数据库操作。

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // 从 configuration 中获取到 指定方法的 MappedStatement (注意:statement 是由 MapperMethod 中的 SqlCommand 的 name 字段传下来的,而name 本身就来源于 MappedStatement 的 id ,所以最终  statement 会是 com.xxx.findUserByName 这种形式)
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 通过 委托 Executor 的 query() 执行真正的数据库操作
      List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      return result;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

我们可以发现 DefaultSqlSession 的所有方法 基本上都有以下2个步骤:

  • 1、 从 configuration 中获取到 指定方法的 MappedStatement
  • 2、 通过 委托 Executor 来 执行真正的数据库操作

我们知道 MappedStatement 内部保存了所要执行的方法的 SqlSource (保存有从Mapper.xml中解析出来的Sql片段信息),然后通过 Executor 的 query() 方法来执行数据库操作。我们先来看下 Executor 继承关系图:

Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?

我们从继承关系图中可以看到 BaseExecutorCachingExecutor ,它们2个分别代表2中缓存机制, BaseExecutor 内部维护了 名为 localCache (PerpetualCache) 的 对象,该对象就是 一级缓存 的实际控制者, CachingExecutor 在二级缓存时使用,其内部实现时通过委托 BaseExecutor 来实现一级缓存的。关于Mybatis缓存机制的内容,大家可以去看看这篇文章—— 聊聊MyBatis缓存机制

不管是一级缓存还是二级缓存机制,其最终还是会调用到 BaseExecutor ,而Mybatis默认的 BaseExecutor 实现是 SimpleExecutor ,所以重点关注这2个类的实现,下面是 BaseExecutor 的 query()源码:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 从 MappedStatement 中获取到 BoundSql(实际上是通过 调用 MappedStatement 中的 SqlSource  的 getBoundSql() 获取)
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 通过参数 解析出 cacheKey ,这个是一级缓存的key 
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

我们可以看到整个方法内部最核心的一点就是 从 MappedStatement 中获取到 BoundSql(实际上是通过 调用 MappedStatement 中的 SqlSource 的 getBoundSql() 获取) ,最后再调用重载方法 query(),这个重发方法做了一级缓存的操作,这里就不描述了,只要知道最后调到了 SimpleExecutor 的 doQuery() 方法就行,查看 doQuery() 方法源码 :

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      // 从  MappedStatement 中获取到 Configuration 
      Configuration configuration = ms.getConfiguration();
      // 通过 Configuration 的 newStatementHandler() 方法创建了一个 StatementHandler 对象
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 调用 prepareStatement() 方法 获取到 Statement 对象 (真正执行静态SQl的接口)
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 调用 StatementHandler.query() 方法执行
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

从这里开始就与JDBC 挂钩了,如果熟悉JDBC的同学应该一眼就知道 Statement 这个对象具体时干嘛的了,按照惯例我们还是先分析下 doQuery() 方法内部执行流程步骤:

  • 1、 从 MappedStatement 中获取到 Configuration
  • 2、 通过 Configuration 的 newStatementHandler() 方法创建了一个 StatementHandler 对象
  • 3、 调用 prepareStatement() 方法 获取到 Statement 对象 (真正执行静态SQl的接口)
  • 4、 调用 StatementHandler.query() 方法执行(其实内部委托 Statement 来执行的)

其中不走2、3、4是重点要解析的。 我们先来看下 步骤 2 ,其中涉及到一个关键对象 StatementHandler ,其继承关系图如下:

Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?

这个结构与Executor 的类似:

  • 1、 SimpleStatementHandler ,这个对应的 就是JDBC 中常用到的 Statement 接口,用于简单SQL的处理
  • 2、 PreparedStatementHandler , 这个对应的就是JDBC中的 PreparedStatement ,用于 预编译SQL 的处理
  • 3、 CallableStatementHandler , 这个对应JDBC中 CallableStatement ,用于执行存储过程相关的处理
  • 4、 RoutingStatementHandler,这个接口是以上三个接口的路由,没有实际操作,只是负责上面三个StatementHandler的创建及调用

回过头来再看 configuration.newStatementHandler() ,不用猜测,肯定创建的是 RoutingStatementHandler ,并且其内部 的 delegate 默认是 PreparedStatementHandler (MappedStatement builder方法指定了默认的 statementType = StatementType.PREPARED )。

步骤三主要的作用就是 预编译并 获取到Statement(由于是 PreparedStatementHandler 所以默认获取到的是 PreparedStatement), 其 prepareStatement() 方法源码:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 创建 Connection 链接,查看源码其是通过 transaction.getConnection() 获取到的。
    Connection connection = getConnection(statementLog);
    // 预编译获取到 PrepareStatement ,即 最终会调用到 connection.prepareStatement() 方法
    stmt = handler.prepare(connection);
    // 设置参数信息,其参数是通过 从 BoundSql 获取
    handler.parameterize(stmt);
    return stmt;
  }

整个流程分3个步骤:

  • 1、 创建 Connection 链接,查看源码其是通过 transaction.getConnection() 获取到的。
  • 2、 预编译获取到 PrepareStatement ,即 最终会调用到 connection.prepareStatement() 方法
  • 3、 设置参数信息,其参数是通过 从 BoundSql 获取

步骤4会 调用 StatementHandler.query() 方法,以 PreparedStatementHandler 为例,其源码如下:

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 执行SQL
    ps.execute();
    // 通过 ResultSetHandler 的 handleResultSets() 方法解析返回数据
    return resultSetHandler.<E> handleResultSets(ps);
  }

这个方法内部就2个流程步骤:

  • 1、 执行SQL
  • 2、 通过 ResultSetHandler 的 handleResultSets() 方法解析返回数据

这里的 ResultSetHandler 就是用来处理 JDBC中的 ResultSet (相信用过JDBC 的同学对这个不陌生),关于如果解析返回数据的逻辑这里就不在详细分析了。

我们通过上面的分析不难发现从 SqlSession 委托 Executor 执行数据库开始,整个 Executor 的执行操作其实就是封装了 JDBC 的一个执行操作,如果熟悉JDBC的同学相信一眼就能看出来。

三、个人总结

SqlSession与 事务 的关系:

  • 1、 同一事务中 不管调用多少次 mapper里的方法 ,最终都是用得同一个 sqlSession,即 一个事务中使用的是同一个sqlSession。
  • 2、 如果没有开启事务,调用一次mapper里的方法将会新建一个 sqlSession 来执行方法。

SqlSession的一次执行流程图:

Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?

本文由博客一文多发平台 OpenWrite 发布!

原文  https://segmentfault.com/a/1190000021084278
正文到此结束
Loading...