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_count
  from user_order uo, order_item oi
  where uo.order_id = oi.order_id
    and uo.order_no = 'D202405082208045788';

但是执行完 SQL 语句之后你有点懵了,数据库的查询结果给出了 3 条数据,这与我们设想的一条订单信息带 3 条订单明细也不一样啊?

难道必须要分步查询了吗?

高阶用法:使用 collection 元素实现一对多关联查询

于是你想到是不是还可以使用 resultMap 元素来实现这种一对多的关联查询呢?终于在查阅了相关资料之后,你发现了 reusltMap 元素的子元素 collection 元素似乎可以解决这个问题。

首先我们来修改 UserOrderDO,为其组合上订单明细信息,如下:

public class UserOrderDO   {

  // 省略 UserOrderDO 自身的字段

  /**
   * 支付订单信息
   */
  private PayOrderDO payOrder;

  /**
   * 用户订单明细
   */
  private List<OrderItemDO> orderItems;
}

接着我们使用 resultMap 元素编写新的映射规则“userOrderContainOrderItemMap”,如下:

<resultMap id="userOrderContainOrderItemMap" type="com.wyz.entity.UserOrderDO" extends="BaseResultMap">
  <collection property="orderItems" javaType="java.util.ArrayList" ofType="com.wyz.entity.OrderItemDO" columnPrefix="oi_">
    <id property="itemId" column="item_id" jdbcType="INTEGER"/>
    <result property="orderId" column="order_id" jdbcType="INTEGER"/>
    <result property="commodityId" column="commodity_id" jdbcType="INTEGER"/>
    <result property="commodityPrice" column="commodity_price" jdbcType="DECIMAL"/>
    <result property="commodityCount" column="commodity_count" jdbcType="INTEGER"/>
  </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 id="selectUserOrderAndOrderItemsByOrderNo" resultMap="userOrderContainOrderItemMap">
  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         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_count
  from user_order uo, order_item oi
  where uo.order_id = oi.order_id
    and uo.order_no = #{orderNo,jdbcType=VARCHAR}
</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#handleResultSets
  • DefaultResultSetHandler#handleResultSet
  • DefaultResultSetHandler#handleRowValues
  • DefaultResultSetHandler#handleRowValuesForNestedResultMap
  • DefaultResultSetHandler#getRowValue
  • DefaultResultSetHandler#applyPropertyMappings
  • DefaultResultSetHandler#applyNestedResultMappings
  • DefaultResultSetHandler#linkObjects

因为这是 MyBatis 中结果集处理的核心源码了,在后面源码分析的部分我会和大家一起学习的,所以这里我们先不细说,感兴趣的小伙伴可以自行阅读源码。

高阶用法:使用 collection 元素实现一对多嵌套查询

与 association 元素一样,除了使用关联查询外,还可以通过嵌套查询的方式实现一对多管理。

首先我们来为 OrderItemMapper 接口添加相关的查询方法:

List<OrderItemDO> selectOrderItemByOrderId(@Param("orderId") Integer orderId);

然后为其添加结果集映射规则和编写相应的 SQL 语句:

<resultMap id="BaseResultMap" type="com.wyz.entity.OrderItemDO">
  <id property="itemId" column="item_id" jdbcType="INTEGER"/>
  <result property="orderId" column="order_id" jdbcType="INTEGER"/>
  <result property="commodityId" column="commodity_id" jdbcType="INTEGER"/>
  <result property="commodityPrice" column="commodity_price" jdbcType="DECIMAL"/>
  <result property="commodityCount" column="commodity_count" jdbcType="INTEGER"/>
</resultMap>

<select id="selectOrderItemByOrderId" resultMap="BaseResultMap">
  select * from order_item
  where order_id = #{orderId, jdbcType=INTEGER}
</select>

下面我们来处理 user_order 表相关的部分,首先是定义 UserOrderMapper 接口中的方法:

UserOrderDO selectUserOrderAndOrderItemsByOrderNoNest(@Param("orderNo") String orderNo);

接着完善映射器中对应的 SQL 语句:

