为了降低业务代码调用核心处理层的成本,MyBatis 采用策略模式提供了一个会话层,以简化操作。

策略模式

MyBatis 在会话层中用到了经典的策略模式模式,所以这里我们就先来介绍一下策略模式相关的知识点。

在编写业务逻辑时,实现某个功能的方法有很多。比如,如果我们需要按购买次数对用户购买的商品进行排序,以了解复购率最高的商品,可以选择多种排序算法,如归并排序、插入排序、选择排序等。根据不同的输入条件、数据量和运行环境,需选择合适的排序算法。许多人可能会用 if...else... 硬编码来选择不同的算法,但这不符合“开放-封闭”原则。添加新算法时必须修改 if...else... 代码块,破坏代码的稳定性。

在策略模式中,我们将每个算法单独封装成不同的算法实现类(这些类都实现了相同的接口)。每个算法实现类可以视作一种策略,我们只需选择不同的策略来解决业务问题。这样,每种算法都相对独立,算法内的变化边界也就清晰明了。新增或减少算法实现也不会影响其他算法的功能。

策略模式的核心类图如下:

策略模式的核心类图
  • 其中 StrategyUser 是算法的调用方,维护了一个 Strategy 对象的引用,用来选择具体的算法实现。

SqlSession

SqlSession 是 MyBatis 对外提供的一个 API 接口,整个 MyBatis 会话层也是围绕 SqlSession 接口展开的,SqlSession 接口中定义了下面几类方法:

  • selectXxx() 方法:用来执行查询操作的方法,SqlSession 会将结果集映射成不同类型的结果对象。例如:selectOne() 方法返回单个 Java 对象,selectList()selectMap() 方法返回集合对象。
  • insert()update()delete() 方法:用来执行 DML 语句。
  • commit()rollback() 方法:用来控制事务。
  • getMapper()getConnection()getConfiguration() 方法:分别用来获取接口对应的 Mapper 对象、底层的数据库连接和全局的 Configuration 配置对象。

如下图所示,MyBatis 提供了两个 SqlSession 接口的实现类,同时提供了 SqlSessionFactory 工厂类来创建 SqlSession 对象。

SqlSessionFactory 接口与 SqlSession 接口的实现类

DefaultSqlSession

我们通常在使用 MyBatis 时会使用默认的实现 DefaultSqlSession。在 DefaultSqlSession 中管理着一个 Executor 对象,通过这个对象来执行数据库操作和事务管理。

在选择使用哪种 Executor 实现时,DefaultSqlSession 使用了策略模式:它扮演了策略模式中的策略使用者(Strategy User)角色,而 Executor 接口则扮演了策略(Strategy)的角色。Executor 接口的不同实现则对应不同的策略实现(Strategy Implementation)。

下面接着来看 DefaultSqlSessionSqlSession 接口的实现。DefaultSqlSession 为每一类数据操作方法提供了多个重载,尤其是 selectXxx() 操作,而且这些 selectXxx() 方法的重载之间有相互依赖的关系,如下图所示:

select() 方法之间的调用关系

通过上图我们可以清晰地看到,所有 selectXxx() 方法最终都是通过调用 Executor.query() 方法执行 SELECT 语句、完成数据查询操作的,之所以有不同的 selectXxx() 重载,主要是对结果对象的需求不同。例如:

  • 我们使用 selectList() 重载时,希望返回的结果对象是一个 List 集合;
  • 使用 selectMap() 重载时,希望查询到的结果集被转换成 Map 类型集合返回;
  • 至于 select() 重载,则会由 ResultHandler 来处理结果对象。

DefaultSqlSession 中的 insert()update()delete() 等修改数据的方法以及 commit()rollback() 等事务管理的方法,同样也有多个重载,它们最终也是委托到 Executor 中的对应方法,完成数据修改操作以及事务管理操作的。

另外,DefaultSqlSession 还维护了一个 dirty 字段来标识缓存中是否有脏数据。在事务管理的相关方法中,DefaultSqlSession 会根据 dirty 字段以及 autoCommit 字段(是否自动提交事务)、用户传入的 force 参数(是否强制提交事务)共同决定是否提交/回滚事务,这部分逻辑位于 isCommitOrRollbackRequired() 方法中,具体实现如下:

  private boolean isCommitOrRollbackRequired(boolean force) {
  return (!autoCommit && dirty) || force;
}
  

DefaultSqlSessionFactory

DefaultSqlSessionFactory 是 MyBatis 中用来创建 DefaultSqlSession 的默认工厂实现。通过 DefaultSqlSessionFactory 工厂类,我们可以有两种方式拿到 DefaultSqlSession 对象。

