Delayed loading of mybatis source code analysis extension

1, What is deferred loading

In the development process, we do not always need to load the user's order information when loading the user's information. This is what we call delayed loading.

1. Definition of delayed loading

Delayed loading is to load when data is needed, and not when data is not needed. Lazy loading is also called lazy loading.

2. Advantages and disadvantages

1) Advantages:

Query from a single table first, and then associate the query from the associated table when necessary, which greatly improves the database performance, because querying a single table is faster than associating multiple tables.

2) Disadvantages:

Because the database query is only performed when the data is needed. In this way, in the case of mass data query, the query also consumes time, which may cause the user waiting time to change and the user experience to decline.

3. When will it be used

In multiple tables:
1) One to many, many to many: delay loading is usually adopted;
2) One to one (many to one): immediate loading is usually used.

4. Note: deferred loading is implemented based on nested queries.

2, How

1. Local delayed loading

There is a fetchType attribute in the association and collection tags. You can modify the local loading policy by modifying its value.

<!-- Enable one to many delayed loading -->
<resultMap id="userMap" type="user">
    <id column="id" property="id"></id>
    <result column="username" property="username"></result>
    <result column="password" property="password"></result>
    <result column="birthday" property="birthday"></result>
    <!--
        fetchType="lazy" Lazy loading strategy
        fetchType="eager" Load policy now
    -->
    <collection property="orderList" ofType="order" column="id" select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy">
    </collection>
</resultMap>
<select id="findAll" resultMap="userMap">
    select * from `user`
</select>

2. Global delayed loading

In the core configuration file of Mybatis, you can use the setting tag to modify the global loading policy.

<settings>
    <!-- Enable global delayed loading function -->
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>

be careful:
The local loading policy takes precedence over the global loading policy.

3. Set the method to trigger delayed loading

After configuring the delayed loading strategy, we found that even if no method of the associated object is called, the query of the associated object will be triggered when you call the equals, clone, hashCode and toString methods of the current object.
We can override the above four methods in the configuration file by using the lazyloadtrigger methods configuration item.

<settings>
    <!-- Enable global delayed loading function -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- All methods delay loading -->
    <setting name="lazyLoadTriggerMethods" value=""/>
</settings>

3, Delayed loading principle

1. Understanding description

The principle is actually very simple, that is, when analyzing the User's member variables, if there is a lazy loading configuration, such as fetchType = "lazy", the User will be converted into a proxy class and returned. And put the lazy load related objects into the ResultLoaderMap and save them. When the get method in the corresponding User object is called to obtain lazy load variables, sql acquisition is performed according to the proxy class.

Summary: delayed loading is mainly realized in the form of dynamic agent, which intercepts the specified method and performs data loading.

MyBatis delayed loading is mainly implemented by Javassist and Cglib. The class diagram shows:

2. Related code classes

Related properties in Configuration class:

public class Configuration {
    
    /**
     * aggressiveLazyLoading: 
     * When on, any method call will load all the properties of the object.
     * Otherwise, each attribute is loaded on demand (see lazyLoadTriggerMethods).
     * The default is true
     */
    protected boolean aggressiveLazyLoading;
    
    // Delayed loading trigger method
    protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
    
    // Enable deferred loading
    protected boolean lazyLoadingEnabled = false;
    
    /** 
     * Javassist proxy factory is used by default
     * @param proxyFactory
     */
    public void setProxyFactory(ProxyFactory proxyFactory) {
        if (proxyFactory == null) {
            proxyFactory = new JavassistProxyFactory();
        }
        this.proxyFactory = proxyFactory;
    }
    
    // Omit..
}

4, View analysis source code

1,DefaultResultSetHandler

The query results of Mybatis are processed by the handleResultSets() method of the ResultSetHandler interface. There is only one implementation of the ResultSetHandler interface, DefaultResultSetHandler. Next, let's look at a core method related to delayed loading.

/**
 * Create result object
 * Tag call path: (all in DefaultResultSetHandler class)
 * handleResultSets -> handleResultSet -> handleRowValues -> 
 * handleRowValuesForNestedResultMap -> getRowValue -> createResultObject
 */
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, 
        String columnPrefix) throws SQLException {
    // reset previous mapping result
    this.useConstructorMappings = false; 
    final List<Class<?>> constructorArgTypes = new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    // Create the real object of the returned result map
    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
            // Judge whether the attribute is configured with nested query. If so, create a proxy object
            if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                // Create a deferred load proxy object
                resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, 
                        objectFactory, constructorArgTypes, constructorArgs);
                break;
            }
        }
    }
    // set current mapping result
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); 
    return resultObject;
}

