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);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/735754.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

论文《Tree Decomposed Graph Neural Network》笔记

【TDGNN】本文提出了一种树分解方法来解决不同层邻域之间的特征平滑问题&#xff0c;增加了网络层配置的灵活性。通过图扩散过程表征了多跳依赖性&#xff08;multi-hop dependency&#xff09;&#xff0c;构建了TDGNN模型&#xff0c;该模型可以灵活地结合大感受场的信息&…

接口性能优化方法总结

接口性能优化是后端开发人员经常碰到的一道面试题&#xff0c;因为它是一个跟开发语言无关的公共问题。 这个问题既可以很简单&#xff0c;也可以相当复杂。 导致接口性能问题的原因多种多样&#xff0c;不同项目的不同接口&#xff0c;其原因可能各不相同。 下面列举几种常…

Codeforces Round 953 (Div. 2) A~F

A.Alice and Books&#xff08;思维&#xff09; 题意&#xff1a; 爱丽丝有 n n n本书。第 1 1 1本书包含 a 1 a_1 a1​页&#xff0c;第 2 2 2本书包含 a 2 a_2 a2​页&#xff0c; … \ldots …第 n n n本书包含 a n a_n an​页。爱丽丝的操作如下&#xff1a; 她把所有的…

Web3设计风格的特点和趋势

Web3的设计风格正随着区块链技术和去中心化理念的兴起而逐渐形成自己的特色。Web3的设计风格反映了这一新兴领域的创新精神和对个性化、社区驱动的重视。随着技术的发展和用户需求的变化&#xff0c;Web3的设计风格预计将继续演进。以下是一些Web3设计风格的特点和趋势。北京木…

【启明智显产品介绍】工业级HMI芯片Model3C详解(二)图像显示

Model3C芯片国产自主的工业级高清显示与智能控制 MCU&#xff0c;配备强大的 2D 图形加速处理器、PNG/JPEG 解码引擎&#xff0c;可以满足多种交互设计场景和多媒体互动需求&#xff0c;具备高可靠性、高开放性&#xff0c;可广泛应用于工业自动化控制、HMI人机交互、串口屏、智…

Python低溫半导体电子束量子波算法计算

&#x1f3af;要点 &#x1f3af;任意维度求解器&#xff0c;绘制三维投影结果 | &#x1f3af;解二维静电场、静磁场 | &#x1f3af;狄利克雷、诺依曼条件几何矩阵算子 | &#x1f3af;算法模拟低溫半导体材料 | &#x1f3af;计算曲面达西流 | &#x1f3af;电子结构计算和…

深入探索 Gradle 自动化构建技术(六、Gradle 插件平台化框架 ByteX 探秘之旅)