第一种方式是通过数据源获取数据库连接,然后在其基础上创建 DefaultSqlSession 对象,其核心实现位于 openSessionFromDataSource() 方法,具体实现如下:

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    // 获取 Environment 对象
    final Environment environment = configuration.getEnvironment();
    // 获取 TransactionFactory 对象
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // 从数据源中创建 Transaction
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 根据配置创建 Executor 对象
    final Executor executor = configuration.newExecutor(tx, execType);
    // 在 Executor 的基础上创建 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();
  }
}
  

第二种方式是上层调用方提供数据库连接,在该连接上直接创建 DefaultSqlSession 对象,这种创建方式的核心逻辑位于 openSessionFromConnection() 方法中,核心实现如下:

  private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
  try {
    boolean autoCommit;
    try {
      // 获取事务提交方式
      autoCommit = connection.getAutoCommit();
    } catch (SQLException e) {
      // Failover to true, as most poor drivers
      // or databases won't support transactions
      autoCommit = true;
    }
    // 获取 Environment 对象、TransactionFactory
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // 通过 Connection 对象创建 Transaction
    final Transaction tx = transactionFactory.newTransaction(connection);
    // 创建 Executor 对象
    final Executor executor = configuration.newExecutor(tx, execType);
    // 创建 DefaultSqlSession 对象
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
  

SqlSessionManager

通过前面的 SqlSession 继承关系图我们可以看到,SqlSessionManager 同时实现了 SqlSessionSqlSessionFactory 两个接口,也就是说,它同时具备操作数据库的能力和创建 SqlSession 的能力

首先来看 SqlSessionManager 创建 SqlSession 的实现。它与 DefaultSqlSessionFactory 的主要区别是:

  • DefaultSqlSessionFactory 只支持多例模式:在线程每次获取 SqlSession 的时候,都会创建新的 SqlSession 对象;
  • SqlSessionManager 则有两种模式:
    • 多例模式:与 DefaultSqlSessionFactory 相同;
    • 单例模式SqlSessionManager 在内部维护了一个 ThreadLocal 类型的字段(localSqlSession)来记录与当前线程绑定的 SqlSession 对象,同一线程从 SqlSessionManager 中获取的 SqlSession 对象始终是同一个,这样就减少了创建 SqlSession 对象的开销。

无论哪种模式,SqlSessionManager 都可以看作是 SqlSessionFactory 的装饰器,SqlSessionManager 的构造方法会传入一个 SqlSessionFactory 对象,然后内部又创建了一个 SqlSession 的代理对象:

  private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
  this.sqlSessionFactory = sqlSessionFactory;
  this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[]{SqlSession.class},
    new SqlSessionInterceptor());
}
  

构造方法中创建的 SqlSession 代理类的 Handler 实现,是 SqlSessionManager 中的内部类 SqlSessionInterceptor,该代理在执行 SqlSession 方法前会尝试从 ThreadLocal 中获取 SqlSession,如果拿到了再执行 SqlSession 目标方法;如果拿不到则利用 SqlSessionManager.openSession() 创建新的 SqlSession 再执行目标方法。具体实现如下:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  // 从 ThreadLocal 中获取 sqlSession
  final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
  if (sqlSession != null) {
    try {
      // 拿到了直接执行目标方法
      return method.invoke(sqlSession, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  } else {
    // 拿不到利用 sqlSessionFactory 创建 SqlSession
    try (SqlSession autoSqlSession = openSession()) {
      try {
        // 执行目标方法
        final Object result = method.invoke(autoSqlSession, args);
        // 提交事务
        autoSqlSession.commit();
        return result;
      } catch (Throwable t) {
        autoSqlSession.rollback();
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }
}
  

使用多例模式

如果要使用 SqlSessionManager 的多例模式,可以直接调用 SqlSessionManager.openSession() 方法,它底层直接调用被装饰的 SqlSessionFactory 对象创建 SqlSession 对象并返回:

  public SqlSession openSession() {
  return sqlSessionFactory.openSession();
}
  

使用单例模式

如果要使用单例模式,则需要调用 startManagedSession() 方法为当前线程绑定 SqlSession 对象,这里的 SqlSession 对象也是由被装饰的 SqlSessionFactory 创建的,具体实现如下:

  public void startManagedSession() {
  this.localSqlSession.set(openSession());
}
  

SqlSession 绑定到 ThreadLocal 后,直接使用 SqlSessionManager 实现的 SqlSession 接口方法进行数据库操作即可自动调用这个单例,以 selectOne 为例:

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

该方法会调用 SqlSession 代理对象的同名方法,由上边的分析可知,代理方法内部会首先从 ThreadLocal 中得到 SqlSession 再使用。