This is the 30th day of my participation in the August Text Challenge.More challenges in August

The code implementation of nested queries and lazy loading is not described here, see article Mybatis caching, lazy loading. Let’s analyze the source code directly and see how lazy loading is executed.

Sequence diagram

sequenceDiagram
participant A as BaseExecutor
participant B as SimpleExecutor
participant C as PreparedStatementHandler
participant D as DefaultResultSetHandler

A ->> A : query
A ->> A : queryFromDatabase
A ->> B : doQuery
B ->> C : query
C ->>+ D : handleResultSets
D ->> D : handleResultSet
D ->> D : handleRowValues
D ->> D : handleRowValuesForSimpleResultMap
D ->> D : getRowValue
D ->> D : applyPropertyMappings
D ->> D : getPropertyMappingValue
D ->> D : getPropertyMappingValue
D -->>- A : List<Object>

The key code

BaseExecutor#query

  • Query method, all queries need to pass through here. If it is a normal query, the query enters the query with queryStack=1 and the query ends with queryStack=0
  • For nested queries, queryStack=1 when entering the nested external query, queryStack=2 when entering the nested internal query, queryStack=1 after the nested internal query, and queryStack=0 after the nested external query.
/** * query data in the database *@paramMs mapping statement *@paramParameter Specifies the parameter object *@paramRowBounds Page turn restrictions *@paramResultHandler resultHandler *@paramKey The cached key *@paramBoundSql query statement *@param<E> Result type *@returnResult list@throws SQLException
 */
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
        // The actuator is closed
        throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) { // New query stack and request to clear cache
        // New query stack, so clear local cache, i.e. clear level cache
        clearLocalCache();
    }
    List<E> list;
    try {
        / * * * query stack, entering for the first time, the pressure of stack * ordinary query, execute queries after pressure stack, stack, after the completion of nested query, query the stack of 0 * for the first time into the stack, after the first query has yet to be completed (query not out of the stack) executed when nested query, will pop down again, so there will be a query * / stack is greater than 1
        queryStack++;
        // Try to get the result from the local cache
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if(list ! =null) {
            // If there is a result IN the local cache, you also need to bind to the IN/INOUT parameter for the CALLABLE statement
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // The local cache has no result, so you need to query the databaselist = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); }}finally {
        // Exit the stack after the query is complete
        queryStack--;
    }
    if (queryStack == 0) {
        // Handle lazy loading operations
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        // issue #601
        deferredLoads.clear();
        // If the local cache scope is STATEMENT, the local cache is cleared immediately
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482clearLocalCache(); }}return list;
}
Copy the code

DefaultResultSetHandler#getRowValue

  • In the getRowValue method, the empty object that receives the result is created first

  • Determine whether to enable automatic mapping. The configurations and enumerations for enabling automatic mapping are as follows

    <setting name="autoMappingBehavior" value="PARTIAL"/>
    Copy the code
    // Automatic mapping options
    public enum AutoMappingBehavior {
    
        /** * Disables auto-mapping. */
        // Disable automatic mapping
        NONE,
    
        /** * Will only auto-map results with no nested result mappings defined inside. */
        // Only single-layer attributes are automatically mapped
        PARTIAL,
    
        /** * Will auto-map result mappings of any complexity (containing nested or otherwise). */
        // Map all attributes, including nested attributes
        FULL
    }
    Copy the code
  • The mapping is performed based on the mapping relationship configured on the resultMap label

/** * Convert a record to an object **@paramRSW result set packaging *@paramResultMap Indicates the result mapping *@paramColumnPrefix columnPrefix *@returnThe converted object *@throws SQLException
 */
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    / / lazy loading
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // Create an empty object for this row
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if(rowValue ! =null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        // Get the MetaObject from the object
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        // Whether to allow automatic mapping of unstated fields
        if (shouldApplyAutomaticMappings(resultMap, false)) {
            // Automatically map an unspecified field (resultType). The Mapping TypeHandler obtains the attribute type by mapping the attribute name to the parameter type of the set method, and obtains the corresponding TypeHandler accordingly
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
        }
        // Remap according to explicit fields (resultMap), retrieve TypeHandler when parsing XML
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}
Copy the code

