你知道Mybatis是如何参与Spring事务的吗?

44 阅读12分钟

书接上回

本文主要对上一篇关于 springboot和 mybatis集成中留下的几个问题通过梳理源码的方式进行解答,主要包括

Mybatis-spring 是如何进行mapperInterface 动态代理生成的 ?

Spring 为什么要 创建一个 SqlSessionTemplate ?

纠正一个错误

上一篇中提到 mapper 接口是在 org.mybatis.spring.mapper.MapperFactoryBean#checkDaoConfig 方法中通过 configuration.addMapper(this.mapperInterface); 将 mapperInterface 添加到 Configuration中的 mapperRegistry 中的,其实是有误的,通过debug调试发现,在checkDaoConfig 方法中,configuration.hasMapper(this.mapperInterface) 已经存在这个mapperInterface了,实际对mapperInterface的处理是在 SqlSessionFactoryBean 的 org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory 方法中进行的,而这里是通过 MybatisAutoConfiguration 配置 sqlSessionFactory 组件时触发的

可以看到由于一般会在application.properties 相关配置文件中添加 如下配置,所以 SqlSessionFactoryBean 在创建 SqlSessionFactory 的时候会对mapper的xml文件进行解析

mybatis.mapper-locations=classpath:mapper/*.xml

之后会进入到 org.apache.ibatis.builder.xml.XMLMapperBuilder#parse 的接口中对 Xml的mapper文件进行解析

这个parse方法主要做了两个事情

  configurationElement(parser.evalNode("/mapper"));

  将mapper.xml 中的 mapper 标签中的内容,也就是定义的各种 select 或者 update 标签的xml标签块,解析成 MappedStatement ,然后存储在 org.apache.ibatis.session.Configuration#mappedStatements 中,供后续获取使用,最终解析的结果如下:

  bindMapperForNamespace()

之后调用就是调用 Configuratation 中的MapperRegistry 通过org.apache.ibatis.binding.MapperRegistry#addMapper 一方面将 mapperInterface的class为key ,以 new MapperProxyFactory<>(type) 为value放入 mapper注册中心的 knownMappers map中

public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw  new BindingException( "Type " + type + " is already known to the MapperRegistry." );
    }
    boolean loadCompleted = false;
    try {
      knownMappers.put(type, new MapperProxyFactory<>(type));
      // It's important that the type is added before the parser is run
      // otherwise the binding may automatically be attempted by the
      // mapper parser. If the type is already known, it won't try.
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

下面详细看下 org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse 方法主要做了什么

public  void parse() {
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    parseCache();
    parseCacheRef();
    for (Method method : type.getMethods()) {
      if (!canHaveStatement(method)) {
        continue;
      }
      if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
          && method.getAnnotation(ResultMap.class) == null) {
        parseResultMap(method);
      }
      try {
        parseStatement(method);
      } catch (IncompleteElementException  e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}

重点看下 loadXmlResource(); 方法

由于 “namespace:com.future.common.mapper.UserMapper” 这个namespace实际上已经在之前解析mapper.xml的时候,已经解析过了,所以这里其实会跳过

private  void loadXmlResource() {
  // Spring may not know the real resource name so we check a flag
  // to prevent loading again a resource twice
  // this flag is set at XMLMapperBuilder#bindMapperForNamespace
  if (!configuration.isResourceLoaded( "namespace:" + type.getName())) {
    String xmlResource = type.getName().replace( '.' , '/' ) + ".xml" ;
    // #1347
    InputStream inputStream = type.getResourceAsStream( "/" + xmlResource);
    if (inputStream == null) {
      // Search XML mapper that is not in the module but in the classpath.
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException  e2) {
        // ignore, resource is not required
      }
    }
    if (inputStream != null) {
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource,
          configuration.getSqlFragments(), type.getName());
      xmlParser.parse();
    }
  }
}

之后会走到 org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement 方法,这个方法其实主要就是查看 MapperInterface中的 method 上是否有 一下注解 ,也就是使用的是注解类型的sql,所以显然也是没有的,下面的ifPresent的逻辑也会跳过,所以最后的解析 mapperInterface 中method 为MappedStatement ,并将其放入 org.apache.ibatis.session.Configuration#mappedStatements 也不会执行了

private  static  final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
.of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,
        InsertProvider.class, DeleteProvider.class)
    .collect(Collectors.toSet());
void parseStatement(Method method) {
  final Class<?> parameterTypeClass = getParameterType(method);
  final  LanguageDriver languageDriver = getLanguageDriver(method);

  getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
    final  SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass,
        languageDriver, method);
    final  SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
    final  Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options) x.getAnnotation())
        .orElse(null);
    final  String mappedStatementId = type.getName() + "." + method.getName();

    //............................  省略

    assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
        // ParameterMapID
        null, parameterTypeClass, resultMapId, getReturnType(method, type), resultSetType, flushCache, useCache,
        // TODO gcode issue #577
        false, keyGenerator, keyProperty, keyColumn, statementAnnotation.getDatabaseId(), languageDriver,
        // ResultSets
        options != null ? nullOrEmpty(options.resultSets()) : null, statementAnnotation.isDirtySelect());
  });
}

综上,所以对于使用了xml格式的写sql的方式,整个 bindMapperForNamespace 方法其实只是把 MapperInterface 放到了 Configuration 中 Mapper的注册中心里了,其他的逻辑,比如加载 MapperInterface对应的mapper的xml文件被跳过了,解析MapperInterface class中 method上是否有注解类型的sql这个逻辑也被跳过了

小总结

以上就是 spring通过调用在 SqlSessionFactoryBean中根据用户传入的xml的path ,手动构造了一个 XMLMapperBuilder ,之后通过 xmlMapperBuilder.parse(); 方法对xml 进行解析,并将xml中的各种定义的sql标签解析为MappedStatement 存入Configuration 中。之后对 xml中namespace对应的class进行注册到 Configuration 中的mapper的注册中心中,同时会尝试反向查找class对应的xml文件进行再次解析(前面就是从xml解析过来的,所以这里不会再次触发解析)。

从这里也可以看出,mybatis会从正反两个方向,通过xml找到namespace对应的mapperinterface class, 以及 MapperFactoryBean 中通过 MapperInterface class 反向查找对应的xml文件进行解析。

Mapper正向查询视角

前面已经通过解析,将xml中各种标签语句解析好了,就等正向的使用了,下面通过一个实际查询的例子来看Mapper正向查询的例子。从之前的文章可知,一个 MapperInterface就对应一个 MapperFactoryBean ,所以当代码中注入 一个 @Autowaire UserMapper userMapper时, 实际注入的类型是通过 org.mybatis.spring.mapper.MapperFactoryBean#getObject 获取到的实例,此处的 getSqlSession() 获取到是 SqlSessionTemplate

public T getObject() throws Exception {
  return getSqlSession().getMapper(this.mapperInterface);
}

@Override
public <T> T getMapper(Class<T> type) {
  return getConfiguration().getMapper(type, this);
}

可以看到在getMapper时,把自己也就是 SqlSessionTemplate 传过去了 ,对比mybatis的默认实现 DefaultSqlSession ,都是传递的this,但是传递的确实不同的 SqlSession的实现,这也说明了Spring 确实要通过 SqlSessionTemplate 解决一些问题, 不然直接使用 DefaultSqlSession 就可以了,至于要解决什么问题,后面会在 SqlSessionTemplate 部分详细讲解下,这里继续梳理下 mybatis查询的正向流程

getmapper最终走到如下的方法 org.apache.ibatis.binding.MapperRegistry#getMapper

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw  new BindingException( "Type " + type + " is not known to the MapperRegistry." );
  }
  try {
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception  e) {
    throw  new BindingException( "Error getting mapper instance. Cause: " + e, e);
  }
}

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


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

由于之前UserMapper class已经添加过了,所以这里会通过 return mapperProxyFactory.newInstance(sqlSession); 再次创建 一个 对 mapperInterface 的代理对象 MapperProxy,后续 mapperInterface 中所有方法的执行都会被 MapperProxy的 invoke 方法拦截

下面看下 MapperProxy 中的逻辑

 @Override
public  Object invoke(Object  proxy, Method  method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return  method.invoke(this, args);
    }
    // 先调用 cachedInvoker
    return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
  } catch (Throwable  t) {
    throw  ExceptionUtil.unwrapThrowable(t);
  }
}


private  MapperMethodInvoker cachedInvoker(Method  method) throws Throwable {
  try {
    return  MapUtil.computeIfAbsent(methodCache, method, m -> {
      if (!m.isDefault()) {
      // 此处会创建一个 PlainMethodInvoker ,内部组合了 MapperMethod
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
      try {
        if (privateLookupInMethod == null) {
          return  new DefaultMethodInvoker(getMethodHandleJava8(method));
        } else {
          return  new DefaultMethodInvoker(getMethodHandleJava9(method));
        }
      } catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException  e) {
        throw  new RuntimeException(e);
      }
    });
  } catch (RuntimeException  re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}

private  static  class  PlainMethodInvoker  implements  MapperMethodInvoker {
  private final MapperMethod mapperMethod;

  public  PlainMethodInvoker(MapperMethod  mapperMethod) {
    this.mapperMethod = mapperMethod;
  }

   //最终还是通过 MapperMethod来执行具体的逻辑
  @Override 
 public  Object invoke(Object  proxy, Method  method, Object[] args, SqlSession  sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
  }
}

可以看到构造一个Invoker,然后通过执行 Invoker的 invoke来执行具体的调用 ,注意这里又把 sqlSession 传入了,此处的SqlSession 是Spring创建的 SqlSessionTemplate ,可以看到下面的 execute 方法中最终还是通过 SqlSession的接口执行查询的。所以最终还是要经过 SqlSessionTemplate

public  Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    // ..........................  省略一些代码
    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);
        // 可以看到这里最终 还是通过调用 sqlSession的查询方法的,而此处的sqlSession是SqlSessionTemplate
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      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

前面铺垫了很多关于 mybatis 最终是如何 关联上 Spring 创建的SqlSessionTemplate 的,下面开始详细看下SqlSessionTemplate 的生命周期,以及为什么Spring 在有了 Mybatis 默认的SqlSession的实现的情况下,依然还要创建这个类

SqlSessionTemplate 的生命周期

创建

SqlSessionTemplate 是在 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration#sqlSessionTemplate 进行自动配置的 , 通过组合SqlSessionFactory 创建了一个 SqlSessionTemplate ,并最终通过构造函数的时候,在内部就默认创建一个SqlSession 的代理类,SqlSessionTemplate本身就实现了 SqlSession,然后在其实现方法中委托给内部创建的 SqlSession 的代理类 去执行操作,所以看了看出 SqlSessionTemplate本身的设计是结合了 适配器 以及门面模式的 。

 @Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
  ExecutorType executorType = this.properties.getExecutorType();
  if (executorType != null) {
    return  new SqlSessionTemplate(sqlSessionFactory, executorType);
  } else {
    return  new SqlSessionTemplate(sqlSessionFactory);
  }
}


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;
  this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}

看下 SqlSessionTemplate 内部创建的动态代理类

如下,所以调用SqlSession接口的方法都会走到这个动态代理里,所以搞懂这个方法相比mybatis默认的DefaultSqlSession 多做了哪些操作,基本上就能看出SqlSessionTemplate的目的了

invoke方法一上来就是先通过一个getSqlSession方法获取一个 SqlSession ,而这个SqlSession 其实也就是mybatis中默认的DefaultSqlSession, 最终的执行还是要通过委托给 DefaultSqlSession来走mybatis原先那一套,但是在获取过程中,spring做了一些操作,下面详细看下getSqlSession 方法

 @Override
public <T> T selectOne(String  statement) {
  return  this.sqlSessionProxy.selectOne(statement);
}


private  class  SqlSessionInterceptor  implements  InvocationHandler {
  @Override
 public  Object invoke(Object  proxy, Method  method, Object[] args) throws Throwable {
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
      Object result = method.invoke(sqlSession, args);
      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);
      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);
      }
    }
  }
}

org.mybatis.spring.SqlSessionUtils#getSqlSession

可以看到 先通过spring的事务管理器中(ThreadLocal)中是否存在SqlSessionFactory对应的SessionHolder,

如果有,就直接返回了。如果没有,说明当前没有事务或者事务还没执行过mybatis的操作流程,此时则会通过SqlSessionFactory 创建一个Sqlsession 。创建完成之后,如果当前存在事务环境,则把<SqlSessionFactory, SqlSessionHolder> 放入ThreadLocal中,之后注册一个 SqlSessionSynchronization 同步器 ,用来被Spring事务执行的时候进行回调,用来管理 SqlSession 的生命周期

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

  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
 
 //通过Spring的事务管理器看下当前ThreadLocal中是否存在以SessionFactory为key的 value(也就是SqlSessionHolder)
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
    return session;
  }

  LOGGER.debug(() -> "Creating a new SqlSession" );
  //如果上面没有获取到,则通过工厂创建一个SqlSession,这里就是Mybatis的原生方法了
  session = sessionFactory.openSession(executorType);

  //之后把获取到的 sqlSession 注册到 Spring的事务管理器上下文中
  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}

private  static  void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
  SqlSessionHolder holder;
  if (TransactionSynchronizationManager.isSynchronizationActive()) {
    Environment environment = sessionFactory.getConfiguration().getEnvironment();

    if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
      LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]" );

      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) {
        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 {
    LOGGER.debug(() -> "SqlSession [" + session
+ "] was not registered for synchronization because synchronization is not active" );
  }

}

总结下:

从上面也可以看出,SqlSessonTemplate 主要就是在通过代理的方式,在SqlSession的创建过程中,来和Spring的事务进行同步,保持在Spring一个事务的上下文中,对SqlSession的生命周期进行管理。这里的管理不仅仅是数据库连接的管理,而是SqlSession实例的管理。

其实如果只是需要保证Mybatis执行时和Spring事务使用的是同一个Connection的话,Spring-mybatis是没有必要对<SqlSessionFactory,SqlSession> 进行管理的。因为在Mybatis最终获取Connection的时候,和SqlSession是没有必然的关系的,比如在

org.apache.ibatis.executor.SimpleExecutor#prepareStatement 中 ,通过 Connection connection = getConnection(statementLog); 获取Connection ,

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;
}

protected Connection getConnection(Log statementLog) throws SQLException {
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  }
  return connection;
}

最终会走到 org.mybatis.spring.transaction.SpringManagedTransaction#openConnection 方法中 ,可以看到是通过 DataSourceUtils.getConnection(this.dataSource) 获取的 ,而这个方法底层可以看到还是通过ThreadLocal获取以datasource为key的 Connection, 所以只要事务管理器配置的数据源和mybatis使用的数据源是同一个,这里获取到的就会是同一个Connection.

private  void openConnection() throws SQLException {
  this.connection = DataSourceUtils.getConnection(this.dataSource);
  this.autoCommit = this.connection.getAutoCommit();
  this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

  LOGGER.debug(() -> "JDBC Connection [" + this.connection + "] will"
+ (this.isConnectionTransactional ? " " : " not " ) + "be managed by Spring" );
}

public  static Connection doGetConnection(DataSource dataSource) throws SQLException {
    Assert.notNull(dataSource, "No DataSource specified" );

    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
      if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
       conHolder.requested();
       if (!conHolder.hasConnection()) {
          logger.debug( "Fetching resumed JDBC Connection from DataSource" );
          conHolder.setConnection(fetchConnection(dataSource));
       }
       return conHolder.getConnection();
    }
    // Else we either got no holder or an empty thread-bound holder here.
    // ....................... 省略若干 ...................

    return con;
}

所以 SqlSessionTemplate 中代理方法中创建 <SqlSessionFactory , SqlSessionHolder> 以及 添加 SqlSessionSynchronization 到事务管理器的ThreadLocal 中的主要目的并不是为了保证事务上下文中Spring和Mybatis使用的Connection是同一个, 而是为了在事务的生命周期中,管理Mybatis的SqlSession对象 .

因为在mybatis中,SqlsessionFactory是一个线程安全的,可以是单例的,主要用来创建SqlSession的,所以Springboot 也是在配置类中申明了一个单例的 SqlsessionFactory 配置。

但是 mybatis 中的SqlSession 不是线程安全的,是一个有状态的对象。以前手动通过mybatis操作数据的时候,比如 ,在同一个线程的同一个方法中,也是会存在一个或者多个mapper的多个数据库操作在一个SqlSession中执行,不可能每一次执行一次DB查询操作都创建一个SqlSession实例,虽然可以这么搞 (DB的Connection是通过Spring的事务管理器获取的,同一个事务方法中),但是会创建大量的SqlSession,所以在 Spring事务开启的时候 ,Spring通过ThreadLocal 的方式从获取SqlSessionFactory 绑定的 SqlSession,就达到了Session 复用的目的。

同时在事务场景下,通过自定义的 SqlSessionSynchronization ,可以在spring事务挂起,提交,回滚等 回调****SqlSessionSynchronization ,从而管理SqlSession

但是在 非事务场景,mapper的每一次操作, 比如mapper的一次查询,再次执行同样的查询,都会创建不同的SqlSession ,这是和手动操作Mybatis有区别的地方,比如下面使用原生mybatis 在一次Session中操作了多次Mapper的操作,从而可以复用SqlSession,这点是有区别的,主要注意下

原生Mybatis操作数据库

// 单次操作(查询)
try (SqlSession session = sqlSessionFactory.openSession()) {
    User user = session.getMapper(UserMapper.class).selectById(1L);
    System.out.println(user);
}

// 多操作
try (SqlSession session = sqlSessionFactory.openSession(true)) { // false = 手动提交
    UserMapper mapper = session.getMapper(UserMapper.class);
    mapper.insert(user1);
    mapper.update(user2);
} catch (Exception e) {
    throw e;
}

一个容易被忽略的点

上面的解析中 , 涉及到了Spring的事务管理器 TransactionSynchronizationManager 组件, 这些内容会在下一篇 Spring 的事务解析中详细介绍下。而且从上面的解析中提到一点,就是 mybaits 获取 DB的 Connection 也是从ThreadLocal 中获取的 ,传递方向是从Spring -> mybatis ,

但是细想一下,这是不是就意味着 Spring是事务方法一进来就提前创建了 Connection 呢? 如果后续方法中如果有分支逻辑提前退出了方法,没有执行任何的db操作,那是不是意味着创建出来的 Connection 没有任何使用的地方 ?

关于这些问题会在下一篇关于Spring 事务的解析中去解答以及给出解决方案

ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

上一篇

mybatis-spring-boot-starter架构设计探秘