文章首發於blog.csdn.net/doujinlong1…
在spring中,dao層大多都是用Mybatis,那麼
1,Mybatis執行sql最重要的是什麼?
在以前對Mybatis的原始碼解讀中,我們知道,Mybatis利用了動態代理來做,最後實現的類是MapperProxy,在最後執行具體的方法時,實際上執行的是:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
複製程式碼
最重要的一步:
mapperMethod.execute(sqlSession, args);
複製程式碼
這裡的sqlSession 其實是在Spring的配置時設定的 sqlSessionTemplate,隨便對其中的一個進行跟進:可以在sqlSessionTemplate類中發現很好這樣的方法,用來執行具體的sql,如:
@Override
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.<T> selectOne(statement, parameter);
}
複製程式碼
這一步就是最後執行的方法,那麼問題來了 sqlSessionProxy 到底是啥呢? 這又得回到最開始。
2,使用mybatis連線mysql時一般都是需要注入SqlSessionFactory,SqlSessionTemplate,PlatformTransactionManager。
其中SqlSessionTemplate是生成sqlSession的模版,來看他的注入過程(註解形式注入):
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
複製程式碼
在這個初始化過程中:
/**
* Constructs a Spring managed {@code SqlSession} with the given
* {@code SqlSessionFactory} and {@code ExecutorType}.
* A custom {@code SQLExceptionTranslator} can be provided as an
* argument so any {@code PersistenceException} thrown by MyBatis
* can be custom translated to a {@code RuntimeException}
* The {@code SQLExceptionTranslator} can also be null and thus no
* exception translation will be done and MyBatis exceptions will be
* thrown
*
* @param sqlSessionFactory
* @param executorType
* @param exceptionTranslator
*/
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property `sqlSessionFactory` is required");
notNull(executorType, "Property `executorType` is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
複製程式碼
}
最後一步比較重要,用java動態代理生成了一個sqlSessionFactory。代理的類是:
/**
* Proxy needed to route MyBatis method calls to the proper SqlSession got
* from Spring`s Transaction Manager
* It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
* pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
*/
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
複製程式碼
}
在sqlSession執行sql的時候就會用這個代理類。isSqlSessionTransactional 這個會判斷是不是有Transactional,沒有則直接提交。如果有則不提交,在最外層進行提交。
其中
getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,SqlSessionTemplate.this.executorType,SqlSessionTemplate.this.exceptionTranslator);
複製程式碼
這個方法用來獲取sqlSession。具體實現如下:
/**
* Gets an SqlSession from Spring Transaction Manager or creates a new one if needed.
* Tries to get a SqlSession out of current transaction. If there is not any, it creates a new one.
* Then, it synchronizes the SqlSession with the transaction if Spring TX is active and
* <code>SpringManagedTransactionFactory</code> is configured as a transaction manager.
*
* @param sessionFactory a MyBatis {@code SqlSessionFactory} to create new sessions
* @param executorType The executor type of the SqlSession to create
* @param exceptionTranslator Optional. Translates SqlSession.commit() exceptions to Spring exceptions.
* @throws TransientDataAccessResourceException if a transaction is active and the
* {@code SqlSessionFactory} is not using a {@code SpringManagedTransactionFactory}
* @see SpringManagedTransactionFactory
*/
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
複製程式碼
這個sqlSession的建立其實看他方法解釋就夠了,“從Spring事務管理器中獲取一個sqlsession,如果沒有,則建立一個新的”,這句話的意思其實就是如果有事務,則sqlSession用一個,如果沒有,就給你個新的咯。
再通俗易懂一點:**如果在事務裡,則Spring給你的sqlSession是一個,否則,每一個sql給你一個新的sqlSession。**這裡生成的sqlSession其實就是DefaultSqlSession了。後續可能仍然有代理,如Mybatis分頁外掛等,不在此次討論的範圍內。
3,第二步的 sqlSession 一樣不一樣到底有什麼影響?
在2中,我們看到如果是事務,sqlSession 一樣,如果不是,則每次都不一樣,且每次都會提交。這是最重要的。
sqlSession,顧名思義,就是sql的一個會話,在這個會話中發生的事不影響別的會話,如果會話提交,則生效,不提交不生效。
來看下sqlSession 這個介面的介紹。
/**
* The primary Java interface for working with MyBatis.
* Through this interface you can execute commands, get mappers and manage transactions.
* 為Mybatis工作最重要的java介面,通過這個介面來執行命令,獲取mapper以及管理事務
* @author Clinton Begin
*/
複製程式碼
註釋很明白了,來一一看看怎麼起的這些作用。
3.1,執行命令。
在第一個小標題中 執行sql最重要的方法就是 this.sqlSessionProxy. selectOne(statement, parameter); 這個方法,而在第二個小標題中我們看到是通過代理來執行的,最後實際上沒有事務則提交sql。這就是執行sql的基本動作了。獲取sqlsession,提交執行Sql。
3.2,獲取mapper。
在我們日常的程式碼中可能不會這麼寫,但是實際上,如果必要我們是可以這麼做的,如:
XxxxxMapper xxxxxMapper = session.getMapper(xxxxxMapper.class);
複製程式碼
一般情況下,如果要這麼做,首先需要注入 sqlSessionFactory,然後利用
sqlSessionFactory.openSession()。
複製程式碼
即可獲取session。
####3.3,事務管理 ####
上面我一直提到一點,sqlSession 那個代理類裡有個操作,判斷這個是不是事務管理的sqlSession,如果是,則不提交,不是才提交,這個就是事務管理了,那麼有個問題,在哪裡提交這個事務呢????
4,事務從哪裡攔截,就從哪裡提交
Spring中,如果一個方法被 @Transactional 註解標註,在生效的情況下(不生效的情況見我寫動態代理的那篇部落格),則最終會被TransactionInterceptor 這個類所代理,執行的方法實際上是這樣的:
@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// Adapt to TransactionAspectSupport`s invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
@Override
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
});
}
複製程式碼
繼續看invokeWithinTransaction這個方法:
/**
* General delegate for around-advice-based subclasses, delegating to several other template
* methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
* as well as regular {@link PlatformTransactionManager} implementations.
* @param method the Method being invoked
* @param targetClass the target class that we`re invoking the method on
* @param invocation the callback to use for proceeding with the target invocation
* @return the return value of the method, if any
* @throws Throwable propagated from the target invocation
*/
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass);
//基本上我們的事務管理器都不是一個CallbackPreferringPlatformTransactionManager,所以基本上都是會從這個地方進入,下面的else情況暫不討論。
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
//獲取具體的TransactionInfo ,如果要用程式設計性事務,則把這塊的程式碼可以借鑑一下。
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation(); //執行被@Transactional標註裡面的具體方法。
}
catch (Throwable ex) {
// target invocation exception
//異常情況下,則直接完成了,因為在sqlsession執行完每一條指令都沒有提交事務,所以表現出來的就是回滾事務。
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
//正常執行完成的提交事務方法 跟進可以看到實際上執行的是:(程式設計性事務的提交)
// ==============txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());===========
commitTransactionAfterReturning(txInfo);
return retVal;
}
// =======================else情況不討論================================
else {
// It`s a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr,
new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
try {
return invocation.proceedWithInvocation();
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
return new ThrowableHolder(ex);
}
}
finally {
cleanupTransactionInfo(txInfo);
}
}
});
// Check result: It might indicate a Throwable to rethrow.
if (result instanceof ThrowableHolder) {
throw ((ThrowableHolder) result).getThrowable();
}
else {
return result;
}
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
}
}
複製程式碼
5,小結,SqlSession 還在別的地方有用到嗎?
其實,Mybatis的一級快取就是 SqlSession 級別的,只要SqlSession 不變,則預設快取生效,也就是說,如下的程式碼,實際上只會查一次庫的:
XxxxxMapper xxxxxMapper = session.getMapper(xxxxxMapper.class);
//對應的sql為: select id from test_info;
xxxxxMapper.selectFromDb();
xxxxxMapper.selectFromDb();
xxxxxMapper.selectFromDb();
複製程式碼
實際上只會被執行一次,感興趣的朋友們可以試試。
但是,在日常使用中,我們都是使用spring來管理Mapper,在執行selectFromDb 這個操作的時候,其實每次都會有一個新的SqlSession,所以,Mybatis的一級快取是用不到的。