赞
踩
正如其名,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是如何实现这3个功能的。基于MyBatis 3.5.3版本。
现阶段MyBatis中,SqlSession主要提供了两种交互方式:
这是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的。
基于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的继承关系(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两个核心组件:
这里以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的源码还是很好理解的。
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是为了解决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中方法很多,但是大都是重载的,这里仅挑几个代表,其它类似。这里来总结下它的几个特点:
localSqlSession
,也就是说会保存一个线程绑定的SqlSession。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的一个开关:
其实知道了SqlSessionManager是如何实现线程安全的,那么也就知道了在Spring环境下的SqlSessionTemplate的实现原理:同样的动态代理,给线程绑定一个SqlSession。只不过在Spring框架下,有一个事务同步管理器TransactionSynchronizationManager,专门保存当前线程相关的资源,这里由它负责保存线程绑定的SqlSession而已。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。