The real power of MyBatis is its mapping statement, and that's where its magic lies. Because of how powerful it is, the mapper's XML file is relatively simple. If you compare it to JDBC code with the same functionality, you'll immediately notice that you've saved nearly 95% of the code. MyBatis is built for SQL and does it better than the normal way.
The SQL map file has a few top-level elements (in the order they should be defined):
- cache – cache configuration for the given namespace.
- cache-ref - A reference to other namespace cache configurations.
- resultMap – is the most complex and powerful element that describes how to load objects from a database result set.
parameterMap- Obsolete! Old-fashioned parameter mapping. Inline parameters are preferred, this element may be removed in the future and is not documented here.- sql – A block of reusable statements that can be referenced by other statements.
- insert – map insert statement
- update – map update statement
- delete – map delete statement
- select – map query statement
The attributes and functions of each tag are not explained here. You can refer to the official documentation: http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html
The previous article introduced the following statement in the source code of mybatis configuration file parsing mappers node, which is parsed by XMLMapperBuilder when the mapper mapping file is obtained from here.
一、XMLMapperBuilder
//mapper mapping files are parsed through XMLMapperBuilder XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse();
//parse mapper file public void parse() { // Determine if the mapping file has been loaded if (!configuration.isResourceLoaded(resource)) { // process mapper node configurationElement(parser.evalNode("/mapper")); // Add the resource to the configuration's loadedResources collection and save it as a HashSet<String> configuration.addLoadedResource(resource); //Register mapper interface bindMapperForNamespace (); } // Process the resultMap node that failed to parse parsePendingResultMaps (); // Handle cache-ref nodes that failed to parse parsePendingCacheRefs (); // Process the sql node that failed to parse 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"); } // record the current namespace builderAssistant.setCurrentNamespace(namespace); // Parse the cache-ref node cacheRefElement(context.evalNode("cache-ref")); // Parse the cache node cacheElement(context.evalNode("cache")); // Parse the parameterMap node, this has been abandoned and is not recommended to use parameterMapElement(context.evalNodes("/mapper/parameterMap")); // Parse the resultMap node resultMapElements(context.evalNodes("/mapper/resultMap")); // Parse the sql node sqlElement(context.evalNodes("/mapper/sql")); // Parse the 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); } }
1.cache node
It completes the creation of the cache by calling the corresponding method of CacheBuilder. Each cache has a unique ID inside, and the value of this id is the namespace. The created cache object is stored in the configured cache cache (the cache is keyed by the ID attribute of the cache, that is, the namespace, which once again reflects the powerful use of the namespace of mybatis).
/** * cache - configure the cache for this namespace. * type- cache implementation class, the default is PERPETUAL, you can use a custom cache implementation class (alias or full class name) * eviction- recovery algorithm, the default is LRU, the optional algorithms are: * LRU - Least Recently Used: Remove objects that have not been used for the longest time. * FIFO - first in, first out: remove objects in the order in which they entered the cache. * SOFT - Soft Reference: Remove objects based on garbage collector state and soft reference rules. * WEAK – Weak References: More aggressive removal of objects based on garbage collector state and weak reference rules. * flushInterval- flush interval, the default is 1 hour, in milliseconds * size- cache size, the default size is 1024, the unit is the number of references * readOnly - read only * @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; }
2.cache-ref node
The cacheRefElement method is responsible for parsing the cache-ref element, and it completes the reference of the cache by calling the corresponding method of the CacheRefResolver. The created cache-ref reference relationship is stored in the configured cacheRefMap cache.
/** * cache-ref – reference cache configuration from other namespaces. * If you don't want to define your own cache, you can use cache-ref to refer to other caches. * Because each cache has namespace as id, * So cache-ref only needs to configure a namespace attribute. * It should be noted that if both cache-ref and cache are configured, the cache shall prevail. * @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); } } }
3. resultMap node
The resultMapElement method is responsible for parsing the resultMap element, and it completes the parsing of the resultMap by calling the corresponding method of the ResultMapResolver. The other child nodes except the discriminator child node under the resultMap node will be parsed into the corresponding ResultMapping object, and each <resultMap> node will be parsed into a ResultMap object, and the created resultMap will be stored in the resultMaps cache of the configuration (the cache is named as namespace The id of +resultMap is the key, which once again reflects the powerful usefulness of the namespace of mybatis).
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; } }
4. sql node analysis
The sql node is used to define reusable sql statement fragments, and the sqlElement method is responsible for parsing sql elements. The id attribute is used to distinguish different sql elements, and multiple sql elements can be configured in the same mapper configuration file.
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)) { // The record is saved in sqlFragments. In fact, in the constructor, you can see that this field points to the sqlFragments collection of the configuration 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; }
二、XMLStatementBuilder
There is another kind of important node in the mapping configuration file that needs to be parsed. In fact, it is the select|insert|update|delete node. These nodes are mainly used to define SQL statements. They are not parsed by XMLMapperBuilder, but by XMLStatementBuilder. A node will be parsed into a MappedStatement object and stored in the configuration object. There are several important steps within this method, and understanding them is helpful to properly configure the statement element.
1.MappedStatement
MappedStatement contains many properties of these nodes, the more important ones are as follows:
private String resource;//The id in the node includes the namespace private SqlSource sqlSource;//SqlSource object, corresponding to a SQL statement private SqlCommandType sqlCommandType;//SQL的类型,insert,delete,select,update
The parsing process code is as follows:
public void parseStatementNode() { // Get the id of the sql node and the databaseId if it does not match the current node and do not load the node, // If there is a node with the same id and the databaseId is not empty, the node is not loaded String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } // Get various attributes of the node 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); // Set the type of sqlCommandType according to the name of the node 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); // Process the include node before parsing the SQL statement XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Process the selectKey node processSelectKeyNodes(id, parameterTypeClass, langDriver); // Complete the parsing of the node. This part is the core SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); // Get three properties of resultSets keyProperty keyColumn String resultSets = context.getStringAttribute("resultSets"); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); KeyGenerator keyGenerator; // Get the id of the selectKeyGenerator corresponding to the selectKey node 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; } // Create MappedStatement object through MapperBuilderAssistant, // and add to the configuration.mappedStatements collection to save builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
2. Parse the include node
Before parsing the statement node, first parse the include node through XMLIncludeTransformer. The change process will replace the SQL fragment defined in the <sql> node with the include node, and replace the ${xx} placeholders with real parameters.
private void applyIncludes(Node source, final Properties variablesContext, boolean included) { if (source.getNodeName().equals("include")) { // ---(2) Process include nodes // Find the <sql> pointed to by the refid attribute, and return a deep cloned Node object Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); Properties toIncludeContext = getVariablesContext(source, variablesContext); // Recursively process include nodes applyIncludes(toInclude, toIncludeContext, true); if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { toInclude = source.getOwnerDocument().importNode(toInclude, true); } // replace the <sql> node with the <include> node source.getParentNode().replaceChild(toInclude, source); while (toInclude.hasChildNodes()) { // Add the child nodes of the <sql> node to the front of the <sql> node toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); } // delete <sql> node after replacement 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++) {// Traverse the child nodes of the current 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 to replace the corresponding placeholders source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); } }
3. Parse the selectKey node
The selectKey node can be defined in the insert and update nodes to solve the problem of auto-increment of the primary key.
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; // Generate SqlSource SqlSource sqlSource = langDriver.createSqlSource (configuration, nodeToHandle, parameterTypeClass); // Only select statements can be configured in the selectKey node SqlCommandType sqlCommandType = SqlCommandType.SELECT; // Create a MappedStatement object and add it to the mappedStatements collection of the configuration to save 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); // Create the corresponding KeyGenerator (primary key auto-increment strategy) and add it to the configuration configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); }
3. Bind the Mapper interface
The namespace of each mapping configuration file can be bound to a Mapper interface and registered in the MapperRegistry.
// bind mapper interface private void bindMapperForNamespace() { //Get the namespace of the mapping file String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { // Parse the type corresponding to the namespace, that is, dao boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) {// Has it been loaded // 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 //register configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType); } } } }