How Mybatis XML maps to methods

Posted by robindean on Sun, 10 Nov 2019 03:48:03 +0100

Preface

Above How Mybatis's method maps to XML This article describes how Mybatis maps method names to statementID s for method splitting, how parameters are parsed into sql in xml, and how return types are handled; it will look at how methods are mapped from the XML side.

XML Mapping Class

In the previous two articles, you learned that the namespace+statementID in xxMapper.xml is mapped by the Mapper class path+method name, while the namespace+statementID block is actually saved in the appedStatement in Configuration at the time of initialization, so we will see the following code when adding or deleting the change check:

MappedStatement ms = configuration.getMappedStatement(statement);

Gets the MapdStatement in Configuration that specifies the namespace+statementID, and maintains the corresponding relationship through Map in Configuration; for example, the most common Select statement block is configured in XML with the following structure:

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
  ...sql Sentence...
</select>

Other additions and deletions except for a few keywords such as keyProperty, keyColumn, and so on, are similar to the select tag; take a look at the related properties in the MappedStatement class:

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
  ...ellipsis...
}

The keywords in the select tag can basically find the corresponding attributes in the class MappedStatement, referring to the official documentation for the meaning each attribute represents: mybatis-3 ; In addition to keywords, there are sql statements corresponding to SqlSource in MappedStatement, which differ dynamically from statically. The corresponding SqlSource also provides related subclasses: StaticSqlSource and DynamicSqlSource, and related sql parsing classes in XMLScriptBuilder:

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

Which SQL is dynamic and which is static is judged by the relevant logic in parseDynamicTags. Here, I will outline the principles of ${} and dynamic tags such as <if>, <set>, <foreach> for DynamicSqlSource or #{} for StaticSqlSource, which is common when parsing dynamic sql; Mybatis provides a special processing class NodeHandler for each tag when parsing dynamic sql.The initialization information is as follows:

  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

After processing, a corresponding SqlNode is generated as follows:

Both dynamic and static SqlSources are ultimately intended to obtain BoundSql, as defined in the SqlSource interface:

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

Here the parameterObject is the sql parameter object generated by the method parameter above, so BoundSql contains sql statements, parameters passed from the client, and parameters configured in XML, which can be mapped directly.

Parameter Mapping

BoundSql is mentioned in the previous section. This section focuses on its properties.

public class BoundSql {

  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Object parameterObject;
  private final Map<String, Object> additionalParameters;
  private final MetaObject metaParameters;
  ...ellipsis...
}

Several attributes have general meaning: the sql statement to be executed, the parameter mapping of the xml configuration, the parameters passed from the client, and additional parameters; a common query is taken as an example to see the general content:

    <select id="selectBlog3" parameterType="hashmap" resultType="blog">
        select * from blog where id = #{id} and author=#{author,jdbcType=VARCHAR,javaType=string}
    </select>

In this case, sql corresponds to:

select * from blog where id = ? and author=?

The parameterMappings correspond to:

ParameterMapping{property='id', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='author', mode=IN, javaType=class java.lang.String, jdbcType=VARCHAR, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}

The parameterObject corresponds to:

{author=zhaohui, id=158, param1=158, param2=zhaohui}

If you know the above parameters, you can use the native PreparedStatement directly to operate the database:

PreparedStatement prestmt = conn.prepareStatement("select * from blog where id = ? and author=?");
prestmt.setLong(1,id);
prestmt.setString(2,author);

In fact, Mybatis is essentially the same as the above statement, so you can see how Mybatis handles parameters, implemented in DefaultParameterHandler as follows:

 public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          } catch (SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

Roughly, by traversing parameterMappings, and then getting the corresponding value from the propertyName to the client parameter parameterObject, there is a problem after getting the value: one is the type of client parameter, the other is the type of XML configuration, how to convert, Mybatis provides TypeHandler to convert, which is an interface class whose implementation includes common useThe basic type, Map, Object, Time, and so on; which type of TypeHandler to use depends on the <javaType=Type>that we configure in the xml; if not, the UnknownTypeHandler uses the UnknownTypeHandler internally, which decides to use the specific TypeHandler based on the type of value; all types within Mybatis registered in the TypeHandler Registry, soTypeHandler Registry is obtained directly from the value type at the time of acquisition; after acquisition, typeHandler.setParameter(ps, i + 1, value, jdbcType) is called directly, and StringTypeHandler is taken as an example to see the implementation:

  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setString(i, parameter);
  }

Use the native PreparedStatement to set the parameter value to the specified location; execute the method after setting the parameter and return the result.

Result Mapping

After execute in the previous section, the ResultSet result set is returned, and if you read it directly, you will see the following code:

ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
      Long id = resultSet.getLong("id");
      String title = resultSet.getString("title");
      String author = resultSet.getString("author");
      String content = resultSet.getString("content");
      ......
}

Once the data for each field is obtained, an object is generated by reflection; this is also true internally in Mybatis, where a result set is obtained by encapsulating a simple configuration, such as resultMap, resultType, and so on. ResultSet is handled internally by Mybatis as ResultSetHandler, which implements the DefaultResultSetHandler class:

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

There are three ways to work with the interface: to work with normal result sets, to work with cursor result sets, and to work with output parameters. You can take a general look at the most commonly used handleResultSets implementations:

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<Object>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
    ...The following omissions...
  }

while looping through the result set, ResultMap is the result set defined in XML, such as a type Blog. When processing the result, an object is created, a type processor TypeHandler is assigned to the object property, and the processor's getResult method is invoked based on the actual type:

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

You can see that the type processor is used to process the settings and get the parameters from the result set, that is, to process the input and output separately; after processing, it actually generates the result set configured in the XML, which may be an object, a list, a hashMap, etc. Another thing to note is that the return value defined in the xxMapper interface needs to be guaranteed and the result set matched in the XML is oneOtherwise, when we return a result set through a proxy object, a type conversion exception will occur.

summary

XML mapping This article is divided into three sections, mapping MappedStatement from Statement block, parameter mapping ParameterMapping, and how the result set is handled by DefaultResultSetHandler. Of course, this article only describes a general mapping process, many details are not mentioned, such as ResultHandler, RowBounds, Cache, etc. Each of the following details will be written separatelyAn article to introduce.

Sample Code Address

Github

Topics: Programming xml SQL Mybatis Java