Understand the Mybatis

MyBatis is the predecessor of Apache’s open source project iBatis. MyBatis eliminates almost all manual setting of JDBC code and parameters and retrieval encapsulation of result sets. It is an excellent Java-based persistence layer framework that supports ordinary SQL queries, stored procedures, and advanced mapping.

MyBatis configuration

Instead of getting bogged down in details when reading the source code, we should have a bird ‘s-eye view, which will help you understand the framework from a higher perspective.

Because this article focuses on the initialization and construction of configuration objects corresponding to configuration files, it will not be covered in the execution part. Let’s start with two important configuration files that we usually write when using Mybatis — Mybatis -config. XML and xxxmapper.xml.

The default configuration file for mybatis is mybatis-config.xml

In the mybatis-config.xml configuration file, we have a special
tag that maps the associated Mapper mapping file.

In fact, the construction process of Mybatis is: the configuration file is parsed into a configuration object loaded into memory.

How does Mybatis parse configuration files into configuration objects

Let’s start with a question: when will this configuration object be used?

We know that in mybatis-config.xml there are some type handlers, type aliases, mappers, database connection information, and so on that are used every time a database connection is CRUD, that is, in every SQL session.

Mybatis uses a SqlSession interface to express and standardize THE Sql session, we need to create a special SqlSessionFactory, which is a factory mode. I’ll briefly draw a UML diagram here so you can review the factory pattern, but that’s not the point of this article.

Mybatis uses factory mode to build SqlSessionFactory and SqlSessionFactoryBuilder to build SqlSessionFactory. And since we need to pass in our Configuration object Configuration when creating the SqlSession, and we know that there are many tags in the mybatis-config. XML Configuration file, This means that when we construct a Configuration object, there will be a lot of field parsing, so the construction of the whole Configuration object is very complicated. The Builder pattern is used in Mybatis to solve this problem. We can look at the source code.

