前言:本文從原始的mybatis原始碼開始分析一條sql語句的執行過程,我們常用的mybatis基本都是spring封裝過的,本文不涉及spring封裝部分。
一、mybatis使用步驟
我們先通過一個簡單的例項回顧一下原生mybatis的使用步驟
場景:我們要通過使用者id獲取使用者的詳細資訊,使用mybatis要經過如下四個步驟(sql如下)
select user_id as userId,user_name userName,age from op_user_info where user_id>#{userId}
複製程式碼
1.配置mybatis-config.xml檔案
其中最重要的兩個配置一個是dataSource(資料來源)、mappers(mapper檔案路徑)
<configuration>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
<mappers>
<mapper class="classpath*:mybatis/UserMapper.xml"/>
</mappers>
....
</configuration>
複製程式碼
2.編寫一個mappr-xx.xml檔案
<mapper namespace="com.alibaba.test.UserDao">
<select id="getUser" parameterType="com.alibaba.test.UserBO"
resultType="com.alibaba.test.UserDO">
select user_id as userId,user_name userName,age from op_user_info where user_id>#{userId}
</select>
......
</mapper>
複製程式碼
3.編寫一個介面Interface
public interface UserDao {
public UserDO getUser(UserBO UserBO)
.......
}
複製程式碼
<!--注意這裡介面的方法名和mappr-xx.xml中的id一一對應-->
4.開始查詢資料庫
注意我們在查詢資料的時候有兩種選擇
4.1 第一種方式,使用SqlSession的方法直接獲取
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
UserBO userBO= new UserBO();
userBO.setUserId(148736);
UserDO user = (UserDO) sqlSession.select("com.alibaba.test.UserDao.getUser", userBO);
} finally {
session.close();
}
複製程式碼
注意mybatis所有的操作增刪改查都是從這句程式碼開始,後面我們分析sql的執行流程時也將從這句程式碼開始。
4.2 第二種方式,通過介面UserDao的實現類獲取
SqlSession session = sqlSessionFactory.openSession();
try {
UserBO userBO= new UserBO();
userBO.setUserId(148736);
UserDao userDao = session.getMapper(UserDao.class);
UserDO user userDao.getUser(userBO);
} finally {
session.close();
}
複製程式碼
分析:第二種方式使用了動態代理的方式獲取了Interface(UserDao)的實現類,最終還是通過第一種方式獲取資料,一直跟蹤程式碼在MapperProxy類中可以找到這段邏輯
public class MapperProxy<T> implements InvocationHandler, Serializable {
@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);
}
}
複製程式碼
所以我們得出結論mybatis所有的操作增刪改查都是從這句程式碼開始
二、mybatis初始化
在此之前我們先了解下mybatis初始化階段做了些什麼事情,順便找出我們的主角SqlSession是怎麼產生的
1.mybatis初始化邏輯
1)String resource = "org/mybatis/example/mybatis-config.xml";
2)InputStream inputStream = Resources.getResourceAsStream(resource);
3)SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
4)SqlSession session = sqlSessionFactory.openSession();
複製程式碼
跟蹤第二行程式碼可以看到mybatis將配置檔案mybatis-config.xml中的所有資訊都解析到了工廠類SqlSessionFactory中,SqlSessionFactory將所有的配置資訊儲存在了Configuration類中。SqlSession就是從SqlSessionFactory中建立的。
2.Configuration類
Configuration中存放了mybatis-config.xml中的所有配置資訊
1)Environment environment封裝了資料來源資訊
2) protected final Map mappedStatements 封裝了mapepr-xx.xml中的資訊。
其中Map的key為mapepr-xx.xml中中的namespace+id,MappedStatement封裝了
三、mybatis中幾個重要的類
SqlSession 作為MyBatis工作的主要頂層API,表示和資料庫互動的會話,完成必要資料庫增刪改查功能
Executor MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護
StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數、將Statement結果集轉換成List集合。
ParameterHandler 負責對使用者傳遞的引數轉換成JDBC Statement 所需要的引數,
ResultSetHandler 負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合;
TypeHandler 負責java資料型別和jdbc資料型別之間的對映和轉換
MappedStatement MappedStatement維護了一條節點的封裝,
SqlSource 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回
BoundSql 表示動態生成的SQL語句以及相應的引數資訊
Configuration MyBatis所有的配置資訊都維持在Configuration物件之中。
四、資料查詢的執行流程
1.通過第一篇文章的分析我們知道所有的執行流程從這句程式碼開始,其中"com.alibaba.test.UserDao.getUser"是
Configuration類中 Map mappedStatements的key值。
UserDO user = (UserDO) sqlSession.select("com.alibaba.test.UserDao.getUser", userBO);
複製程式碼
2.進入select方法發現首先根據statementId從 Map mappedStatements中獲取到了封裝的MappedStatement,然後將資料查詢操作委託給了Executor executor。
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
複製程式碼
3.跟蹤executor的query方法。說道Executor,mybatis有三種,他們的區別如下
SimpleExecutor:每執行一次update或select,就開啟一個Statement物件,用完立刻關閉Statement物件。(可以是Statement或PrepareStatement物件)
ReuseExecutor:執行update或select,以sql作為key查詢Statement物件,存在就使用,不存在就建立,用完後,不關閉Statement物件,而是放置於Map內,供下一次使用。(可以是Statement或PrepareStatement物件),程式碼如下
BatchExecutor:執行update(沒有select,JDBC批處理不支援select),將所有sql都新增到批處理中(addBatch()),等待統一執行(executeBatch()),它快取了多個Statement物件,每個Statement物件都是addBatch()完畢後,等待逐一執行executeBatch()批處理的;
我們進入SimpleExecutor的query方法,在這個方法中通過ms.getBoundSql(parameter)生成了具體執行的sql。並將結果存在了BoundSql中。併為當前的查詢建立一個快取Key ,至此我們得到了一個可以正常執行的完整sql。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1.根據具體傳入的引數,動態地生成需要執行的SQL語句,用BoundSql物件表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2.為當前的查詢建立一個快取Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
複製程式碼
4.繼續跟蹤query(ms, parameter, rowBounds, resultHandler, key, boundSql);
這段程式碼中根據上一步獲取的CacheKey從快取中獲取結果,如果快取結果為空則呼叫
queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
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;
}
複製程式碼
5.我們繼續跟蹤queryFromDatabase。這個方法先執行查詢返回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 {
//4. 執行查詢,返回List 結果,然後 將查詢的結果放入快取之中
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;
}
複製程式碼
6.根據 doQuery(ms, parameter, rowBounds, resultHandler, boundSql);方法。以上都是抽象類BaseExecutor中的方法。至此進入預設實現類SimpleExecutor。
SimpleExecutor.doQuery原始碼
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 根據既有的引數,建立StatementHandler物件來執行查詢操作
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//建立java.Sql.Statement物件,傳遞給StatementHandler物件
stmt = prepareStatement(handler, ms.getStatementLog());
//呼叫StatementHandler.query()方法,返回List結果集
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
複製程式碼
該函式的作用如下:
6.1 根據既有的引數,建立StatementHandler物件來執行查詢操作,在這段程式碼中我們發現了 interceptorChain.pluginAll(statementHandler) ,我們經常見到的mybatis攔截器就是在這個地方開始生效的。
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
複製程式碼
6.2 呼叫prepareStatement方法建立java.Sql.Statement物件,並對建立的Statement物件設定引數,即設定SQL 語句中 ? 設定為指定的引數 ,最後傳遞給StatementHandler物件
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;
}
複製程式碼
6.3 呼叫StatementHandler.query()方法
StatementHandler物件負責設定Statement物件中的查詢引數、處理JDBC返回的resultSet,將resultSet加工為List
6.3.1進入PreparedStatementHandler的query方法,該函式中終於看到了我們熟悉的程式碼。進行了最終的資料庫查詢操作。並將結果交給了ResultSetHandler處理
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
複製程式碼
6.3.2 跟蹤ResultSetHandler的實現類DefaultResultSetHandler。找到了handleResultSets方法。
ResultSetHandler的handleResultSets(Statement) 方法會將Statement語句執行後生成的resultSet 結果集轉換成List 結果集
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<Object>();
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);
}
複製程式碼
五、總結
sql執行流程總結如下:
1.呼叫SqlSession的select方法,傳入StatementId(Mappr-xx.xml的namespace+id)和查詢條件引數
2.呼叫Configuration的getMappedStatement方法獲取StatementId對應的MappedStatement,並呼叫Executor的query方法
3.呼叫Executor的query方法,根據入參獲取具體的sql,封裝到BoundSql中
4.建立StatementHandler物件執行查詢操作
5.ResultSetHandler將查詢結果轉化為需要的格式