MyBatis映射器:一对多关联查询

news2025/1/17 6:00:37

大家好,我是王有志,一个分享硬核 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);

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

天气冷电脑不能启动找不到硬盘

https://diy.zol.com.cn/2004/0611/101994.shtml

es的检索-DSL语法和Java-RestClient实现

基本语法&#xff1a; GET /索引库名/_search {"query": {"查询类型": {"查询条件"}} }RestClient的导入在RestClient操作索引库和文档有介绍 查询所有&#xff1a; # 查询所有 GET /test/_search {"query": {"match_all"…

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

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

【高考选专业 | 家长篇】2024,计算机何去何从?小P老师带你看

目录 2024年&#xff0c;计算机相关专业还值得选择吗&#xff1f;1.行业竞争现状2.专业前景分析 2024年&#xff0c;计算机相关专业还值得选择吗&#xff1f; 随着2024年高考落幕&#xff0c;数百万高三学生又将面临人生中的重要抉择&#xff1a;选择大学专业。有人欢喜&#x…

2024最新版:C++用Vcpkg搭配VS2022安装matplotlib-cpp库

matplotlib-cpp是一个用于在C中使用matplotlib绘图库的头文件库。它提供了一个简单的接口&#xff0c;使得在C中创建和显示图形变得更加容易。这个库的灵感来自于Python的matplotlib库&#xff0c;它使得在C中进行数据可视化变得更加便捷。 matplotlib-cpp允许在C中使用类似Py…

linux使用docker部署kafka集群

1、拉取kafka docker pull wurstmeister/kafka docker pull wurstmeister/zookeeper 2、创建网络 docker network create app-kafka 3、启动zookeeper docker run -d \--name zookeeper \-p 2181:2181 \--network app-kafka \--restart always \wurstmeister/zookeeper …

Java基础的重点知识-06-String、Arrays、Math、static关键字

文章目录 String类(java.lang)static关键字Arrays类(java.uitl)Math类&#xff08;java.lang&#xff09; String类(java.lang) java.lang.String 类代表字符串。 特征&#xff1a; 字符串不变&#xff1a;字符串的值在创建后不能被更改。因为String对象是不可变的&#xff…

【2024德国签证】资金证明详解|从线上开户到签证申请超详细指南!

保证金介绍 1.什么是德国保证金&#xff08;自保金 Sperrkonto&#xff09;&#xff1f; 来自非欧洲国家的国际学生必须提供经济资金证明&#xff0c;以证明你能支持你在德国的生活和学习。 保证金用途&#xff1a;非欧盟国家的公民&#xff0c;需要签证来德国&#xff0c;必…

安徽理工大学2计算机考研情况,招收计算机专业的学院和联培都不少!

安徽理工大学&#xff08;Anhui University of Science and Technology&#xff09;&#xff0c;位于淮南市&#xff0c;是安徽省和应急管理部共建高校&#xff0c;安徽省高等教育振兴计划“地方特色高水平大学”建设高校&#xff0c;安徽省高峰学科建设计划特别支持高校&#…

C++初学者指南第一步---13.聚合类型

C初学者指南第一步—13.聚合类型 文章目录 C初学者指南第一步---13.聚合类型1. 类型分类&#xff08;简化&#xff09;2. 如何定义和使用3. 为什么选择自定义类型/数据聚合&#xff1f;4. 聚合类型初始化5.混合6. 复制7. 值和引用的语义8.聚合的向量(std::vector)9.最令人烦恼的…

python使用pywebview打造一个现代化的可视化GUI界面

&#x1f308;所属专栏&#xff1a;【python】✨作者主页&#xff1a; Mr.Zwq✔️个人简介&#xff1a;一个正在努力学技术的Python领域创作者&#xff0c;擅长爬虫&#xff0c;逆向&#xff0c;全栈方向&#xff0c;专注基础和实战分享&#xff0c;欢迎咨询&#xff01; 您的…

不使用ES,如何使用MySQL实现模糊匹配

1.业务场景 例如&#xff1a;我们实现一个公司的申请审批流程&#xff0c;整个业务流程涉及到两种角色&#xff0c;分别是商务角色与管理员角色&#xff0c;整个流程如下图所示&#xff1a; 核心流程总结为一句话&#xff1a;商务角色申请添加公司后由管理员进行审批。 商务在…

Android中如何动态的调整Dialog的背景深暗

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 在 Android 开发中&#xff0c;当你使用 Dialog 或 DialogFragment 时&#xff0c;可以通过设置 Window 的背景变暗来突出它的可见性。这个效果…

数据虚拟化、Data Fabric(数据编织)的兴起,对数据管理有何帮助?

数字化时代&#xff0c;虚拟化&#xff08;Virtualization&#xff09;并不是一个很陌生的词汇&#xff0c;它是现代数据中心资源管理的核心技术之一&#xff0c;是对 IT 资源&#xff08;如服务器、存储设备、网络设备等&#xff09;的抽象&#xff0c;通过屏蔽 IT 资源的物理…

C语言入门4-函数和程序结构

函数举例 读取字符串&#xff0c;如果字符串中含有ould则输出该字符串&#xff0c;否则不输出。 #include <stdio.h>// 函数声明 int getLine(char s[], int lim); int strindex(char s[], char t[]);int main() {char t[] "ould"; // 要查找的目标子字符串…

windows和linux路径斜杆转换脚本,打开即用

前言&#xff1a; windows和linux的目录路径斜杆是相反的&#xff0c;在ssh或者其他什么工具在win和ubuntu传文件时候经常需要用到两边的路径&#xff0c;有这个工具就不用手动去修改斜杆反斜杠了。之前有个在线网站&#xff0c;后来挂了&#xff0c;就想着自己搞一个脚本来用。…

批量重命名神器揭秘:一键实现文件夹随机命名,自定义长度轻松搞定!

在数字化时代&#xff0c;我们经常需要管理大量的文件夹&#xff0c;尤其是对于那些需要频繁更改或整理的文件来说&#xff0c;给它们进行批量重命名可以大大提高工作效率。然而&#xff0c;传统的重命名方法既繁琐又耗时&#xff0c;无法满足高效工作的需求。今天&#xff0c;…

【C++】初始化列表、匿名对象、static成员、友元、内部类

文章目录 一、初始化列表构造函数体赋值初始化列表explicit关键字 二、匿名对象三、static成员四、友元友元函数友元类 五、内部类六、练习题 一、初始化列表 构造函数体赋值 实际上&#xff0c;构造函数的函数体内&#xff0c;并不是对 对象 初始化的地方&#xff0c;而是对…

电子杂志制作的必备软件:轻松提升制作效率

​电子杂志作为一种新型的媒体形式&#xff0c;具有互动性强、内容丰富、传播范围广等特点。随着互联网的普及&#xff0c;越来越多的企业和个人开始关注和投入电子杂志的制作。然而&#xff0c;电子杂志的制作过程往往复杂繁琐&#xff0c;需要付出大量的时间和精力。为了提高…

微信小程序学习(六):常用原生 API

&#x1f517;API官方文档 1、网络请求 wx.request({// 接口地址&#xff0c;仅为示例&#xff0c;并非真实的接口地址url: example.php,// 请求的参数data: { x: },// 请求方式 GET|POST|PUT|DELETEmethod: GET,success (res) {console.log(res.data)},fail(err) {console.…