[source code analysis] talk about your understanding of MyBatis result set mapping and parameter binding

[total] MyBatis result set mapping is that in the process of MyBatis parsing Mapper.xml mapping file, < ResultMap > tag will be parsed into ResultMap object. After MyBatis executes a select statement and obtains the ResultSet result set, it will be handed over to the associated ResultSetHandler for subsequent mapping processing. ResultSetHandler defines three methods, including: It is not used to process different query return values. MyBatis only provides a ResultSetHandler interface implementation, that is, DefaultResultSetHandler.

ResultSetHandler interface and the default implementation of DefaultResultSetHandler, where the entry of a single result set mapping is in the handleResultSets() method. The handlerowvaluesforssimpleresultmap() method is the core step to implement simple mapping, which involves six core steps: preprocessing ResultSet, finding and determining ResultMap, creating and filling mapping result object, automatic mapping, normal mapping and storing mapping result object.

public interface ResultSetHandler {
    // Map ResultSet to Java object
    <E> List<E> handleResultSets(Statement stmt) throws SQLException;
    // Map ResultSet to cursor object
    <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
    // Processing the output parameters of stored procedures
    void handleOutputParameters(CallableStatement cs) throws SQLException;
}

[sub] mapping process of ResultSet in MyBatis

1, Result set processing entry

Defaultresultsethandler implements the {handleResultSets() method and supports the processing of one or more resultsets. org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets.  

The getFirstResultSet() method and getNextResultSet() method are used when traversing multiple result sets. The bottom layer of these two methods is to detect whether there are subsequent ResultSet objects. After the detection is successful, the next ResultSet object will be obtained through the getResultSet() method. The ResultSet object obtained here will be wrapped as a ResultSetWrapper object and returned.

ResultSetWrapper is mainly used to encapsulate some metadata of ResultSet, which records the name of each column in the ResultSet, the corresponding Java type, JdbcType type and the corresponding TypeHandler of each column. The ResultSetWrapper can intersect the columns of the underlying ResultSet with the columns mapped by a ResultMap to obtain the mapped columns and unmapped columns, which are recorded in the mappedColumnNamesMap set and unmapped columnnamesmap set respectively. These two sets are map < string, list < string > > types. The outermost Key is the id of ResultMap, and Value is the set of column names participating in the mapping and the set of column names not mapped respectively.

    public List<Object> handleResultSets(Statement stmt) throws SQLException {
        // Used to record the Java objects mapped out by each ResultSet
        final List<Object> multipleResults = new ArrayList<>();
        int resultSetCount = 0;

        // Obtain the first ResultSet from the Statement, which has compatible processing logic for different databases,
        // The ResultSet obtained here will be encapsulated into a ResultSetWrapper object and returned
        ResultSetWrapper rsw = getFirstResultSet(stmt);

        // Get all ResultMap rules associated with this SQL statement. If one SQL statement can produce multiple resultsets,
        // So when writing mapper When mapping XML files, we can configure multiple in the resultMap attribute of the SQL tag
        // The id of < resultmap > tag is separated by "," to realize the mapping of multiple result sets
        List<ResultMap> resultMaps = mappedStatement.getResultMaps();

        int resultMapCount = resultMaps.size();
        validateResultMapsCount(rsw, resultMapCount);
        while (rsw != null && resultMapCount > resultSetCount) { // Traverse the ResultMap collection
            ResultMap resultMap = resultMaps.get(resultSetCount);

            // Process the ResultSet according to the mapping rules defined in the ResultMap, and add the mapped Java object to the
            // Saved in the multipleResults collection
            handleResultSet(rsw, resultMap, multipleResults, null);

            // Get next ResultSet
            rsw = getNextResultSet(stmt);

            // Clean up the nestedResultObjects collection, which is used to store intermediate data
            cleanUpAfterHandlingResultSet();
            resultSetCount++; // Increment ResultSet number
        }
        // The following logic deals with nested mapping according to the name of ResultSet. You can ignore this code for the time being,
        // Nested mapping will be described in detail later
        ... 
        // Return all mapped Java objects
        return collapseSingleResultList(multipleResults);
    }

2, Simple mapping

handleResultSet() method, which will judge whether the currently processed ResultSet is a nested mapping or an outer mapping according to the fourth parameter, parentMapping. Will rely on the handleRowValues() method to complete the processing of the result set (it can also be seen from the method name that the handleRowValues() method is used to process multiple rows of records, that is, a result set).

As for the handleRowValues() method, the ResultMap containing nested mapping will be processed through the handlerowvaluesfornedresultmap() method, and the simple ResultMap without nested mapping will be processed through the handlerowvaluesforssimpleresultmap() method, as shown below:

    public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
        if (resultMap.hasNestedResultMaps()) { // Process flow for determining whether nested mappings are included
            ensureNoRowBounds();
            checkResultHandler();
            handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
        } else { // Processing of simple mapping
            handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
        }
    }

