MyBatis 的真正強大在於它的映射語句,也是它的魔力所在。因爲它的異常強大,映射器的 XML 文件就顯得相對簡單。若是拿它跟具備相同功能的 JDBC 代碼進行對比,你會當即發現省掉了將近 95% 的代碼。MyBatis 就是針對 SQL 構建的,而且比普通的方法作的更好。html
SQL 映射文件有不多的幾個頂級元素(按照它們應該被定義的順序):java
對每一個標籤的屬性以及做用,這裏不作解釋, 能夠參考官方文檔:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.htmlnode
上一篇文章介紹了mybatis配置文件解析mappers節點的源碼中有以下語句,從這裏獲得mapper映射文件時經過XMLMapperBuilder解析的。算法
//mapper映射文件都是經過XMLMapperBuilder解析 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse();
//解析mapper文件 public void parse() { // 判斷是否已經加載過改映射文件 if (!configuration.isResourceLoaded(resource)) { // 處理mapper節點 configurationElement(parser.evalNode("/mapper")); // 將resource添加到configuration的loadedResources集合中保存 它是HashSet<String> configuration.addLoadedResource(resource); //註冊mapper接口 bindMapperForNamespace(); } // 處理解析失敗的resultMap節點 parsePendingResultMaps(); // 處理解析失敗的cache-ref節點 parsePendingCacheRefs(); // 處理解析失敗的sql節點 parsePendingStatements(); }
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); // 解析cache-ref節點 cacheRefElement(context.evalNode("cache-ref")); // 解析cache節點 cacheElement(context.evalNode("cache")); // 解析parameterMap節點,這個已經被廢棄,不推薦使用 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析resultMap節點 resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析sql節點 sqlElement(context.evalNodes("/mapper/sql")); // 解析statement 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); } }
它經過調用CacheBuilder的相應方法完成cache的建立。每一個cache內部都有一個惟一的ID,這個id的值就是namespace。建立好的cache對象存入configuration的cache緩存中(該緩存以cache的ID屬性即namespace爲key,這裏再次體現了mybatis的namespace的強大用處)。sql
/** * cache- 配置本定命名空間的緩存。 * type- cache實現類,默認爲PERPETUAL,可使用自定義的cache實現類(別名或完整類名皆可) * eviction- 回收算法,默認爲LRU,可選的算法有: * LRU– 最近最少使用的:移除最長時間不被使用的對象。 * FIFO– 先進先出:按對象進入緩存的順序來移除它們。 * SOFT– 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。 * WEAK– 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象。 * flushInterval- 刷新間隔,默認爲1個小時,單位毫秒 * size- 緩存大小,默認大小1024,單位爲引用數 * readOnly- 只讀 * @param context * @throws Exception */ private void cacheElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache); currentCache = cache; return cache; }
cacheRefElement方法負責解析cache-ref元素,它經過調用CacheRefResolver的相應方法完成cache的引用。建立好的cache-ref引用關係存入configuration的cacheRefMap緩存中。數據庫
/** * cache-ref–從其餘命名空間引用緩存配置。 * 若是你不想定義本身的cache,可使用cache-ref引用別的cache。 * 由於每一個cache都以namespace爲id, * 因此cache-ref只須要配置一個namespace屬性就能夠了。 * 須要注意的是,若是cache-ref和cache都配置了,以cache爲準。 * @param context */ private void cacheRefElement(XNode context) { if (context != null) { configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); try { cacheRefResolver.resolveCacheRef(); } catch (IncompleteElementException e) { configuration.addIncompleteCacheRef(cacheRefResolver); } } }
resultMapElement方法負責解析resultMap元素,它經過調用ResultMapResolver的相應方法完成resultMap的解析。resultMap節點下除了discriminator子節點的其餘子節點都會解析成對應的ResultMapping對象,而每一個<resultMap>節點都會被解析成一個ResultMap對象,建立好的resultMap存入configuration的resultMaps緩存中(該緩存以namespace+resultMap的id爲key,這裏再次體現了mybatis的namespace的強大用處)。緩存
private void resultMapElements(List<XNode> list) throws Exception { for (XNode resultMapNode : list) { try { resultMapElement(resultMapNode); } catch (IncompleteElementException e) { // ignore, it will be retried } } } private ResultMap resultMapElement(XNode resultMapNode) throws Exception { return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList()); } private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception { ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); String extend = resultMapNode.getStringAttribute("extends"); Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); Class<?> typeClass = resolveClass(type); Discriminator discriminator = null; List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); resultMappings.addAll(additionalResultMappings); List<XNode> resultChildren = resultMapNode.getChildren(); for (XNode resultChild : resultChildren) { if ("constructor".equals(resultChild.getName())) { processConstructorElement(resultChild, typeClass, resultMappings); } else if ("discriminator".equals(resultChild.getName())) { discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); } else { List<ResultFlag> flags = new ArrayList<ResultFlag>(); if ("id".equals(resultChild.getName())) { flags.add(ResultFlag.ID); } resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); } } ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try { return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } }
sql節點用來定義可重用的sql語句片斷, sqlElement方法負責解析sql元素。id屬性用於區分不一樣的sql元素,在同一個mapper配置文件中能夠配置多個sql元素。mybatis
private void sqlElement(List<XNode> list) throws Exception { if (configuration.getDatabaseId() != null) { sqlElement(list, configuration.getDatabaseId()); } sqlElement(list, null); } private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception { for (XNode context : list) { String databaseId = context.getStringAttribute("databaseId"); String id = context.getStringAttribute("id"); id = builderAssistant.applyCurrentNamespace(id, false); if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { // 記錄到sqlFragments中保存,其實 構造函數中能夠看到該字段指向了configuration的sqlFragments集合中 sqlFragments.put(id, context); } } } private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { if (requiredDatabaseId != null) { if (!requiredDatabaseId.equals(databaseId)) { return false; } } else { if (databaseId != null) { return false; } // skip this fragment if there is a previous one with a not null databaseId if (this.sqlFragments.containsKey(id)) { XNode context = this.sqlFragments.get(id); if (context.getStringAttribute("databaseId") != null) { return false; } } } return true; }
映射配置文件中還有一類比較重要的節點須要解析,其實就是select|insert|update|delete 節點,這些節點主要用於定義SQL語句,他們不在由XMLMapperBuilder進行解析,而是由XMLStatementBuilder負責進行解析,每一個節點會被解析成MappedStatement對象並存入到configuration對象中去。在這個方法內有幾個重要的步驟,理解他們對正確的配置statement元素頗有幫助。app
MappedStatement包含了這些節點的不少屬性,其中比較重要的以下:函數
private String resource;//節點中的id 包括命名空間 private SqlSource sqlSource;//SqlSource對象,對應一條SQL語句 private SqlCommandType sqlCommandType;//SQL的類型,insert,delete,select,update
解析過程代碼以下:
public void parseStatementNode() { // 獲取sql節點的id以及databaseId若是和當前不匹配不加載改節點, // 若是存在id相同且databaseId不爲空的節點也不在加載改節點 String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } // 獲取節點的多種屬性 Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); String resultMap = context.getStringAttribute("resultMap"); String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); Class<?> resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); // 根據節點的名稱設置sqlCommandType的類型 String nodeName = context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // 在解析SQL語句以前先處理include節點 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // 處理selectKey節點 processSelectKeyNodes(id, parameterTypeClass, langDriver); // 完成節點的解析 該部分是核心 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); // 獲取resultSets keyProperty keyColumn三個屬性 String resultSets = context.getStringAttribute("resultSets"); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); KeyGenerator keyGenerator; // 獲取selectKey節點對應的selectKeyGenerator的id 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; } // 經過MapperBuilderAssistant建立MappedStatement對象, // 並添加到configuration.mappedStatements集合中保存 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
在解析statement節點以前首先經過XMLIncludeTransformer解析include節點改過程會將include節點替換<sql>節點中定義的sql片斷,並將其中的${xx}佔位符換成真實的參數,
private void applyIncludes(Node source, final Properties variablesContext, boolean included) { if (source.getNodeName().equals("include")) { // ---(2)處理include節點 // 查找refid屬性指向的<sql>,返回的是深克隆的Node對象 Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); Properties toIncludeContext = getVariablesContext(source, variablesContext); //遞歸處理include節點 applyIncludes(toInclude, toIncludeContext, true); if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { toInclude = source.getOwnerDocument().importNode(toInclude, true); } // 將<include>節點替換<sql>節點 source.getParentNode().replaceChild(toInclude, source); while (toInclude.hasChildNodes()) { // 將<sql>節點的子節點添加到<sql>節點的前面 toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); } // 替換後刪除<sql>節點 toInclude.getParentNode().removeChild(toInclude); } else if (source.getNodeType() == Node.ELEMENT_NODE) {// ---(1) if (included && !variablesContext.isEmpty()) { // replace variables in attribute values NamedNodeMap attributes = source.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) {// 遍歷當前sql的子節點 Node attr = attributes.item(i); attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); } } NodeList children = source.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { applyIncludes(children.item(i), variablesContext, included); } } else if (included && source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {// ---(3) // replace variables in text node 替換對應的佔位符 source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); } }
在insert,update節點中能夠定義selectKey節點來解決主鍵自增問題。
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) { String resultType = nodeToHandle.getStringAttribute("resultType"); Class<?> resultTypeClass = resolveClass(resultType); StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString())); String keyProperty = nodeToHandle.getStringAttribute("keyProperty"); String keyColumn = nodeToHandle.getStringAttribute("keyColumn"); boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER")); //defaults boolean useCache = false; boolean resultOrdered = false; KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; Integer fetchSize = null; Integer timeout = null; boolean flushCache = false; String parameterMap = null; String resultMap = null; ResultSetType resultSetTypeEnum = null; // 生成SqlSource SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass); // selectKey節點中只能配置select語句 SqlCommandType sqlCommandType = SqlCommandType.SELECT; // 建立MappedStatement對象,並添加到configuration的mappedStatements集合中保存 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null); id = builderAssistant.applyCurrentNamespace(id, false); MappedStatement keyStatement = configuration.getMappedStatement(id, false); // 建立對應的KeyGenerator(主鍵自增策略),添加到configuration中 configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); }
每一個映射配置文件的命名空間能夠綁定一個Mapper接口,並註冊到MapperRegistry中。
// 綁定mapper接口 private void bindMapperForNamespace() { //獲取映射文件的命名空間 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { // 解析命名空間對應的類型 即dao boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) {// 是否已經加載了 // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource //註冊 configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType); } } } }