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

Posted 木兮君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精通Mybatis之结果集处理流程与映射体系(重点mybatis嵌套子查询,循环依赖解决方案)相关的知识,希望对你有一定的参考价值。

前言

大家五一快乐啊,上次小编写了映射体系一,具体讲了MetaObject反射工具的使用以及源码解释,接下来讲一下真正的映射体系。

手动 自动映射

手动映射配置

xml

 <!-- 复合映射-->
    <resultMap id="baseMap" type="entity.Company">
        <id property="id" column="id"/>
        <result property="companyName" column="company_name" jdbcType="VARCHAR"/>
        <association property="legalPerson" column="id">
            <id property="id" column="id"/>
            <result property="name" column="name"/>
        </association>
    </resultMap>

    <!-- 外部映射-->
    <resultMap id="baseMap1" type="entity.Company">
        <id property="id" column="id"/>
        <result property="companyName" column="company_name" jdbcType="VARCHAR"/>
        <association property="legalPerson" column="id" resultMap="legalPersonMap">
        </association>
    </resultMap>

    <resultMap id="legalPersonMap" type="entity.LegalPerson">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>
    <!--    嵌套查询-->
    <resultMap id="baseMap2" type="entity.Company">
        <id property="id" column="id"/>
        <result property="companyName" column="company_name" jdbcType="VARCHAR"/>
        <association property="legalPerson" column="id" select="selectByCompanyId"/>
    </resultMap>

    <select id="selectByCompanyId" resultType="entity.LegalPerson"/>

这个比较基础,当然也可以使用java代码,小伙伴自行研究啊。
一个ResultMap 中包含多个ResultMapping 表示一个具体的JAVA属性到列的映射,其主要值如下:

result id属性
property类属性名(必填)
column数据库列名(必填)
jdbcTypejdbc类型(自动推导)
javaTypejava类型(自动推导)
TypeHandler类型处理器(自动推导)

ResultMapping 有多种表现形式如下:

  1. constructor:构建参数字段
  2. id:ID字段
  3. result:普通结构集字段
  4. association:1对1关联字段
  5. collection:1对多集合关联字段

上面手动映射图如下:
在这里插入图片描述

自动映射配置

    <resultMap id="baseMap" type="entity.Company" autoMapping="true">       
    </resultMap>

自动映射条件

  1. 列名和属性名同时存在(勿略大小写)
  2. 当前列未手动设置映射
  3. 属性类别存在TypeHandler
  4. 开启autoMapping (默认开启)

自动映射图如下:
在这里插入图片描述

嵌套子查询

小编用上面的示例写一个简单测试代码这里用了lombok:
类测试类以及接口代码:

public interface CompanyMapper {

     Company selectById(@Param("id")Long id);
}
@Data
public class Company {

    private Long id;

    private String companyName;

    private LegalPerson legalPerson;
    private List<Department> departmentList;

}
@Data
public class LegalPerson {

    private Long id;

    private String name;
    
    private Long companyId;
}

@Data
public class Department {

    private Long id;

    private String departmentName;

    private Long companyId;

    private List<Employee> employeeList;
}
@Test
    public void associationTest() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectById(1L);
            System.out.println(company.getLegalPerson());
        }
    }

xml配置:

 <resultMap id="CompanyMap" type="entity.Company">
        <id property="id" column="id"/>
        <result property="companyName" column="company_name" jdbcType="VARCHAR"/>
        <association property="legalPerson" column="id" select="selectByCompanyId"/>
        <collection property="departmentList" column="id" select="selectDepartByCompanyId"/>
    </resultMap>

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

    <select id="selectByCompanyId" resultMap="LegalPersonMap">
        select * from legal_person where company_id = #{companyId}
    </select>
    <select id="selectDepartByCompanyId" resultMap="DepartmentMap">
        select * from department where company_id = #{companyId}
    </select>
    <select id="selectById" resultMap="CompanyMap"  parameterType="java.lang.Long">
        select * from company where id = #{id}
    </select>

提问:当这里需要传递多个参数时该怎么写?
这里小编写伪xml column = “companyId =id,name=companyName” ,然后下面就可以company_id=#{companyId} and name = #{name}

上面小编为什么写这样的示例呢?其实小编想说明一个问题,我们来讨论一个这样的问题:假设我们的法人有一家公司,然后公司下面的法人又是自己,那当我们查询法人的时候要填充公司属性,但公司填充的时候又查询到这个法人,那这样不就死循环了吗?那mybatis到底会不会出现死循环呢

答案:当然不会死循环了(不信大家自己试一下,小编已经试过了),其实mybatis作者也想到了这个问题,spring中是不是也会有循环依赖的问题,那mybatis是如何解决的呢,那下面小编继续讲解其中的原理。

循环依赖

循环依赖流程图:
在这里插入图片描述
根据上面的流程图,小编带大家看一下源码,大家也可以自己打断点调试。在上一篇博客中,封装行对象的时候用到DefaultResultSetHandler#getRowValue方法,里面用到了applyPropertyMappings方法,因为是嵌套查询的最终会用到getPropertyMappingValue方法:

