Analysis of MyBatis delayed loading principle

🏆 Analysis of MyBatis delayed loading principle

1. Introduction and use of delayed loading

This paper will analyze the principle of delayed loading (lazy loading) provided by MyBatis.

1.1 what is delayed loading?

In short, delayed loading is to load when data is needed, and not when data is not needed.

Suppose there are two tables in the database, user table AND order table (one to many relationship). Suppose a user has many orders, do you need to query the order data associated with the current user when querying the user?

Generally speaking, when querying user information, it is better to use user orders. In particular, delayed loading is usually recommended for one to many multi table queries, because single label queries must be faster than multi table associated queries. First query from a single table, and then perform associated queries in associated tables when necessary, which will improve database performance.

1.2 how to enable deferred loading

MyBatis does not enable delayed loading by default. Enabling delayed loading requires some simple configuration.

First, you can set the setting property in the MyBatis SqlMapper core configuration file to set the global lazy load

 <settings>
        <!-- Enable global deferred loading,The default value is true -->
        <setting name="lazyLoadingEnabled" value="true" />
        <!-- Set global active lazy load,The default value is true -->
        <setting name="aggressiveLazyLoading" value="false"/>
 </settings>

Or configure the fetchType tag in the mapper configuration file to set local lazy loading

<resultMap id="userMap" type="com.mryan.pojo.User">
    <id property="id" column="id"></id>
    <result property="username" column="username"></result>
    <!--fetchType="lazy"  Lazy loading strategy  fetchType="eager"  Load policy now -->
    <collection property="orderList" ofType="com.mryan.pojo.Order"
                select="com.mryan.mapper.IOrderMapper.findOrderByUid" column="id" fetchType="lazy">

        <id property="id" column="uid"/>
        <result property="orderTime" column="ordertime"/>
        <result property="total" column="total"/>
    </collection>
</resultMap>


<select id="findOrderByUid" resultType="com.mryan.pojo.Order">
select *
from orders
where uid = #{uid}
</select>

Note that the local lazy loading strategy has higher priority than the global lazy loading strategy

1.3 test the effect of delayed loading

To test multi table Association, the table structure and pojo structure are as follows:

User

public class User implements Serializable {
    private Integer id;
    private String username;
    // User associated order data
    private List<Order> orderList;
//ellipsis

Orders

public class Order implements Serializable {
    private Integer id;
    private String orderTime;
    private Double total;
    // Indicates which user the order belongs to
    private User user;
//ellipsis

After completing the above configuration, run a unit test to see if the delayed loading takes effect

    /*
    Test deferred load query
   */
@Test
public void TEST_QUERY_LAZY()throws IOException{
        InputStream inputStream=Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory factory=new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession=factory.openSession();
        User user=sqlSession.selectOne("com.mryan.mapper.IUserMapper.findById",1);
        //The output statement below the deferred load takes effect does not involve the orders table, so the orders related log will not be printed
        System.out.println("user: "+user.getUsername());
        //Relevant SQL statements will be executed only when the orders table is involved, and the orders execution log will be loaded (when to delay loading and when to check)
        System.out.println("orders: "+user.getOrderList());
        }

It can be found that only the sql statement for querying the user is started when user.getUsername(), while the sql statement for querying the order is triggered when user.getOrderList(). It can be seen that the delayed loading takes effect, so that there is no need to query, and it is used in query.

2. Analysis of delayed loading source code

The following will read the source code and analyze the principle of Mybatis delayed loading.

In the first article in the MyBatis series Analysis on SQL execution process of MyBatis
Explained the SQL execution process. Briefly review the general process. The execution of SQL query statements is distributed by SqlSession and managed by the Executor. The StatementHandler is responsible for JDBC
Statement operation, and then send it to ParameterHandler, which is responsible for converting the parameters passed by the user, processing the SQL parameters, then executing the SQL statement, and finally encapsulating the returned results through ResultSetHandler.

According to the test of the delayed loading function just now, we can also roughly find the breakthrough entry. When the result is encapsulated through the final ResultSetHandler, we can load the corresponding target object result according to the instance name of the calling getting method. Doesn't it realize the delayed loading function.

MyBatis actually does this. Next, check the source code to confirm our conjecture

2.1 source code analysis

/**
 * MyBatis to configure
 *
 * @author Clinton Begin
 */
public class Configuration {
  
      /**
     * When on, any method call will load all the properties of the object. Otherwise, each attribute is loaded on demand (see lazyLoadTriggerMethods)
     */
    protected boolean aggressiveLazyLoading;

    /**
     * Specifies which object's method triggers a deferred load.
     */
    protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
  
