Mybatis source code analysis

1. About Mybatis

There is no connection pool at the bottom of JDBC, and frequent creation and association links are needed to operate the database. It consumes a lot of resources; writing native JDBC code in java, once we want to modify sql, java needs to be compiled as a whole, which is not conducive to system maintenance; returning result result set also needs hard coding.

So there are some excellent persistence layer frameworks that support customized SQL, stored procedures, and advanced mapping. Mybatis is such a framework, which avoids almost all JDBC code and manual setting of parameters and obtaining result sets. You can use simple XML or annotations to configure and map native types and interfaces.

We also mentioned that there are two ways to implement Mybatis, one is annotation, the other is XML. Annotation method is not suitable for more complex sql such as associated query, which is not convenient (Collection) to manage sql; while XML configuration method is tedious and cumbersome, but it can write some complex query sql statements. We usually use XML configuration, unless the interaction between the project and the database is simple and does not need complicated sql statements.

 

2. Analyze the execution process of Mybatis

After XML is configured, we will write code in java. The general execution process is as follows:

Reader reader = Resources.getResourceAsReader("mybatis.cfg.xml");
//Or InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession  session = sessionFactory.openSession();
session.getMapper
//session.selectOne
sqlSession.commit();
sqlSession.close();

First load the configuration file, then create the SqlSessionFactory, then build the sql session, execute the sql statement through the SqlSession, manually submit for adding, deleting and changing, and finally close the session.

Of course, you don't like XML. It's OK to replace it with java code

DataSource dataSource = BlogDataSourceFactory.getBlogDataSource(); 
TransactionFactory transactionFactory = new JdbcTransactionFactory();  
Environment environment =  new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(BlogMapper.class); 
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); 
//The back is the same as the front

Before we understand the source code, we should understand some interfaces or classes of Mybatis, which is helpful for the source code analysis.

1,Configuration

Manage mysql-config.xml global configuration relationship class

2,SqlSessionFactory

Session management factory interface

3,Session

SqlSession is a user (Programmer) oriented interface. There are many ways to operate the database in SqlSession

4,Executor

The executor is an interface (basic executor, cache executor). SqlSession operates the database through the executor

5,MappedStatement

The underlying encapsulation object stores and encapsulates the operation database, including sql statements, input and output parameters

6,StatementHandler

Specific operation database related handler interface

7,ResultSetHandler

The handler interface of the returned result of the specific operation database

 

Now let's take a look at the implementation process one by one. The first is to load the configuration file into memory, which is an input stream.

InputStream inputStream = Resources.getResourceAsStream("mybatis.xml")

After that, we will create SqlSessionFactory. Let's see how to create it

SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);

There will be various ways to build, and eventually call

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      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.
      }
    }
  }

The configuration file is loaded, and the next step is to parse it.

 private void parseConfiguration(XNode root) {
    try {
      //Step by step analysis
      //issue #117 read properties first
      //1.properties
      propertiesElement(root.evalNode("properties"));
      //2. Type alias
      typeAliasesElement(root.evalNode("typeAliases"));
      //3. plug-in
      pluginElement(root.evalNode("plugins"));
      //4. Target factory
      objectFactoryElement(root.evalNode("objectFactory"));
      //5. Object packaging factory
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //6. setting
      settingsElement(root.evalNode("settings"));
      // read it after objectFactory and objectWrapperFactory issue #631
      //7. environment
      environmentsElement(root.evalNode("environments"));
      //8.databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //9. Type processor
      typeHandlerElement(root.evalNode("typeHandlers"));
      //10. mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

Let's focus on how to parse mapper files to store sql statements

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //10.4 auto scan all mappers under 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) {
            //10.1 using classpath
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //Mapper is complex, call XMLMapperBuilder
            //Note that in the for loop, each mapper will be re created with an XMLMapperBuilder to parse
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //10.2 use absolute url path
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //Mapper is complex, call XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            //10.3 using java class names
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            //Add this 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.");
          }
        }
      }
    }
  }

We see that the mapper file, mapperParser.parse(), will be parsed first, and then the parsed information will be stored in the configuration

