资讯动态

【12.MyBatis源码剖析与架构实战】11.嵌套查询循环引⽤源码剖析

发布时间:2026/10/14 8:37:53 来源:尧图企业网站定制
MyBatis 嵌套查询循环引用源码深度剖析(含流程图)在 MyBatis 中,当两个实体相互引用(如User↔Address),且双方都通过association的select属性配置了嵌套查询时,若没有特殊处理,查询时会发生无限递归,最终导致栈溢出。MyBatis 通过一级缓存(localCache)和ResultLoaderMap加载状态标记巧妙地解决了循环引用问题,确保在同一个SqlSession内,同一个对象只创建一次,并且循环引用能够正确建立。一、循环引用示例考虑以下实体与映射:publicclassUser{privateLongid;privateStringname;privateAddressaddress;// 持有 Address}publicclassAddress{privateLongid;privateStringstreet;privateUseruser;// 持有 User}Mapper XML:!-- UserMapper.xml --resultMapid="userMap"type="User"idcolumn="id"property="id"/resultcolumn="name"property="name"/associationproperty="address"column="address_id"select="com.example.AddressMapper.selectById"fetchType="eager"//resultMap!-- AddressMapper.xml --resultMapid="addressMap"type="Address"idcolumn="id"property="id"/resultcolumn="street"property="street"/associationproperty="user"column="user_id"select="com.example.UserMapper.selectById"fetchType="eager"//resultMap当调用UserMapper.selectById(1)时,会触发以下调用链:查询User(id=1, address_id=100)加载User.address→ 执行AddressMapper.selectById(100)查询Address(id=100, user_id=1)加载Address.user→ 执行UserMapper.selectById(1),回到步骤1若不加控制,将无限循环。二、MyBatis 解决循环引用的核心机制2.1 一级缓存(localCache)每个SqlSession有一个PerpetualCache实例(存储在BaseExecutor.localCache)。当从ResultSet构建一个对象时,会根据主键生成CacheKey,并在构建完成后放入localCache。后续任何查询(包括嵌套查询)在构建相同主键的对象时,会优先从localCache中获取,而不是重新创建。2.2createRowKey:生成对象缓存键DefaultResultSetHandler.createRowKey方法根据ResultMap中配置的id列(或如果无显式 id,则使用所有列),从当前ResultSet行提取值,生成唯一的CacheKey。privateCacheKeycreateRowKey(ResultMapresultMap,ResultSetWrapperrsw,StringcolumnPrefix)throwsSQLException{finalCacheKeycacheKey=newCacheKey();finalListResultMappingidMappings=resultMap.getIdResultMappings();for(ResultMappingidMapping:idMappings){finalStringcolumn=prependPrefix(idMapping.getColumn(),columnPrefix);if(column!=null){finalObjectvalue=getColumnValue(rsw,idMapping.getJavaType(),column);cacheKey.update(column);cacheKey.update(value);}}returncacheKey;}2.3getRowValue中的缓存复用在DefaultResultSetHandler.getRowValue中,会首先尝试从localCache获取已创建的对象:privateObjectgetRowValue(ResultSetWrapperrsw,ResultMapresultMap,StringcolumnPrefix,ObjectpartialValue,ResultMappingparentMapping)throwsSQLException{finalStringresultMapId=resultMap.getId();finalCacheKeyrowKey=createRowKey(resultMap,rsw,columnPrefix);// 尝试从一级缓存获取ObjectrowValue=partialValue!=null?partialValue:localCache.getObject(rowKey);if(rowValue!=nullrowValue!=EXECUTION_PLACEHOLDER){// 如果已经存在,直接返回(避免重复创建)returnrowValue;}// 否则创建对象并放入缓存...rowValue=createResultObject(rsw,resultMap,lazyLoader,columnPrefix);if(rowKey!=null){localCache.putObject(rowKey,rowValue);}// 填充属性...returnrowValue;}2.4ResultLoaderMap防止递归加载对于延迟加载的属性,ResultLoaderMap会在加载前移除对应的LoadPair,因此同一属性不会重复触发加载。但对于eager加载(立即加载)的情况,ResultLoaderMap不起作用,主要依赖一级缓存。三、循环引用解决流程(以 eager 为例)结合上述机制,循环引用的完整解决流程如下(

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