精通Mybatis之结果集处理流程与映射体系(无死角懒加载讲解)

Posted 木兮君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精通Mybatis之结果集处理流程与映射体系(无死角懒加载讲解)相关的知识,希望对你有一定的参考价值。

前言

开始上班了,小编继上篇博客精通Mybatis之结果集处理流程与映射体系(重点mybatis嵌套子查询,循环依赖解决方案)(二),今天讲结果集处理与映射的倒数第二篇,本来以为三篇能结束的结果发现不够用了,先讲懒加载吧。话不多说进入主题。

懒加载

懒加载是为改善解析对象属性时,大量的嵌套子查询的并发问题。设置懒加载后,只有在使用指定属性时才会加载,从而分散SQL请求。mybatis懒加载原理是基于动态代理实现的。
基本配置以及代码示例(小编依旧使用公司下多个部门的嵌套查询,设置部门查询的时候使用懒加载):

<resultMap id="DepartmentMap" type="entity.Department">
        <id property="id" column="id"/>
        <result property="companyId" column="company_id" />
        <result property="departmentName" column="department_name"/>
    </resultMap>

<resultMap id="CompanyMap2" type="entity.Company">
        <id property="id" column="id"/>
        <result property="companyName" column="company_name" jdbcType="VARCHAR"/>
        <collection property="departmentList" column="id" select="selectDepartmentByCompanyId" fetchType="lazy"/>
    </resultMap>
    
 <select id="selectDepartmentByCompanyId" resultMap="DepartmentMap">
        select * from department where company_id = #{companyId}
    </select>

    <select id="selectCompanyById" resultMap="CompanyMap2"  parameterType="java.lang.Long">
        select * from company where id = #{id}
    </select>

测试代码:

public class LazyLoadTest {

    SqlSessionFactory sqlSessionFactory;
    Configuration configuration;

    @Before
    public void init() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        configuration = sqlSessionFactory.getConfiguration();
        configuration.setLazyLoadTriggerMethods(new HashSet<>());
    }

    @Test
    public void collectionLazyTest() {
        try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectCompanyById(1L);
            System.out.println(company.getDepartmentList().size());
        }
    }
}

好了小编根据示例先讲一下懒加载的配置和细节:

  1. 在嵌套子查询中指定 fetchType=“lazy” 即可设置懒加载。在调用company.getDepartmentList时才会真正加载。此外调用company的:“equals”, “clone”, “hashCode”, “toString” 均会触发当前对象所有未执行的懒加载。(“equals”, “clone”, “hashCode”, “toString” 这些方法是在configuration中lazyLoadTriggerMethods的属性,类型为Set,上面小编为了不让company直接懒加载[因为debug下会执行toString方法]从而设置了空hashSet)
  2. 通过设置全局参数aggressiveLazyLoading=true ,也可指定调用对象任意方法触发所有懒加载。
参数描述
lazyLoadingEnabled全局懒加载开关 默认false
aggressiveLazyLoading任意方法触发加载 默认false。
fetchType加载方式 eager实时 lazy懒加载。默认eager
  1. set方法覆盖,当调用setXXX方法手动设置属性之后,对应的属性懒加载将会被移除,不会覆盖手动设置的值。
  2. 当对象经过序列化和反序列化之后,默认不在支持懒加载。但如果在全局参数中设置了configurationFactory类,而且采用JAVA原生序列化是可以正常执行懒加载的。其原理是将懒加载所需参数以及配置一起进行序列化,反序列化后在通过configurationFactory获取configuration构建执行环境。需要配置:
public static class ConfigurationFactory {       
	public static Configuration getConfiguration() {        
 		return configuration; } 
}

第三与第四点小编通过代码再次说明一下:
set方法覆盖

	//set方法覆盖
 	@Test
    public void collectionLazySetTest() {
        try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectCompanyById(1L);
            company.setDepartmentList(new ArrayList<>());
            System.out.println(company.getDepartmentList().size());
        }
    }

测试结果:
set方法覆盖
在这里插入图片描述
没set方法覆盖:
在这里插入图片描述
对象序列化反序列化

	@Test
    public void collectionLazySerializableTest() throws Exception{
        try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectCompanyById(1L);
            byte[] bytes = writeObject(company);
            Company companySer = (Company)readObject(bytes);
            System.out.println(companySer.getDepartmentList());
        }
    }

    private  byte[] writeObject(Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(out);
        outputStream.writeObject(obj);
        return out.toByteArray();
    }

    private  Object readObject(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        ObjectInputStream inputStream = new ObjectInputStream(in);
        return inputStream.readObject();
    }

测试结果:
在这里插入图片描述

修改完毕后:

    <settings>
    	//这里是内部静太类用$标识
        <setting name="configurationFactory" value="test.LazyLoadTest$ConfigurationFactory"/>
    </settings>
@Test
 public void collectionLazySerializableTest() throws Exception{
        try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectCompanyById(1L);
            byte[] bytes = writeObject(company);
            Company companySer = (Company)readObject(bytes);
            System.out.println(companySer.getDepartmentList());
        }
    }
public static class ConfigurationFactory {
        public static Configuration getConfiguration() {
            return configuration; }
    }

测试结果:
在这里插入图片描述

