MyBatis 接口层:策略模式与 SqlSession

为了降低业务代码调用核心处理层的成本,MyBatis 通过策略模式为我们提供了一个接口层,以简化操作。

1 策略模式

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

我们在编写业务逻辑的时候,可能有很多方式都可以实现某个具体的功能。例如,按照购买次数对一个用户购买的全部商品进行排序,从而粗略地得知该用户复购率最高的商品,我们可以使用多种排序算法来实现这个功能,例如,归并排序、插入排序、选择排序等。在不同的场景中,我们需要根据不同的输入条件、数据量以及运行时环境,选择不同的排序算法来完成这一个功能。很多同学可能在实现这个逻辑的时候,会用 if...else... 的硬编码方式来选择不同的算法,但这显然是不符合“开放 —— 封闭”原则的,当需要添加新的算法时,只能修改这个 if...else... 代码块,添加新的分支,这就破坏了代码原有的稳定性。

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

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

策略模式的核心类图

2 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 接口的实现类

2.1 DefaultSqlSession

默认情况下,我们在使用 MyBatis 的时候用的都是 DefaultSqlSession 这个默认实现DefaultSqlSession 中维护了一个 Executor 对象,通过它来完成数据库操作以及事务管理。DefaultSqlSession 在选择使用哪种 Executor 实现的时候,使用到了策略模式:DefaultSqlSession 扮演了策略模式中的 StrategyUser 角色,Executor 接口扮演的是 Strategy 角色,Executor 接口的不同实现则对应 StrategyImpl 的角色。

另外,DefaultSqlSession 还维护了一个 dirty 字段来标识缓存中是否有脏数据,它在执行 update() 方法修改数据时会被设置为 true,并在后续参与事务控制,决定当前事务是否需要提交或回滚。

下面接着来看 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 字段以及 autoCommit 字段(是否自动提交事务)、用户传入的 force 参数(是否强制提交事务)共同决定是否提交/回滚事务,这部分逻辑位于 isCommitOrRollbackRequired() 方法中,具体实现如下:

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

3 DefaultSqlSessionFactory

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 获取 Environment 对象
final Environment environment = configuration.getEnvironment();
// 获取 TransactionFactory 对象
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 从数据源中创建 Transaction
final Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根据配置创建 Executor 对象
final Executor executor = configuration.newExecutor(tx, execType);
// 在 Executor 的基础上创建 DefaultSqlSession 对象
return new DefaultSqlSession(configuration, executor, autoCommit);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
boolean autoCommit;
try {
  // 获取事务提交方式
  autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
  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);

4 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 的代理对象:

1
2
3
4
5
6
7
private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
  this.sqlSessionFactory = sqlSessionFactory;
  this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[]{SqlSession.class},
    new SqlSessionInterceptor());
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 对象并返回:

1
2
3
public SqlSession openSession() {
  return sqlSessionFactory.openSession();
}

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

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

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

1
2
3
public <T> T selectOne(String statement) {
  return sqlSessionProxy.selectOne(statement);
}
  • 该方法会调用 SqlSession 代理对象的同名方法,由上边的分析可知,代理方法内部会首先从 ThreadLocal 中得到 SqlSession 再使用。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容