<select id="selectUserOrderAndOrderItemsByOrderNoNest" resultMap="userOrderContainOrderItemNestMap">
  select *
  from user_order
  where order_no = #{orderNo, jdbcType=VARCHAR}
</select>

我们来编写 UserOrderDO 的映射规则“userOrderContainOrderItemNestMap”,如下:

<resultMap id="userOrderContainOrderItemNestMap" type="com.wyz.entity.UserOrderDO" extends="BaseResultMap">
  <collection property="orderItems"
              javaType="java.util.ArrayList"
              ofType="com.wyz.entity.OrderItemDO"
              select="com.wyz.mapper.OrderItemMapper.selectOrderItemByOrderId"
              column="{orderId=order_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 List<UserOrderDO> userOrders;
}

接着我们为 UserMapper 接口中定义方法:

UserDO selectUserByUserId(@Param("userId") Integer userId);

再来写 SQL 语句,有了前面的经验,很快就能想到联表查询一次数据库交就可以互搞定,代码如下:

<select id="selectUserByUserId" resultMap="userMap">
  select 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_count
  from user u, user_order uo, pay_order po, order_item oi
  where u.user_id = #{userId, jdbcType=INTEGER}
    and u.user_id = uo.user_id
    and uo.order_id = po.order_id
    and uo.order_id = oi.order_id
</select>

下面我们开始定义映射规则“userMap”,如下:

<resultMap id="userMap" type="com.wyz.entity.UserDO">
  <id property="userId" column="user_id" jdbcType="INTEGER"/>
  <result property="name" column="name" jdbcType="VARCHAR"/>
  <result property="age" column="age" jdbcType="INTEGER"/>
  <result property="gender" column="gender" jdbcType="VARCHAR"/>
  <result property="idType" column="id_type" jdbcType="INTEGER"/>
  <result property="idNumber" column="id_number" jdbcType="VARCHAR"/>
  <collection property="userOrders" javaType="java.util.ArrayList" ofType="com.wyz.entity.UserOrderDO" columnPrefix="uo_">
    <id property="orderId" column="order_id" jdbcType="INTEGER"/>
    <result property="userId" column="user_id" jdbcType="INTEGER"/>
    <result property="orderNo" column="order_no" jdbcType="VARCHAR"/>
    <result property="orderPrice" column="order_price" jdbcType="DECIMAL"/>
    <result property="orderStatus" column="order_status" jdbcType="INTEGER"/>
    <result property="createDate" column="create_date" jdbcType="DATE"/>
    <result property="payDate" column="pay_date" jdbcType="DATE"/>
    <association property="payOrder" javaType="com.wyz.entity.PayOrderDO" columnPrefix="po_">
      <id property="payOrderId" column="pay_order_id" jdbcType="INTEGER"/>
      <result property="orderId" column="order_id" jdbcType="INTEGER"/>
      <result property="payOrderNo" column="pay_order_no" jdbcType="VARCHAR"/>
      <result property="payAmount" column="pay_amount" jdbcType="DECIMAL"/>
      <result property="payChannel" column="pay_channel" jdbcType="INTEGER"/>
      <result property="payStatus" column="pay_status" jdbcType="INTEGER"/>
      <result property="createDate" column="create_date" jdbcType="DATE"/>
      <result property="finishDate" column="finish_date" jdbcType="DATE"/>
    </association>
    <collection property="orderItems" javaType="java.util.ArrayList" ofType="com.wyz.entity.OrderItemDO" columnPrefix="oi_">
      <id property="itemId" column="item_id" jdbcType="INTEGER"/>
      <result property="orderId" column="order_id" jdbcType="INTEGER"/>
      <result property="commodityId" column="commodity_id" jdbcType="INTEGER"/>
      <result property="commodityPrice" column="commodity_price" jdbcType="DECIMAL"/>
      <result property="commodityCount" column="commodity_count" jdbcType="INTEGER"/>
    </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 id="selectUserByUserId" resultMap="userMap">
  select 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_count
  from user u, user_order uo, pay_order po, order_item oi
  where u.user_id = #{userId, jdbcType=INTEGER}
    and u.user_id = uo.user_id
    and uo.order_id = po.order_id
    and uo.order_id = oi.order_id
