mybatis 不會(huì)直接和數(shù)據(jù)庫(kù)進(jìn)行打交道,mybatis 其實(shí)是對(duì) jdbc api 的進(jìn)一步封裝,最終和數(shù)據(jù)庫(kù)打交道的仍然是 jdbc 。
1. MyBatis基本構(gòu)成
- SqlSessionFactoryBuilder(構(gòu)造器):它會(huì)根據(jù)配置信息或者代碼來(lái)生成SqlSessionFactory(工廠接口);
- 生命周期:它的作用就是一個(gè)構(gòu)造器,一旦我們構(gòu)建了SqlSessionFactory,SqlSessionFactoryBuilder的作用就已經(jīng)完結(jié)。
所以它的生命周期僅存在于方法局部。
- 生命周期:它的作用就是一個(gè)構(gòu)造器,一旦我們構(gòu)建了SqlSessionFactory,SqlSessionFactoryBuilder的作用就已經(jīng)完結(jié)。
- SqlSessionFactory:依靠工廠來(lái)生成SqlSession(會(huì)話);
- 生命周期:SqlSessionFactory的作用是創(chuàng)建SqlSession,而SqlSession就是一個(gè)會(huì)話,相當(dāng)于JDBC中的Connection對(duì)象。每次應(yīng)用程序需要訪問(wèn)數(shù)據(jù)庫(kù),我們就通過(guò)SqlSessionFactory創(chuàng)建SqlSession,
所以SqlSessionFactory應(yīng)該在MyBatis應(yīng)用的整個(gè)生命周期中。而如果我們多次創(chuàng)建同一個(gè)數(shù)據(jù)庫(kù)的SqlSessionFactory,則每次創(chuàng)建SqlSessionFactory會(huì)打開(kāi)更多的數(shù)據(jù)庫(kù)連接(Connection)資源,那么連接資源就很快會(huì)被耗盡。因此SqlSessionFactory是一個(gè)全局單例,對(duì)應(yīng)一個(gè)數(shù)據(jù)庫(kù)連接池。
- 生命周期:SqlSessionFactory的作用是創(chuàng)建SqlSession,而SqlSession就是一個(gè)會(huì)話,相當(dāng)于JDBC中的Connection對(duì)象。每次應(yīng)用程序需要訪問(wèn)數(shù)據(jù)庫(kù),我們就通過(guò)SqlSessionFactory創(chuàng)建SqlSession,
- SqlSession:是一個(gè)既可以發(fā)送SQL去執(zhí)行并返回結(jié)果,也可以獲取Mapper的接口。
- 生命周期:SqlSession相當(dāng)于一個(gè)JDBC的Connection對(duì)象,
在一次請(qǐng)求事務(wù)會(huì)話后,我們會(huì)將其關(guān)閉。
- 生命周期:SqlSession相當(dāng)于一個(gè)JDBC的Connection對(duì)象,
- SQL Mapper:它是由一個(gè)Java接口和XML文件(或注解)構(gòu)成的,需要給出對(duì)應(yīng)的SQL和映射規(guī)則。它負(fù)責(zé)發(fā)送SQL去執(zhí)行,并返回結(jié)果;
- 生命周期:Mapper的作用是發(fā)送SQL,然后返回我們需要的結(jié)果,因此它應(yīng)該在
一個(gè)SqlSession事務(wù)方法之內(nèi),是一個(gè)方法級(jí)別的東西,聲明周期與SqlSession相同。
- 生命周期:Mapper的作用是發(fā)送SQL,然后返回我們需要的結(jié)果,因此它應(yīng)該在
2. SqlSessionFactoryBuilder
通過(guò)XMLConfigBuilder解析配置的XML文件,讀出配置參數(shù)并存入Configuration類(lèi)中。(MyBatis中幾乎所有的配置都是存在這里的)
使用了建造者模式,主要過(guò)程就是解析xml配置文件,new SqlSessionFactory
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
/**
* 1. 根據(jù)inputStream等信息創(chuàng)建XMLConfigBuilder對(duì)象;
* 2. XMLConfigBuilder會(huì)將XML配置文件的信息轉(zhuǎn)換為Document對(duì)象;
*/
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
/**
* 3. parser.parse()會(huì)處理每個(gè)node并返回Configuration對(duì)象,解析此Node節(jié)點(diǎn)的子Node,獲取相關(guān)屬性:properties, settings, typeAliases,typeHandlers,
* objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers
* 4. 如解析typeHandlers的過(guò)程就是將TypeHandler.class注冊(cè)到一個(gè)hashmap中
* 5. 最后調(diào)用build方法 new DefaultSqlSessionFactory(config)
*/
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
3. SqlSessionFactory
使用Configuration對(duì)象去創(chuàng)建SqlSessionFactory(默認(rèn)的實(shí)現(xiàn)為DefaultSqlSessionFactory)。
主要涉及二級(jí)緩存的Executor,直接new DefaultSqlSession
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
/**
* new Transaction, transactionFactory的實(shí)現(xiàn)類(lèi)有JdbcTransactionFactory, ManagedTransactionFactory,SpringManagedTransactionFactory
* Spring與MyBatis整合后使用SpringManagedTransactionFactory,將事務(wù)委托給Spring
*/
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
/**
* 創(chuàng)建Executor(事務(wù)包含在其中)
* 如果開(kāi)啟了二級(jí)緩存,會(huì)創(chuàng)建CachingExecutor,一個(gè)裝飾器
* 先去SqlSessionFactory級(jí)別的二級(jí)緩存中查,如果查到就使用,查不到則調(diào)用原有Executor的查詢方法
*/
final Executor executor = configuration.newExecutor(tx, execType);
// 直接new 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 {
//最后清空錯(cuò)誤上下文
ErrorContext.instance().reset();
}
}
4. SqlSession
流程:
SqlSession -> Executor -> StatementHandler(調(diào)用prepare方法進(jìn)行預(yù)編譯) -> ParameterHandler(設(shè)置預(yù)編譯sql的參數(shù)) -> StatementHandler調(diào)用PreparedStatement執(zhí)行sql -> ResultHandler封裝結(jié)果- Mapper映射是通過(guò)動(dòng)態(tài)代理實(shí)現(xiàn)的,在MapperProxy中會(huì)根據(jù)SQL的類(lèi)型(insert、update、delete、select)調(diào)用SqlSession的對(duì)應(yīng)方法;
- SqlSession中的insert、update、delete、select方法實(shí)際上是調(diào)用Executor的對(duì)應(yīng)方法;
- SqlSession下的四大對(duì)象
- Executor代表執(zhí)行器,由它來(lái)調(diào)度StatementHandler、ParameterHandler、ResultHandler等來(lái)執(zhí)行對(duì)應(yīng)的SQL;
- StatementHandler的作用是使用數(shù)據(jù)庫(kù)的Statement(PreparedStatement(預(yù)編譯的,效率高,可以使用占位符代替參數(shù)從而多次執(zhí)行))來(lái)執(zhí)行sql操作;
- ParameterHandler用于SQL對(duì)參數(shù)的處理,使用TypeHandler向PreparedStatement中設(shè)置參數(shù);
- ResultHandler是進(jìn)行最后數(shù)據(jù)集(ResultSet)的封裝返回的處理;
4.1 select
這里以DefaultSqlSession為例。
DefaultSqlSession中有多個(gè)select方法,如selectOne, selectMap, selectList等,但都是以selectList為基礎(chǔ),如selectOne是調(diào)用selectList,然后list.get(0)得到結(jié)果。selectMap也是先selectList,然后遍歷得到的list,轉(zhuǎn)換為Map。
所以我們來(lái)看一下selectList
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
/**
* 根據(jù)statement獲取MappedStatement
* statement其實(shí)就是MappedStatement的id,為被調(diào)用Mapper的類(lèi)名,如com.example.shiro.mapper.UserMapper.selectByPrimaryKey,見(jiàn)下圖
*/
MappedStatement ms = configuration.getMappedStatement(statement);
/**
* 調(diào)用之前創(chuàng)建的Executor query方法,沒(méi)有開(kāi)啟二級(jí)緩存則使用BaseExecutor
* 注意這里傳入的ResultHandler為null,
* 在后續(xù)query過(guò)程中,如果ResultHandler為null則先嘗試從緩存中取,
* 如果ResultHandler不為null則不會(huì)嘗試從緩存中取
*/
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();
}
}

