MyBatis review the old and learn the new - underlying operating principle

Posted by ubuntu-user on Wed, 26 Jan 2022 16:56:14 +0100

preparation [References]

public class MainClass {
  public static void main(String[] args) throws Exception {
    String resources = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resources);

    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    Student student = sqlSession.selectOne("org.apache.ibatis.dao.StudentMapper.getStudent",1);
    System.out.println(student.toString());
    sqlSession.close();
  }
}

How does MyBatis get the data source
This is our MyBatis config The four key attributes of the configuration database in XML are to see how MyBatis parses this configuration file block.

<environments default="development">
   <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
      </dataSource>
    </environment>
 </environments>

For data source acquisition, we start the analysis from the above code fragment in sqlsessionfactorybuilder() Start with the build (InputStream) method. Here you see an example of the XMLConfigBuilder class.

SqlSessionFactoryBuilder

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //parser. The parse () method returns the Configuration object and then calls build(Configuration config).
      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.
      }
    }
}

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

XMLConfigBuilder#parse()

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

When you see that the parseConfiguration parameter is XNode, you know that the xml node must be parsed in this method. You can debug and check the value of the root parameter. After trying to debug, you find that the content is our configuration file mybatis config xml file content. Next, we see the familiar environments node on line 22. Let's directly look at the environmentsElement method.

<configuration>
    <properties resource="db.properties"/>    
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>        
        <setting name="cacheEnabled" value="true"/>        
        <setting name="lazyLoadingEnabled" value="true"/>        
        <setting name="aggressiveLazyLoading" value="false"/>        
        <setting name="localCacheScope" value="SESSION"/>        
    </settings>
    <typeAliases>
        <typeAlias alias="Student" type="org.apache.ibatis.domain.Student"/>        
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>            
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>                
                <property name="url" value="${jdbc.url}"/>                
                <property name="username" value="${jdbc.username}"/>               
                <property name="password" value="${jdbc.password}"/>               
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/StudentMapper.xml"/>        
    </mappers>
</configuration>

XMLConfigBuilder#environmentsElement
The content of this method parameter XNode is part of the configuration file. Here you can see the place to parse the datasource node. Here you use DataSourceFactory to return a datasource. How do you get the DataSourceFactory and datasource.

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
          break;
        }
      }
    }
  }

Resolve DataSourceFactory
Here you see getdeclaraedconstructor() Newinstance() reflects instantiating a PoolDataSourceFactory.

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

public PooledDataSourceFactory() {
  this.dataSource = new PooledDataSource();
}

}

Parse DataSource
After obtaining PooledDataSourceFactory, call dsfactory Getdatasource () gets the data source. The important code here is the last Configuration Setenvironment. Finally, the parsed Environment object will be assigned to the Configuration object, which is also a very heavyweight object in the MyBatis framework.

How does MyBatis get SQL statements [References]
Parsing SQL statement Code Chain

At the beginning of the article, we use sqlsession Selectone () method to obtain the SQL statement, so how does the SQL statement parse and obtain it? We mentioned xmlconfigbuilder when analyzing the data source above When you see the parseconfiguration method, you will call the mapperElement method.

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          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) {
            ErrorContext.instance().resource(resource);
            try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              mapperParser.parse();
            }
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            try(InputStream inputStream = Resources.getUrlAsStream(url)){
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
              mapperParser.parse();
            }
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

This method will parse package, resource, URL and class in turn according to the configuration file to configure mapper.

<mappers>
    <mapper resource="mapper/StudentMapper.xml"/>
</mappers>

Here, we configure it in the resource mode, so this method program will be executed from the logic of Line 12.

XMLStatementBuilder[References]
The key logic for parsing SQL statements is in the parseStatementNode method of this class. The method will call mapperbuilderassistant # Addmappedstatement(), a very important class MappedStatement object in MyBatis, will be. The key line of code of this method is to generate MappedStatement object first, and finally put the generated MappedStatement object into the Map Dictionary of Configuration object.

It should be intuitive to see the code in this method, which is to parse some attributes of the select tag in XML, such as useCache, flushCache, etc. these tags can view the MyBatis document, which is really detailed.

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

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    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);

    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    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;
    }

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

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

