Mybatis的循环引用


Mybatis中对循环引用及关联查询都做了很好的处理。博主觉得这块非常难。这里只是把我知道的讲出来。

循环引用

什么循环引用博主这里就不介绍了。网上一大堆例子。熟悉Spring的都知道Spring中也有循环引用。Mybatis循环引用和Spring中的循环引用一样。解决办法也基本相同。

框架解决方法相同点不同点
Mybatis空占位符、延迟加载(放在一级缓存中)居于缓存解决mybatis只用了一个缓存、并且发现循环引用是使用了延迟加载的方法
SpringIoc暴露创建对象工厂基于缓存解决SpringIoc用了三级缓存,发现循环引用直直接通过synchronized同步阻塞

Mybatis中利用的是空占位符,和延迟加载来处理循环依赖。

数据准备

// 作者信息表,book_id为书本Id
CREATE TABLE `author` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(50) NOT NULL COMMENT '用户名',
  `book_id` bigint(20) NOT NULL COMMENT 'book_id',
  `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1262313675412185119 DEFAULT CHARSET=utf8 COMMENT='作者表';
// Author对应的Java对象
public class Author {
    private Long id;
    private String  name;
    private Book book;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }
}
// 书籍信息表,其中author_id是author表中的Id,author表和book相互依赖

CREATE TABLE `book` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(50) NOT NULL COMMENT '用户名',
  `author_id` bigint(20) NOT NULL COMMENT 'author_id',
  `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1262313675412185119 DEFAULT CHARSET=utf8 COMMENT='书籍表';
// Book对应的java类
public class Book {

    private Long id;

    private String name;

    private Author author;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }
}
// Mapper.java文件
public interface AuthorMapper {

    public Author selectAuthorByid(Long id);

    public Book selectBookByid();
}

// XML脚本文件下面着重解释下这个文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wwl.mybatis.dao.AuthorMapper">
    //查询author表的映射信息
    <resultMap id="authorMap" type="com.wwl.mybatis.dao.Author">
        <result column="id" property="id"></result>
        <result column="name" property="name"></result>
        //com.wwl.mybatis.dao.Author对象信息在的book属性需要通过book_id作为参数来查询selectBookByid来获取book信息
        <collection property="book" column="book_id" select="selectBookByid" fetchType="eager"></collection>
    </resultMap>
    
    <resultMap id="bookMap" type="com.wwl.mybatis.dao.Book">
        <result column="id" property="id"></result>
        <result column="name" property="name"></result>
        //com.wwl.mybatis.dao.Book对象信息在的author属性需要通过author_id作为参数来查询selectAuthorByid来获取author信息
        //这里和前面查询产生了循环依赖的问题。
        <collection property="author" column="author_id" select="selectAuthorByid" fetchType="eager"></collection>
    </resultMap>

   <select id="selectAuthorByid" resultMap="authorMap">
        select * from author where id = #{id}
   </select>

    <select id="selectBookByid" resultMap="bookMap">
        select * from  book where id = id = #{id}
    </select>
// 测试循环依赖的展示
@Test
    public void test_author(){
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = factoryBuilder.build(ExecutorTest.class.getResourceAsStream("/mybatis-config.xml"));
        SqlSession sqlSession = factory.openSession();
        AuthorMapper authorMapper = sqlSession.getMapper(AuthorMapper.class);
        Author author = authorMapper.selectAuthorByid(1L);
        System.out.println("authorName:"+author.getName());
        System.out.println("author.bookeName:"+author.getBook().getName());
    }

下面我们开看现象,实际操作过程中并未出现死循环。
我们先看下直接结果。

// 我们可以很清晰的看到在实际的查询中只查了2次,并未出现死循环。
14:55:51,393 DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction:54 - Opening JDBC Connection
14:55:53,262 DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource:54 - Created connection 608519258.
14:55:53,263 DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction:54 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2445445a]
14:55:53,313 DEBUG com.wwl.mybatis.dao.AuthorMapper.selectAuthorByid:54 - ==>  Preparing: select * from author where id = 1 
14:55:53,415 DEBUG com.wwl.mybatis.dao.AuthorMapper.selectAuthorByid:54 - ==> Parameters: 
15:02:15,370 DEBUG com.wwl.mybatis.dao.AuthorMapper.selectBookByid:54 - ====>  Preparing: select * from book where id = 1 
15:02:15,373 DEBUG com.wwl.mybatis.dao.AuthorMapper.selectBookByid:54 - ====> Parameters: 
15:06:41,604 DEBUG com.wwl.mybatis.dao.AuthorMapper.selectBookByid:54 - <====      Total: 1
15:06:42,431 DEBUG com.wwl.mybatis.dao.AuthorMapper.selectAuthorByid:54 - <==      Total: 1
authorName:作者1
author.bookeName:book1

