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