MapperBuilderAssistant

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");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

How does MyBatis operate the database
Actuator
Actuators in MyBatis are used to encapsulate statements to perform JDBC operations. MyBatis contains three types of actuators: simpleexecution, reuseexecution, and batchexecutor. Simpleexecution is used by default.

@Override
 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
 }

Establish mapping relationship between metadata and entity of MySQL query [References]
So how exactly do we establish a mapping relationship between a piece of mysql metadata and a java entity queried by our select statement? Let's start with the PreparedStatementHandler#query method.

@Override
 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    //Are these two lines of code very friendly? That's what we do in JDBC.
    //1. Execute SQL
    PreparedStatement ps = (PreparedStatement) statement;
    //2. Processing result set
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
 }

Result set mapping
DefaultResultSetHandler#handleResultSets

@Override
 public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    //1. The resultMap attribute of the < Select > tag is used to load the mapped entity object data
    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    //2. Get the first result set data
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    //3. Here is the ResultMap to be mapped
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    //4. Here is the number of resultmaps to be mapped
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    //5. Cycle through each ResultMap
    while (rsw != null && resultMapCount > resultSetCount) {
      //6. Loop out the ResultMap (id,type) entity name to be mapped
      ResultMap resultMap = resultMaps.get(resultSetCount);
      //7. Get the query results from the rsw result set parameters, and then load the query results into multipleResults according to the resultMap mapping information
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
    
    //The resultSet property of the select tag is obtained here
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
}

DefaultResultSetHandler#handleResultSet

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          //1. Create DefaultResultHandler to process the result set
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          //2. Here is the final method for processing the mapping result set
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      closeResultSet(rsw.getResultSet());
    }
  }

DefaultResultSetHandler#handleRowValues

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      //nested result mappings 
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      //Simple result mapping
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

ResultSetWrapper
Another key class not mentioned in the above method call chain is ResultSetWrapper. There are three sets in its constructor, namely columnNames, JDBC types and classNames. When you see these three arrays, does the intention of MyBatis framework come to mind to manage JDBC instead of you and establish the mapping from the result set to java objects.

public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
    super();
    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.resultSet = rs;
    final ResultSetMetaData metaData = rs.getMetaData();
    final int columnCount = metaData.getColumnCount();
    for (int i = 1; i <= columnCount; i++) {
      columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
      jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
      classNames.add(metaData.getColumnClassName(i));
    }
  }

DefaultResultSetHandler#handleRowValuesForSimpleResultMap

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    //1. Because we encapsulated the result set when creating the ResultSetWrapper, now take out the result set
    ResultSet resultSet = rsw.getResultSet();
    //2. Paging information
    skipRows(resultSet, rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      ///3. Encapsulate the query results into POJO s (this line of code focuses on)
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }

DefaultResultSetHandler#getRowValue

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        //Encapsulating the result set corresponds the result of sql to the entity class encapsulation
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

DefaultResultSetHandler#applyAutomaticMappings
In this method, row and column fields are processed cyclically and entity attributes are mapped.

private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
    //1. Encapsulate your returned result set object
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
      //2. Assign values to entity class objects
      for (UnMappedColumnAutoMapping mapping : autoMapping) {
        //3. Take values from SQL according to the entity attributes, get the result value of SQL and the attribute value of entity class, and encapsulate them in the metaObject object object.
        final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
          metaObject.setValue(mapping.property, value);
        }
      }
    }
    //The return value is a boolean value
    return foundValues;
  }

The metadata of three fields can be seen in the autoMapping collection. OK, at this step, the return value of the outer getRowValue will get the returned entity object.
Finally, welfare from Xiaobian

Xiaobian has sorted out a collection of interview materials for real questions from big factories and the latest Java core technology in 2021. The partners who need to get them can Pay attention to me in private Get it for free. The programming world is always open to all people who love programming. This is a free, equal and shared world. I always believe in it.
If you like the sharing of Xiaobian, you can praise and pay attention. Xiaobian continues to share the latest articles and benefits for you

Topics: Programming Programmer