我们来看下运行时数据。
在这里插入图片描述
从运行时候数据我们可以看到,虽然对象产生了循环依赖,但是依赖的对象都是同一个。

源码解析

前面博主讲过Mybatis用来处理结果集映射的时候是在DefaultResultSetHandler里面来完成的。基于上的实例我给大家画个流程图
Mybatis在对循环引用的处理主要也是在DefaultResultSetHandler进行,在DefaultResultSetHandler对循环引用进行判断然后调用ResultLoader对引用对象进行解析。下面给大家大致画了下循环引用的流程图。虽然复杂,但是一定要耐心看。
在这里插入图片描述
接下来给大家介绍下上面每一步做什么,配合着源码一起研究。下面序号对应上面流程图编号。这里中间简单流程会有些忽略。但不影响整体逻辑理解。
1.查询返回ResultSet,并交由DefaultResultSetHandler进行解析。
4.结果集解析,判断book属性是否为需要另外一个结果。

//1.这里是对普通结果集处理
//2.对结果集属性需要关联其他结果处理
//大家一定要更handleRowValuesForNestedResultMap方法进去看,看到getPropertyMappingValue方法才会到第五步。
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    //结果集中的属性book需要通过查询book表获取对应的结果集解析数据(即book对象)。
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      //处理需要引用的结果集。
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      //普通结果集处理
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

5.这里判断一级缓存是否有,有从一级缓存获取数据(book我们没有查询一级缓存没有),否则直接创建ResultLoader执行数据库查询。

// 一级缓存是否有需要关联的结果集,有直接从缓存中获取,否则重新查询数据。
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    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 = DEFERED;
      } else {
        //创建ResultLoader对象
        final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
        //是否为懒加载
        if (propertyMapping.isLazy()) {
          lazyLoader.addLoader(property, metaResultObject, resultLoader);
          value = DEFERED;
        } else {
          //通过ResultLoader的loadResult去获取结果属性。
          value = resultLoader.loadResult();
        }
      }
    }
    return value;
  }

我们这里来分析下ResultLoader的loadResult和selectList的代码。

// 加载关联的结果集
public Object loadResult() throws SQLException {
    //调用获取结果集方法
    List<Object> list = selectList();
    //解析返回的结果集
    resultObject = resultExtractor.extractObjectFromList(list, targetType);
    return resultObject;
  }
// An highlighted block
private <E> List<E> selectList() throws SQLException {
    Executor localExecutor = executor;
    //盘点当前线程Id和执行关联查询是否为同一个线程,如果不是直接创建一个新的执行器。
    if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
      localExecutor = newExecutor();
    }
    try {
      //执行器执行查询操作,这里最终会使用到调用DefaultResultSetHandler的handleResultSets来对结果集解析。
      //这里会解析到Book中的author属性,这里也是递归调用地方。上图8,9,10和2,3,4的步骤一致。这里就不过多赘述。
         return localExecutor.<E> query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
    } finally {
      if (localExecutor != executor) {
        localExecutor.close(false);
      }
    }
  }

11.我们看下上面的DefaultResultSetHandler的getNestedQueryMappingValue,之前已经查询过author对象,缓存中存在一个空的占位符。executor.isCached(nestedQuery, key)返回为true。故添加到deferredLoads列表中进行延期加载。
16.BaseExecutor进行懒加载处理。

//这里对延迟加载进行分析
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--;
    }
    //所有的查询都结束之后
    if (queryStack == 0) {
      //循环deferredLoads延迟载列表,给延迟加载对上赋值
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

总结

Mybatis循环引用主要是通过递归调用、一级缓存和延迟加载来解决。这里面的调用链路非常长。需要自己慢慢调试。我们学习主要是为了学校框架的解决思路。上图有问题可以和博主留言。咋们一起探讨。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值