  /**
     * Global switch to delay loading. When on, all associated objects will delay loading. In a specific association, the switch state of the item can be overridden by setting the fetchType property.
     */
    protected boolean lazyLoadingEnabled = false;
   
}

Lazyloading enabled of Configuration corresponds to the fetchType tag configured in the mapper Configuration file

Because we have the basis for analyzing the SQL execution process, we will directly locate the key code this time.

DefaultResultSetHandler#handleResultSets()

This method is the core code that encapsulates the result set after executing the SQL query statement

    // Process {@ link java.sql.ResultSet} result set
    @Override
    public List<Object> handleResultSets(Statement stmt) throws SQLException {
        ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

        // The result set of multiple resultsets. Each ResultSet corresponds to an object object. In fact, each object is a list < Object > object.
        // Without considering the multiple resultsets of stored procedures, ordinary queries actually have one ResultSet, that is, multipleResults can only have one element at most.
        final List<Object> multipleResults = new ArrayList<>();

        int resultSetCount = 0;
        // Get the first ResultSet object and encapsulate it into a ResultSetWrapper object
        ResultSetWrapper rsw = getFirstResultSet(stmt);

        // Get ResultMap array
        // Without considering the multiple resultsets of stored procedures, an ordinary query actually has one ResultSet, that is, resultMaps is only one element.
        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
        int resultMapCount = resultMaps.size();
        validateResultMapsCount(rsw, resultMapCount); // check
        while (rsw != null && resultMapCount > resultSetCount) {
            // Get ResultMap object
            ResultMap resultMap = resultMaps.get(resultSetCount);
            // Key code!!! Process ResultSet and add results to multipleResults
            handleResultSet(rsw, resultMap, multipleResults, null);
            // Get the next ResultSet object and encapsulate it into a ResultSetWrapper object
            rsw = getNextResultSet(stmt);
            // clear
            cleanUpAfterHandlingResultSet();
            // resultSetCount ++
            resultSetCount++;
        }

        // Because 'mappedStatement.resultSets' is only used in stored procedures, this series will not be considered temporarily and can be ignored
        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++;
            }
        }

        // If it is a multipleResults single element, the first element is returned
        return collapseSingleResultList(multipleResults);
    }

Directly look at the main method handleResultSet(rsw, resultMap, multipleResults, null);

This method is mainly used to process the ResultSet and add the results to multipleResults

// Process the ResultSet and add the results to multipleResults
    private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
        try {
            // It is ignored for the time being because this method is called only in the case of stored procedures, and parentMapping is not empty
            if (parentMapping != null) {
                handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
            } else {
                // If there is no custom resultHandler, a default DefaultResultHandler is created
                if (resultHandler == null) {
                    DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
                    // Core code!!! Process each Row row returned by ResultSet
                    handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
                    // Add the processing results of defaultResultHandler to multipleResults
                    multipleResults.add(defaultResultHandler.getResultList());
                } else {
                    //Core code!!! Process each Row row returned by ResultSet
                    handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
                }
            }
        } finally {
            // issue #228 (close resultsets)
            // Close the ResultSet object
            closeResultSet(rsw.getResultSet());
        }
    }

The handleRowValues method processes each Row returned by the ResultSet, and calls the createResultObject() method to create the result object, which completes the lazy loading related processing logic.

 // Create mapped result object
    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
        // useConstructorMappings, indicating whether to create the result object using the construction method. Reset it here
        this.useConstructorMappings = false; // reset previous mapping result
        final List<Class<?>> constructorArgTypes = new ArrayList<>(); // An array of parameter types that records the constructor used
        final List<Object> constructorArgs = new ArrayList<>(); // An array that records the parameter values of the constructor used
        // Create mapped result object
        Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            // If there is an embedded query and deferred loading is turned on, a proxy object for the result object is created
            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;
                }
            }
        }
        // Determine whether to use the construction method to create the result object
        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
        return resultObject;
    }

Simplify the above code to find the key points

  // Create mapped result object
    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
         //Omit code
        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            // If there is an embedded query and deferred loading is turned on, a proxy object for the result object is created
            final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
            for (ResultMapping propertyMapping : propertyMappings) {
                // issue gcode #109 && issue #149
                //If lazy load configuration is currently enabled
                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                    //Create proxy object
                    resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                    break;
                }
            }
        }
        // Determine whether to use the construction method to create the result object
        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
        return resultObject;
    }

In fact, when you locate here, you remove some clouds. In the process of creating the mapped result object, you will judge whether the delayed loading configuration is enabled. If the delayed loading configuration is enabled, you will create a proxy object and return the results.

So how does the program know whether the deferred loading configuration is enabled?

Through the ResultMapping#lazy method

Result mapping is the mapping of each result field

Why create a proxy object?

Here we make a guess. In the unit test in Chapter 1.3, in order to verify the delayed loading function.

    /*
    Test deferred load query
   */