private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
      
    if (propertyMapping.getNestedQueryId() != null) {
      //是否是嵌套查询 这个方法就开始和上面流程图差不多了
      return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    } else if (propertyMapping.getResultSet() != null) {
      addPendingChildRelation(rs, metaResultObject, propertyMapping);   // TODO is that OK?
      return DEFERRED;
    } else {
       //直接获取值
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      return typeHandler.getResult(rs, column);
    }
  }

开始嵌套查询

private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    //准备参数 ,获取mappedStatement,动态sql 准备换成key
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    final String property = propertyMapping.getProperty();
    final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
    final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
    final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
    Object value = null;
    if (nestedQueryParameterObject != null) {
      final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
      final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
      final Class<?> 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);
        if (propertyMapping.isLazy()) {
          //看是否懒加载
          lazyLoader.addLoader(property, metaResultObject, resultLoader);
          value = DEFERRED;
        } else {
         //否则直接查询
          value = resultLoader.loadResult();
        }
      }
    }
    return value;
  }

延迟加载是在主查询结束后再将属性值填充进去。其具体实现在baseExecutor

 @Override
  public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    DeferredLoad deferredLoad = new DeferredLoad(resultObject, property, key, localCache, configuration, targetType);
    //是否可延迟加载
    if (deferredLoad.canLoad()) {
      //可以直接延迟加载
      deferredLoad.load();
    } else {
      //需要延迟加载的放入deferredLoads 这里就是法人下面查找的公司
      deferredLoads.add(new DeferredLoad(resultObject, property, key, localCache, configuration, targetType));
    }
  }
public boolean canLoad() {
	 //本地缓存有且不是EXECUTION_PLACEHOLDER值,在第一次查询的时候会有占位符
      return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
    }

    public void load() {
      @SuppressWarnings("unchecked")
      // we suppose we get back a List
      //然后直接从key拿值,然后赋值即可
      List<Object> list = (List<Object>) localCache.getObject(key);
      Object value = resultExtractor.extractObjectFromList(list, targetType);
      resultObject.setValue(property, value);
    }

上面的流程图和源代码已经对应起来了,但是大家可能还没明白mybatis怎么解决循环依赖的。
首先先说结论,第一使用一级缓存,第二个使用延迟加载(这里也间接说明了一级缓存是不能关闭的),这边还用了一个queryStack参数和缓存占位符,
下面咱们再次捋一下
1、查询公司 queryStack=0
2、公司下的法人 queryStack=1
3、法人下的公司 queryStack=2
其中第一次查询将自己设置缓存和第三次查询时是同一家公司,所以会走一级缓存,不会重复查询数据库,接着将自己放入到延迟加载集合中,回到主查询,加载延迟加载中的属性值即可。

小编完整说明一下循环依赖的解决:
再次贴一下代码BaseExecutor代码:

 @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) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
     //查询一次 加一次查询堆
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      //查询完毕 减一次查询堆
      queryStack--;
    }
    //当查询到0的时候就是返回主查询,进行延迟装载
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      //装载完延迟后清空延迟加载器
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //查询前放入占位符
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
    //查询并且赋值相应的参数值,会涉及上面的嵌套查询
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
  1. 第一次查询公司的时候:BaseExecutor.query方法中 queryStack++,并且设置一级缓存占位符;
  2. 在嵌套查询,即第二次查询法人的时候,因为不是懒加载则会再次BaseExecutor.query这个方法,queryStack++然后设置查询法人的一级缓存占位符
  3. 查询法人信息的时候又查询公司,即第三次查询公司有缓存了,因为还不能直接加载,则放入了deferredLoads
  4. 返回结果,先是法人查询填充属性完毕queryStack – ,删除法人一级缓存的占位符,将法人放入一级缓存中,之后是公司查询填充法人信息完毕queryStack – ,删除公司一级缓存占位符,将公司放入一级缓存中
  5. queryStack为0,执行延迟加载,里面有刚刚放入的一个,并且可以加载了(公司一级缓存不为空也不是占位符),延迟加载使用metaObject设置对应的值即可。

总结

今天主要讲了映射体系中的手动自动,以及嵌套子查询和循环依赖mybatis是如何解决的,还有懒加载和延迟加载是两回事情,希望小编讲得足够明白了。最近小编在讲解过程中留下来一些问题,主要是希望大家不仅仅是看看,更得实践,否则容易忘记,一起加油啊。接下来小编会讲解懒加载,大家继续关注吧

以上是关于精通Mybatis之结果集处理流程与映射体系(重点mybatis嵌套子查询,循环依赖解决方案)的主要内容,如果未能解决你的问题,请参考以下文章

精通Mybatis之结果集处理流程与映射体系(联合查询与嵌套映射)

MyBatis框架之第二篇

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

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

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

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