再來(lái)看一下BaseExecutor#query()方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
/**
* BoundSql中包含需要?jiǎng)討B(tài)生成的sql語(yǔ)句,以及對(duì)應(yīng)的參數(shù)
*/
BoundSql boundSql = ms.getBoundSql(parameter);
/**
* 根據(jù)statementId, params, rowBounds來(lái)構(gòu)建一個(gè)key值,MyBatis認(rèn)為這幾個(gè)參數(shù)能夠代表同一個(gè)sql
*/
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
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.");
}
// 當(dāng)queryStack == 0時(shí)才清空緩存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// 保證在執(zhí)行過(guò)程中不會(huì)清空緩存
queryStack++;
/**
* localCache一級(jí)緩存,內(nèi)部為一個(gè)HashMap,線程不安全的
* resultHandler DefaultSqlSession中傳入的ResultHandler為null
* 注意:這里從cache中獲取到的結(jié)果強(qiáng)轉(zhuǎn)為list,queryFromDatabase會(huì)先傳入一個(gè)占位符,如果此時(shí)有另一個(gè)線程進(jìn)來(lái),再?gòu)?qiáng)轉(zhuǎn)則會(huì)拋異常,相當(dāng)于做了多線程操作的處理
*/
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 緩存中沒(méi)有則從數(shù)據(jù)庫(kù)中查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
//延遲加載隊(duì)列中所有元素
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
//清空延遲加載隊(duì)列
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;
/**
* 在緩存中放入一個(gè)占位符 enum類(lèi)型
* 當(dāng)?shù)谝粋€(gè)線程正常向數(shù)據(jù)庫(kù)中查詢時(shí),第二個(gè)線程也執(zhí)行了相同的查詢
* 在BaseExecutor#query方法中(List<E>) localCache.getObject(key),此時(shí)報(bào)類(lèi)型轉(zhuǎn)換異常,防止多線程操作緩存
*/
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
/**
* 調(diào)用StatementHandler#query方法
* 如實(shí)現(xiàn)類(lèi)PreparedStatementHandler,就是調(diào)用PreparedStatement#excute方法
*/
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 刪除占位符
localCache.removeObject(key);
}
//將查詢結(jié)果放入緩存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
// 直接返回緩存結(jié)果的引用
return list;
}
可以看到,在queryFromDatabase中直接返回了緩存結(jié)果的引用,這樣就會(huì)出現(xiàn)臟讀,如下代碼:
在一次查詢后,修改查詢結(jié)果,下一次查詢時(shí)直接從緩存中查詢,結(jié)果會(huì)發(fā)現(xiàn)第二次的查詢結(jié)果也被修改了
@Transactional(rollbackFor = Exception.class)
public User getUser(Long id) {
User user = userMapper.selectByPrimaryKey(id);
log.info("User: {}", user.getUsername());
user.setUsername("test-mybatis-cache");
User user2 = userMapper.selectByPrimaryKey(id);
log.info("User2: {}", user2.getUsername());
return user;
}
緩存中返回的引用,在一次事務(wù)中MyBatis不會(huì)清空緩存,所以修改引用后,下次查詢得到的結(jié)果會(huì)有問(wèn)題