@Test
public void TEST_QUERY_LAZY()throws IOException{
        InputStream inputStream=Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory factory=new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession=factory.openSession();
        User user=sqlSession.selectOne("com.mryan.mapper.IUserMapper.findById",1);
        //The following output statement does not involve the orders table, so the orders related log will not be printed
        System.out.println("user: "+user.getUsername());
        //Relevant SQL statements will be executed only when the orders table is involved in the delayed loading effect, and the orders execution log will be loaded (when to use the delayed loading and when to check)
        System.out.println("orders: "+user.getOrderList());
        }

After the sqlSession.selectOne statement is executed, if a proxy object is created to dynamically judge whether the geting method of the proxy object is a local attribute requiring delayed loading, for example, getorderlist is determined that the delayed loading configuration is effective and enters the proxy object interceptor method, then the query statement saved by the implementation will be sent separately to query the orderList, Then set orderList through reflection, and then complete the call to getorderlist. In fact, we guess the basic principle of delayed loading,

To summarize the conjecture: delayed loading is realized through dynamic agent, and the agent intercepts the specified getting method to perform data loading.

So how does MyBatis handle it? As follows:

There are two implementations of MyBatis proxy factory interface. Javassits mode is used by default. The specific implementation is as follows:

/**
 * Agent factory interface
 *
 * @author Eduardo Macarron
 */
public interface ProxyFactory {

    // Set the property. It is currently an empty implementation. Skip it temporarily
    void setProperties(Properties properties);

    // Create proxy object
    Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);

}
/**
 * Javassist based ProxyFactory implementation class
 *
 * @author Eduardo Macarron
 */
@SuppressWarnings("Duplicates")
public class JavassistProxyFactory implements org.apache.ibatis.executor.loader.ProxyFactory {

 
@Override
    public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
    }

}

The interception method of proxy class is as follows (irrelevant code is omitted for easy reading):

 @Override
        public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
                        if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
                            // Load all deferred loaded properties
                            if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                                lazyLoader.loadAll();
                            // If the setting method is called, deferred loading is no longer used
                            } else if (PropertyNamer.isSetter(methodName)) {
                                final String property = PropertyNamer.methodToProperty(methodName);
                                lazyLoader.remove(property); // remove
                            // If the getting method is called, a deferred load is performed
                            } else if (PropertyNamer.isGetter(methodName)) {
                                final String property = PropertyNamer.methodToProperty(methodName);
                                if (lazyLoader.hasLoader(property)) {
                                    lazyLoader.load(property);
                                }
                            }
                        }
                    }
                }
                // Continue with the original method
                return methodProxy.invoke(enhanced, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }

In the interception method, judge whether the currently executed method has the delayed loading attribute. If the delayed loading is enabled and the getting method is used, execute the load loading method. If not, continue to execute the original method logic.

In fact, the load method is to execute an SQL query method and set the result into the proxy object

   public void load(final Object userObject) throws SQLException {
            if (this.metaResultObject == null || this.resultLoader == null) {
                //Omit code
                // Get Configuration object
                final Configuration config = this.getConfiguration();
                // Get MappedStatement object
                final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
                // Get the corresponding MetaObject object
                this.metaResultObject = config.newMetaObject(userObject);
                // Create ResultLoader object
                this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
                        metaResultObject.getSetterType(this.property), null, null);
            }
					//Key code!! resultLoader.loadResult();
            this.metaResultObject.setValue(property, this.resultLoader.loadResult());
        }

The key code resultLoader.loadResult() actively calls the query method once and extracts the results.

    /**
     * Loading results
     *
     * @return result
     */
    public Object loadResult() throws SQLException {
        // Query results
        List<Object> list = selectList();
        // Extraction results
        resultObject = resultExtractor.extractObjectFromList(list, targetType);
        // Return results
        return resultObject;
    }

Finally, set the loading result to the proxy object through the setValue method.

At this point, the whole process of delayed loading is over.

3. Summary

In fact, it is consistent with our previous conjecture,

Delayed loading is mainly realized through the dynamic agent mode. The agent intercepts the specified methods, so as to use and check them now, set the results to the agent object, return and execute data loading.

Sprinkle flowers 🌹🌹🌹🌹 But it's not over. As an excellent framework, MyBatis uses quite a lot of design patterns, which are quite beautiful. In the next article, let's go into the design pattern of MyBatis.

Notice

Next article: 🏆 MyBatis design pattern

This article has been included in CodeWars series. Welcome Star to continuously output high-quality technical articles
Link me!

More technical articles, please pay attention to the official account, let's make progress together.

Tags: Java Mybatis source code

Posted on Mon, 20 Sep 2021 13:04:14 -0400 by qaokpl