The default proxyFactory implementation class in configuration is JavassistProxyFactory, that is, javassistProxy is used to create proxy objects by default.

protected ProxyFactory proxyFactory = new JavassistProxyFactory();

2. JavasisstProxyFactory implementation

public class JavassistProxyFactory implements org.apache.ibatis.executor.loader.ProxyFactory {

    /**
     * Interface implementation
     * @param target Target result object
     * @param lazyLoader Delay loading objects
     * @param configuration to configure
     * @param objectFactory Object factory
     * @param constructorArgTypes Construction parameter type
     * @param constructorArgs Construction parameter value
     * @return 
     */
    @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);
    }
    
    // Omit..

    // Proxy object implementation, core logic execution
    private static class EnhancedResultObjectProxyImpl implements MethodHandler {

        public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, 
                ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
            final Class<?> type = target.getClass();
            EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, 
                    objectFactory, constructorArgTypes, constructorArgs);
            Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
            PropertyCopier.copyBeanProperties(type, target, enhanced);
            return enhanced;
        }
        
        static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, 
                List<Object> constructorArgs) {
            
            ProxyFactory enhancer = new ProxyFactory();
            enhancer.setSuperclass(type);
            
            try {
                // Determine whether the method exists by obtaining the object method
                type.getDeclaredMethod(WRITE_REPLACE_METHOD);
                // ObjectOutputStream will call writeReplace of objects returned by writeReplace
                if (LogHolder.log.isDebugEnabled()) {
                    LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
                }
            } catch (NoSuchMethodException e) {
                // The method is not found, implement the interface
                enhancer.setInterfaces(new Class[] { WriteReplaceInterface.class });
            } catch (SecurityException e) {
                // nothing to do here
            }
            
            Object enhanced;
            Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
            Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
            try {
                // Create a new proxy object
                enhanced = enhancer.create(typesArray, valuesArray);
            } catch (Exception e) {
                throw new ExecutorException("Error creating lazy proxy.  Cause: " + e, e);
            }
            // Set up agent executor
            ((Proxy) enhanced).setHandler(callback);
            return enhanced;
        }

        /** 
         * Proxy object execution
         * @param enhanced Original object
         * @param method Original object method
         * @param methodProxy Proxy method
         * @param args Method parameters
         * @return 
         * @throws Throwable 
         */
        @Override
        public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
            final String methodName = method.getName();
            try {
                synchronized (lazyLoader) {
                    if (WRITE_REPLACE_METHOD.equals(methodName)) {
                        // Ignore, no specific role found
                        Object original;
                        if (constructorArgTypes.isEmpty()) {
                            original = objectFactory.create(type);
                        } else {
                            original = objectFactory.create(type, constructorArgTypes, constructorArgs);
                        }
                        PropertyCopier.copyBeanProperties(type, enhanced, original);
                        if (lazyLoader.size() > 0) {
                            return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, 
                                    constructorArgTypes, constructorArgs);
                        } else {
                            return original;
                        }
                    } else {
                        // The number of delayed loads is greater than 0
                        if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
                            // aggressive one-time loadability requires all deferred load attributes or contains methods that trigger deferred load
                            if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                                // Load all at once
                                lazyLoader.loadAll();
                            } else if (PropertyNamer.isSetter(methodName)) {
                                // Judge whether it is a set method. The set method does not need to delay loading
                                final String property = PropertyNamer.methodToProperty(methodName);
                                lazyLoader.remove(property);
                            } else if (PropertyNamer.isGetter(methodName)) {
                                final String property = PropertyNamer.methodToProperty(methodName);
                                if (lazyLoader.hasLoader(property)) {
                                    // Delay loading a single attribute
                                    lazyLoader.load(property);
                                }
                            }
                        }
                    }
                }
                return methodProxy.invoke(enhanced, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
    }
}

5, Precautions

1. IDEA debugging problem

When aggressiveLazyLoading = true is configured, when using IDEA for debugging, if the breakpoint hits the agent execution logic, you will find that the code loaded late can never enter and will always be executed in advance. The main reason is aggressiveLazyLoading, because the method of delaying loading objects has been triggered in the debugger form of IDEA during debugging.

Article content output source: pull hook education Java high salary training camp;

Keywords: Java Mybatis

Added by chaser7016 on Thu, 20 Jan 2022 02:06:00 +0200