public void test1(){ //1. Collection 提供了两个方法 stream() 与 parallelStream() List list new ArrayList<>(); Stream stream list.stream(); //获取一个顺序流 Stream parallelStream list.parallelStream(); //获取一个并行流 //2. 通过 Arrays 中的 stream…

使用Spring Boot构建全栈应用程序:从理论到实践

文章目录 引言第一章 项目初始化1.1 使用Spring Initializr生成项目1.2 创建基本项目结构 第二章 后端服务开发2.1 定义实体类2.2 创建Repository接口2.3 实现Service类2.4 创建Controller类 第三章 前端集成3.1 使用Thymeleaf模板引擎3.2 创建前端控制器 第四章 安全配置4.1 S…

AcWing算法基础课笔记——求组合数4

求组合数Ⅳ 用来解决求 C a b C_a^b Cab​的问题&#xff08;没有模运算&#xff09; 解决办法&#xff1a;分解质因数&#xff0c;实现高精度乘法。 C a b a ! b ! ( a − b ) ! C_a^b \frac{a!}{b!(a - b)!} Cab​b!(a−b)!a!​ 其中 a ! a! a!可以用 p p p的倍数来表示…

Linux中web集群-nginx负载均衡及案例

概述 代理&#xff1a;外卖&#xff0c;中介&#xff0c;中间商&#xff0c;用户无法直接做事情&#xff0c;通过中介进行处理 用户–》代理–》节点&#xff0c;后面只有一个节点&#xff0c;一般使用的是nginx代理功能即可&#xff0c;如果是集群就需要使用nginx负载均衡 …

世界名著精选,免费!精选了数百本世界名著,打包带走!

世界名著精选是一款汇集了全球经典文学作品的电子书阅读软件。是一个旨在让经典文学作品更易于接触和阅读的平台。它不仅收录了世界各地的经典名著&#xff0c;还通过现代技术手段&#xff0c;让阅读变得更加便捷和个性化。 软件链接&#xff1a;免费&#xff01;几百种资源&a…

【数据结构与算法】图论 详解

何为完全图、稀疏图、稠密图。 完全图&#xff1a;完全图是一种简单的无向图&#xff0c;其中每对不同的顶点之间都恰好有一条边。对于有n个顶点的完全图&#xff0c;它包含n(n-1)/2条边。在有向图中&#xff0c;如果任意两个顶点之间都存在方向相反的两条边&#xff0c;包含n(…

OA流程节点超时功能

在OA系统中&#xff0c;节点超时功能是一个关键的技术特性&#xff0c;它能够确保流程的顺畅进行&#xff0c;避免任务因为个别环节的延误而影响整体进度。本文将探讨OA系统中节点超时功能的技术实现和优化策略。 参考泛微OA ecology E9 前台设置&#xff1a; 系统管理员在管…

2000年 - 2022年 Fama-French三因子模型数据+代码

Fama-French三因子模型是由著名经济学家尤金法玛&#xff08;Eugene Fama&#xff09;和肯尼斯法兰奇&#xff08;Kenneth French&#xff09;提出的&#xff0c;旨在改进资本资产定价模型&#xff08;CAPM&#xff09;&#xff0c;更全面地解释资产收益率的变化。该模型认为&a…

pytest测试框架pytest-rerunfailures插件重试失败用例

Pytest提供了丰富的插件来扩展其功能&#xff0c;介绍下插件pytest-rerunfailures &#xff0c;用于在测试用例失败时自动重新运行这些测试用例。 pytest-rerunfailures官方显示的python和pytest版本限制&#xff1a; Python 3.8pytest 7.2 或更新版本 此插件可以通过以下可…

CentOS 7 内核 3.10 升级 6.5.2 (RPM 直装 + 源码编译)

方案一 直接基于 RPM 在线升级&#xff08;简单&#xff0c;速度快&#xff09; rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org yum install https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm -y # &#xff08;选项一&#xff09;升级最新版内…

【GD32】从零开始学兆易创新32位微处理器——RTC实时时钟+日历例程

1 简介 RTC实时时钟顾名思义作用和墙上挂的时钟差不多&#xff0c;都是用于记录时间和日历&#xff0c;同时也有闹钟的功能。从硬件实现上来说&#xff0c;其实它就是一个特殊的计时器&#xff0c;它内部有一个32位的寄存器用于计时。RTC在低功耗应用中可以说相当重要&#xf…

Linux操作系统段式存储管理、 段页式存储管理

1、段式存储管理 1.1分段 进程的地址空间&#xff1a;按照程序自身的逻辑关系划分为若干个段&#xff0c;每个段都有一个段名&#xff08;在低级语言中&#xff0c;程序员使用段名来编程&#xff09;&#xff0c;每段从0开始编址。内存分配规则&#xff1a;以段为单位进行分配…

Redis-事务-watch-unwatch

文章目录 1、监视key2、提交事务 1、监视key 打开两个窗口&#xff0c;第一个窗口先监视key&#xff0c;然后开始事务&#xff0c;然后再打开第二个窗口&#xff0c;修改balance为0 2、提交事务 此时事务被打断

分布式定时任务系列10:XXL-job源码分析之路由策略

传送门 分布式定时任务系列1&#xff1a;XXL-job安装 分布式定时任务系列2&#xff1a;XXL-job使用 分布式定时任务系列3&#xff1a;任务执行引擎设计 分布式定时任务系列4&#xff1a;任务执行引擎设计续 分布式定时任务系列5&#xff1a;XXL-job中blockingQueue的应用 …