在前面的探索中,我們已經(jīng)知道了 MyBatis 是如何 getMapper 并執(zhí)行 Mapper 接口中的方法來進(jìn)行數(shù)據(jù)庫操作的雷滋。那么今天我們就來看看 Mapper 方法執(zhí)行的“前因”:獲取語句+參數(shù)映射内狗。
我們還是按照之前的方式凫碌,使用 debug 在入口代碼上打斷點(diǎn),步入源碼有送。入口代碼為:
List<Author> list = mapper.selectByName("Sylvia");
對(duì)應(yīng)的 SQL:
<select id="selectByName" resultMap="AuthorMap" >
select
id, name, sex, phone
from author
where name = #{name}
</select>
1 從 Mapper XML 讀取 SQL
首先淌喻,我們要思考一下,因?yàn)?SQL 語句是定義在 Mapper XML 中的雀摘,那么毫無疑問它會(huì)去讀取該 Mapper XML 中的內(nèi)容裸删。可是它會(huì)在什么時(shí)候讀取呢阵赠?答案是在獲取 SqlSessionFactory 的時(shí)候涯塔,即執(zhí)行:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
為了驗(yàn)證,我們可以在這行打個(gè)斷點(diǎn)來一探究竟清蚀。
我們一直往下走匕荸,直到 org.apache.ibatis.builder.xml.XMLConfigBuilder 的 parseConfiguration 方法,我們?cè)谥耙呀?jīng)見過這個(gè)方法了枷邪,它的作用是解析 mybatis-config.xml 文件中的配置榛搔,將相應(yīng)的元素內(nèi)容轉(zhuǎn)換到 Configuration 等類中:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我們要看的是 Mapper XML 中 SQL 是不是在這個(gè)地方讀取的,理所應(yīng)當(dāng)進(jìn)入最后一行解析代碼:mapperElement(root.evalNode("mappers"));。這個(gè)時(shí)候我們會(huì)進(jìn)入到 org.apache.ibatis.builder.xml.XMLConfigBuilder 的 mapperElement 方法:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}//略...
}
}
}
}
在這個(gè)方法中我們可以看到它在解析 mybatis-config.xml 中的 mappers 元素內(nèi)容践惑。注意最后一行代碼:mapperParser.parse(); 看名字它似乎就是想要去解析 Mapper XML 的內(nèi)容绑洛,進(jìn)入 org.apache.ibatis.builder.xml.XMLMapperBuilder 的 parse() 方法:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
繼續(xù)進(jìn)入第 3 行代碼,進(jìn)入到 configurationElement() 方法:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
果然童本,這個(gè)方法就是用來解析 Mapper XML 的,我們可以看到平時(shí)在 Mapper XML 中常用的 resultMap脸候、sql 等穷娱。很明顯倒數(shù)第 5 行就是要讀取 SQL 語句的,所以我們直接進(jìn)入第 5 行:buildStatementFromContext(context.evalNodes("select|insert|update|delete"));运沦,進(jìn)入到 org.apache.ibatis.builder.xml.XMLStatementBuilder 的 parseStatementNode 方法()泵额。這時(shí),我們就能看到它對(duì)每個(gè)元素進(jìn)行了解析(由于篇幅過長携添,我們省略掉部分源代碼嫁盲,感興趣的同學(xué)可以動(dòng)手查看):
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
//太長省略啦...
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//太長省略啦...
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
我們先進(jìn)入第 6 行,直到 org.apache.ibatis.scripting.xmltags.XMLScriptBuilder 的 parseScriptNode() 方法:
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
通過第 7 行代碼烈掠,我們會(huì)發(fā)現(xiàn)它將 sql 語句封裝在了 sqlSource 中(感興趣的話可以跟進(jìn)去看看)羞秤。此時(shí)的 sqlSource 是這個(gè)樣子噠:
好了這個(gè)時(shí)候我們已經(jīng)拿到包含了 sql 語句的 sqlSource 了,那么我們來繼續(xù)看一下它要拿這個(gè) sqlSource 來做什么左敌●埃回到 parseStatementNode() 方法,繼續(xù)進(jìn)入 builderAssistant.addMappedStatement(...) 代碼:
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
//參數(shù)太長省略啦...) {
//...
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
//...
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
首先我們要進(jìn)入第 6 行看一下 org.apache.ibatis.mapping.MappedStatement 類的 Builder 構(gòu)造方法矫限,我們可以看出此時(shí) sqlSource 賦值給了 mappedStatement 這個(gè)成員變量的 sqlSource哺哼,也就是說此時(shí)該 Builder 就持有了包含 SQL 的 sqlSource:
public static class Builder {
private MappedStatement mappedStatement = new MappedStatement();
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
//略...
}
那么接著我們從這個(gè)構(gòu)造方法出來繼續(xù)往下看,從倒數(shù)第 3 行代碼
configuration.addMappedStatement(statement); 就可以看出叼风,包含 SQL 的 statement 最終存進(jìn)了 configuration取董。Configuration 中有個(gè) Map 類型的成員變量 mappedStatements,如下:
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
那么點(diǎn)進(jìn)去 configuration.addMappedStatement(statement);无宿,不出意外茵汰,它一定是在給 mappedStatements 變量 put 內(nèi)容,而 key 就是語句的 id:
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
Bingo!
好了懈贺,現(xiàn)在就讓我們先牢記這個(gè)結(jié)論:Mapper XML 中的 SQL 語句(們)在構(gòu)建 SqlSessionFactory 的時(shí)候存入了 Configuration 實(shí)例的 mappedStatements (Map 類型)中经窖。
2 參數(shù)映射
現(xiàn)在就讓我們用文章開頭的入口代碼來 debug 進(jìn)入,看看 MyBatis 在執(zhí)行 SQL 之前是如何處理語句和參數(shù)的梭灿。其實(shí)上一節(jié)我們已經(jīng)跟蹤過 selectList 方法了画侣,只是上次我們的關(guān)注點(diǎn)在數(shù)據(jù)庫方法執(zhí)行上,現(xiàn)在我們就把關(guān)注點(diǎn)放在 SQL 處理和參數(shù)映射上堡妒。
我們跳過前面的代碼直到 org.apache.ibatis.executor.SimpleExecutor 的 doQuery(...) 方法:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
進(jìn)入第 7 行代碼配乱,走到 org.apache.ibatis.executor.SimpleExecutor 的 prepareStatement(...) 方法:
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;
}
該方法即為處理 SQL 和參數(shù)的核心方法。其中,第 3 行代碼為獲取數(shù)據(jù)庫連接搬泥,第 4 行代碼為獲取 PreparedStatement桑寨,第 5 行代碼為參數(shù)映射。我們依次來看一下這三行代碼具體的實(shí)現(xiàn)忿檩。
2.1 獲取連接 Connection connection = getConnection(statementLog);
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
進(jìn)入方法尉尾,我們看到它先是通過 Transaction 獲取了一個(gè)連接,然后判斷日志級(jí)別是不是 debug 燥透,如果是沙咏,就會(huì)執(zhí)行 ConnectionLogger.newInstance(...) 方法,不是則直接返回 connection班套。其中肢藐,MyBatis 的 Transaction 接口主要負(fù)責(zé)獲取和關(guān)閉連接、提交和回滾事務(wù)吱韭,它有兩個(gè)實(shí)現(xiàn)類:
- JdbcTransaction 直接使用了 JDBC 的提交和回滾設(shè)置吆豹。
- ManagedTransaction 幾乎什么也不做,翻開源碼你會(huì)看到提交和回滾的方法里只有一行注釋代碼 “Does nothing”理盆,它讓容器來管理事務(wù)的全生命周期(例如 JEE 應(yīng)用服務(wù)器的上下文)痘煤。
- 當(dāng)然我們也可以自定義自己的實(shí)現(xiàn)類,有興趣的同學(xué)可以自己玩兒一下(可參考文末附的項(xiàng)目實(shí)踐)猿规。
另外速勇,關(guān)于 ConnectionLogger.newInstance(...) 方法,根據(jù) statementLog.isDebugEnabled() 的判斷條件我們很容易能想到它是要處理連接時(shí)的日志輸出的坎拐》炒牛基于查看語句處理的目的,我們還是要跟進(jìn)去看一下是否進(jìn)行了其他操作:
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
還是熟悉的代碼哼勇,還是熟悉的配方:動(dòng)態(tài)代理都伪!通過 ConnectionLogger 的代理動(dòng)態(tài)生成 Connection 類。
2.2 獲取 PreparedStatement 對(duì)象 :stmt = handler.prepare(connection, transaction.getTimeout());
這行代碼用來獲取執(zhí)行語句的 PreparedStatement积担,我們跟入代碼直到 org.apache.ibatis.executor.statement.PreparedStatementHandler 的 instantiateStatement(...) 方法:
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
//省略啦...
} else {
return connection.prepareStatement(sql);
}
}
我們看到在這里調(diào)用了 Connection 對(duì)象的 prepareStatement(...) 方法陨晶,從(1)的分析中我們知道,如果我們開啟了 debug 日志級(jí)別帝璧,那么此時(shí)的 Connection 為動(dòng)態(tài)代理生成的類先誉,調(diào)用其方法一定會(huì)進(jìn)入代理類的 invoke(...)。那么現(xiàn)在的烁,就讓我們跟進(jìn)代理類 ConnectionLogger 來一探究竟吧:
@Override
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else {
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
我們來解讀一下這個(gè)方法:首先它會(huì)在調(diào)用 connection 方法的時(shí)候根據(jù)方法名進(jìn)行判斷褐耳,我們以第 8 行處的
if 為例。如果是 connection.prepareStatement(...)渴庆,則首先會(huì)打印相應(yīng)的連接日志铃芦。然后雅镊,重點(diǎn)來了,它會(huì)獲取一個(gè) PreparedStatement刃滓,之后再調(diào)用 PreparedStatementLogger.newInstance(...) 方法并覆蓋前一行獲取的 PreparedStatement 對(duì)象仁烹。 PreparedStatementLogger 和 ConnectionLogger 的實(shí)現(xiàn)非常像:
public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
ClassLoader cl = PreparedStatement.class.getClassLoader();
return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
}
和獲取連接時(shí)打印日志和動(dòng)態(tài)生成 PreparedStatement 一樣,這里也通過動(dòng)態(tài)代理的方式打印日志并動(dòng)態(tài)生成下一步要用到的結(jié)果集 ResultSet 咧虎。這里不再深入探討了卓缰,等到我們看結(jié)果映射的時(shí)候再來討論。大家可以看出來砰诵,MyBatis 作為一個(gè)優(yōu)秀的 ORM 框架僚饭,有很多值得我們學(xué)習(xí)的地方,光是動(dòng)態(tài)代理的使用就很有意思了胧砰。
好了,我們已經(jīng)拿到 PreparedStatement 了苇瓣,下一步就是要處理參數(shù)了尉间。
2.3 參數(shù)映射:handler.parameterize(stmt);
我們?cè)谖臋n篇就知道了 MyBatis 通過 TypeHandler 來進(jìn)行參數(shù)和結(jié)果映射,這里既然要分析參數(shù)映射击罪,那么我們猜測它應(yīng)該會(huì)通過 TypeHandler 去實(shí)現(xiàn)哲嘲。不急,我們來慢慢驗(yàn)證一下:
handler 是 RoutingStatementHandler 類型的媳禁,但是它通過裝飾器模式委托給了 PreparedStatementHandler 來執(zhí)行(它們都是 StatementHandler 接口的實(shí)現(xiàn)類)眠副,并將上面我們得到的 PreparedStatement 作為參數(shù)傳遞了進(jìn)去。而跟入到 PreparedStatementHandler 我們又發(fā)現(xiàn)它通過 parameterHandler (DefaultParameterHandler 類型)來執(zhí)行竣稽,我們依次跟入會(huì)看到:
org.apache.ibatis.executor.statement.RoutingStatementHandler 類:
@Override
public void parameterize(Statement statement) throws SQLException {
delegate.parameterize(statement);
}
org.apache.ibatis.executor.statement.PreparedStatementHandler 類:
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
org.apache.ibatis.scripting.defaults.DefaultParameterHandler 類:
@Override
public void setParameters(PreparedStatement ps) {
//...
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//這里是對(duì) value 的賦值囱怕,略...
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);//主要是這行P宓摹O劢唷磷蜀!
} //...
}
}
}
}
注意 try 中的代碼:typeHandler丁侄!這里就驗(yàn)證了我們開始的猜測惹苗。它將參數(shù)值 value失尖,參數(shù)索引位置(i+1)和 jdbcType 作為參數(shù)傳遞給 typeHandler(該測試代碼中為 ObjectTypeHandler 類型) 的 setParameter 方法善榛。ObjectTypeHandler 繼承自 BaseTypeHandler 類戴而,同時(shí)繼承了其 setParameter 方法砾肺,于是下一步會(huì)進(jìn)入 BaseTypeHandler 類:
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
//...
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
//...
}
}
}
它又調(diào)用了自己(子類 ObjectTypeHandler)的 setNonNullParameter(...) 方法挽霉,進(jìn)入:
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
handler.setParameter(ps, i, parameter, jdbcType);
}
setNonNullParameter(...) 方法的第一行代碼是通過參數(shù)值實(shí)際類型(parameter.getClass())和 jdbcType 自動(dòng)推算其 TypeHandler,這也印證了我們?cè)谖臋n篇中多次提到的 TypeHandler 不需要顯式地定義变汪,MyBatis 會(huì)自動(dòng)推算出來侠坎。我們繼續(xù)深入方法的第二行代碼,直到 StringTypeHandler 的 setNonNullParameter(...):
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
}
終于看到我們熟悉的 JDBC 代碼了裙盾,那么參數(shù)在這里就被填充進(jìn) PreparedStatement 中了硅蹦。
好了荣德,到此為止 PreparedStatement 就完全準(zhǔn)備好了,這時(shí)它就可以執(zhí)行了童芹。
附:
當(dāng)前版本:mybatis-3.5.0
官網(wǎng)文檔:MyBatis
項(xiàng)目實(shí)踐:MyBatis Learn
手寫源碼:MyBatis 簡易實(shí)現(xiàn)