Mybatis的核心——SqlSession解讀

不會汪汪的貓咪發表於2019-03-03

文章首發於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的一級快取是用不到的。

相關文章