用凡科帮别人做网站,哪个网站做自行车评测的,南通建设网站哪家好,详情图模板大家好#xff0c;我是王有志#xff0c;一个分享硬核 Java 技术的金融摸鱼侠#xff0c;欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。
在学习完上一篇文章《MyBatis映射器#xff1a;一对一关联查询》后#xff0c;相信你已经掌握了如何在 MyBatis 映射器…大家好我是王有志一个分享硬核 Java 技术的金融摸鱼侠欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。
在学习完上一篇文章《MyBatis映射器一对一关联查询》后相信你已经掌握了如何在 MyBatis 映射器中实现一对一关联查询。那么今天我们就趁热打铁来学习如何在 MyBatis 映射器中使用 resultMap 元素实现一对多关联查询。
数据库中的一对多关联查询
实现了查询用户订单及支付订单信息之后老板提出了新的想法“订单明细也得加进去”。于是你开始在背后蛐蛐老板它就不能一次性把所有需求都提出来吗但是蛐蛐归蛐蛐活还是得干的。
订单信息中添加查询订单明细的需求也很简单无非是连个表的事情这有什么难的说干就干于是你很快就写完了 SQL 语句
select uo.order_id,uo.user_id,uo.order_no,uo.order_price,uo.order_status,uo.create_date,uo.pay_date,oi.item_id,oi.order_id,oi.commodity_id,oi.commodity_price,oi.commodity_countfrom user_order uo, order_item oiwhere uo.order_id oi.order_idand uo.order_no D202405082208045788;但是执行完 SQL 语句之后你有点懵了数据库的查询结果给出了 3 条数据这与我们设想的一条订单信息带 3 条订单明细也不一样啊 难道必须要分步查询了吗
高阶用法使用 collection 元素实现一对多关联查询
于是你想到是不是还可以使用 resultMap 元素来实现这种一对多的关联查询呢终于在查阅了相关资料之后你发现了 reusltMap 元素的子元素 collection 元素似乎可以解决这个问题。
首先我们来修改 UserOrderDO为其组合上订单明细信息如下
public class UserOrderDO {// 省略 UserOrderDO 自身的字段/*** 支付订单信息*/private PayOrderDO payOrder;/*** 用户订单明细*/private ListOrderItemDO orderItems;
}接着我们使用 resultMap 元素编写新的映射规则“userOrderContainOrderItemMap”如下
resultMap iduserOrderContainOrderItemMap typecom.wyz.entity.UserOrderDO extendsBaseResultMapcollection propertyorderItems javaTypejava.util.ArrayList ofTypecom.wyz.entity.OrderItemDO columnPrefixoi_id propertyitemId columnitem_id jdbcTypeINTEGER/result propertyorderId columnorder_id jdbcTypeINTEGER/result propertycommodityId columncommodity_id jdbcTypeINTEGER/result propertycommodityPrice columncommodity_price jdbcTypeDECIMAL/result propertycommodityCount columncommodity_count jdbcTypeINTEGER//collection
/resultMap这与我们使用 association 元素实现一对一关联查询非常相似而且 collection 元素中使用的大部分的属性也都在 association 元素中出现过唯一需要特别关注的是 collection 元素中的 ofType 属性它与 javaType 属性组合在一起共同声明了 UserOrderDO 对象中 orderItems 字段的类型javaType 属性用于声明 orderItems 字段在 UserOrderDO 中的“原始”类型即该字段为一个集合ArrayList类型而 ofType 属性声明了集合中元素的类型即该集合中存储的是 OrderItemDO 类型的元素。
接着我们来定义 UserOrderMapper 接口中的方法
UserOrderDO selectUserOrderAndOrderItemsByOrderNo(Param(orderNo) String orderNo);然后我们来编写UserOrderMapper#selectUserOrderAndOrderItemsByOrderNo方法对应的 MyBatis 映射器中的 SQL 语句
select idselectUserOrderAndOrderItemsByOrderNo resultMapuserOrderContainOrderItemMapselect uo.order_id,uo.user_id,uo.order_no,uo.order_price,uo.order_status,uo.create_date,uo.pay_date,oi.item_id as oi_item_id,oi.order_id as oi_order_id,oi.commodity_id as oi_commodity_id,oi.commodity_price as oi_commodity_price,oi.commodity_count as oi_commodity_countfrom user_order uo, order_item oiwhere uo.order_id oi.order_idand uo.order_no #{orderNo,jdbcTypeVARCHAR}
/select最后我们来写单元测试的代码
public void selectUserOrderAndOrderItemsByOrderNo() {UserOrderDO userOrder userOrderMapper.selectUserOrderAndOrderItemsByOrderNo(D202405082208045788);System.out.println(查询结果);System.out.println(JSON.toJSONString(userOrder, JSONWriter.Feature.PrettyFormat));
}执行单元测试代码我们来观察控制台输出的结果 可以看到在 MyBatis 的日志中SQL 语句查询出的结果是 3 条数据但是在我们输出的查询结果里只有一条 UserOrderDO 的数据而 UserOrderDO 对象的 orderItems 字段中却有 3 条数据。
这是因为 MyBatis 在处理结果集时将 3 条数据进行合并形成一条 UserOrderDO 的数据合并结果集的主要方法如下
DefaultResultSetHandler#handleResultSetsDefaultResultSetHandler#handleResultSetDefaultResultSetHandler#handleRowValuesDefaultResultSetHandler#handleRowValuesForNestedResultMapDefaultResultSetHandler#getRowValueDefaultResultSetHandler#applyPropertyMappingsDefaultResultSetHandler#applyNestedResultMappingsDefaultResultSetHandler#linkObjects
因为这是 MyBatis 中结果集处理的核心源码了在后面源码分析的部分我会和大家一起学习的所以这里我们先不细说感兴趣的小伙伴可以自行阅读源码。
高阶用法使用 collection 元素实现一对多嵌套查询
与 association 元素一样除了使用关联查询外还可以通过嵌套查询的方式实现一对多管理。
首先我们来为 OrderItemMapper 接口添加相关的查询方法
ListOrderItemDO selectOrderItemByOrderId(Param(orderId) Integer orderId);然后为其添加结果集映射规则和编写相应的 SQL 语句
resultMap idBaseResultMap typecom.wyz.entity.OrderItemDOid propertyitemId columnitem_id jdbcTypeINTEGER/result propertyorderId columnorder_id jdbcTypeINTEGER/result propertycommodityId columncommodity_id jdbcTypeINTEGER/result propertycommodityPrice columncommodity_price jdbcTypeDECIMAL/result propertycommodityCount columncommodity_count jdbcTypeINTEGER/
/resultMapselect idselectOrderItemByOrderId resultMapBaseResultMapselect * from order_itemwhere order_id #{orderId, jdbcTypeINTEGER}
/select下面我们来处理 user_order 表相关的部分首先是定义 UserOrderMapper 接口中的方法
UserOrderDO selectUserOrderAndOrderItemsByOrderNoNest(Param(orderNo) String orderNo);接着完善映射器中对应的 SQL 语句
select idselectUserOrderAndOrderItemsByOrderNoNest resultMapuserOrderContainOrderItemNestMapselect *from user_orderwhere order_no #{orderNo, jdbcTypeVARCHAR}
/select我们来编写 UserOrderDO 的映射规则“userOrderContainOrderItemNestMap”如下
resultMap iduserOrderContainOrderItemNestMap typecom.wyz.entity.UserOrderDO extendsBaseResultMapcollection propertyorderItemsjavaTypejava.util.ArrayListofTypecom.wyz.entity.OrderItemDOselectcom.wyz.mapper.OrderItemMapper.selectOrderItemByOrderIdcolumn{orderIdorder_id}/
/resultMap最后我们编写单元测试代码如下
public void selectUserOrderAndOrderItemsByOrderNoNest() {UserOrderDO userOrder userOrderMapper.selectUserOrderAndOrderItemsByOrderNoNest(D202405082208045788);System.out.println(查询结果);System.out.println(JSON.toJSONString(userOrder, JSONWriter.Feature.PrettyFormat));
}执行单元测试可以看到如下结果 可以看到在控制台输出的执行结果中执行了两条 SQL 语句分别用于查询 user_order 表的数据和 order_item 表的数据而在数据的结果中MyBatis 也将这些数据进行了合并。
高阶用法多层级结果集关联查询
实现了上面的所有需求后你的老板还是不满足它又提出了新的想法“为什么不能在查询用户时把该用户所有的订单订单明细和支付订单全部查询出来呢”于是你再一次在背后蛐蛐了你的老板并埋头苦干。
有了前面的经验你很快就想到了 resultMap 元素可以解决无法就是多套几层罢了。为了更好的进行展示多层映射规则我们需要补充一些数据我在附录中提供了补充数据的 SQL 脚本可以先添加到数据库中
目前我们的 UserOrderDO 对象中已经组合了 PayOrderDO 和 OrderItemDO那么我们无非就是把 UserOrderDO 组合到 UserDO 对象中代码如下
public class UserDO {// 省略 UserDO 自身的字段/*** 用户订单*/private ListUserOrderDO userOrders;
}接着我们为 UserMapper 接口中定义方法
UserDO selectUserByUserId(Param(userId) Integer userId);再来写 SQL 语句有了前面的经验很快就能想到联表查询一次数据库交就可以互搞定代码如下
select idselectUserByUserId resultMapuserMapselect u.user_id,u.name,u.age,u.gender,u.id_type,u.id_number,uo.order_id as uo_order_id,uo.user_id as uo_user_id,uo.order_no as uo_order_no,uo.order_price as uo_order_price,uo.order_status as uo_order_status,uo.create_date as uo_create_date,uo.pay_date as uo_pay_date,po.pay_order_id as po_pay_order_id,po.order_id as po_order_id,po.pay_order_no as po_pay_order_no,po.pay_amount as po_pay_amount,po.pay_channel as po_pay_channel,po.pay_status as po_pay_status,po.create_date as po_create_date,po.finish_date as po_finish_date,oi.item_id as oi_item_id,oi.order_id as oi_order_id,oi.commodity_id as oi_commodity_id,oi.commodity_price as oi_commodity_price,oi.commodity_count as oi_commodity_countfrom user u, user_order uo, pay_order po, order_item oiwhere u.user_id #{userId, jdbcTypeINTEGER}and u.user_id uo.user_idand uo.order_id po.order_idand uo.order_id oi.order_id
/select下面我们开始定义映射规则“userMap”如下
resultMap iduserMap typecom.wyz.entity.UserDOid propertyuserId columnuser_id jdbcTypeINTEGER/result propertyname columnname jdbcTypeVARCHAR/result propertyage columnage jdbcTypeINTEGER/result propertygender columngender jdbcTypeVARCHAR/result propertyidType columnid_type jdbcTypeINTEGER/result propertyidNumber columnid_number jdbcTypeVARCHAR/collection propertyuserOrders javaTypejava.util.ArrayList ofTypecom.wyz.entity.UserOrderDO columnPrefixuo_id propertyorderId columnorder_id jdbcTypeINTEGER/result propertyuserId columnuser_id jdbcTypeINTEGER/result propertyorderNo columnorder_no jdbcTypeVARCHAR/result propertyorderPrice columnorder_price jdbcTypeDECIMAL/result propertyorderStatus columnorder_status jdbcTypeINTEGER/result propertycreateDate columncreate_date jdbcTypeDATE/result propertypayDate columnpay_date jdbcTypeDATE/association propertypayOrder javaTypecom.wyz.entity.PayOrderDO columnPrefixpo_id propertypayOrderId columnpay_order_id jdbcTypeINTEGER/result propertyorderId columnorder_id jdbcTypeINTEGER/result propertypayOrderNo columnpay_order_no jdbcTypeVARCHAR/result propertypayAmount columnpay_amount jdbcTypeDECIMAL/result propertypayChannel columnpay_channel jdbcTypeINTEGER/result propertypayStatus columnpay_status jdbcTypeINTEGER/result propertycreateDate columncreate_date jdbcTypeDATE/result propertyfinishDate columnfinish_date jdbcTypeDATE//associationcollection propertyorderItems javaTypejava.util.ArrayList ofTypecom.wyz.entity.OrderItemDO columnPrefixoi_id propertyitemId columnitem_id jdbcTypeINTEGER/result propertyorderId columnorder_id jdbcTypeINTEGER/result propertycommodityId columncommodity_id jdbcTypeINTEGER/result propertycommodityPrice columncommodity_price jdbcTypeDECIMAL/result propertycommodityCount columncommodity_count jdbcTypeINTEGER//collection/collection
/resultMap最后我们来搞定单元测试的代码
public void selectUserByUserId() {UserDO user userMapper.selectUserByUserId(1);System.out.println(查询结果);System.out.println(JSON.toJSONString(user, JSONWriter.Feature.PrettyFormat));
}当你自信满满的执行单元测试后控制台输出的结果却有些出乎意料 可以看到MyBatis 执行的 SQL 语句是正常的输出的查询结果也是正常的可以在最终的结果集映射上出了问题PayOrderDO 对象和 OrderItemDO 对象并没有映射成功。
columnPrefix 属性导致的映射失败
是不是 MyBatis 不支持映射规则的多层嵌套呢
其实不是的在 MyBatis 中使用多层嵌套规则且每层嵌套规则都配置了 columnPrefix 属性时在为下层映射规则的查询字段起别名时需要将上层的嵌套映射规则配置的 columnPrefix 属性作为前缀然后再拼接本层的 columnPrefix 属性的配置而在 resultMap 元素的配置中每层只需要配置自己的前缀即可。
在上面的多层嵌套映射规则的例子中映射规则“userMap”不需要改变SQL 语句需要修改成如下图右侧所示的内容 我把图中右侧的 SQL 语句粘到了这里
select idselectUserByUserId resultMapuserMapselect u.user_id,u.name,u.age,u.gender,u.id_type,u.id_number,uo.order_id as uo_order_id,uo.user_id as uo_user_id,uo.order_no as uo_order_no,uo.order_price as uo_order_price,uo.order_status as uo_order_status,uo.create_date as uo_create_date,uo.pay_date as uo_pay_date,po.pay_order_id as uo_po_pay_order_id,po.order_id as uo_po_order_id,po.pay_order_no as uo_po_pay_order_no,po.pay_amount as uo_po_pay_amount,po.pay_channel as uo_po_pay_channel,po.pay_status as uo_po_pay_status,po.create_date as uo_po_create_date,po.finish_date as uo_po_finish_date,oi.item_id as uo_oi_item_id,oi.order_id as uo_oi_order_id,oi.commodity_id as uo_oi_commodity_id,oi.commodity_price as uo_oi_commodity_price,oi.commodity_count as uo_oi_commodity_countfrom user u, user_order uo, pay_order po, order_item oiwhere u.user_id #{userId, jdbcTypeINTEGER}and u.user_id uo.user_idand uo.order_id po.order_idand uo.order_id oi.order_id
/select你可以替换掉 SQL 语句再执行单元测试看看结果pay_order 表和 order_item 表的数据是不是已经映射到结果里了呢
透过源码分析 columnPrefix 属性的逻辑
不感兴趣的可以先跳过这部分内容因为在后面的源码分析篇中我们也会涉及到这部分内容。
注意本文中涉及到源码的部分我只保留了相关内容的源码因此删减和改动的部分会非常多我会尽量保证展示出来的源码能够清晰的解释这段逻辑。
我们先找到DefaultResultSetHandler#handleResultSets方法的源码部分源码如下
public ListObject handleResultSets(Statement stmt) throws SQLException {final ListObject multipleResults new ArrayList();int resultSetCount 0;// 获取首行数据ResultSetWrapper rsw getFirstResultSet(stmt);ListResultMap resultMaps mappedStatement.getResultMaps();while (rsw ! null) {ResultMap resultMap resultMaps.get(resultSetCount);// 处理结果集handleResultSet(rsw, resultMap, multipleResults, null);// 获取下一行数据rsw getNextResultSet(stmt);resultSetCount;}
}DefaultResultSetHandler#handleResultSets方法的主要功能是逐行解析结果集数据我们接着来看第 10 行中调用的DefaultResultSetHandler#handleResultSet方法
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, ListObject multipleResults, ResultMapping parentMapping) throws SQLException {if (parentMapping ! null) {handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);} else if (resultHandler null) {DefaultResultHandler defaultResultHandler new DefaultResultHandler(objectFactory);handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);}
}删减过后DefaultResultSetHandler#handleResultSet方法非常简单其实原始代码也很简单我们不需要过多关注这个方法直接看第 3 行和第 6 行中调用的DefaultResultSetHandler#handleRowValues方法部分源码如下
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler? resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {if (resultMap.hasNestedResultMaps()) {handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);} else {handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);}
}DefaultResultSetHandler#handleRowValues方法的源码也很简。不过需要解释下第 2 行中 if 语句的调用的ResultMap#hasNestedResultMaps方法该方法返回 ResultMap 中 hasNestedResultMaps 字段的值该字段的值在解析 MyBatis 映射器中的 resultMap 元素时确定如果该 resultMap 元素定义的映射规则存在嵌套映射规则则 hasNestedResultMaps 的值为 true否则为 false。
对于我们使用的映射规则“userMap”来说我们嵌套了 3 层因此在这里的条件语句中会执行第 3 行的DefaultResultSetHandler#handleRowValuesForNestedResultMap方法部分源码如下
private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler? resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {final DefaultResultContextObject resultContext new DefaultResultContext();Object rowValue previousRowValue;while (shouldProcessMoreRows(resultContext, rowBounds) !resultSet.isClosed() resultSet.next()) {final ResultMap discriminatedResultMap resolveDiscriminatedResultMap(resultSet, resultMap, null);final CacheKey rowKey createRowKey(discriminatedResultMap, rsw, null);Object partialObject nestedResultObjects.get(rowKey);// 获取每行的数据rowValue getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);}
}DefaultResultSetHandler#handleRowValuesForNestedResultMap方法删减之后就简单很多了该方法的主要作用是调用DefaultResultSetHandler#getRowValue方法转换结果集数据不过需要注意下调用DefaultResultSetHandler#getRowValue方法时第 4 个参数此时为 null。
我们继续向下来看第 9 行调用的DefaultResultSetHandler#getRowValue方法部分源码如下
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {final String resultMapId resultMap.getId();Object rowValue partialObject;rowValue createResultObject(rsw, resultMap, lazyLoader, columnPrefix);if (rowValue ! null !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final MetaObject metaObject configuration.newMetaObject(rowValue);boolean foundValues this.useConstructorMappings;// 处理当前层级的字段映射规则foundValues applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;// 处理嵌套的字段映射规则foundValues applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;}return rowValue;
}先来看DefaultResultSetHandler#getRowValue方法的声明第 4 个参数的变量名是“columnPrefix”即我们在映射规则中配置的 columnPrefix 属性不过在首次调用DefaultResultSetHandler#getRowValue方法的时候 columnPrefix 参数的值为 null。
接下来我们进入第 11 行中调用的DefaultResultSetHandler#applyNestedResultMappings方法该方法用于处理嵌套层级的字段映射部分源码如下
private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {boolean foundValues false;for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {final String nestedResultMapId resultMapping.getNestedResultMapId();if (nestedResultMapId ! null resultMapping.getResultSet() null) {// 获取 columnPrefix final String columnPrefix getColumnPrefix(parentPrefix, resultMapping);// 获取嵌套规则中的配置final ResultMap nestedResultMap getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);final CacheKey rowKey createRowKey(nestedResultMap, rsw, columnPrefix);final CacheKey combinedKey combineKeys(rowKey, parentRowKey);Object rowValue nestedResultObjects.get(combinedKey);// 解析嵌套规则中的结果集if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {rowValue getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);if (rowValue ! null) {linkObjects(metaObject, resultMapping, rowValue);foundValues true;}}}}return foundValues;
}DefaultResultSetHandler#applyNestedResultMappings负责处理嵌套映射规则中的每个字段的映射逻辑。
首先来看第 3 行 for 循环语句ResultMap 的 propertyResultMappings 字段中存储了 resultMap 元素中每个 id 元素result 元素association 元素和 collection 元素的解析结果因此这里是遍历 resultMap 元素中的每项映射规则的配置。
第 4 行中的 ResultMap 的 nestedResultMapId 字段存储了嵌套映射规则的 ID这个 ID 是由 MyBatis 自动生成的其形式如com.wyz.mapper.UserMapper.mapper_resultMap[userMap]_collection[userOrders]存储了 resuMap 的 ID嵌套映射规则的类型以及该嵌套规则对应的字段名。
紧接着是第 5 行的的 if 条件语句要求 nestedResultMapId 不为空的情况下才会执行 if 条件语句中的逻辑也就是说只有嵌套映射规则才会执行 if 条件语句中的逻辑。
再来看第 7 行中调用的DefaultResultSetHandler#getColumnPrefix方法该方法用于获取 columnPrefix 属性中的配置完成源码如下
private String getColumnPrefix(String parentPrefix, ResultMapping resultMapping) {final StringBuilder columnPrefixBuilder new StringBuilder();if (parentPrefix ! null) {columnPrefixBuilder.append(parentPrefix);}if (resultMapping.getColumnPrefix() ! null) {columnPrefixBuilder.append(resultMapping.getColumnPrefix());}return columnPrefixBuilder.length() 0 ? null : columnPrefixBuilder.toString().toUpperCase(Locale.ENGLISH);
}我们来分析这段源码首先是第一次调用时 parentPrefix 的值为 null如果此时嵌套映射规则中配置了 columnPrefix 属性例如在解析映射规则 userMap 时解析到了下面的配置时
collection propertyuserOrders javaTypejava.util.ArrayList ofTypecom.wyz.entity.UserOrderDO columnPrefixuo_根据源码中的逻辑此时返回的值为“uo_”。
我们回到DefaultResultSetHandler#applyNestedResultMappings方法中的第 15 行代码此时会递归调用DefaultResultSetHandler#getRowValue方法不过此时传入的是嵌套规则中的配置。
那么后面的就很好理解了当遍历到嵌套映射规则时会递归调用DefaultResultSetHandler#getRowValue方法此时传入的 columnPrefix 参数就有了值再次执行DefaultResultSetHandler#getColumnPrefix方法时就是将传入的 columnPrefix 参数与嵌套规则中 columnPrefix 属性的配置组合起来。
那么我们回到最开始的 SQL 语句与映射规则“userMap”中当遍历到 payOrder 的嵌套映射规则时此时的前缀应该为“uo_po_”而遍历到 orderItems 的嵌套映射规则时此时的前缀应该为“uo_oi_”。
高阶用法多层级结果集嵌套查询
上面我们实现的通过 user_id 查询用户信息用户订单信息订单明细信息以及支付信息的功能中除了最开始的 pay_order 表和 order_utem 表的数据无法映射外还存在一个问题那就是在联表查询的 SQL 语句中查询结果是笛卡尔积的形式。
不过由于我们的测试数据非常少而且表结构非常简单SQL 语句对性能的影响可以湖绿不急。不过一旦数据量上来或者联表查询的 SQL 语句设计不合理那么对整体性能的影响可能是灾难级的此时与其守着一次数据库交互倒不如拆分成多个 SQL 语句分别查询了。
说干就干我们已经知道了如何在 resultMap 元素中嵌套子查询语句那么改写映射规则“userMap”就非常简单了代码如下
resultMap iduserNestMap typecom.wyz.entity.UserDO extendsBaseResultMapcollection propertyuserOrdersjavaTypejava.util.ArrayListofTypecom.wyz.entity.UserOrderDOselectcom.wyz.mapper.UserOrderMapper.selectUserOrderByUserIdcolumn{userIduser_id}association propertypayOrderjavaTypecom.wyz.entity.PayOrderDOselectcom.wyz.mapper.PayOrderMapper.selectPayOrderByOrderIdcolumn{orderIdorder_id}/collection propertyorderItemsjavaTypejava.util.ArrayListofTypecom.wyz.entity.OrderItemDOselectcom.wyz.mapper.OrderItemMapper.selectOrderItemByOrderIdcolumn{orderIdorder_id}//collection
/resultMap可以看到新的映射规则“userNestMap”分为 3 层
第 1 层是 user 表与 Java 对象 UserDO 的映射规则直接继承了 UserMapper 的映射规则集“BaseResultMap”第 2 层是 usser_order 表与 Java 对象 UserOrderDO 的映射规则使用了子查询UserOrderMapper#selectUserOrderByUserId第 3 层 pay_order 表与 Java 对象 PayOrderDO以及 order_item 表与 Java 对象 OrderItemDO 的映射规则分别使用了子查询PayOrderMapper#selectPayOrderByOrderId和OrderItemMapper#selectOrderItemByOrderId。
Tips这里就不展示 3 个子查询方法了反正也得改。
当你做好“万全”的准备之后执行单元测试控制台的输出再一次让你出乎意料 明明写了在映射规则中写了 3 个子查询再加上主查询 SQL 语句应该执行 4 条 SQL 语句的可是为什么只执行了两条语句呢
再仔细观察控制台输出的 SQL 执行记录你发现了最外层查询 user 表的 SQL 语句和第 2 层查询 user_order 表的 SQL 语句都执行了只有第 3 层查询 pay_order 表和查询 order_item 表的两条 SQL 语句没有执行难道是 MyBatis 不支持 3 层嵌套子查询
还真是这样的MyBatis 最多只能在一个映射规则中支持两层嵌套子查询即只允许主查询语句“拥有”子查询语句而子查询不能再“拥有”子查询语句。
透过源码分析多层嵌套子查询
造成这种现象的原因是因为 MyBatis 在解析 resultMap 元素时只会解析一层嵌套的子查询语句。
来看 MyBatis 解析 resultMap 元素的源码我们直接从XMLMapperBuilder#resultMapElements方法入手部分源码如下
private void resultMapElements(ListXNode list) {for (XNode resultMapNode : list) {resultMapElement(resultMapNode);}
}该方法用于遍历映射器文件中的所有 resultMap 元素定义的映射规则并逐个进行解析。
接下来看第 3 行调用的XMLMapperBuilder# resultMapElement方法
private ResultMap resultMapElement(XNode resultMapNode, ListResultMapping additionalResultMappings, Class? enclosingType) {Class? typeClass resolveClass(type);ListResultMapping resultMappings new ArrayList(additionalResultMappings);ListXNode resultChildren resultMapNode.getChildren();for (XNode resultChild : resultChildren) {resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));}
}XMLMapperBuilder# resultMapElement方法用于遍历 resultMap 元素的所有子元素并根据子元素类型的不同执行不同的处理逻辑这里省略了其它类型子元素的判断逻辑和处理逻辑如果是 id 元素result 元素association 元素和 collection 元素等会调用第 6 行中的XMLMapperBuilder#buildResultMappingFromContext方法源码如下
private ResultMapping buildResultMappingFromContext(XNode context, Class? resultType, ListResultFlag flags) {String property;if (flags.contains(ResultFlag.CONSTRUCTOR)) {property context.getStringAttribute(name);} else {property context.getStringAttribute(property);}String column context.getStringAttribute(column);String javaType context.getStringAttribute(javaType);String jdbcType context.getStringAttribute(jdbcType);String nestedSelect context.getStringAttribute(select);String nestedResultMap context.getStringAttribute(resultMap, () - processNestedResultMappings(context, Collections.emptyList(), resultType));String notNullColumn context.getStringAttribute(notNullColumn);String columnPrefix context.getStringAttribute(columnPrefix);String typeHandler context.getStringAttribute(typeHandler);String resultSet context.getStringAttribute(resultSet);String foreignColumn context.getStringAttribute(foreignColumn);boolean lazy lazy.equals(context.getStringAttribute(fetchType, configuration.isLazyLoadingEnabled() ? lazy : eager));Class? javaTypeClass resolveClass(javaType);Class? extends TypeHandler? typeHandlerClass resolveClass(typeHandler);JdbcType jdbcTypeEnum resolveJdbcType(jdbcType);return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}这是XMLMapperBuilder#buildResultMappingFromContext方法的全部源码了先来看第 11 行中获取了子元素中 select 属性的配置即我们的子查询语句。
接着来看第 12 行中调用的XMLMapperBuilder#processNestedResultMappings方法源码如下
private String processNestedResultMappings(XNode context, ListResultMapping resultMappings, Class? enclosingType) {if (Arrays.asList(association, collection, case).contains(context.getName()) context.getStringAttribute(select) null) {validateCollection(context, enclosingType);ResultMap resultMap resultMapElement(context, resultMappings, enclosingType);return resultMap.getId();}return null;
}在XMLMapperBuilder#processNestedResultMappings方法中第 2 行的 if 条件语句中判断了子元素的类型属于
association 元素collection 元素或 case 元素中的一种并且没有配置 select 属性时进入条件语句中递归调用XMLMapperBuilder#resultMapElement方法解析映射规则。
这也就是说当 resultMap 中配置了多层级的嵌套规则时MyBatis 会将每层规则单独解析如果是嵌套的子查询就不会继续向下解析了这也就是为什么在我们的多层级嵌套子查询的映射规则“userNestMap”中无法解析到第 3 层的嵌套子查询语句。
Tips前面的“透过源码分析 columnPrefix 属性的逻辑”中我们提到到嵌套映射规则就是在这里生成的。
改写多层嵌套子查询
那么我们来改写多层嵌套子查询映射规则“userNestMap”首先我们要做的是将 3 层嵌套子查询改成两层那么我们需要将查询 pay_order 表的子查询与查询 order_item 表的子查询移动到查询 user_order 表的数据的映射规则中。
前面我们已经写了映射规则“userOrderContainOrderItemNestMap”并且集成了 order_item 表的子查询并且上一篇文章《MyBatis映射器一对一关联查询》中的映射规则“userOrderContainPayOrderNestMap”集成了 pay_order 表的子查询那么我们将两者结合一下不就包含了两个子查询了吗代码如下
resultMap iduserOrderContainOrderItemNestMap typecom.wyz.entity.UserOrderDO extendsuserOrderContainPayOrderNestMapcollection propertyorderItemsjavaTypejava.util.ArrayListofTypecom.wyz.entity.OrderItemDOselectcom.wyz.mapper.OrderItemMapper.selectOrderItemByOrderIdcolumn{orderIdorder_id}/
/resultMap改写完映射规则之后我们还要为 UserOrderMapper 接口添加一个新的接口方法因为 user 表与 user_order 表是通过 user_id 字段进行关联的如下
ListUserOrderDO selectUserOrderByUserIdNest(Param(userId)Integer userId);接着是 UserMapper 接口对应的映射器中的 SQL 语句如下
select idselectUserOrderByUserIdNest resultMapuserOrderContainOrderItemNestMapselect * from user_order where user_id #{userId,jdbcTypeINTEGER}
/select做完这些准备工作之后我们就来改写映射规则“userMap”如下
resultMap iduserNestMap typecom.wyz.entity.UserDO extendsBaseResultMapcollection propertyuserOrdersjavaTypejava.util.ArrayListofTypecom.wyz.entity.UserOrderDOselectcom.wyz.mapper.UserOrderMapper.selectUserOrderByUserIdNestcolumn{userIduser_id}/collection
/resultMap最后我们执行单元测试来观察控制台的输出 可以看到在输出的查询语句中比我们想象中的要多这是因为该用户有两个订单而我们嵌套的子查询语句只允许传入单个订单 ID因此需要根据订单 ID 查询多次支付订单信息和订单明细信息。
当然了你可以修改这个映射规则允许部分简单的联表查询以减少执行 SQL 语句的次数减少与数据库的交互。不过我是累了就留给大家自行实现吧~~
最后我再补充一点 association 元素和 collection 元素中是有一个属性叫做 resultMap 的你可以用它来引入其它的映射规则来减少配置这个也留给大家自行探索吧。
附录数据补充
补充数据用于测试多层嵌套关联查询SQL 脚本如下
-- 用户信息
INSERT INTO user (user_id, name, age, gender, id_type, id_number) VALUES (2, 陈二, 18, M, 1, 1101012000808186531);-- 用户订单信息
INSERT INTO user_order (order_id, user_id, order_no, order_price, order_status, create_date, pay_date) VALUES (3, 2, D202405202033475889, 100.00, 1, 2024-05-20, 2024-05-21);-- 支付订单信息
INSERT INTO pay_order (pay_order_id, order_id, pay_order_no, pay_amount, pay_channel, pay_status, create_date, finish_date) VALUES (3, 3, Z202405202033475889, 100.00, 755, 1, 2024-05-21, 2024-05-21);-- 订单明细
INSERT INTO order_item (item_id, order_id, commodity_id, commodity_price, commodity_count) VALUES (4, 2, 350891, 77.00, 100);
INSERT INTO order_item (item_id, order_id, commodity_id, commodity_price, commodity_count) VALUES (5, 2, 330001, 220.00, 10);
INSERT INTO order_item (item_id, order_id, commodity_id, commodity_price, commodity_count) VALUES (6, 3, 330002, 100.00, 1);