1.MyBatis工作流程简述
传统工作模式
public static void main(String[] args) {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
String name = "tom";
List<User> list = sqlSession.selectList("com.demo.mapper.UserMapper.getUserByName",params);
}
1.创建SqlSessionFactoryBuilder,根据mybatis-config.xml配置文件;来build SqlSessionFactory
2.SqlSessionFactory.openSession()来创建SqlSeeion
3.SqlSession执行具体语句
SqlSessionFactoryBuild -> SqlSessionFactory -> SqlSession
使用mapper接口
public static void main(String[] args) {
//前三步都相同
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
//这里不再调用SqlSession 的api,而是获得了接口对象,调用接口中的方法。
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> list = mapper.getUserByName("tom");
}
JDBC完整实例
由上可以看到,最终执行语句的是Statement这个对象,其实在Mybatis最终也是这个对象真正执行语句。
下面来看看Mybatis的流程及原理
1.获取SqlSession
1.首先,SqlSessionFactoryBuillder去读取Mybatis-config.xml配置文件,然后build一个DefaultSessionFactory
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//这儿创建DefaultSessionFactory对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
Configuration对象的结构和xml配置文件的对象几乎相同。
也就是说,初始化配置文件信息的本质就是创建Configuration,将解析到的xml数据封装到Configuration内部的属性中。
发散一下,既然解析xml只是将xml的数据封装到Configuration中,我们可以创建一个Configguration对象,然后手动设置一些我们需要的属性参数。
2.拿到DefaultSqSessionFactory后,可以通过SqlSessionFactory去获取SqlSession。
/**
* 通常一系列openSession方法最终都会调用本方法
* @param execType
* @param level
* @param autoCommit
* @return
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
final Executor executor = configuration.newExecutor(tx, execType);
//关键看这儿,创建了一个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();
}
}
SqlSession是一个接口,他有两个实现类:DefaultSqlSession和SqlSessionManager(弃用)
SqlSession是Mybatis中用于和数据库交互的顶层类,通常与ThreadLocal绑定,一个会话使用一个Sqlsession,并且在使用完毕后close。
SqlSession中的两个最重要的参数,configuration与初始化时的相同,Executor为执行器,
Executor执行器
由上可以看到,在build Sqlsession的时候,通过configuration来获取Environment,然后获取到事务Transaction(Transaction的作用可见另一个文档,其实现类主要用于更具DataSource获取Connection)。
Executor也是一个接口,他有三个常用的实现类BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statements),SimpleExecutor(普通的执行器,默认)。
MapperProxy
在mybatis中,我们看到只有mapper接口,并没有实现类,这是因为mybatis会为每一个mapper接口生成一个mapperProxy动态代理类。
开始之前介绍一下Mybatis初始化时对接口的处理,MapperRegistry是Configuration的一个属性,它内部维护了一个Hsahmap用于存放mapper接口的工厂类,每个接口对应一个工厂类,mappers中可以配置接口的包路劲。
- 当解析mappers标签时,它会判断解析到的是mapper配置文件时,会再将对应配置文件中的增删改查标签一一封装成MapperedStatement对象,存入mappedStatements。
- 当判断解析到接口时,会创建此接口对应的MapperProxyFactory对象,存入HashMap中,key = 接口的字节码对象,value = 此接口对应的MapperProxyFactory对象。
这是上面解析xml文件的时候会做到
//MapperRegistry类
public class MapperRegistry {
private final Configuration config;
//这个类中维护一个HashMap存放MapperProxyFactory
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
//解析到接口时添加接口工厂类的方法
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//重点在这行,以接口类的class对象为key,value为其对应的工厂对象,构造方法中指定了接口对象
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
————————————————
版权声明:本文为CSDN博主「Coder648」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43184769/article/details/91126687
然后就是获取MapperProxy了
//DefaultSqlSession中的getMapper
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
//configuration中的给getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
//MapperRegistry中的getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//从MapperRegistry中的HashMap中拿MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过动态代理工厂生成示例。
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
//MapperProxyFactory类中的newInstance方法
public T newInstance(SqlSession sqlSession) {
// 创建了JDK动态代理的Handler类
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 调用了重载方法
return newInstance(mapperProxy);
}
//MapperProxy类,实现了InvocationHandler接口
public class MapperProxy<T> implements InvocationHandler, Serializable {
//省略部分源码
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
// 构造,传入了SqlSession,说明每个session中的代理对象的不同的!
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
//省略部分源码
}
//重载的方法,由动态代理创建新示例返回。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
在动态代理返回了实例后,我们就可以直接调用mapper类中的方法了,说明在MapperProxy中的invoke方法中,已经为我们实现了方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//判断调用是是不是Object中定义的方法,toString,hashCode这类非。是的话直接放行。
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 重点在这:MapperMethod最终调用了执行的方法
return mapperMethod.execute(sqlSession, args);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判断mapper中的方法类型,最终调用的还是SqlSession中的方法
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
在这里我们看到,兜兜转转还是回到了SqlSession这里。
我们来选取SqlSession中一个方法来分析
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
//CRUD实际上是交给Excetor去处理, excutor其实也只是穿了个马甲而已,小样,别以为穿个马甲我就不认识你嘞!
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();
}
}
所以,Sql内部还是交给了Excetor去处理。
但其实Excetor也只是穿了一个马甲而已,最终一层层的,最终会来到doQuery方法。
//此方法在SimpleExecutor的父类BaseExecutor中实现
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示
BoundSql boundSql = ms.getBoundSql(parameter);
//为本次查询创建缓存的Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
//进入query的重载方法中
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 如果缓存中没有本次查找的值,那么从数据库中查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
//从数据库查询
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 查询的方法
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 将查询结果放入缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
// SimpleExecutor中实现父类的doQuery抽象方法
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 传入参数创建StatementHanlder对象来执行查询
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 创建jdbc中的statement对象
stmt = prepareStatement(handler, ms.getStatementLog());
// StatementHandler进行处理
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
// 创建Statement的方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//条代码中的getConnection方法经过重重调用最后会调用openConnection方法,从连接池中获得连接。
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
//从连接池获得连接的方法
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
//从连接池获得连接
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
setDesiredAutoCommit(autoCommit);
}
//进入StatementHandler进行处理的query,StatementHandler中默认的是PreparedStatementHandler
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
//原生jdbc的执行
ps.execute();
//处理结果返回。
return resultSetHandler.handleResultSets(ps);
}
首先来看看MappedStatement
- 作用:MappedStatment与Mapper配置文件中一个select/update/insert/delete节点相对应。mapper中配置的标签都被封装到了此对象中,主要用途是描绘一条SQL语句。
- 初始化过程:回顾到刚开始介绍的加载配置文件的过程中,会对mybatis-config.xml中的各个标签都进行解析。其中由mappers标签用来引用mapper.xml或者配置mapper接口的配置。 ```java
这样的一个select标签会在初始化配置文件时被解析封装成一个MappedStatement对象,然后存储在Configuration对象的mappedStatements属性中,mappedStatements 是一个HashMap,存储时key = 全限定类名 + 方法名,value = 对应的MappedStatement对象。
```java
Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
再看上面的
stmt = prepareStatement(handler, ms.getStatementLog());
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
//执行语句之前的准备
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
StatementHandler是最终的执行语句,看一下是实现类PreparedStatementHandler(最常用,封装的是PreparedStatement)
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//到此,原形毕露, PreparedStatement(预编译的SQl语句),PreparedStatement可以有效的防止Sql注入
PreparedStatement ps = (PreparedStatement) statement;
//Sql语句执行
ps.execute();
//结果交给了ResultSetHandler 去处理,对结果做处理
return resultSetHandler.<E> handleResultSets(ps);
}
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
//此时stmt里面已经有查询出来的结果了
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
2.Mybatis拦截器
开发当中我们经常需要对执行的sql加上自定义的处理,比如分页(日志打印可通过配置开启,默认不开启),这个时候我们可以自定义MyBatis拦截器,只需要实现Interceptor接口
@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class,Object.class, RowBounds.class,ResultHandler.class})})
public class MyPageInterceptor implements Interceptor {
private static final Logger logger= LoggerFactory.getLogger(MyPageInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
logger.warn(invocation.toString());
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o,this);
}
@Override
public void setProperties(Properties properties) {
logger.warn(properties.toString());
}
}
3.mybatis批量执行sql语句
@Service
public class ProposedService{
@Autowired
SqlSessionFactory sqlSessionFactory;
@Transactional
public void insertBatch(List<TRiskToleranceAnswer> tRiskToleranceAnswerList) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
TRiskToleranceAnswerMapper mapper = sqlSession.getMapper(TRiskToleranceAnswerMapper.class);
for (int i = 0; i < tRiskToleranceAnswerList.size(); i++) {
mapper.insertSelective(tRiskToleranceAnswerList.get(i));
}
sqlSession.flushStatements();
}
}
4.SqlSession线程安全问题
在Mybatis中SqlSession默认有DefaultSqlSession和SqlSessionManager两个实现类。
DefaultSqlSession是真正的实现类调用Executor,但是这里是线程不安全的
这里的线程是指我们在代码中直接openSqlSession,然后用这个SqlSession多次调用,如果直接使用mybatis中的mapper,他会每次执行sql的时候都去创建一个新Sqlsession,这样其实不会造成不安全问题。
DefaultSqlSession线程不安全原理
DefaultSqlSession实际上真正执行的是BaseExecutor,BaseExecutor里面有一个cache,它是一个map,负责存储sql的查询结果的一个缓存。如果多线程的情况下,会有线程不安全的问题。
SqlSessionManager
Mybatis又实现了对SqlSession和SQLSessionFactory的封装类SqlSessionManager,线程安全并通过localSqlSession实现复用从而提高性能。
SqlSessionManager维护了一个ThreadLoacl,将sqlSession与线程绑定,当执行sql时,调用代理类中的拦截器,拦截器中会获取sqlsession
private class SqlSessionInterceptor implements InvocationHandler {
public SqlSessionInterceptor() {
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//由调用者决定当前线程是否复用 SqlSession
SqlSession sqlSession = (SqlSession)SqlSessionManager.this.localSqlSession.get();
if (sqlSession != null) {
try {
return method.invoke(sqlSession, args);
} catch (Throwable var12) {
throw ExceptionUtil.unwrapThrowable(var12);
}
} else {
//如果不复用,则每次调用都新建 SqlSession 并使用后销毁
SqlSession autoSqlSession = SqlSessionManager.this.openSession();
Object var7;
try {
Object result = method.invoke(autoSqlSession, args);
autoSqlSession.commit();
var7 = result;
} catch (Throwable var13) {
autoSqlSession.rollback();
throw ExceptionUtil.unwrapThrowable(var13);
} finally {
autoSqlSession.close();
}
return var7;
}
}
}
DefaultSqlSession和SqlSessionManager之间的区别:
1、单例模式下DefaultSqlSession是线程不安全的,而SqlSessionManager是线程安全的;
2、SqlSessionManager可以选择通过localSqlSession这个ThreadLocal变量,记录与当前线程绑定的SqlSession对象,供当前线程循环使用,从而避免在同一个线程多次创建SqlSession对象造成的性能损耗;
3、使用DefaultSqlSession为了保证线程安全需要为每一个操作都创建一个SqlSession对象,其性能可想而知;
SqlSessionTemplate
SqlSessionTemplate是MyBatis专门为Spring提供的,支持Spring框架的一个SqlSession获取接口。主要是为了继承Spring,并同时将是否共用SqlSession的权限交给Spring去管理。
同样是创建代理类
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
Assert.notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
}
private class SqlSessionInterceptor implements InvocationHandler {
private SqlSessionInterceptor() {
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//获取同一个线程的复用sqlSession,如果没有则新生成一个并存到线程私有存储中
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
try {
//实际调用DefaultSession的对应方法
Object result = method.invoke(sqlSession, args);
//判断当前sqlSession是否被Spring管理,如果没有直接commit
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
unwrapped = result;
} catch (Throwable var11) {
unwrapped = ExceptionUtil.unwrapThrowable(var11);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw (Throwable)unwrapped;
} finally {
if (sqlSession != null) {
//正常返回将线程私有sqlSession调用次数减一
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
return unwrapped;
}
}
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
Assert.notNull(executorType, "No ExecutorType specified");
//从线程私有存储中获取SqlSession
SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
//没有则新建一个DefaultSqlSession
session = sessionFactory.openSession(executorType);
//存到线程私有存储中
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
}
本质原理其实和SqlSessionManager一样,但是存储的机制不一样。
究其根本SqlSession真正的实现类只有DefaultSqlSession,SqlSessionManager和SqlSessionTemplate都是通过代理转发到DefaultSqlSession对应方法。
单例模式下的DefaultSqlSession不是线程安全的,SqlSessionManager和SqlSessionTemplate线程安全的根本就是每一个线程对应的SqlSession都是不同的。如果每一个操作都创建一个SqlSession对象,操作完又进行销毁导致性能极差。通过线程私有ThreadLocal存储SqlSession进行复用,从而提高性能。
5.Mybatis缓存问题
5.1 一级缓存
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement
,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache
,最后返回结果给用户。
我们来看看如何使用MyBatis一级缓存。开发者只需在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION
或者STATEMENT
,默认是SESSION
级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT
级别,可以理解为缓存只对当前执行的这一个Statement
有效。
<setting name="localCacheScope" value="SESSION"/>
- 开启一级缓存,范围为会话级别,调用三次
getStudentById
,只有第一次会查询数据库,后续使用了一级缓存。
- 在修改操作后执行的相同查询,查询了数据库,一级缓存失效。
- 验证一级缓存只在数据库会话内部共享,不同的会话缓存不能共用。
主要学习BaseExecutor
的内部实现
在一级缓存的介绍中提到对Local Cache
的查询和写入是在Executor
内部完成的。在阅读BaseExecutor
的代码后发现Local Cache
是BaseExecutor
内部的一个成员变量,如下代码所示。
public abstract class BaseExecutor implements Executor {
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
BaseExecutor
成员变量之一的PerpetualCache
,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。如下代码所示:
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
在源码分析的最后,如果是insert/delete/update
方法,缓存就会刷新的原因。
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
一级缓存默认的是在一个SqlSession中。
- MyBatis一级缓存的生命周期和SqlSession一致。
- MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
- MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
5.2 二级缓存
如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
sqlSession1
查询完数据后,不提交事务,sqlSession2
相同的查询不命中缓存sqlSession1
查询完数据后,当提交事务时,sqlSession2
相同的查询命中缓存- 在
sqlSession3
更新数据库,并提交事务后,sqlsession2
的StudentMapper namespace
下的查询走了数据库,没有走Cache。 - MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况。
为什么要开启事务
由于使用了数据库连接池,默认每次查询完之后自动commit,这就导致两次查询使用的不是同一个sqlSessioin,根据一级缓存的原理,它将永远不会生效。 当我们开启了事务,两次查询都在同一个sqlSession中,从而让第二次查询命中了一级缓存。读者可以自行关闭事务验证此结论。 2、两种一级缓存模式 一级缓存的作用域有两种:session(默认)和statment,可通过设置local-cache-scope 的值来切换,默认为session。 二者的区别在于session会将缓存作用于同一个sqlSesson,而statment仅针对一次查询,所以,local-cache-scope: statment可以理解为关闭一级缓存。