目录
概述
正如其名,SqlSession代表着一次SQL会话,是MyBatis提供给用户的顶层接口,用户通过它来访问数据库。源码对该类的注释如下:
/**
* The primary Java interface for working with MyBatis.
* Through this interface you can execute commands, get mappers and manage transactions.
*
* @author Clinton Begin
*/
public interface SqlSession extends Closeable {
正如注释所说,SqlSession主要有3个功能:
- 执行命令:这正是SqlSession最主要的用处:执行SQL语句。
- 获取Mapper:这是SqlSession提供的另一种交互方式,也就是现阶段最常用的通过Mapper接口来执行命令的方式。随后说。
- 管理事务。
接下来将从源码出发,分析SqlSession是如何实现这3个功能的。基于MyBatis 3.5.3版本。
SqlSession提供的交互方式
现阶段MyBatis中,SqlSession主要提供了两种交互方式:
1. 基于Statement ID
这是MyBatis最传统的交互方式,首先来解释下这个Statement ID是什么。
这个Statement ID本质是一个字符串,相当于是MyBatis给SQL语句定义的唯一主键,由mapper的命名空间namespace+SQL中的id组成,比如对于如下StudentMapper.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.novalue.project.test.mapper.StudentMapper">
<select id="getOne" parameterType="int" resultType="Student">
select * from `student` where id = #{id}
</select>
</mapper>
这条SQL的Statement ID就是cn.novalue.project.test.mapper.StudentMapper.getOne
。
SqlSession提供了增删改查很多重载方法,来满足用户的种种需求。但其实这些方法都是类似的:都需要提供一个String类型的Statement ID,以及SQL参数(若有参数的话)。所以可以对它有个初步的认识:SqlSession正是通过Statement ID定位到了xml文件中的sql标签(若有参数则设置参数),然后执行sql的。
2. 基于Mapper接口
基于Statement ID虽然简单易懂,但是其实际操作却并不优雅,想象一下每次要写一长串字符串。。。它不符合面向对象语言的概念和面向接口编程的编程习惯。由于面向接口的编程是面向对象的大趋势,所以这种交互方式应运而生。但其实这种方式也是依托于第1种的,只不过MyBatis把Statement ID相关的操作封装了起来。
习惯上,一般一个xml配置文件,对应一个真实数据库表,而这个配置文件,又是以<mapper>
为root结点(如上边的StudentMapper.xml)。所以为了实现基于Mapper接口交互,MyBatis 将配置文件中的每一个<mapper>
节点抽象为一个 Mapper 接口,而这个接口中声明的方法和跟<mapper>
节点中的<select|update|delete|insert>
节点项一一对应,即<select|update|delete|insert>
节点的id值为Mapper 接口中的方法名称,parameterType
值表示Mapper 对应方法的入参类型,而resultMap
值则对应了Mapper 接口表示的返回值类型或者返回结果集的元素类型。
根据MyBatis 的配置规范配置好后,通过SqlSession.getMapper(XXXMapper.class)
方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper 实例,我们使用Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定Statement ID,底层还是通过SqlSession.select("statementId",parameterObject);
或者SqlSession.update("statementId",parameterObject);
等等来实现对数据库的操作。
正是有了这种对应关系,所以现在我们可以对Statement ID有一个全新的认识:Mapper全限定类名+方法名,因为xml文件中的namespace是Mapper接口的全限定类名,sql标签的id是Mapper接口中的方法名。这也就是为什么Mybatis的Mapper接口中不允许有重载方法,因为那样Statement ID就重复了,而它是作为唯一主键存在的。
SqlSession执行分析
首先看下SqlSession的继承关系(SqlSessionTemplate其实是org.mybatis.spring
包下的):
Mybatis对SqlSession功能上的实现,其实仅有一个子类:DefaultSqlSession(虽然还有个SqlSessionManager以及针对Spring环境下的SqlSessionTemplate,但它们其实还是委托DefaultSqlSession的,这两个子类后边再说)。所以只要来看这个类即可:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
DefaultSqlSession主要有两个字段,这两个字段又涉及MyBatis两个核心组件:
- Configuration:MyBatis的大仓库,保存了几乎所有MyBatis要用的东西,人手一份。
- Executor:MyBatis真正的执行器,SqlSession几乎所有功能,其实就是委托它实现的。
这里以select
操作为例,分析下SqlSession的源码。
DefaultSqlSession的所有select
相关重载方法,最后都会来到public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds)
。这个方法的源码如下:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
异常简单,仅仅是从大仓库Configuration中获取到一个MappedStatement,然后委托Executor去执行。这里简单说下:MappedStatement是MyBatis对SQL语句的封装,对应于xml文件中的一个<select|update|delete|insert>
节点,或者说Mapper接口中的一个方法。
其它的数据库操作方法类似,几乎都是直接委托Executor来执行,所以DefaultSqlSession的源码还是很好理解的。
创建SqlSession的方式
MyBatis到处使用工厂模式创建对象,SqlSession也是如此。与此对应的顶层接口是SqlSessionFactory,并且针对DefaultSqlSession,也有其对应实现:DefaultSqlSessionFactory。该类实现了工厂接口中的众多重载方法,但还是按照惯例,最终落实到一个或几个具体方法上。这里就是openSessionFromDataSource
和openSessionFromConnection
,也就是从数据源和数据库连接中创建。源码如下:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// Configuration中取Environment
final Environment environment = configuration.getEnvironment();
// Environment中取配置的事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 新建事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 新建执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 创建DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
// 处理异常
// ...
} finally {
}
}
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;
}
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
final Transaction tx = transactionFactory.newTransaction(connection);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
} finally {
}
}
无非还是从大仓库Configuration中取东西,然后组合、配置,最后返回DefaultSqlSession,还是比较简单的。
而工厂又进一步是由SqlSessionFactoryBuilder构建生成的,其读入MyBatis的配置文件(也即mybatis-config.xml),并返回一个DefaultSqlSessionFactory。
由于DefaultSqlSession的具体实现都委托给Executor,而Executor内部的一级缓存直接是HashMap实现的,所以正如DefaultSqlSession源码注释所说:它不是线程安全的。所以SqlSessionManager应运而生。
SqlSessionManager
SqlSessionManager是为了解决DefaultSqlSession线程不安全问题而来的。首先来看下类定义:
public class SqlSessionManager implements SqlSessionFactory, SqlSession {
private final SqlSessionFactory sqlSessionFactory;
private final SqlSession sqlSessionProxy;
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
// 私有构造方法
private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class},
new SqlSessionInterceptor());
}
// 创建对象的工厂方法
public static SqlSessionManager newInstance(Reader reader) {
return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, null));
}
// 自有方法
public void startManagedSession() {
this.localSqlSession.set(openSession());
}
// SqlSessionFactory接口的方法
@Override
public SqlSession openSession() {
return sqlSessionFactory.openSession();
}
// SqlSession接口定义的方法
@Override
public <T> T selectOne(String statement) {
return sqlSessionProxy.selectOne(statement);
}
SqlSessionManager中方法很多,但是大都是重载的,这里仅挑几个代表,其它类似。这里来总结下它的几个特点:
- 同时实现了SqlSessionFactory和SqlSession接口(自己生自己?)
- 分别持有SqlSessionFactory和SqlSession类型的属性字段,且对应接口的方法全是直接委托这两个字段执行。
- 持有一个 ThreadLocal<SqlSession> 类型的字段
localSqlSession
,也就是说会保存一个线程绑定的SqlSession。 - 构造方法是私有的,仅对外提供静态的工厂方法。且入参也只是简单地新建一个DefaultSqlSessionFactory而已(
new SqlSessionFactoryBuilder().build(reader, null, null)
方法最终新建一个DefaultSqlSessionFactory)。 - 对字段
sqlSessionProxy
的赋值应用了JDK的动态代理,涉及类SqlSessionInterceptor。 - 除了实现这两个接口的方法,其自身还有一些重载方法:
startManagedSession
,一旦调用了这个方法,就给自身的localSqlSession
中放入一个SqlSession(其实也就是DefaultSqlSession)。
综上可以看到,SqlSessionManager也基本都是委托自身的两个字段去做事,sqlSessionFactory
就是简单的DefaultSqlSessionFactory,没什么说的,只不过sqlSessionProxy
是以动态代理的方式创建的,那么对它的调用,最终都会走到对应的InvocationHandler#invoke
方法中,所以关于线程安全的秘密只能在localSqlSession
和类SqlSessionInterceptor中,下面就来看下这个内部类定义:
private class SqlSessionInterceptor implements InvocationHandler {
public SqlSessionInterceptor() {
// Prevent Synthetic Access
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 从ThreadLocal中获取SqlSession
final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
// 如果有线程绑定的SqlSession,就用它来执行方法
if (sqlSession != null) {
try {
return method.invoke(sqlSession, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 如果没有线程绑定的SqlSession
} else {
// 则从工厂中新建一个
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);
}
}
}
}
}
这个类的内容也算简单明了。当sqlSessionProxy
在执行有关SqlSession接口方法时就会来到这里,首先从自身内部字段localSqlSession
中尝试获取SqlSession,如果有,就用线程绑定的;如果没有,再从工厂新建。实际干活的还是DefaultSqlSession。这也就是为什么它能够实现线程安全的SqlSession。
而也只有调用了startManagedSession
的重载方法,才会往localSqlSession
中放SqlSession。所以startManagedSession
方法相当于SqlSessionManager的一个开关:
- 若没有调用该重载方法,它实际上就是普通的DefaultSqlSession,每次用 每次新建。
- 调用该重载方法后,SqlSessionManager就会给当前线程绑定一个DefaultSqlSession,以后都使用线程绑定的这个,从而实现线程安全。
SqlSessionTemplate
其实知道了SqlSessionManager是如何实现线程安全的,那么也就知道了在Spring环境下的SqlSessionTemplate的实现原理:同样的动态代理,给线程绑定一个SqlSession。只不过在Spring框架下,有一个事务同步管理器TransactionSynchronizationManager,专门保存当前线程相关的资源,这里由它负责保存线程绑定的SqlSession而已。