// This is in the SqlSessionFactoryBuilder class
// There are many ways to build factories in SqlSessionFactoryBuilder
// This is the main thread, because other build methods call this method
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
  // Create a stream parsed from the configuration file
  // Build the Builder class XmlConfigBuilder for the Configuration object
  // The parse method is then called to build the Configuration object
  XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
  // The build method with Configuration is eventually called
  // Build the final SqlSessionFactory
  return build(parser.parse());
} catch (Exception e) {
  throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
  ErrorContext.instance().reset();
  try {
    inputStream.close();
  } catch (IOException e) {
    // Intentionally ignore. Prefer previous error.}}}Copy the code

So we can draw a simple flow chart.

How does Mybatis parse configuration files

The parse() method in the XMLConfigBuilder class parses and builds the Configuration object.

So let’s go down this path and see what the underlying principles look like.

public Configuration parse(a) {
// If it has already been parsed, an error is reported
if (parsed) {
  throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// The root node is Configuration
// Parsing is still here
// I need to explain something here
// "/configuartion" is an xpath syntax
// Mybatis wraps an xpath parser to parse XML
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

// Parse the configuration
private void parseConfiguration(XNode root) {
try {
  // step by step
  //issue #117 read properties first
  //1.properties
  propertiesElement(root.evalNode("properties"));
  //2. Type aliases
  typeAliasesElement(root.evalNode("typeAliases"));
  / / 3. The plug-in
  pluginElement(root.evalNode("plugins"));
  //4. Object factory
  objectFactoryElement(root.evalNode("objectFactory"));
  //5. Object packaging factory
  objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  / / 6. Set up
  settingsElement(root.evalNode("settings"));
  // read it after objectFactory and objectWrapperFactory issue #631
  / / 7. Environment
  environmentsElement(root.evalNode("environments"));
  //8.databaseIdProvider
  databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  // type handler
  typeHandlerElement(root.evalNode("typeHandlers"));
  / / 10. The mapping
  mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
  throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: "+ e, e); }}Copy the code

Does this sound familiar to you?

It’s just some tag configuration in the configuration file. So let’s draw a picture of that to make it clear.

The more important mappers

As you can see above, there is a lot of tag parsing involved in the construction of the entire Configuration object, so Mybatis uses the Builder pattern in a clever way. However, there is too much Configuration information in this article for me to analyze the source code. We only need a rough idea of what to do), so I picked the all-important
tag parsing for source analysis.

Let’s look at the source again, this time the mapperElement(XNode parent) method in XmlConfigBuilder. Of course it is best to look at the configuration information format

<mappers>
  <! -- This configuration mode combined with the following source code look -->
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <package name="org.mybatis.builder"/>
</mappers>
Copy the code
private void mapperElement(XNode parent) throws Exception {
if(parent ! =null) {
  for (XNode child : parent.getChildren()) {
    if ("package".equals(child.getName())) {
      // Automatically scan all mappers in the package
      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) {
        // Use the classpath
        ErrorContext.instance().resource(resource);
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // The mapperBuilder is called XMLMapperBuilder
        // Notice that in the for loop each mapper reconstructs an XMLMapperBuilder to parse
        // Notice that the builder also passed in configuration
        // In other words, configuration objects corresponding to mapper mapping files must also be encapsulated in Configuration
        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
        mapperParser.parse();
      } else if (resource == null&& url ! =null && mapperClass == null) {
        // Use the absolute URL path
        ErrorContext.instance().resource(url);
        InputStream inputStream = Resources.getUrlAsStream(url);
        // The mapperBuilder is called XMLMapperBuilder
        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
        mapperParser.parse();
      } else if (resource == null && url == null&& mapperClass ! =null) {
        // Use the Java class nameClass<? > mapperInterface = Resources.classForName(mapperClass);// Add the mapping directly to the configuration
        configuration.addMapper(mapperInterface);
      } else {
        throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
      }
    }
  }
}
}
Copy the code

We iterate over the child tags in the Mappers tag, and for the three resource identifiers (Resource, URL,class) in addition to the package, Each mapper child tag builds an XMLMapperBuilder to build and parse the corresponding Mapper mapping configuration file. In fact, these resource flags tell the application to find the corresponding XXxmapper.xml mapping file, and then apply the builder pattern to build the corresponding xxxmapper.xml configuration object.

Let’s look at how the XmlMapperBuilder builder builds the corresponding “Mapper” configuration object.

public void parse(a) {
// Prevent reloading if not already loaded
if(! configuration.isResourceLoaded(resource)) {// Mapper is configured here
  configurationElement(parser.evalNode("/mapper"));
  // Mark it as already loaded
  configuration.addLoadedResource(resource);
  // Bind the mapper to the namespace
  bindMapperForNamespace();
}
// Can be ignored
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}

private void configurationElement(XNode context) {
try {
  / / 1. Configuration namespace
  // Remember the namespace thing
  String namespace = context.getStringAttribute("namespace");
  if (namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
  }
  builderAssistant.setCurrentNamespace(namespace);
  // Cache can be ignored
  / / 2. Configuration cache - ref
  cacheRefElement(context.evalNode("cache-ref"));
  / / 3. Configure cache
  cacheElement(context.evalNode("cache"));
  //4. Configure parameterMap(deprecated, old-style parameter mapping)
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  // Mybatis is a very important feature
  //5. Configure resultMap(Advanced function)
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  //6. Configure SQL (define reusable SQL snippets)
  sqlElement(context.evalNodes("/mapper/sql"));
  / / 7. Configuration select | insert | update | delete
  // Here is the real thread
  // The real configuration object MappedStatement in Mapper will be created based on the previous SQL fragment
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
  throw new BuilderException("Error parsing Mapper XML. Cause: "+ e, e); }}/ / to select | insert | update | delete tags node list to build the Statement
private void buildStatementFromContext(List<XNode> list) {
  / / determine DatabaseId
  if(configuration.getDatabaseId() ! =null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  // call this method
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
  // Build all statements, a mapper can have many select
  // The constructor pattern is used again
  // The statement is complex and the core is in there, so call XMLStatementBuilder
  final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
  try {
    / / the main line
    / / core XMLStatementBuilder parseStatementNode
    statementParser.parseStatementNode();
  } catch (IncompleteElementException e) {
      // If the SQL statement is incomplete, write it down and insert it in configurationconfiguration.addIncompleteStatement(statementParser); }}}Copy the code

The long string of code above is actually a chain call. Let’s draw the process so you can understand it.

The next step is to build the MappedStatement object in the class XMLStatementBuilder.

// Follow the parse build method above
public void parseStatementNode(a) {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    // If databaseId does not match, exit
    if(! databaseIdMatchesCurrent(id, databaseId,this.requiredDatabaseId)) {
      return;
    }

    // Indicates the number of result rows returned by the driver in each batch
    Integer fetchSize = context.getIntAttribute("fetchSize");
    // The timeout period
    Integer timeout = context.getIntAttribute("timeout");
    // References external parameterMap, deprecated
    String parameterMap = context.getStringAttribute("parameterMap");
    // The first three are not important
    // Parameter type this is important for parameter mapping
    String parameterType = context.getStringAttribute("parameterType"); Class<? > parameterTypeClass = resolveClass(parameterType);// It is important to reference external resultMap(advanced functionality)
    // Mybatis is a core feature
    String resultMap = context.getStringAttribute("resultMap");
    // Result type
    String resultType = context.getStringAttribute("resultType");
    // Scripting language, the new features of Mybatis3.2 are not important
    String lang = context.getStringAttribute("lang");
    // Get the language driver is not importantLanguageDriver langDriver = getLanguageDriver(lang); Class<? > resultTypeClass = resolveClass(resultType);/ / the result set type, FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE one of
    String resultSetType = context.getStringAttribute("resultSetType");
    / / type of STATEMENT, the STATEMENT | PREPARED | a CALLABLE
    // Get the Statement type that needs to be mapped to JDBC
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    / / get command type (select | insert | update | delete)
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    // Whether to cache the select results
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    // Only for nested result SELECT statements: if true, it is assumed that nested result sets are included or grouped, so that no references to previous result sets occur when a primary result row is returned.
    // This prevents memory from running out when retrieving nested result sets. Default value: false.
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered".false);

    // Include Fragments before parsing
    // Parse 
      
       SQL fragment before parsing this XMLMapperBuilder above
      
    // is already built and needs to be called and parsed
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    SelectKey involves requiring some special relationship to set the value of the primary key
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    / / do understand
    // Parse to SqlSource, usually DynamicSqlSource
    // Both Dynamic and Raw will be resolved to static
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    //(for INSERTS only) mark a property. MyBatis sets its value either via getGeneratedKeys or via the selectKey child element of the INSERT statement
    String keyProperty = context.getStringAttribute("keyProperty");
    //(for INSERTS only) mark a property. MyBatis sets its value either via getGeneratedKeys or via the selectKey child element of the INSERT statement
    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))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }
    // The real thread is here
	// Call the helper class to actually create the MappedStatement and add it to the Configuration
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
Copy the code

The parseStatementNode method is a bit long, but you can see that it does nothing more than parse each CRUD tag. There are a few important ones. ParameterType, ResultMap, parsed to SqlSource, Sql fragment parsed… The other is actually branch, you can go to understand.

After some parsing of the properties, XMLStatementBuilder delegates these properties to the helper object MapperBuilderAssistant to build the MappedStatement.

// Many parameters.
public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<? > parameterType, String resultMap, Class<? > resultType, ResultSetType resultSetType,boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {
    
    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    
    // Prefix the ID with namespace
    // Here's the thing
    // Remember the namespace above?
    // This uses the ID of the CRUD tag itself plus the namespace build unique ID
    // This is mainly because all CRUD tag configuration objects in mapper files are stored directly
    // In configuration, to prevent some tag ids from being duplicated
    id = applyCurrentNamespace(id, false);
    // Is a select statement
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    // Very classic constructor pattern, return the object that needs to be built can be chain called
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
    statementBuilder.resource(resource);
    statementBuilder.fetchSize(fetchSize);
    statementBuilder.statementType(statementType);
    statementBuilder.keyGenerator(keyGenerator);
    statementBuilder.keyProperty(keyProperty);
    statementBuilder.keyColumn(keyColumn);
    statementBuilder.databaseId(databaseId);
    statementBuilder.lang(lang);
    statementBuilder.resultOrdered(resultOrdered);
    statementBuilder.resulSets(resultSets);
    setStatementTimeout(timeout, statementBuilder);

    Parameter mapping is important here
    // This is generally empty because parameterMap is deprecated
    ParameterType is the Class that gets passed in
    setStatementParameterMap(parameterMap, parameterType, statementBuilder);
    //2. Result mapping is also important
    setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
    setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

    MappedStatement statement = statementBuilder.build();
    / / build good call configuration. AddMappedStatement
    // Add configuration to complete the process.
    configuration.addMappedStatement(statement);
    return statement;
}
Copy the code

We found that the MappedStatement was eventually built through the HELPER class of XMLMapperBuilder and passed into the Configuration. We can now refine the above flow chart a bit more.

Parameter mapping and result mapping

Parameter Mapping Process

Let’s take a look at the parameter mapping code that has not been fully analyzed

private void setStatementParameterMap( String parameterMap, Class
        parameterTypeClass, MappedStatement.Builder statementBuilder) {
    // Add namespace to parameterMap, but since parameterMap is deprecated, null is usually returned
    parameterMap = applyCurrentNamespace(parameterMap, true);

    if(parameterMap ! =null) {
      try {
        statementBuilder.parameterMap(configuration.getParameterMap(parameterMap));
      } catch (IllegalArgumentException e) {
        throw new IncompleteElementException("Could not find parameter map "+ parameterMap, e); }}else if(parameterTypeClass ! =null) {
      // Parse the class objects generated by parameterType
      List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
      // Construct a build class within the ParameterMap class
      // This is mainly an assignment to parameterTypeClass and parameterMapping is just passed in as an empty list
      ParameterMap.Builder inlineParameterMapBuilder = new ParameterMap.Builder(
          configuration,
          statementBuilder.id() + "-Inline",
          parameterTypeClass,
          parameterMappings);
      // Build a ParameterMap from an internal build class and pass it into a configuration objectstatementBuilder.parameterMap(inlineParameterMapBuilder.build()); }}Copy the code

Because parameterMap is deprecated, setting parameters is mostly around parameterType, In summary, parameterType is used to build a ParameterMap object (in this case, built using an internal builder within ParameterMap). The ParameterMap object is then stored in the MappedStatement.

In fact, the ParameterMap object only has three fields, or even two. I’m going to write a simple ParameterMap class.

public class ParameterMap {
  private String id;
  privateClass<? > type;ParameterMapping deprecates this field
  // Refactoring is expected later
  private List<ParameterMapping> parameterMappings;
}
Copy the code

The official documentation has already removed this element.

Result mapping process

I said parameter mapping, but the result mapping is pretty much the same.

private void setStatementResultMap( String resultMap, Class
        resultType, ResultSetType resultSetType, MappedStatement.Builder statementBuilder) {
    / / application namespace
    resultMap = applyCurrentNamespace(resultMap, true);

    List<ResultMap> resultMaps = new ArrayList<ResultMap>();
    if(resultMap ! =null) {
      // Parse the ResultMap
      / / through here, and division You can be written as xxxResultMap, xxxResultMap but I haven't found someone was used
      String[] resultMapNames = resultMap.split(",");
      for (String resultMapName : resultMapNames) {
        try {
          // resultMapName
          // Remove the 
      
        tag that has been parsed in configuration
      
          // Get the corresponding resultMap in the configuration and add it to the resultMaps
          resultMaps.add(configuration.getResultMap(resultMapName.trim()));
        } catch (IllegalArgumentException e) {
          throw new IncompleteElementException("Could not find result map "+ resultMapName, e); }}}else if(resultType ! =null) {
      / / resultType parsing
      //<select id="selectUsers" resultType="User">
      // In this case,MyBatis automatically creates a ResultMap behind the scenes to map columns to JavaBean attributes based on the attribute name.
      // If the column names do not match exactly, you can use the alias of the select clause on the column names to match the labels.
      // Create an inline result map. Set the resultType to an inline result map.
      / / behind are then DefaultResultSetHandler createResultObject () is used
      / / DefaultResultSetHandler getRowValue () is used
      ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
          configuration,
          statementBuilder.id() + "-Inline",
          resultType,
          new ArrayList<ResultMapping>(),
          null);
      // Encapsulates the resultMap collection
      resultMaps.add(inlineResultMapBuilder.build());
    }
    // Add the resultMap collection to the configuration
    statementBuilder.resultMaps(resultMaps);
    // This is added directly to the configuration
    statementBuilder.resultSetType(resultSetType);
}
Copy the code

The latest version of the result map is written in the build process.

Basically, you get a resultMap or resultType value and then build a resultMap from that value and pass it into the MappedStatement configuration.

At this point we should be able to draw a general flowchart for building the MappedStatement object.

conclusion

In fact, the entire Mybatis initialization process is to parse the configuration file into a configuration object loaded into memory for later execution. The mybatis Configuration file will be stored in the corresponding Configuration object, and the mapping Configuration file will parse the CRUD tag and store it in the MappedStatement object. Eventually the MappedStatement object is added to the collection and stored in the Configuration. This mainly uses the factory pattern and the builder pattern.

Mybatis mainly has two execution modules, the first is the construction part mentioned in this article, and the other part is the execution part of Sql, about which I will share in the next article.