我們之前介紹過MappedStatement表示的是XML中的一個SQL。類當中的很多字段都是SQL中對應的屬性。我們先來了解一下這個類的屬性:
public final class MappedStatement { private String resource; private Configuration configuration; //sql的ID private String id; //嘗試影響驅動程序每次批量返回的結果行數和這個設置值相等 private Integer fetchSize; //SQL超時時間 private Integer timeout; //Statement的類型,STATEMENT/PREPARE/CALLABLE private StatementType statementType; //結果集類型,FORWARD_ONLY/SCROLL_SENSITIVE/SCROLL_INSENSITIVE private ResultSetType resultSetType; //表示解析出來的SQL private SqlSource sqlSource; //緩存 private Cache cache; //已廢棄 private ParameterMap parameterMap; //對應的ResultMap private List<ResultMap> resultMaps; private boolean flushCacheRequired; private boolean useCache; private boolean resultOrdered; //SQL類型,INSERT/SELECT/DELETE private SqlCommandType sqlCommandType; //和SELECTKEY標簽有關 private KeyGenerator keyGenerator; private String[] keyProperties; private String[] keyColumns; private boolean hasNestedResultMaps; //數據庫ID,用來區分不同環境 private String databaseId; private Log statementLog; private LanguageDriver lang; //多結果集時 private String[] resultSets; MappedStatement() { // constructor disabled } ... }
對一些重要的字段我都增加了備注,方便理解。其中真正表示SQL的字段是SqlSource這個對象。
SqlSource接口很簡單,只有一個getBound方法:
public interface SqlSource { BoundSql getBoundSql(Object parameterObject); }
它有很多實現,需要我們重點關注的是StaticSqlSource,RawSqlSource和DynamicSqlSource。在正式學習他們前,我們先了解一下Mybatis動態SQL和靜態SQL的區別。
動態SQL表示這個SQL節點中含有${}或是其他動態的標簽(比如,if,trim,foreach,choose,bind節點等),需要在運行時根據傳入的條件才能確定SQL,因此對於動態SQL的MappedStatement的解析過程應該是在運行時。
而靜態SQL是不含以上這個節點的SQL,能直接解析得到含有占位符形式的SQL語句,而不需要根據傳入的條件確定SQL,因此可以在加載時就完成解析。所在在執行效率上要高於動態SQL。
而DynamicSqlSource和RawSqlSource就分別對應了動態SQL和靜態SQL,它們都封裝了StaticSqlSource。
我們先從簡單的入手,了解靜態SQL的解析過程。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper"> <select id="selectAllAuthors" resultType="org.apache.ibatis.domain.blog.Author"> select * from author </select> </mapper>
這是我們要解析的XML文件,mapper節點下只有一個select節點。
public class XmlMapperBuilderTest { @Test public void shouldSuccessfullyLoadXMLMapperFile() throws Exception { Configuration configuration = new Configuration(); String resource = "org/apache/ibatis/builder/AuthorMapper.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder builder = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); builder.parse(); inputStream.close(); } }
這是我們測試解析過程的代碼。我們可以看到解析是由XMLMapperBuilder開始的。我們先了解一下它的字段:
public class XMLMapperBuilder extends BaseBuilder { //用來解析XML private final XPathParser parser; //再解析完成后,用解析所得的屬性來幫助創建各個對象 private final MapperBuilderAssistant builderAssistant; //保存SQL節點 private final Map<String, XNode> sqlFragments; //... }
它還從父類中繼承了configuration(配置對象),typeAliasRegistry(類型別名注冊器)和typeHandlerRegistry(類型處理器注冊器)。
接下來看一下它的parse方法:
public void parse() { //判斷是否已經加載過資源 if (!configuration.isResourceLoaded(resource)) { //從mapper根節點開始解析 configurationElement(parser.evalNode("/mapper")); //將該資源添加到為已經加載過的緩存中 configuration.addLoadedResource(resource); //將解析的SQL和接口中的方法綁定 bindMapperForNamespace(); } //對一些未完成解析的節點再解析 parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
主要的解析過程在configurationElement中:
private void configurationElement(XNode context) { try { //解析mapper的namespace屬性,並設置 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); //解析<cache-ref>節點,它有一個namespace屬性,表示引用該命名空間下的緩存 cacheRefElement(context.evalNode("cache-ref")); //解析<cache>節點,可以設置緩存類型和屬性,或是指定自定義的緩存 cacheElement(context.evalNode("cache")); //已廢棄,不再使用 parameterMapElement(context.evalNodes("/mapper/parameterMap")); //解析resultMap節點 resultMapElements(context.evalNodes("/mapper/resultMap")); //解析<SQL>節點,SQL節點可以使一些SQL片段被復用 sqlElement(context.evalNodes("/mapper/sql")); //解析SQL語句(select|insert|update|delete節點) 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); } }
我們關注SQL語句的解析過程,上述buildStatementFromContext(List<XNode>)方法會增加dateBaseId的參數,然后調用另一個重載方法:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { //遍歷XNode節點 for (XNode context : list) { //為每個節點創建XMLStatementBuilder對象, final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { //解析Node statementParser.parseStatementNode(); } catch (IncompleteElementException e) { //對不能完全解析的節點添加到incompleteStatement,在parsePendingStatements方法中再解析 configuration.addIncompleteStatement(statementParser); } } }
先看看XMLStatementBuilder對象:
public class XMLStatementBuilder extends BaseBuilder { private final MapperBuilderAssistant builderAssistant; private final XNode context; private final String requiredDatabaseId; // ... }
含有的字段相對簡單,不再具體解釋。直接看parseStatementNode方法:
public void parseStatementNode() { //獲取id String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); //驗證databaseId是否匹配 if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); //已廢棄 String parameterMap = context.getStringAttribute("parameterMap"); //參數類型;將會傳入這條語句的參數類的完全限定名或別名。這個屬性是可選的,因為 MyBatis 可以通過 TypeHandler 推斷出具體傳入語句的參數,默認值為 unset。 String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); //結果類型;外部 resultMap 的命名引用。 String resultMap = context.getStringAttribute("resultMap"); //結果類型;表示從這條語句中返回的期望類型的類的完全限定名或別名。注意如果是集合情形,那應該是集合可以包含的類型,而不能是集合本身。不能和resultMap同時使用。 String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); Class<?> resultTypeClass = resolveClass(resultType); //結果集類型;FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一個,默認值為 unset (依賴驅動)。 String resultSetType = context.getStringAttribute("resultSetType"); //STATEMENT,PREPARED 或 CALLABLE 的一個。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,默認值:PREPARED。 StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); String nodeName = context.getNode().getNodeName(); //SQLCommand類型 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; //flushCache;在執行語句時表示是否刷新緩存 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); //是否對該語句進行二級緩存;默認值:對 select 元素為 true。 boolean useCache = context.getBooleanAttribute("useCache", isSelect); //根嵌套結果相關 boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); //引入SQL片段 // Include Fragments before parsing XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them. //處理selectKey processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); // String resultSets = context.getStringAttribute("resultSets"); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); //設置主鍵自增的方式 KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } //通過buildAssistant將解析得到的參數設置構造成MappedStatement對象 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
將解析得到參數通過BuilderAssistant.addMappedStatement方法,解析得到MappedStatement對象。
上面已經說過sqlsource表示的一個SQL語句,因此我們關注langDriver.createSqlSource這個方法。看XMLLanguageDriver這個實現。
@Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); }
可以看到他將創建sqlsource的工作交給了XMLScrpitBuilder(又一個創建者模式的應用)。來看parseScriptNode方法:
public SqlSource parseScriptNode() { //解析SQL語句節點,創建MixedSqlNode對象 MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource = null; //根據是否是動態的語句,創建DynamicSqlSource或是RawSqlSource對象,並返回 if (isDynamic) { sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; }
MixedSqlNode是SqlNode的一個實現,包含了各個子節點,用來遍歷輸出子節點。SqlNode還有很多不同的實現,分別對應不同的節點類型。對應關系如下:
| SqlNode實現 | 對應SQL語句中的類型 |
| TextSqlNode | ${} |
| IfSqlNode | If節點 |
| TrimSqlNode/WhereSqlNode/SetSqlNode | Trim/Where/Set節點 |
| Foreach節點 | foreach標簽 |
| ChooseSqlNode節點 | choose/when/otherwhise節點 |
| ValDeclSqlNode節點 | bind節點 |
| StaticTextSqlNode | 不含上述節點 |
除了StaticTextSqlNode節點外,其余對應的都是動態語句。
因此我們本文的關注點在StaticTextSqlNode。
讓我們對應文初sql語句的解析來看一下parseDynamicTags方法,為了便於理解,我將在右邊注釋出每一步的結果
protected MixedSqlNode parseDynamicTags(XNode node) {// node是我們要解析的SQL語句: <select resultType="org.apache.ibatis.domain.blog.Author" id="selectAllAuthors">select * from author</select> List<SqlNode> contents = new ArrayList<SqlNode>(); //獲取SQL下面的子節點 NodeList children = node.getNode().getChildNodes();//這里的children只有一個節點; //遍歷子節點,解析成對應的sqlNode類型,並添加到contents中 for (int i = 0; i < children.getLength(); i++) { XNode child = node.newXNode(children.item(i));//第一個child節點就是SQL中的文本數據:select * from author //如果是文本節點,則先解析成TextSqlNode對象 if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { //獲取文本信息 String data = child.getStringBody("");//data:select * from author //創建TextSqlNode對象 TextSqlNode textSqlNode = new TextSqlNode(data); //判斷是否是動態Sql,其過程會調用GenericTokenParser判斷文本中是否含有"${"字符 if (textSqlNode.isDynamic()) {//如果是動態SQL,則直接使用TextSqlNode類型,並將isDynamic標識置為true contents.add(textSqlNode); isDynamic = true; } else {//不是動態sql,則創建StaticTextSqlNode對象,表示靜態SQL contents.add(new StaticTextSqlNode(data)); } } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { //其他類型的節點,由不同的節點處理器來對應處理成本成不同的SqlNode類型 String nodeName = child.getNode().getNodeName(); NodeHandler handler = nodeHandlerMap.get(nodeName); if (handler == null) { throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); } handler.handleNode(child, contents); isDynamic = true; } } //用contents構建MixedSqlNode對象 return new MixedSqlNode(contents); }
上述過程中,我們主要關注靜態SQL的解析過程,對於動態SQL的解析將在之后介紹。
得到MixedSqlNode后,靜態的SQL會創建出RawSqlSource對象。
看一下RawSqlSource:
public class RawSqlSource implements SqlSource { //內部封裝的sqlSource對象,getBoundSql方法會委托給這個對象 private final SqlSource sqlSource; public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) { this(configuration, getSql(configuration, rootSqlNode), parameterType); } public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { //創建sqlSourceBuilder SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> clazz = parameterType == null ? Object.class : parameterType; //解析sql,創建StaticSqlSource對象 sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>()); } //獲取sql語句 private static String getSql(Configuration configuration, SqlNode rootSqlNode) { DynamicContext context = new DynamicContext(configuration, null); //這里的rootSqlNode就是之前得到的MixedSqlNode,它會遍歷內部的SqlNode,逐個調用sqlNode的apply方法。StaticTextSqlNode會直接context.appendSql方法 rootSqlNode.apply(context); return context.getSql(); } @Override public BoundSql getBoundSql(Object parameterObject) { return sqlSource.getBoundSql(parameterObject); } }
代碼相對簡單,主要的步驟就是(1)通過SqlNode獲得原始SQL語句;(2)創建SqlSourceBuilder對象,解析SQL語句,並創建StaticSqlSource對象;(3)將getBoundSql方法委托給內部的staticSqlSource對象。
其中比較關鍵的一步是解析原始SQL語句,並創建StaticSqlSource對象。因此我們繼續看SqlSourceBuilder對象。
public class SqlSourceBuilder extends BaseBuilder { private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName"; public SqlSourceBuilder(Configuration configuration) { super(configuration); } public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { //創建TokenHandler,用來將原始Sql中的'#{}' 解析成'?' ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); //解析原始sql String sql = parser.parse(originalSql); //創建出StaticSqlSource對象 return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); } private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); private Class<?> parameterType; private MetaObject metaParameters; public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) { super(configuration); this.parameterType = parameterType; this.metaParameters = configuration.newMetaObject(additionalParameters); } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } @Override public String handleToken(String content) { //解析'#{}'中的參數,創建ParameterMapping對象 parameterMappings.add(buildParameterMapping(content)); //將'#{}'替換成'?' return "?"; } private ParameterMapping buildParameterMapping(String content) { Map<String, String> propertiesMap = parseParameterMapping(content); String property = propertiesMap.get("property"); Class<?> propertyType; if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params propertyType = metaParameters.getGetterType(property); } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { propertyType = parameterType; } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { propertyType = java.sql.ResultSet.class; } else if (property == null || Map.class.isAssignableFrom(parameterType)) { propertyType = Object.class; } else { MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); if (metaClass.hasGetter(property)) { propertyType = metaClass.getGetterType(property); } else { propertyType = Object.class; } } ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); Class<?> javaType = propertyType; String typeHandlerAlias = null; for (Map.Entry<String, String> entry : propertiesMap.entrySet()) { String name = entry.getKey(); String value = entry.getValue(); if ("javaType".equals(name)) { javaType = resolveClass(value); builder.javaType(javaType); } else if ("jdbcType".equals(name)) { builder.jdbcType(resolveJdbcType(value)); } else if ("mode".equals(name)) { builder.mode(resolveParameterMode(value)); } else if ("numericScale".equals(name)) { builder.numericScale(Integer.valueOf(value)); } else if ("resultMap".equals(name)) { builder.resultMapId(value); } else if ("typeHandler".equals(name)) { typeHandlerAlias = value; } else if ("jdbcTypeName".equals(name)) { builder.jdbcTypeName(value); } else if ("property".equals(name)) { // Do Nothing } else if ("expression".equals(name)) { throw new BuilderException("Expression based parameters are not supported yet"); } else { throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + parameterProperties); } } if (typeHandlerAlias != null) { builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); } return builder.build(); } private Map<String, String> parseParameterMapping(String content) { try { return new ParameterExpression(content); } catch (BuilderException ex) { throw ex; } catch (Exception ex) { throw new BuilderException("Parsing error was found in mapping #{" + content + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex); } } } }
parse方法主要分為以下幾步:
(1)創建了ParameterMappingTokenHandler對象
(2)將ParameterMappingTokenHandler對象傳入GenericTokenParser的構造函數中,創建GenericTokenParser對象
(3)通過GenericTokenParser對象解析原始SQL,這個過程中會將#{}替換成?,並將#{}中的參數,解析形成ParamterMapping對象
(4)用得到的SQL和ParamterMapping對象創建StaticSqlSource對象。
解析完成后回到一開始的XMLMapperBuilder,它會在資源添加到已加載的列表中,並bindMapperForNamespace方法中為創建的MappedStatement添加命名空間。