How does the handlerowvaluesforssimpleresultmap() method map a ResultSet? The core steps of this method can be summarized as follows.

    private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
            throws SQLException {
        DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
        // 1. Execute skipRows() method to skip redundant records and locate to the specified row.
        skipRows(rsw.getResultSet(), rowBounds);
        // 2. Use shouldProcessMoreRows() method to check whether there are still data records to be mapped.
        while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
            // 3. If there are records to be mapped, first process the Discriminator used in the mapping through the resolveDiscriminatedResultMap() method to determine the ResultMap actually used in the mapping.
            ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
            // 4. Map a row of records in the ResultSet through the getRowValue() method. The mapping rule uses the ResultMap determined in step 3.
            Object rowValue = getRowValue(rsw, discriminatedResultMap);
            // 5. Execute the storeObject() method to record the mapped Java object returned in step 4.
            storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
        }
    }

The handlerowvaluesforssimpleresultmap() method how to map a ResultSet is summarized as follows: Step 1 Preprocessing of ResultSet (); step 2. Determine the ResultMap, process the < discriminator > tag through the resolveDiscriminatedResultMap() method, and determine the ResultMap object finally used in this mapping operation; Step 3 The mapping result object is created and parsed by the resolvediscriminatedresultmap () method. Finally, we determine which ResultMap the current record uses for mapping. Next, map the columns according to the ResultMap rules to get the final Java object. This part of logic is completed in the getRowValue() method to be introduced below. Its core steps are as follows:

    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
        final ResultLoaderMap lazyLoader = new ResultLoaderMap();
        // Create a mapped result object based on the value of the type attribute of the ResultMap
        Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
        if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            final MetaObject metaObject = configuration.newMetaObject(rowValue);
            boolean foundValues = this.useConstructorMappings;
            // According to the configuration and global information of ResultMap, decide whether to automatically map the columns that are not explicitly mapped in ResultMap
            if (shouldApplyAutomaticMappings(resultMap, false)) {
                foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
            }
            // *According to the ResultMap mapping rules, the column values in the ResultSet are mapped with the attribute values in the result object*
            foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
            // If no attribute is mapped, you need to decide how to return the result value according to the global configuration,
            // Different scenarios and configurations here may return complete result objects, empty result objects, or null
            foundValues = lazyLoader.size() > 0 || foundValues;
            rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
        }
        return rowValue;
    }

Since we focus on the mapping process of simple ResultSet, next we focus on how the createResultObject() overload method creates the mapping result object.

    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
        this.useConstructorMappings = false; // reset previous mapping result
        final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
        final List<Object> constructorArgs = new ArrayList<Object>();
        Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
        // If this information is not empty, you can determine the unique constructor in the result type.
        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
            for (ResultMapping propertyMapping : propertyMappings) {
                // issue gcode #109 && issue #149
                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                    resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                    break;
                }
            }
        }
        this.useConstructorMappings = (resultObject != null && !constructorArgTypes.isEmpty()); // set current mapping result
        return resultObject;
    }

    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
            throws SQLException {
        final Class<?> resultType = resultMap.getType();
        // Get the type of the result object specified by the type attribute in the ResultMap, and create the MetaClass pair corresponding to the type
        final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
        // Get the < constructor > tag information configured in the ResultMap (that is, the corresponding ResultMapping object collection)
        final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
        // Create the result object in different ways according to four different scenes
        if (hasTypeHandlerForResultObject(rsw, resultType)) {
            // Scenario 1: there is only one column in the ResultSet, and a TypeHandler can be found to complete the mapping of the column to the target result type. At this time, the column value in the ResultSet can be directly read and the result object can be obtained through TypeHandler conversion.
            // This part of logic is implemented in the createPrimitiveResultObject() method. This scenario is mostly used for processing Java primitive types.
            return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
        } else if (!constructorMappings.isEmpty()) {
            // Scenario 2: if the < constructor > tag is configured in the ResultMap, it will first resolve the type of the construction method parameter specified in the < constructor > tag, obtain the corresponding argument value from the data row to be mapped, and then call the corresponding construction method through reflection to create the result object.
            // This logic is implemented in the createParameterizedResultObject() method.
            return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
        } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
            // Scenario 3: if the above two scenarios are not satisfied, try to find the default construction method to create the result object. Here, use the objectfactory The underlying principle of the create () method is the reflection mechanism of Java.
            return objectFactory.create(resultType);
        } else if (shouldApplyAutomaticMappings(resultMap, false)) {
            // Scenario 4: finally, it will detect whether the automatic mapping function has been enabled. If it is enabled, it will try to find an appropriate construction method to create the result object. Here, we will first find the construction method of @ AutomapConstructor annotation. After the search fails, we will try to find the construction method that each parameter has a TypeHandler that can map to the ResultSet column. After determining the construction method to be used, we will also create the object through ObjectFactory.
            // This logic is implemented in the createByConstructorSignature() method.
            return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
        }
        throw new ExecutorException("Do not know how to create an instance of " + resultType);
    }