懒加载原理及内部结构

讲完了懒加载的结论,那小编肯定得证明原理,首先根据上面的示例代码,查看一下company的结构:
在这里插入图片描述
代理之后Bean会包含一个MethodHandler,内部在包含一个Map用于存放待执行懒加载,执行前懒加载前会移除。LoadPair用于针对反序列化的Bean准备执行环境。ResultLoader用于执行加载操作,执行前原执行器如果关闭会创建一个新的执行器(这里会引出一个小编之前没有讲解的Executor => ClosedExecutor)。特定属性如果加载失败,不会在进行二次加载。
内部结构图以及执行流程:
在这里插入图片描述
代理bean的创建过程
在这里插入图片描述
代理过程发生在结果集解析,创建对象之后(DefaultResultSetHandler.createResultObject),如果对应的属性设置了懒加载,则会通过ProxyFactory 创建代理对象,该对象继承自原对象,然后将对象的值全部拷贝到代理对像。并设置相应MethodHandler(原对象直接抛弃)

懒加载源码阅读

接下来小编带大家来一起阅读一下源码:
先是代理对象的创建:
DefaultResultSetHandler#createResultObject方法

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
   ...
    //创建company对象
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
      //三个映射 ,再department的时候为子查询且是懒加载
      for (ResultMapping propertyMapping : propertyMappings) {
        // issue gcode #109 && issue #149
        if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
          //对company对象进行代理
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
  }

JavassistProxyFactory#createProxy方法

public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      final Class<?> type = target.getClass();
      //创建EnhancedResultObjectProxyImpl 
      EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
      //根据EnhancedResultObjectProxyImpl 创建compay的代理对象
      Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
      //之后是将原对象的属性copy进入代理对象
      PropertyCopier.copyBeanProperties(type, target, enhanced);
      return enhanced;
    }

创建完代理对象后,然后获取:
JavassistProxyFactory#invoke方法

@Override
    public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
     //原对象的method方法
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
         //不会进这儿
          if (WRITE_REPLACE_METHOD.equals(methodName)) {
            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 {
            //懒加载大于0并且不是finalize方法
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
              //是否任意方法就加载或者在"equals", "clone", "hashCode", "toString"方法里面
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                lazyLoader.loadAll();
                //如果是set方法 则将懒加载移除,直接赋值
              } else if (PropertyNamer.isSetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                lazyLoader.remove(property);
                //为get方法,根据属性名称获取懒加载器有就加载
              } else if (PropertyNamer.isGetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                if (lazyLoader.hasLoader(property)) {
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        return methodProxy.invoke(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

ResultLoaderMap#load

public boolean load(String property) throws SQLException {
	//取出LoadPair ,并且移除懒加载属性(懒加载之前所以如果pair.load()失败则不会进行二次加载)
    LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
    if (pair != null) {
     //开始加载
      pair.load();
      return true;
    }
    return false;
  }

ResultLoaderMap$LoadPair#load

public void load(final Object userObject) throws SQLException {
      if (this.metaResultObject == null || this.resultLoader == null) {
        if (this.mappedParameter == null) {
          throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
                  + "required parameter of mapped statement ["
                  + this.mappedStatement + "] is not serializable.");
        }

        final Configuration config = this.getConfiguration();
        final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
        if (ms == null) {
          throw new ExecutorException("Cannot lazy load property [" + this.property
                  + "] of deserialized object [" + userObject.getClass()
                  + "] because configuration does not contain statement ["
                  + this.mappedStatement + "]");
        }

        this.metaResultObject = config.newMetaObject(userObject);
        //这里有个ClosedExecutor 继承BaseExecutor,只是将isClosed设置为true
        this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
                metaResultObject.getSetterType(this.property), null, null);
      }

      /* We are using a new executor because we may be (and likely are) on a new thread
       * and executors aren't thread safe. (Is this sufficient?)
       *
       * A better approach would be making executors thread safe. */
      //这段代码有点多余,因为上面已经赋值了,即使进去也是一样的
      if (this.serializationCheck == null) {
        final ResultLoader old = this.resultLoader;
        this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
                old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
      }
	 //设置属性值
	 //this.resultLoader.loadResult()
      this.metaResultObject.setValue(property, this.resultLoader.loadResult());
    }

ResultLoader#selectList

public Object loadResult() throws SQLException {
	//通过executor 直接查询库
    List<Object> list = selectList();
    //设置值
    resultObject = resultExtractor.extractObjectFromList(list, targetType);
    return resultObject;
  }

  private <E> List<E> selectList() throws SQLException {
    Executor localExecutor = executor;
    if (Thread.currentThread(以上是关于精通Mybatis之结果集处理流程与映射体系(无死角懒加载讲解)的主要内容,如果未能解决你的问题,请参考以下文章

精通Mybatis之结果集处理流程与映射体系(重点mybatis嵌套子查询,循环依赖解决方案)

MyBatis从入门到精通:MyBatis高级结果映射之一对多映射

Mybatis从入门到精通系列 15——嵌套查询与嵌套结果

精通Mybatis之插件体系(与中间件实现的一些思考)

精通Mybatis之插件体系(与中间件实现的一些思考)

mybatis从入门到精通 结果映射