去掉@Transactional后變得正常了

其實(shí)SqlSession 一級(jí)緩存的查詢工作流程為:
- 對(duì)于某個(gè)查詢,根據(jù)statementId, params, rowBounds來(lái)構(gòu)建一個(gè)key值,根據(jù)這個(gè)key值去緩存Cache中取出對(duì)應(yīng)的key值存儲(chǔ)的緩存結(jié)果;
- 判斷從Cache中根據(jù)特定的key值取的數(shù)據(jù)數(shù)據(jù)是否為空,即是否命中;
- 如果命中,則直接將緩存結(jié)果返回;
- 如果沒(méi)命中:
a. 去數(shù)據(jù)庫(kù)中查詢數(shù)據(jù),得到查詢結(jié)果;
b. 將key和查詢到的結(jié)果分別作為key,value對(duì)存儲(chǔ)到Cache中;
c. 將查詢結(jié)果返回;
MyBatis認(rèn)為,對(duì)于兩次查詢,如果以下條件都完全一樣,那么就認(rèn)為它們是完全相同的兩次查詢:
- 傳入的 statementId
- 查詢時(shí)要求的結(jié)果集中的結(jié)果范圍 (結(jié)果的范圍通過(guò)rowBounds.offset和rowBounds.limit表示);
- 這次查詢所產(chǎn)生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語(yǔ)句字符串(boundSql.getSql() )
- 傳遞給java.sql.Statement要設(shè)置的參數(shù)值
現(xiàn)在分別解釋上述四個(gè)條件:
- 傳入的statementId,對(duì)于MyBatis而言,你要使用它,必須需要一個(gè)statementId,它代表著你將執(zhí)行什么樣的Sql;
- MyBatis自身提供的分頁(yè)功能是通過(guò)RowBounds來(lái)實(shí)現(xiàn)的,它通過(guò)rowBounds.offset和rowBounds.limit來(lái)過(guò)濾查詢出來(lái)的結(jié)果集,這種分頁(yè)功能是基于查詢結(jié)果的再過(guò)濾,而不是進(jìn)行數(shù)據(jù)庫(kù)的物理分頁(yè);
由于MyBatis底層還是依賴于JDBC實(shí)現(xiàn)的,那么,對(duì)于兩次完全一模一樣的查詢,MyBatis要保證對(duì)于底層JDBC而言,也是完全一致的查詢才行。而對(duì)于JDBC而言,兩次查詢,只要傳入給JDBC的SQL語(yǔ)句完全一致,傳入的參數(shù)也完全一致,就認(rèn)為是兩次查詢是完全一致的。
上述的第3個(gè)條件正是要求保證傳遞給JDBC的SQL語(yǔ)句完全一致;第4條則是保證傳遞給JDBC的參數(shù)也完全一致;
即3、4兩條MyBatis最本質(zhì)的要求就是:
調(diào)用JDBC的時(shí)候,傳入的SQL語(yǔ)句要完全相同,傳遞給JDBC的參數(shù)值也要完全相同。
4.2 insert & delete
// DefaultSqlSession#insert
@Override
public int insert(String statement) {
return insert(statement, null);
}
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
// DefaultSqlSession#delete
@Override
public int delete(String statement) {
return update(statement, null);
}
@Override
public int delete(String statement, Object parameter) {
return update(statement, parameter);
}
可以看到insert和delete方法其實(shí)就是調(diào)用了update(會(huì)清空一級(jí)緩存),下面來(lái)看一下update方法
4.3 update
@Override
public int update(String statement) {
return update(statement, null);
}
@Override
public int update(String statement, Object parameter) {
try {
/**
* dirty置為true,在commit和rollback方法中會(huì)判斷isCommitOrRollbackRequired(),
* 如果dirty為true則表明需要commit,會(huì)調(diào)用transaction.commit();
*/
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
// 直接調(diào)用Executor#update
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
看一下BaseExecutor#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.");
}
/**
* 先清空本地緩存
* localCache.clear();
*/
clearLocalCache();
// doUpdate,一個(gè)模板方法,交給子類(lèi)實(shí)現(xiàn)
return doUpdate(ms, parameter);
}
SimpleExecutor#doUpdate
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
/**
* 創(chuàng)建StatementHandler
* StatementHandler負(fù)責(zé)處理Mybatis與JDBC之間Statement的交互,如PreparedStatement
*/
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
/**
* 1. 創(chuàng)建JDBC連接Connection(使用連接池)
* 2. 調(diào)用StatementHandler#prepare方法,通過(guò)Connection創(chuàng)建PreparedStatement
* 3. 調(diào)用StatementHandler#parameterize方法,向PreparedStatement中設(shè)置sql參數(shù),TypeHandler就是在這里生效的
*/
stmt = prepareStatement(handler, ms.getStatementLog());
/**
* 直接調(diào)用PreparedStatement#execute方法,執(zhí)行sql并返回受影響行數(shù)
*/
return handler.update(stmt);
} finally {
/**
* 關(guān)閉statement
*/
closeStatement(stmt);
}
}
總體來(lái)說(shuō),BaseExecutor#update方法比較簡(jiǎn)單,無(wú)非就是先清空本地一級(jí)緩存,再調(diào)用PreparedStatement執(zhí)行sql。
4.4 commit & rollback
@Override
public void commit(boolean force) {
try {
/**
* isCommitOrRollbackRequired(),根據(jù)dirty、autoCommit、force判斷是否需要提交或回滾
* 先清空緩存,再transaction.commit()
*/
executor.commit(isCommitOrRollbackRequired(force));
/**
* dirty置為false,下次無(wú)需提交或回滾
*/
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public void rollback(boolean force) {
try {
/**
* isCommitOrRollbackRequired(),根據(jù)dirty、autoCommit、force判斷是否需要提交或回滾
* 先清空緩存,再transaction.rollback()
*/
executor.rollback(isCommitOrRollbackRequired(force));
/**
* dirty置為false,下次無(wú)需提交或回滾
*/
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error rolling back transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
- 當(dāng)使用MyBatis-Spring時(shí),在org.mybatis.spring.SqlSessionTemplate中會(huì)調(diào)用SqlSession#commit,如果添加了@Transactional注解,在commit后sql才會(huì)生效,如果有沒(méi)有添加注解commit前sql就已經(jīng)生效了。這里無(wú)論是否添加了@Transactional注解都需要執(zhí)行SqlSession#commit主要是考慮到有的數(shù)據(jù)庫(kù)必須在close前調(diào)用commit或rollback;
- 當(dāng)使用MyBatis-Spring時(shí),@Transactional注解會(huì)使用Spring的事務(wù),則不會(huì)調(diào)用SqlSession#rollback方法;
5. MyBatis-Spring
文檔:http://mybatis.org/spring/zh/getting-started.html
- 一個(gè)使用 MyBatis-Spring 的其中一個(gè)主要原因是它允許 MyBatis 參與到 Spring 的事務(wù)管理中。而不是給 MyBatis 創(chuàng)建一個(gè)新的專(zhuān)用事務(wù)管理器,MyBatis-Spring 借助了 Spring 中的 DataSourceTransactionManager 來(lái)實(shí)現(xiàn)事務(wù)管理。
- 一旦配置好了 Spring 的事務(wù)管理器,你就可以在 Spring 中按你平時(shí)的方式來(lái)配置事務(wù)。并且支持 @Transactional 注解和 AOP 風(fēng)格的配置。
在事務(wù)處理期間,一個(gè)單獨(dú)的 SqlSession 對(duì)象將會(huì)被創(chuàng)建和使用。當(dāng)事務(wù)完成時(shí),這個(gè) session 會(huì)以合適的方式提交或回滾。 - 不能在 Spring 管理的 SqlSession 上調(diào)用 SqlSession.commit(),SqlSession.rollback() 或 SqlSession.close() 方法。如果這樣做了,就會(huì)拋出 UnsupportedOperationException 異常。在使用注入的映射器時(shí),這些方法也不會(huì)暴露出來(lái)。
-
DefaultSqlSession中的一級(jí)緩存就是一個(gè)HashMap,它不是線程安全的,MyBatis-Spring中SqlSessionTemplate是線程安全的,它將SqlSession存儲(chǔ)在org.springframework.transaction.support.TransactionSynchronizationManager中,TransactionSynchronizationManager中使用ThreadLocal變量保存SqlSession。每個(gè)線程過(guò)來(lái)都是一個(gè)獨(dú)立的SqlSession,所以能夠保證線程安全。https://my.oschina.net/u/3145456/blog/1841572
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
6. SQL Mapper
- 它是由一個(gè)Java接口和XML文件(或注解)構(gòu)成的,需要給出對(duì)應(yīng)的SQL和映射規(guī)則。它負(fù)責(zé)發(fā)送SQL去執(zhí)行,并返回結(jié)果;
在Spring啟動(dòng)時(shí)(getBean),會(huì)初始化所有的Mapper類(lèi),并生成對(duì)應(yīng)的代理類(lèi)MapperProxy。- 當(dāng)執(zhí)行Mapper中的方法時(shí)(如userMapper.insert(user)),會(huì)調(diào)用Mapper的代理類(lèi)MapperProxy(所以Mapper需要是接口),MapperProxy會(huì)調(diào)用SqlSessionTemplate的對(duì)應(yīng)方法,如下:
@Override
public int insert(String statement, Object parameter) {
return this.sqlSessionProxy.insert(statement, parameter);
}
- 而這里的sqlSessionProxy是在SqlSessionTemplate的構(gòu)造函數(shù)中創(chuàng)建的動(dòng)態(tài)代理類(lèi)(主要處理了SqlSession的線程安全問(wèn)題,最終還是直接調(diào)用DefaultSqlSession的對(duì)應(yīng)方法)
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;
// 創(chuàng)建SqlSession的動(dòng)態(tài)代理類(lèi),需要看下SqlSessionInterceptor
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 1. 嘗試去TransactionSynchronizationManager的Threadlocal中尋找SqlSession(線程安全的)
* 1.1 如果能夠獲取到,則直接返回(此時(shí)需要計(jì)數(shù)器加一,用于記錄SqlSession被獲取了多少次)
* 1.2 如果獲取不到
* 1.2.1 創(chuàng)建新的SqlSession
* 1.2.2 如果當(dāng)前存在事務(wù),則向TransactionSynchronizationManager的Threadlocal中注冊(cè)新創(chuàng)建的SqlSession
* 1.2.3 如果沒(méi)有事務(wù),則不會(huì)向TransactionSynchronizationManager注冊(cè),所以每次都是新的SQLSession
*/
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 可以看到invoke函數(shù)的參數(shù)中proxy是沒(méi)有被用到的,這里直接傳入的是動(dòng)態(tài)獲取的SqlSession
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) {
/**
* 關(guān)閉SqlSession
* 如果TransactionSynchronizationManager中存在SqlSession,減少一個(gè)計(jì)數(shù)(holder.released()),并不直接close SqlSession
* 如果TransactionSynchronizationManager不存在,直接調(diào)用SqlSession#close方法
*/
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
- 關(guān)閉SqlSession:
- 如果不存在事務(wù),每次執(zhí)行一個(gè)Mapper的方法時(shí),都會(huì)創(chuàng)建一個(gè)新的SqlSession,執(zhí)行完畢后關(guān)閉;
- 如果存在事務(wù),在事務(wù)的過(guò)程中,使用的是相同的SqlSession,事務(wù)結(jié)束后,會(huì)關(guān)閉SqlSession;
- 也就是說(shuō),在事務(wù)中才會(huì)用到SqlSession的一級(jí)緩存,而無(wú)事務(wù)時(shí),沒(méi)法觸發(fā)一級(jí)緩存。
7. MyBatis兩級(jí)緩存
- 默認(rèn)開(kāi)啟一級(jí)緩存,PerpetualCache對(duì)象就是使用HashMap來(lái)做的(
一級(jí)緩存只是相對(duì)于SqlSession而言) -
在參數(shù)和SQL完全一樣的情況下,我們使用同一個(gè)SqlSession對(duì)象調(diào)用同一個(gè)Mapper的方法,往往只執(zhí)行一次SQL,如果沒(méi)有聲明需要刷新,并且緩存沒(méi)有超時(shí)的情況下,SqlSession都只會(huì)取出當(dāng)前緩存的數(shù)據(jù),而不會(huì)再次發(fā)送SQl到數(shù)據(jù)庫(kù)。 - 如果使用不同的SqlSession對(duì)象,因?yàn)椴煌腟qlSession都是相互隔離的,所以用相同的Mapper、參數(shù)和方法,會(huì)發(fā)送多次SQL到數(shù)據(jù)庫(kù)。
- 二級(jí)緩存是在Mapper級(jí)別的,默認(rèn)是不開(kāi)啟的,且要求返回的POJO必須是可序列化的,即實(shí)現(xiàn)Serializable接口。實(shí)現(xiàn)Serializable接口主要是因?yàn)榫彺娌灰欢ㄊ窃趦?nèi)存中,也可能在磁盤(pán)中,所以需要進(jìn)行序列化和反序列化。(開(kāi)啟二級(jí)緩存后默認(rèn)insert、update、delete會(huì)刷新緩存,緩存使用LRU或FIFO等最近最少使用算法來(lái)回收)
- 二級(jí)緩存是SqlSessionFactory層面的,生命周期與SqlSessionFactory、Configuration對(duì)象相同
- 默認(rèn)系統(tǒng)緩存是MyBatis所在服務(wù)器的本地緩存,如果想使用redis等緩存服務(wù)器,MyBatis也支持自定義緩存,需要實(shí)現(xiàn)org.apache.ibatis.cache.Cache。
一級(jí)緩存的具體實(shí)現(xiàn)已經(jīng)在上面闡述過(guò)了,所以這里只討論下二級(jí)緩存。
二級(jí)緩存與一級(jí)緩存其機(jī)制相同,默認(rèn)也是采用 PerpetualCache,HashMap存儲(chǔ),不同在于其存儲(chǔ)作用域?yàn)?Mapper(Namespace),并且可自定義存儲(chǔ)源。
7.1 select
CachingExecutor是一個(gè)裝飾器,豐富了如SimpleExecutor的功能,提供了二級(jí)緩存的支持。
CachingExecutor#query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
/**
* 1. 從MappedStatement中獲取cache
* 1.1 Spring boot中想要開(kāi)啟了二級(jí)緩存需要在Mapper上添加@CacheNamespace注解,如果添加了該注解,則這里可以獲取到cache
* 1.2 MappedStatement是在啟動(dòng)時(shí),注冊(cè)到org.apache.ibatis.session.Configuration中的
* 1.2.1 每個(gè)Mapper的每個(gè)方法都會(huì)生成一個(gè)MappedStatement,且Mapper中保存了一個(gè)cache對(duì)象
* a. 如果沒(méi)有開(kāi)啟二級(jí)緩存,cache對(duì)象為空
* b. 如果開(kāi)啟了二級(jí)緩存,則每個(gè)Mapper的不同方法共享同一個(gè)cache對(duì)象,即mybatis的二級(jí)緩存是Mapper級(jí)別的
*/
Cache cache = ms.getCache();
if (cache != null) {
/**
* 是否需要刷新緩存,默認(rèn)情況下,select不需要刷新緩存
*/
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
/**
* tcm.getObject(cache, key)實(shí)際上就是去參數(shù)中的cache獲取緩存
*/
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
/**
* 如果沒(méi)有查詢到,則會(huì)去被代理的Executor(如SimpleExecutor)查詢
* delegate.query也會(huì)去查詢一級(jí)緩存
*/
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
/**
* 該方法并沒(méi)有直接將查詢的結(jié)果對(duì)象存儲(chǔ)到其封裝的二級(jí)緩存Cache對(duì)象中,
* 而是暫時(shí)保存到entriesToAddOnCommit集合中,
* 在事務(wù)提交時(shí)(CachingExecutor#commit)才會(huì)將這些結(jié)果從entriesToAddOnCommit集合中添加到二級(jí)緩存中
*/
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
/**
* 未開(kāi)啟二級(jí)緩存,則直接調(diào)用原Executor
*/
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
下圖可以看到,開(kāi)啟了二級(jí)緩存后,每個(gè)Mapper的不同方法共享同一個(gè)cache對(duì)象,不同Mapper的cache對(duì)象不同


7.2 insert, delete, update
CachingExecutor#update
當(dāng)SqlSession執(zhí)行,insert, delete, update時(shí),如果開(kāi)啟了二級(jí)緩存會(huì)調(diào)用CachingExecutor#update方法
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
//是否需要刷新緩存,insert,delete,update要刷新緩存
flushCacheIfRequired(ms);
// 直接調(diào)用原Executor
return delegate.update(ms, parameterObject);
}
開(kāi)啟二級(jí)緩存時(shí),SqlSession的聲明周期與之前相同:
- 如果不存在事務(wù),每次執(zhí)行一個(gè)Mapper的方法時(shí),都會(huì)創(chuàng)建一個(gè)新的SqlSession,執(zhí)行完畢后關(guān)閉;
- 如果存在事務(wù),在事務(wù)的過(guò)程中,使用的是相同的SqlSession,事務(wù)結(jié)束后,會(huì)關(guān)閉SqlSession;
7.3 二級(jí)緩存的缺點(diǎn)
- 容易出現(xiàn)臟數(shù)據(jù):
a. 由于二級(jí)緩存是Mapper級(jí)別的,如UserMapper中出現(xiàn)了查詢Role表的SQL,因?yàn)镽oleMapper與UserMapper的二級(jí)緩存不同,所以使用RoleMapper更新Role表并不會(huì)刷新UserMapper中查詢Role表的SQL;
b. 同理,當(dāng)在Mapper中出現(xiàn)關(guān)聯(lián)查詢時(shí),其他Mapper修改了關(guān)聯(lián)的數(shù)據(jù)表,則一定會(huì)出現(xiàn)臟數(shù)據(jù); - 緩存粒度只到Mapper,無(wú)法獲取更細(xì)粒度的緩存;
-
分布式場(chǎng)景下,必然會(huì)出現(xiàn)臟數(shù)據(jù);
a. 一級(jí)緩存如果為開(kāi)啟事務(wù),則每一個(gè)sql對(duì)應(yīng)一個(gè)SqlSession對(duì)應(yīng)一個(gè)一級(jí)緩存,所以不會(huì)出現(xiàn)臟數(shù)據(jù);如果開(kāi)啟了事務(wù),則在兩次查詢的間隙有他人修改,可能會(huì)出現(xiàn)臟數(shù)據(jù)(未強(qiáng)制加鎖); - 所以說(shuō)使用二級(jí)緩存,還不如自己在業(yè)務(wù)層做一次緩存;
8. MyBatis延遲加載
當(dāng)真正使用到這個(gè)數(shù)據(jù)時(shí)才會(huì)發(fā)送sql語(yǔ)句。(級(jí)聯(lián)時(shí),默認(rèn)將關(guān)聯(lián)的屬性都查詢出來(lái),如果開(kāi)啟了延遲加載,則使用到關(guān)聯(lián)屬性時(shí)才會(huì)查找)
實(shí)現(xiàn)原理:使用動(dòng)態(tài)代理,會(huì)生成一個(gè)動(dòng)態(tài)代理對(duì)象,里面保存著相關(guān)的SQL和參數(shù),一旦我們使用這個(gè)代理對(duì)象的方法,它會(huì)進(jìn)入到動(dòng)態(tài)代理對(duì)象的代理方法里,方法里面會(huì)通過(guò)發(fā)送sql和參數(shù),就可以把對(duì)應(yīng)的結(jié)果從數(shù)據(jù)庫(kù)中查找回來(lái)。
MyBatis延遲加載是通過(guò)動(dòng)態(tài)代理實(shí)現(xiàn)的,當(dāng)調(diào)用配置為延遲加載的屬性方法時(shí)(如getXXX()),此時(shí)會(huì)調(diào)用動(dòng)態(tài)代理對(duì)象的get方法,會(huì)發(fā)送sql到db查詢數(shù)據(jù),這些操作是通過(guò)SqlSession來(lái)執(zhí)行的。由于在和某些框架集成時(shí),SqlSession的生命周期交給了框架來(lái)管理,因此當(dāng)對(duì)象超出SqlSession生命周期調(diào)用時(shí),會(huì)由于鏈接關(guān)閉等問(wèn)題而拋出異常。因而在與Spring集成時(shí),需要注意SqlSession的聲明周期。
8.1 延遲加載的優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):先從單表查詢,需要時(shí)再去查詢另一張表(兩次查詢),如果并沒(méi)有用到另一張表中的數(shù)據(jù),可以加快查詢速度。
- 如果使用延遲加載,使用關(guān)聯(lián)查詢的話,在數(shù)據(jù)量很大的情況下,關(guān)聯(lián)查詢jion兩張大表,效率會(huì)很低。而如果延遲加載第二次延遲查詢命中索引概率大的話,效率會(huì)更高。
-
一定要進(jìn)行關(guān)聯(lián)查詢嗎?- 關(guān)聯(lián)查詢效率很低,尤其是兩張大表jion的情況下,所以要盡可能避免這種情況!
- 如果非要兩張大表jion的話,可以不作為實(shí)時(shí)場(chǎng)景,讓它作為一個(gè)定時(shí)任務(wù)去跑,第一次跑的數(shù)據(jù)量大可能耗時(shí)長(zhǎng),后面采用按時(shí)間增量更新的策略,根據(jù)時(shí)間切分的好后面每次跑的數(shù)據(jù)量就不會(huì)太大。
- 如果一張大表跟一張小表jion,那么可以將小表緩存,單查一張大表將查詢得到的結(jié)果(肯定比全量jion數(shù)據(jù)要少)再去jion。
- 大表單查很慢,可以多個(gè)線程去查,如果大表做了分庫(kù)分表或者按時(shí)間分區(qū),查詢方式就又有不同。
- 盡量將一個(gè)大sql拆分為多個(gè)小sql,大sql會(huì)長(zhǎng)時(shí)間占用連接,影響其他sql。
- 缺點(diǎn):因?yàn)橹挥挟?dāng)需要用到數(shù)據(jù)時(shí),才會(huì)進(jìn)行數(shù)據(jù)庫(kù)查詢,這樣在大批量數(shù)據(jù)查詢時(shí),因?yàn)椴樵児ぷ饕惨臅r(shí)間,所以可能造成用戶等待時(shí)間變長(zhǎng),造成用戶體驗(yàn)下降。
9. 如何將jdbc查詢的結(jié)果轉(zhuǎn)換為相應(yīng)對(duì)象的?
- 當(dāng)調(diào)用SimpleExecutor#query()方法時(shí),如果沒(méi)有命中緩存,則會(huì)queryFromDatabase,最終會(huì)使用如PreparedStatement執(zhí)行查詢;
- ResultSetHandler#handleResultSets()方法,將resultSet轉(zhuǎn)為對(duì)應(yīng)的對(duì)象。
a. 獲取resultSet中的所有列名,并獲得列名對(duì)應(yīng)的值;
b. 查找是否有匹配的TypeHandler,如果有的話,調(diào)用TypeHandler的getResult() → getNullableResult()方法,獲得通過(guò)TypeHandler轉(zhuǎn)換后的屬性值;
c. 利用反射new對(duì)象,根據(jù)列名獲得對(duì)象的setXX()方法,再使用反射調(diào)用待生成對(duì)象的setXX()方法,將屬性設(shè)置進(jìn)去;
d. 將設(shè)置了屬性的對(duì)象存入ResultHandler中;
e. 這樣僅僅是處理了一條記錄,如果查詢結(jié)果有多條記錄,還會(huì)循環(huán)這個(gè)過(guò)程。
f. 利用ResultHandler處理對(duì)象。
i. 如DefaultResultHandler,內(nèi)部維護(hù)了一個(gè)List,查詢得到的n條記錄都會(huì)存在這里
ii. 最終返回的數(shù)據(jù)會(huì)判斷List中的個(gè)數(shù),如果只有一個(gè)就get(0),只返回一個(gè)對(duì)象。如果有多個(gè)會(huì)返回這個(gè)List