Step 4 Automatic mapping. After creating the result object, you can start mapping each field. In the simple mapping process, you will first check whether automatic mapping is enabled through the shouldaplyautomaticmappings() method, mainly in the following two places. When it is determined that the current ResultMap needs to be automatically mapped, it will be automatically mapped through the applyAutomaticMappings() method.

    private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
        // Create an unmapped columnautomapping object to associate the column with the corresponding property. Column names, property names, and related typehandlers are recorded in unmapped column automapping.
        List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
        boolean foundValues = false;
        if (!autoMapping.isEmpty()) {
            // Traverse the above to get the unmapped columnautomapping collection, read the column value through the TypeHandler and convert it into the corresponding Java type, and then set it into the corresponding attribute through MetaObject.
            for (UnMappedColumnAutoMapping mapping : autoMapping) {
                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')
                    metaObject.setValue(mapping.property, value);
                }
            }
        }
        return foundValues;
    }

Step 5 5 After the normal mapping completes the automatic mapping, MyBatis will execute the applyPropertyMappings() method to process the columns explicitly to be mapped in the ResultMap. The getPropertyMappingValue() method mainly deals with the mapping of three scenarios. The first is the mapping of basic types. In this scenario, the column values can be directly read from the ResultSet through the TypeHandler and returned after conversion; The second and third scenarios are nested mapping and multi result set mapping, respectively

    private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
            throws SQLException {
        final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
        boolean foundValues = false;
        // First, specify the column name collection to be mapped from the ResultSetWrapper and the ResultMapping object collection defined in the ResultMap.
        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
        // Traverse all ResultMapping sets, add the specified prefix to the column property value for each ResultMapping object to get the final column name, and then execute the getPropertyMappingValue() method to complete the mapping to get the corresponding property value.
        for (ResultMapping propertyMapping : propertyMappings) {
            String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
            if (propertyMapping.getNestedResultMapId() != null) {
                // the user added a column attribute to a nested result map, ignore it
                column = null;
            }
            // If the attribute value is successfully obtained, it is set to the corresponding attribute through the MetaObject object associated with the result object.
            if (propertyMapping.isCompositeResult()
                    || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
                    || propertyMapping.getResultSet() != null) {
                // It mainly deals with the mapping of three scenarios
                // The first is the mapping of basic types. In this scenario, you can directly read the column values from the ResultSet through the TypeHandler and return them after conversion;
                // The second is nested mapping
                // The third is the mapping of multiple result sets
                Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
                // issue #541 make property optional
                final String property = propertyMapping.getProperty();
                if (property == null) {
                    continue;
                } else if (value == DEFERED) {
                    foundValues = true;
                    continue;
                }
                if (value != null) {
                    foundValues = true;
                }
                if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
                    // gcode issue #377, call setter on nulls (value is not 'found')
                    metaObject.setValue(property, value);
                }
            }
        }
        return foundValues;
    }

Step 6 6 Store objects. Through the above five steps, we have completed the processing of simple mapping and obtained a complete result object. Next, we will save the result object to the appropriate location through the storeObject() method. If it is a sub mapping in a nested mapping, we need to save the result object to the properties of the outer object; If it is a result object of a normal mapping or an outer mapping, we need to save the result object to ResultHandler.

    private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
        if (parentMapping != null) {
            // In the scene of nested query or nested mapping, you need to save the result object to the corresponding attribute of the outer object
            linkToParents(rs, parentMapping, rowValue);
        } else {
            // For scenes with normal mapping (no nested mapping) or outer mapping in nested mapping, you need to save the result object to ResultHandler
            callResultHandler(resultHandler, resultContext, rowValue);
        }
    }

Summary: in this lecture, we focus on result set mapping. First, we introduce the ResultSetHandler interface and the default implementation of DefaultResultSetHandler, and explain the entry of single result set mapping: handleResultSet() method. The core steps of mapping with handlerowvaluesforssimpleresultmap () method are analyzed in detail, including six core steps: preprocessing ResultSet, finding and determining ResultMap, creating and filling mapping result object, automatic mapping, normal mapping and storing mapping result object.

Keywords: Java Mybatis source code

Added by melefire on Sun, 19 Dec 2021 19:30:54 +0200