mybatis作爲一款持久層框架,最爲主要的職責,當然是執行我們在mapper映射文件中寫的sql語句,但是sql語句也分兩種:
(1)普通sql語句
(2)動態sql語句,包含了<if | foreach | choose | when | otherwise | where | set | trim>等可以動態拼接sql的標籤
本篇博客將以普通的sql語句執行,根據debug源碼的方式,跟蹤sql是如何解析,以及最後是如何執行的。測試代碼,將以之前博客 MyBatis入門之一對多、多對多查詢 的代碼爲例。首先看看mapper映射文件及測試代碼:
UserMapper.xml:
<mapper namespace="com.qxf.mapper.UserMapper">
<!-- 自定義結果映射:
id:是主鍵
property:是Java對象的屬性名
column:是數據庫中列名,或者查詢中取的列的別名
-->
<resultMap id="userMap" type="User">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="password" column="password"></result>
<result property="isValid" column="is_valid"></result>
<!-- 一對多關聯查詢,使用collection,其中:
collection中的property屬性blogs:是User類中的屬性名稱,對應就可以了(private List<Blog> blogs);
javaType:是User類中的blogs這個屬性,屬於哪個Java類型,這裏當然是屬於java.util.List
ofType: 是List泛型中的類型,這裏是Blog類型
id中的column是t_blog表的id,只是在爲了避免重名衝突,查詢的時候取了別名blog_id
-->
<collection property="blogs" javaType="java.util.List" ofType="Blog">
<id property="id" column="blog_id"></id>
<result property="title" column="title"></result>
<result property="userId" column="user_id"></result>
</collection>
</resultMap>
<!--id 與接口的方法名相同,注意:
這裏的返回值類型不再是resultType了,而是resultMap
而且resultMap的值,就是上面<resultMap>標籤的id屬性值
-->
<select id="getUserAndBlogByUserId" parameterType="string" resultMap="userMap">
select u.id,u.username,u.password,u.is_valid,b.id as blog_id,b.title,b.user_id
from t_user u,t_blog b
where u.id = b.user_id
and u.id=#{id}
</select>
</mapper>
測試代碼:
public class One2ManyQuery {
public static void main(String[] args) throws IOException {
//讀取配置信息
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
//根據配置信息,創建SqlSession工廠
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
//SqlSession工廠創建SqlSession
SqlSession sqlSession = factory.openSession();
//獲取接口的代理對象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserAndBlogByUserId("123");
System.out.println(user);
}
}
這裏,將以下面這句代碼爲起點,我的想法是將mybatis的原理進行劃分成幾個模塊分析,首先從結果分析,然後再倒推。所以現在,關於mybatis如何讀取並解析配置文件的、如何創建SqlSession的、如何創建接口的代理對象的,後面有時間再寫,敬請關注。
User user = mapper.getUserAndBlogByUserId("123");
step into跟進去,發現會調用MapperProxy的invoke()方法,證實了上面的mapper是接口的代理對象:
再跟進去:發現execute()方法,會先判斷sql的類型:INSERT、UPDATE、DELETE、SELECT、FLUSH,我們這裏是select查詢,所以我們只需要關注select分支語句:
//execute()方法的2個全局變量
//result:是方法的返回值,Object類型
//param: 是方法參數,Object類型
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
//最終會走到這裏,先進行參數解析,其中args是我們傳入的參數,Object[]類型
param = this.method.convertArgsToSqlCommandParam(args);
//然後執行sqlSession.selectOne獲得sql語句執行結果
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
所以這裏又分爲2步:
(1)解析sql參數爲Object類型
(2)執行sqlSession.selectOne獲得sql語句執行結果
一、sql參數是如何解析的?
重點關注這句代碼:
param = this.method.convertArgsToSqlCommandParam(args);
一路跟進去,發現最終執行的是ParamNameResolver的getNamedParams()方法
public Object getNamedParams(Object[] args) {
int paramCount = this.names.size();
if (args != null && paramCount != 0) {
//沒有參數註解,並且只有一個參數,則直接返回第一個參數
if (!this.hasParamAnnotation && paramCount == 1) {
return args[(Integer)this.names.firstKey()];
} else {
//否則,返回一個ParamMap對象
Map<String, Object> param = new ParamMap();
int i = 0;
for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
//下面的意思是,將傳進來的參數封裝成Map對象
//其中key爲:arg0、arg1....以及param1、param2...
//而value就是我們依次傳入的參數值
Entry<Integer, String> entry = (Entry)var5.next();
param.put((String)entry.getValue(), args[(Integer)entry.getKey()]);
String genericParamName = "param" + String.valueOf(i + 1);
if (!this.names.containsValue(genericParamName)) {
param.put(genericParamName, args[(Integer)entry.getKey()]);
}
}
return param;
}
} else {
return null;
}
}
那麼,ParamMap又是什麼鬼?其源碼如下:
public static class ParamMap<V> extends HashMap<String, V> {
private static final long serialVersionUID = -2212268410512043556L;
public ParamMap() {
}
public V get(Object key) {
if (!super.containsKey(key)) {
throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + this.keySet());
} else {
return super.get(key);
}
}
}
根據源碼,ParamMap繼承了HashMap,只是重寫了get()方法,所以完全可以將它看做是一個HashMap。
根據上面分析:
(1)如果只有一個參數的話,則直接返回一個Object參數
(2)如果有多個參數的話,則會返回一個Map對象,其中key爲:arg0、arg1....以及param1、param2...
而value就是我們依次傳入的參數值
二、sqlSession.selectOne()方法是如何執行的?
result = sqlSession.selectOne(this.command.getName(), param);
其中,第一個參數是namespace+id組成的字符串,這裏是"com.qxf.mapper.UserMapper.getUserAndBlogByUserId"
第二個參數,是我們傳入的字符串參數,"123"
先看看selectOne的樣子:
發現,它最終還是調用了DefaultSqlSession的selectList()方法,然後根據返回結果判斷:
(1)如果返回的list大小隻有一個,則符合預期,直接返回list的第一個元素
(2)如果list的大小,大於1個,那麼就會拋出異常
(3)返回null值
所以,現在的目標,自然是轉向了DefaultSqlSession的selectList()方法,最終執行重載的方法如下:
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
//根據statement=namespace+id=com.qxf.mapper.UserMapper.getUserAndBlogByUserId
//在配置信息中查找對應的MappedStatement
//每個MappedStatement,其實就是我們的<select|update|insert|delete>等標籤及其內容
MappedStatement ms = this.configuration.getMappedStatement(statement);
//通過執行器executor的query()方法執行
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
其中,RowBounds是一個內存分頁對象,包含offset和limit兩個屬性。
目光轉移到,executor的query()方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//通過傳入的參數,構造BoundSql對象,就是給它相應的屬性賦值,這裏不細究
BoundSql boundSql = ms.getBoundSql(parameterObject);
//創建緩存的key值
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
//執行重載方法
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
這裏邊有很多重載的方法,就不一一列舉了,因爲是第一次查詢,不存在緩存,所以後面會執行到BaseExecutor的queryFromDatabase()方法,顧名思義,就是從數據庫查詢
繼而轉向doQuery()方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
List var9;
try {
Configuration configuration = ms.getConfiguration();
//構建StatementHandler對象
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//創建Statement
stmt = this.prepareStatement(handler, ms.getStatementLog());
//執行查詢
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
}
return var9;
}
這裏有幾個關鍵的地方:
(1)StatementHandler 是什麼?
(2)參數是如何設置的?
(3)sql是怎樣執行的?
(4)結果集是怎樣處理的?
(一)StatementHandler 是什麼?
對這行代碼進行跟蹤:
//構建StatementHandler對象
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
先看下繼承關係(圖片來源於https://www.cnblogs.com/cxuanBlog/p/11295488.html):
StatementHandler的默認實現類是RoutingStatementHandler,而RoutingStatementHandler持有一個StatementHandler的對象,也就是變量delegate,而變量delegate則會根據statementType來創建,這裏會創建PreparedStatementHandler:
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
StatementHandler主要負責操作 Statement 對象與數據庫進行交互。
而創建PreparedStatementHandler對象,會調用父類的構造方法:
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.objectFactory = configuration.getObjectFactory();
if (boundSql == null) { // issue #435, get the key before calculating the statement
generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}
this.boundSql = boundSql;
//創建參數處理器對象ParameterHandler
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
//創建結果處理器對象ResultSetHandler
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}
重點關注參數處理器和結果處理器,後面都會用到:
//創建參數處理器對象ParameterHandler
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
//創建結果處理器對象ResultSetHandler
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
(二)參數是如何設置的?
回到doQuery方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
List var9;
try {
Configuration configuration = ms.getConfiguration();
//構建StatementHandler對象
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//創建Statement
stmt = this.prepareStatement(handler, ms.getStatementLog());
//執行查詢
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
}
return var9;
}
我們重點關注這句:
//創建Statement
stmt = this.prepareStatement(handler, ms.getStatementLog());
跟進去,會發現調用一個方法 handler.parameterize(stmt);
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//獲取連接
Connection connection = getConnection(statementLog);
//創建一個Statement對象,這裏創建的是PreparedStatement對象
stmt = handler.prepare(connection, transaction.getTimeout());
//處理參數
handler.parameterize(stmt);
return stmt;
}
那麼,我們現在看看, handler.parameterize(stmt)方法的實現,點進去發現有很多重載的方法,後面會調用在
(一)StatementHandler 是什麼? 中,指到的參數處理器的setParameters()方法:
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();
if (parameterMappings != null) {
for(int i = 0; i < parameterMappings.size(); ++i) {
ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
//屬性名
String propertyName = parameterMapping.getProperty();
Object value;
if (this.boundSql.hasAdditionalParameter(propertyName)) {
value = this.boundSql.getAdditionalParameter(propertyName);
} else if (this.parameterObject == null) {
value = null;
} else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {
value = this.parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = this.configuration.getJdbcTypeForNull();
}
try {
//設置參數,value是屬性值
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (SQLException | TypeException var10) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10);
}
}
}
}
}
重點關注:
//設置參數,value是屬性值
typeHandler.setParameter(ps, i + 1, value, jdbcType);
最終會發現,它調用的JDBC中PreparedStatement的對應的setXXX(i, parameter);
至此,我們傳遞的參數就設置好了。
(三)sql是怎樣執行的?
還是回到doQuery方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
List var9;
try {
Configuration configuration = ms.getConfiguration();
//構建StatementHandler對象
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//創建Statement
stmt = this.prepareStatement(handler, ms.getStatementLog());
//執行查詢
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
}
return var9;
}
重點看這句:
//執行查詢
var9 = handler.query(stmt, resultHandler);
可以看到,調用的是StatementHandler的query方法,更爲具體的來說,是調用PreparedStatementHandler的query方法:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
層層跟進之後,發現,還是調用JDBC中PreparedStatement的execute()方法執行sql語句的。
至此,sql語句也已經執行完了。
(四)結果集是怎樣處理的?
緊接着上面這行代碼:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
//執行預編譯的sql語句
ps.execute();
//處理結果集
return resultSetHandler.handleResultSets(ps);
}
所以,我們要看的是:
//處理結果集
return resultSetHandler.handleResultSets(ps);
看看它的具體實現:
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
//獲取第一個結果集(ps.getResultSet()即可獲取),並將結果集進行包裝
ResultSetWrapper rsw = getFirstResultSet(stmt);
//獲取我們配置的ResultMap
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);
}
我們重點關注這句,主要看看是怎麼給實體類的屬性賦值的:
//處理結果集
handleResultSet(rsw, resultMap, multipleResults, null);
看看它的具體實現:
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
//處理行值
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
然後又會調用 handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
//這裏會判斷,有沒有嵌套的resultMap
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
//代碼會執行到這裏
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
執行到 handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
該方法內部會調用:
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
繼而調用applyPropertyMappings()方法,對屬性進行賦值:
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
//獲取列名
String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.getNestedResultMapId() != null) {
// the user added a column attribute to a nested result map, ignore it
column = null;
}
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
//根據列名獲取該列的值,resultSet.getXXX(列名) 獲取
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
// issue #541 make property optional
final String property = propertyMapping.getProperty();
if (property == null) {
continue;
} else if (value == DEFERRED) {
foundValues = true;
continue;
}
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
// gcode issue #377, call setter on nulls (value is not 'found')
//給屬性賦值,property是實體類的屬性名,value則是查詢的值
metaObject.setValue(property, value);
}
}
}
return foundValues;
}
具體表現爲:
(1)獲取列值:
//根據列名獲取該列的值,resultSet.getXXX(列名) 獲取
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
(2)給屬性賦值:
//給屬性賦值,property是實體類的屬性名,value則是查詢的值
metaObject.setValue(property, value);
基本上是這樣了。
第一次寫原理類的東西,感覺一下子追源碼追得太深,並不好。