Session 会话模块
本文将介绍 MyBatis 的 session 会话模块,以及策略模式的概念、SqlSession、DefaultSqlSession、DefaultSqlSessionFactory、SqlSessionManager 的原理与实现。
为了降低业务代码调用核心处理层的成本,MyBatis 采用策略模式提供了一个会话层,以简化操作。
策略模式
MyBatis 在会话层中用到了经典的策略模式模式,所以这里我们就先来介绍一下策略模式相关的知识点。
在编写业务逻辑时,实现某个功能的方法有很多。比如,如果我们需要按购买次数对用户购买的商品进行排序,以了解复购率最高的商品,可以选择多种排序算法,如归并排序、插入排序、选择排序等。根据不同的输入条件、数据量和运行环境,需选择合适的排序算法。许多人可能会用 if...else...
硬编码来选择不同的算法,但这不符合“开放-封闭”原则。添加新算法时必须修改 if...else...
代码块,破坏代码的稳定性。
在策略模式中,我们将每个算法单独封装成不同的算法实现类(这些类都实现了相同的接口)。每个算法实现类可以视作一种策略,我们只需选择不同的策略来解决业务问题。这样,每种算法都相对独立,算法内的变化边界也就清晰明了。新增或减少算法实现也不会影响其他算法的功能。
策略模式的核心类图如下:
![策略模式的核心类图](images/strategy.webp)
- 其中
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 接口的实现类](images/sql-session.webp)
DefaultSqlSession
我们通常在使用 MyBatis 时会使用默认的实现 DefaultSqlSession
。在 DefaultSqlSession
中管理着一个 Executor
对象,通过这个对象来执行数据库操作和事务管理。
在选择使用哪种 Executor
实现时,DefaultSqlSession
使用了策略模式:它扮演了策略模式中的策略使用者(Strategy User)角色,而 Executor
接口则扮演了策略(Strategy)的角色。Executor
接口的不同实现则对应不同的策略实现(Strategy Implementation)。
下面接着来看 DefaultSqlSession
对 SqlSession
接口的实现。DefaultSqlSession
为每一类数据操作方法提供了多个重载,尤其是 selectXxx()
操作,而且这些 selectXxx()
方法的重载之间有相互依赖的关系,如下图所示:
![select() 方法之间的调用关系](images/default-sql-session.webp)
通过上图我们可以清晰地看到,所有 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
同时实现了 SqlSession
和 SqlSessionFactory
两个接口,也就是说,它同时具备操作数据库的能力和创建 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
再使用。