private void configurationElement(XNode context) {
    try {
      //1. Configure namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      //2. Configure cache ref
      cacheRefElement(context.evalNode("cache-ref"));
      //3. Configure cache
      cacheElement(context.evalNode("cache"));
      //4. Configure parametermap (obsolete, old style parameter map)
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //5. Configure resultmap (advanced function)
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //6. Configure SQL (define reusable SQL code segments)
      sqlElement(context.evalNodes("/mapper/sql"));
      //7. Configure select Insert update delete todo
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

We will analyze the familiar tags such as resultMap, parameterMap, etc. here, let's focus on the sql statement. Finally, it will enter the parseStatementNode of XMLStatementBuilder

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    //Exit if databaseId does not match
    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");
    //Timeout time
    Integer timeout = context.getIntAttribute("timeout");
    //Reference to external parameterMap, obsolete
    String parameterMap = context.getStringAttribute("parameterMap");
    //Parameter type
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    //Reference external resultmap (advanced function)
    String resultMap = context.getStringAttribute("resultMap");
    //Result type
    String resultType = context.getStringAttribute("resultType");
    //Script language, new function of mybatis 3.2
    String lang = context.getStringAttribute("lang");
    //Get language driven
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    //Result set type, one of forward only scroll sensitive scroll innovative
    String resultSetType = context.getStringAttribute("resultSetType");
    //Statement type, one of STATEMENT|PREPARED|CALLABLE
    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 select results
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //Only applicable to the nested result select statement: if it is true, it is assumed that the nested result set or grouping is included, so that when a main result row is returned, there will be no reference to the previous result set.
    //This makes it possible to get nested result sets without running out of memory. Default value: false. 
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    //Parse the < include > sql fragment before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    //Before parsing, parse < selectkey >
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //Resolve to SqlSource, generally DynamicSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    //(only useful for insert) mark an attribute, MyBatis will set its value through getGeneratedKeys or through the selectKey sub element of the insert statement
    String keyProperty = context.getStringAttribute("keyProperty");
    //(only useful for insert) mark an attribute, MyBatis will set its value through getGeneratedKeys or through the selectKey sub 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();
    }

	//Go to the assistant class again
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

In the end, these parsed information will all exist in the Configuration class, and I only list the commonly used properties

public class Configuration {

//Mapped statement, stored in Map
  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
  //Cache, in Map
  protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
  //Result Map, in Map
  protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");

}

Finally, create DefaultSqlSessionFactory and return it to SqlSessionFactory

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

Now create sqlSession

public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

Transaction will be created by default in every sql execution, so it will be submitted manually after execution, and the Executor of sql statement will also be created. Finally, the DefaultSqlSession is created and returned to SqlSession. Then the SqlSession is created. The creation process will pass both the configuration of sql information wrapper and the Executor to SqlSession.

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //Generate a transaction through the transaction factory
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //Generate an actuator (the transaction is contained in the actuator)
      final Executor executor = configuration.newExecutor(tx, execType);
      //Then generate a DefaultSqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      //If there is an error opening the transaction, close it
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      //Last clear error context
      ErrorContext.instance().reset();
    }
  }

The next step is to execute sql statements to analyze and query multiple statements

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //Find the corresponding MappedStatement according to the statement id
      MappedStatement ms = configuration.getMappedStatement(statement);
      //Instead, use the executor to query the results. Note that the ResultHandler passed in here is null
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

You can see that configuration.getMappedStatement(statement) takes the corresponding statement in configuration and executes it through the executor

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //Get bound sql
    BoundSql boundSql = ms.getBoundSql(parameter);
    //Create cache Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    //query
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

The final call is that we usually use jdbc to execute sql statements to query the database. By default, mbatis will use cache, so I omitted to query the cache first and put the results in the cache finally

 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }

The database is submitted, and the executor is finally completed through jdbc

public void commit(boolean force) {
    try {
      //Use the actuator instead to commit
      executor.commit(isCommitOrRollbackRequired(force));
      //After each commit, the dirty flag is set to false
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

 

Another way is to execute statements through interfaces. In spring, integrating mybatis to execute statements is to get proxy objects through getMapper

public <T> T getMapper(Class<T> type) {
    //Finally, MapperRegistry.getMapper will be called
    return configuration.<T>getMapper(type, this);
  }

Creating objects with MapperProxyFactory

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
protected T newInstance(MapperProxy<T> mapperProxy) {
    //Using the dynamic agent of JDK to generate mapper
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

The logic of proxy object execution is in MapperProxy, which implements the InvocationHandler interface. Then the specific logic is in the invoke method

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //After proxy, the invoke method will be called when all Mapper's methods are called
    //Not every method needs to call a proxy Object for execution. If this method is a general method (toString, hashCode, etc.) in Object, it does not need to be executed
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //This is optimized. Go to the cache to find MapperMethod
    //Find the sql statement executed by the corresponding method (ID in xml)
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //implement
    return mapperMethod.execute(sqlSession, args);
  }

Let's continue to see how the proxy object executes the sql statement. First, determine the type of statement. insert|update|delete|select, and call the four major methods of SqlSession

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    //It can be seen that there are four situations during execution: insert|update|delete|select and call the four major methods of SqlSession respectively
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        //If there is a result processor
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        //If the result has multiple records
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        //If the result is a map
        result = executeForMap(sqlSession, args);
      } else {
        //Otherwise, it's a record
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

Let's see the statement execution of the query, and the final call is sqlSession.select, which goes back to the previous way, and the later implementation is not covered.

private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
    MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
    if (void.class.equals(ms.getResultMaps().get(0).getType())) {
      throw new BindingException("method " + command.getName() 
          + " needs either a @ResultMap annotation, a @ResultType annotation," 
          + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
    }
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
    } else {
      sqlSession.select(command.getName(), param, method.extractResultHandler(args));
    }
  }

Finally, close the session

public void close() {
    try {
      //Switch to actuator to close
      executor.close(isCommitOrRollbackRequired(false));
      //After each close, the dirty flag is set to false
      dirty = false;
    } finally {
      ErrorContext.instance().reset();
    }
  }

 

 

 

 

 

 

 

 

 

Published 40 original articles, won praise 52, visited 40000+
Private letter follow

Tags: SQL xml Mybatis Database

Posted on Fri, 06 Mar 2020 00:15:37 -0800 by gorskyLTD