DefaultResultSetHandler#createResultObject

  • In the createResultObject method, the receiver object for the result set is created based on the Type attribute in the resultMap tag
  • If the current attribute is a nested query, and the lazy loading, will create the receiving object proxy objects, the proxy class is EnhancedResultObjectProxyImpl
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false; // reset previous mapping result
    finalList<Class<? >> constructorArgTypes =new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if(resultObject ! =null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
        for (ResultMapping propertyMapping : propertyMappings) {
            // issue gcode #109 && issue #149
            // If the query is nested and lazily loaded, create a proxy object
            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;
}
Copy the code

DefaultResultSetHandler#applyPropertyMappings

  1. To obtainResultSetThe column
  2. To obtainresultMapThe mapping configuration in the tag
  3. A method is calledgetPropertyMappingValueGets the corresponding result value
  4. Get the attribute name for the mapping
  5. callmetaObject.setValueMethod to assign a value to a property
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
        throws SQLException {
    // Get the column name in the ResultSet
    final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
    boolean foundValues = false;
    // Obtain the resultMap mapping configuration
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    for (ResultMapping propertyMapping : propertyMappings) {
        // Get the full column name in the mapping configuration
        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(propertyMapping.isCompositeResult() || (column ! =null&& mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) || propertyMapping.getResultSet() ! =null) {
            // Get the corresponding value based on the column name
            Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
            // issue #541 make property optional
            // Get the attribute name in the mapping configuration
            final String property = propertyMapping.getProperty();
            if (property == null) {
                continue;
            } else if (value == DEFERRED) {
                // If the load is lazy, the current query is set to succeed and the next loop is entered without assigning the current attribute
                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')
                // Set the value for the propertymetaObject.setValue(property, value); }}}return foundValues;
}
Copy the code

DefaultResultSetHandler#getPropertyMappingValue

  • Is called if the current mapping is a nested querygetNestedQueryMappingValueMethod to get the value of the corresponding column
  • If it is a common query, it is directly calledTypeHandlerIn thegetResultMethod to get the value of the corresponding column
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
        throws SQLException {
    if(propertyMapping.getNestedQueryId() ! =null) {
        // Perform nested queries
        return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    } else if(propertyMapping.getResultSet() ! =null) {
        // Handle the resultSet attribute (this attribute is paired with the multi-result set query attribute resultSets)
        addPendingChildRelation(rs, metaResultObject, propertyMapping);   // TODO is that OK?
        return DEFERRED;
    } else {
        // Common query
        finalTypeHandler<? > typeHandler = propertyMapping.getTypeHandler();final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
        returntypeHandler.getResult(rs, column); }}Copy the code

DefaultResultSetHandler#getNestedQueryMappingValue

  • If the load is not lazy, the query is executed immediately based on the loaded parametersBaseExecutor#queryAnd returns the result
  • In lazy loading mode, the lazyLoader uses all uppercase values of the current attribute name as the key, encapsulates the conditions required for the query as values, and stores them in the lazyLoader
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
        throws SQLException {
    // Get the parameters needed for the nested query
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    final String property = propertyMapping.getProperty();
    final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
    finalClass<? > nestedQueryParameterType = nestedQuery.getParameterMap().getType();final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
    Object value = null;
    // The query parameter is not empty
    if(nestedQueryParameterObject ! =null) {
        / / access to SQL
        final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
        / / generated CacheKey
        final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
        finalClass<? > targetType = propertyMapping.getJavaType();if (executor.isCached(nestedQuery, key)) {
            executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
            value = DEFERRED;
        } else {
            final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
            // Whether to load lazily
            if (propertyMapping.isLazy()) {
                // Lazy load, put lazy load attributes into the lazyLoader
                lazyLoader.addLoader(property, metaResultObject, resultLoader);
                value = DEFERRED;
            } else {
                // Execute the query directly without lazy loadingvalue = resultLoader.loadResult(); }}}return value;
}
Copy the code

If lazy loading is performed, the lazy loading property value is null, and the query ends. If not lazily loaded, the nested internal query will end by assigning this attribute to the result of the query. Lazy loading, on the other hand, returns a proxy object that goes into the Intercept method of the proxy class when a method in the object is called.

Because the CGLIB proxy is configured in the XML, this goes into the CglibProxyFactory class. You can configure the CGLIB proxy or JDK proxy here.

<! -- Specify the proxy tool Mybatis uses to create lazy-loading objects. CGLIB | JAVASSIST-->
<setting name="proxyFactory" value="CGLIB"/>
Copy the code

CglibProxyFactory.EnhancedResultObjectProxyImpl#intercept

  • Normal method call, going into the else code block

  • The aggressive load attribute aggressive can be configured in XML

    <! When enabled, any method call loads all properties of the object. Otherwise, each attribute is loaded on demand, default is false-->
    <setting name="aggressiveLazyLoading" value="false"/>
    Copy the code
  • When the property’s get method is called and the property is included in the lazyLoader, the value of the property is loaded

/** * Proxy class interceptor method *@paramEnhanced proxy object itself *@paramMethod Specifies the method to be called@paramArgs Arguments to each method called *@paramMethodProxy is used to call the parent class's proxy *@returnMethod returns the value *@throws Throwable
 */
@Override
public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    // Retrieve the name of the method called in the propped class
    final String methodName = method.getName();
    try {
        synchronized (lazyLoader) { // Prevent concurrent loading of attributes
            if (WRITE_REPLACE_METHOD.equals(methodName)) { // The writeReplace method is called
                // Create a primitive object
                Object original;
                if (constructorArgTypes.isEmpty()) {
                    original = objectFactory.create(type);
                } else {
                    original = objectFactory.create(type, constructorArgTypes, constructorArgs);
                }
                // Copy the properties of the proxied object into the newly created object
                PropertyCopier.copyBeanProperties(type, enhanced, original);
                if (lazyLoader.size() > 0) { // Lazy loading property exists
                    // More information is returned, not only about the original object, but also about lazy loading Settings. So use CglibSerialStateHolder for one encapsulation
                    return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
                } else {
                    // There are no unloaded attributes, so return the original object for serialization
                    returnoriginal; }}else {
                if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) { // Lazy attribute exists and finalize method is not called
                    if (aggressive || lazyLoadTriggerMethods.contains(methodName)) { // Methods with aggressive lazy loading set or called are methods that trigger global lazy loading
                        // Complete lazy loading of all attributes
                        lazyLoader.loadAll();
                    } else if (PropertyNamer.isSetter(methodName)) {  // The property write method is called
                        // Clear lazy load Settings for this property. This property does not need to be lazily loaded
                        final String property = PropertyNamer.methodToProperty(methodName);
                        lazyLoader.remove(property);
                    } else if (PropertyNamer.isGetter(methodName)) { // The property read method was called
                        final String property = PropertyNamer.methodToProperty(methodName);
                        // If the property is a lazy loaded property that has not been loaded, lazy loading is performed
                        if (lazyLoader.hasLoader(property)) {
                            lazyLoader.load(property);
                        }
                    }
                }
            }
        }
        // Trigger the corresponding method of the proxied class. Methods other than writeReplace, such as accessor methods, toString methods, and so on, can do this
        return methodProxy.invokeSuper(enhanced, args);
    } catch (Throwable t) {
        throwExceptionUtil.unwrapThrowable(t); }}Copy the code

Above is the realization principle of lazy loading of Mybatis.