</select>

你可以替换掉 SQL 语句,再执行单元测试看看结果,pay_order 表和 order_item 表的数据是不是已经映射到结果里了呢?

透过源码分析 columnPrefix 属性的逻辑

不感兴趣的可以先跳过这部分内容,因为在后面的源码分析篇中,我们也会涉及到这部分内容。

注意,本文中涉及到源码的部分,我只保留了相关内容的源码,因此删减和改动的部分会非常多,我会尽量保证展示出来的源码能够清晰的解释这段逻辑。

我们先找到DefaultResultSetHandler#handleResultSets方法的源码,部分源码如下:

public List<Object> handleResultSets(Statement stmt) throws SQLException {
  final List<Object> multipleResults = new ArrayList<>();
  int resultSetCount = 0;
  // 获取首行数据
  ResultSetWrapper rsw = getFirstResultSet(stmt);
  List<ResultMap> 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, List<Object> 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 DefaultResultContext<Object> 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 property="userOrders" javaType="java.util.ArrayList" ofType="com.wyz.entity.UserOrderDO" columnPrefix="uo_">

根据源码中的逻辑,此时返回的值为“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 id="userNestMap" type="com.wyz.entity.UserDO" extends="BaseResultMap">
  <collection property="userOrders"
    javaType="java.util.ArrayList"
    ofType="com.wyz.entity.UserOrderDO"
    select="com.wyz.mapper.UserOrderMapper.selectUserOrderByUserId"
    column="{userId=user_id}">
    <association property="payOrder"
      javaType="com.wyz.entity.PayOrderDO"
      select="com.wyz.mapper.PayOrderMapper.selectPayOrderByOrderId"
      column="{orderId=order_id}"/>
    <collection property="orderItems"
      javaType="java.util.ArrayList"
      ofType="com.wyz.entity.OrderItemDO"
      select="com.wyz.mapper.OrderItemMapper.selectOrderItemByOrderId"
      column="{orderId=order_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#selectPayOrderByOrderIdOrderItemMapper#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(List<XNode> list) {
  for (XNode resultMapNode : list) {
    resultMapElement(resultMapNode);
  }
}

该方法用于遍历映射器文件中的所有 resultMap 元素定义的映射规则,并逐个进行解析。

接下来看第 3 行调用的XMLMapperBuilder# resultMapElement方法:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
  Class<?> typeClass = resolveClass(type);
  List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
  List<XNode> 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, List<ResultFlag> 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, List<ResultMapping> 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 id="userOrderContainOrderItemNestMap" type="com.wyz.entity.UserOrderDO" extends="userOrderContainPayOrderNestMap">
  <collection property="orderItems"
              javaType="java.util.ArrayList"
              ofType="com.wyz.entity.OrderItemDO"
              select="com.wyz.mapper.OrderItemMapper.selectOrderItemByOrderId"
              column="{orderId=order_id}"/>
</resultMap>

改写完映射规则之后,我们还要为 UserOrderMapper 接口添加一个新的接口方法,因为 user 表与 user_order 表是通过 user_id 字段进行关联的,如下:

List<UserOrderDO> selectUserOrderByUserIdNest(@Param("userId")Integer userId);

接着是 UserMapper 接口对应的映射器中的 SQL 语句,如下:

<select id="selectUserOrderByUserIdNest" resultMap="userOrderContainOrderItemNestMap">
  select * from user_order where user_id = #{userId,jdbcType=INTEGER}
</select>

做完这些准备工作之后,我们就来改写映射规则“userMap”,如下:

<resultMap id="userNestMap" type="com.wyz.entity.UserDO" extends="BaseResultMap">
  <collection property="userOrders"
              javaType="java.util.ArrayList"
              ofType="com.wyz.entity.UserOrderDO"
              select="com.wyz.mapper.UserOrderMapper.selectUserOrderByUserIdNest"
              column="{userId=user_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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术范王有志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值