java source learning - Mybatis creates statement and result set generation

Mybatis creates statement and result set generation

Previous: Mybatis(3) executes sql procedure

statementHandler

In the Configuration class of Mybatis, there are the following three methods. If we want to generate a statement, we need to use a statement processor

  public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

In the following method of generating statement, we can see that there are parameterHandler and resultSetHandler in statement

  protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;

    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

The function of parameterHandler is very simple. First, it contains the types of jdbc and java. In MappedStatement, it stores the basic information of sql and the parameter information to be replaced

When generating the final sql, you need this parameterHandler to work with you
resultSetHandler is used to process the result when the query of sql returns


In the prepareStatement method, we can see that the handler is used to generate a statement, and then the input parameter is set in the parameterize method

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // Get the input parameter name from boundSql, as in sqlSource in the screenshot above
    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;
          // Get the input parameter name from ParameterMapping
          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 {
          	// parameterObject is the input parameter that we write in mapper. If @ Param annotation exists
          	// Then the key will be the value of @ Param
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            // Use propertyName as key to get value in the parameter map
            value = metaObject.getValue(propertyName);
          }
          // Get the java type of propertyName
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
          	// Replace the placeholder in sql with the value obtained
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

The getValue method of mateObject can bind parameters through the parameter name obtained above

  public Object getValue(String name) {
    PropertyTokenizer prop = new PropertyTokenizer(name);
    if (prop.hasNext()) {
      MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
      if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
        return null;
      } else {
        return metaValue.getValue(prop.getChildren());
      }
    } else {
      return objectWrapper.get(prop);
    }
  }

The typeHandler obtained earlier is object. Here, use getClass of parameter(value) to get the real type

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    // Insert data with key I and value parameter into columnMap of ps
    handler.setParameter(ps, i, parameter, jdbcType);
  }

Finally, this setParameter will call the setNString method of PreparedStatement, and this method will be finally executed by the agent is the setString method of HikariProxyPreparedStatement, but I can't download the source code here So we have to guess

// Replace * * NOT SPECIFIED * * at var1 location with var2
public void setString(int var1, String var2) throws SQLException {
        try {
            ((PreparedStatement)super.delegate).setString(var1, var2);
        } catch (SQLException var4) {
            throw this.checkException(var4);
        }
    }

Result set processing

In the previous step, we finished processing the sql statement. After submitting it to the database, the database will return the result set. We need to process the result set and put the result into the javabean

  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);
      // Got a statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      // Start to query with statement's processor
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // Here, a request is initiated to submit the sql to the database for execution
    ps.execute();
    // Start processing results after the above execution
    return resultSetHandler.handleResultSets(ps);
  }

Let's first look at the original data returned by the database in the results of delegate


You can see that what my sql statement returns is indeed all the contents of my table. The second figure is the field name, and their value s are stored in the form of byte [] in the first figure
So it is clear here that our subsequent processing operations are to create a key value according to the contents of these two graphs or directly put it into our javabean as the return value of the method



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

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

    int resultSetCount = 0;
    // The content screenshot of rsw is below. According to the screenshot, we extract our key and put it in columnName
    ResultSetWrapper rsw = getFirstResultSet(stmt);
	// Get the resultMap, which is our method return value. What I define here is UserDTO
	// The type in the type of resultMap is the return type we defined
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // Process result set
      // Whether it's selectOne or not, it's a list
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

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


How to deal with result set

  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // An Object is generated here. The content is UserDTO
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      // Generate a metaObject for subsequent operations, and put the UserDTO into the metaObject
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      // This is to determine whether hump mapping is enabled. I have enabled it here
      if (shouldApplyAutomaticMappings(resultMap, false)) {
      	// After enabling hump mapping, get the mapping relationship and put the value into UserDTO
        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;
    }
    // The user dto after all values are injected is returned here
    return rowValue;
  }
  private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
      for (UnMappedColumnAutoMapping mapping : autoMapping) {
      	// Get the value of the key column in rsw's resultSet
      	// As I said above, get byte [] in delegate and return after conversion
        final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
          // gcode issue #377, call setter on nulls (value is not 'found')
          // Here, we inject the value into our user dto with the key after hump mapping
          metaObject.setValue(mapping.property, value);
        }
      }
    }
    return foundValues;
  }

Finally, let's look at the entry queryFromDatabase method, which opens the first level cache of mybatis,
My understanding of L1 cache is that in the case of high concurrency, there may be many same requests. We only need to actually request the database once, without wasting resources, and the results of this database query can be used for new query requests generated in this query
So we can see that in this method, a cache is generated before doQuery, and the data obtained is injected into the cache after doQuery. Obviously, when a data becomes hot data, this cache can be used to reduce the number of database queries
But when I actually use it, I send multiple requests continuously without triggering the cache, because this localCache is not the same localCache, because the cache life cycle exists in a SqlSession
After querying the data, we found that if you want to use the first level cache, you need to open the Transaction and use the annotation @ Transaction



  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // Here is the list received, so when we process the result set, we get a list
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

Finally, the list returned by the queryFromDatabase method will go back to selectList, and the selectList method is invoked in the selectOne method. In the end, it is judged whether the size of the list is 1. If not, the but found x abnormal information of selectOne is thrown, if so, the result is returned.

Postscript

It's almost the end of learning the source code of mybatis. It's found that mybatis has poor scalability, because after reading so many methods, there is almost no interface oriented programming

Tags: SQL Database Mybatis Java

Posted on Sat, 27 Jun 2020 05:40:32 -0400 by maxpouliot