当前位置: 首页 > news >正文

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 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_countfrom user_order uo, order_item oiwhere uo.order_id = oi.order_idand 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_itemwhere order_id = #{orderId, jdbcType=INTEGER}
</select>

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

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

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

<select id="selectUserOrderAndOrderItemsByOrderNoNest" resultMap="userOrderContainOrderItemNestMap">select *from user_orderwhere 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_countfrom user u, user_order uo, pay_order po, order_item oiwhere u.user_id = #{userId, jdbcType=INTEGER}and u.user_id = uo.user_idand uo.order_id = po.order_idand 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_countfrom user u, user_order uo, pay_order po, order_item oiwhere u.user_id = #{userId, jdbcType=INTEGER}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 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);

相关文章:

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

大家好&#xff0c;我是王有志&#xff0c;一个分享硬核 Java 技术的金融摸鱼侠&#xff0c;欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。 在学习完上一篇文章《MyBatis映射器&#xff1a;一对一关联查询》后&#xff0c;相信你已经掌握了如何在 MyBatis 映射器…...

100多个ChatGPT指令提示词分享

当前&#xff0c;ChatGPT几乎已经占领了整个互联网。全球范围内成千上万的用户正使用这款人工智能驱动的聊天机器人来满足各种需求。然而&#xff0c;并不是每个人都知道如何充分有效地利用ChatGPT的潜力。其实有许多令人惊叹的ChatGPT指令提示词&#xff0c;可以提升您与ChatG…...

vue2和vue3数据代理的区别

前言&#xff1a; vue2 的双向数据绑定是利⽤ES5的⼀个 API &#xff0c;Object.defineProperty( )对数据进行劫持结合发布订阅模式的方式来实现的。 vue3 中使⽤了 ES6的Proxy代理对象&#xff0c;通过 reactive() 函数给每⼀个对象都包⼀层Proxy&#xff0c;通过 Proxy监听属…...

已解决ApplicationException异常的正确解决方法,亲测有效!!!

已解决ApplicationException异常的正确解决方法&#xff0c;亲测有效&#xff01;&#xff01;&#xff01; 目录 问题分析 出现问题的场景 报错原因 解决思路 解决方法 分析错误日志 检查业务逻辑 验证输入数据 确认服务器端资源的可用性 增加对特殊业务情况的处理…...

「前端+鸿蒙」鸿蒙应用开发-常用UI组件-图片-参数

在鸿蒙应用开发中,图片组件是展示图像的关键UI元素。以下是详细介绍图片组件的三个主要参数:图片尺寸、图片缩放和图片插值,并提供相应的示例代码。 图片尺寸 图片尺寸指的是图片组件在界面上显示的宽度和高度。你可以使用像素(px)或其他单位来指定尺寸。 width: 设置图片…...

Tobii Pro Lab 1.232是全球领先的眼动追踪研究实验软件

Tobii Pro Lab是全球领先的眼动追踪研究实验软件。软件功能强大且拥有友好的用户界面&#xff0c;使眼动追踪研究变得更加简单、高效。该软件提供了很高的灵活性&#xff0c;可运行高级实验&#xff0c;深入了解注意力和认知过程。 获取软件安装包以及永久授权联系邮箱:289535…...

【flink实战】flink-connector-mysql-cdc导致mysql连接器报类型转换错误

文章目录 一. 报错现象二. 方案二&#xff1a;重新编译打包flink-connector-cdc1. 排查脚本2. 重新编译打包flink-sql-connector-mysql-cdc-2.4.0.jar3. 测试flink环境 三. 方案一&#xff1a;改造flink连接器 一. 报错现象 flink sql任务是&#xff1a;mysql到hdfs的离线任务&…...

【Linux】系统文件IO·文件描述符fd

前言 C语言文件接口 C 语言读写文件 1.C语言写入文件 2.C语言读取文件 stdin/stdout/stderr 系统文件IO 文件描述符fd&#xff1a; 文件描述符分配规则&#xff1a; 文件描述符fd&#xff1a; 前言 我们早在C语言中学习关于如何用代码来管理文件&#xff0c;比如文件的…...

【计算机网络篇】数据链路层(6)共享式以太网_网络适配器_MAC地址

文章目录 &#x1f354;网络适配器&#x1f354;MAC地址&#x1f5d2;️IEEE 802局域网的MAC地址格式&#x1f4d2;IEEE 802局域网的MAC地址发送顺序&#x1f95a;单播MAC地址&#x1f95a;广播MAC地址&#x1f95a;多播MAC地址&#x1f50e;小结 &#x1f354;网络适配器 要将…...

导入别人的net文件报红问题sdk

1. 使用cmd命令 dotnet --info 查看自己使用的SDK版本 2.直接找到项目中的 global.json 文件&#xff0c;右键打开&#xff0c;直接修改版本为本机的SDK版本&#xff0c;就可以用了...

LangChain 介绍

In recent times, you would probably have heard of many AI applications, one of them being chatpdf.com. 在最近&#xff0c;你可能听说过很多的AI应用&#xff0c;chatpdf.com就是其中的一个。 On this website, you can upload your own PDF. After uploading, you ca…...

【区分vue2和vue3下的element UI Avatar 头像组件,分别详细介绍属性,事件,方法如何使用,并举例】

在 Vue 2 的 Element UI 和 Vue 3 的 Element Plus 中&#xff0c;Avatar 头像组件可能并没有直接作为官方组件库的一部分。然而&#xff0c;为了回答你的问题&#xff0c;我将假设 Element UI 和 Element Plus 在未来的版本中可能添加了 Avatar 组件&#xff0c;或者我们将使用…...

数据分析必备:一步步教你如何用matplotlib做数据可视化(10)

1、Matplotlib 二维箭头图 箭头图将速度矢量显示为箭头&#xff0c;其中分量(u&#xff0c;v)位于点(x&#xff0c;y)。 quiver(x,y,u,v)上述命令将矢量绘制为在x和y中每个对应元素对中指定的坐标处的箭头。 参数 下表列出了quiver()函数的参数 - x - 1D或2D阵列&#xff0c;…...

Stable Diffusion部署教程,开启你的AI绘图之路

本文环境 系统&#xff1a;Ubuntu 20.04 64位 内存&#xff1a;32G 环境安装 2.1 安装GPU驱动 在英伟达官网根据显卡型号、操作系统、CUDA等查询驱动版本。官网查询链接https://www.nvidia.com/Download/index.aspx?langen-us 注意这里的CUDA版本&#xff0c;如未安装CUD…...

三生随记——诡异的牙线

在小镇的角落&#xff0c;坐落着一间古老的牙医诊所。这所诊所早已荒废多年&#xff0c;窗户上爬满了藤蔓&#xff0c;门板上的油漆斑驳脱落&#xff0c;仿佛诉说着无尽的沉寂与孤独。然而&#xff0c;在午夜时分&#xff0c;偶尔会有低沉的呻吟声从紧闭的诊所里传出&#xff0…...

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

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

学习笔记——路由网络基础——路由转发

六、路由转发 1、最长匹配原则 最长匹配原则 是支持IP路由的设备默认的路由查找方式(事实上几乎所有支持IP路由的设备都是这种查找方式)。当路由器收到一个IP数据包时&#xff0c;会将数据包的目的IP地址与自己本地路由表中的表项进行逐位(Bit-By-Bit)的逐位查找&#xff0c;…...

Python网络安全项目开发实战,如何防命令注入

注意:本文的下载教程,与以下文章的思路有相同点,也有不同点,最终目标只是让读者从多维度去熟练掌握本知识点。 下载教程: Python网络安全项目开发实战_防命令注入_编程案例解析实例详解课程教程.pdf 在Python网络安全项目开发中,防止命令注入(Command Injection)是一项…...

程序员如何高效读代码?

程序员高效读代码的技巧包括以下几点&#xff1a; 明确阅读目的&#xff1a;在开始阅读代码之前&#xff0c;先明确你的阅读目的。是为了理解整个系统的架构&#xff1f;还是为了修复一个具体的bug&#xff1f;或者是为了了解某个功能是如何实现的&#xff1f;明确目的可以帮助…...

全面分析一下前端框架Angular的来龙去脉,分析angular的技术要点和难点,以及详细的语法和使用规则,底层原理-小白进阶之路

Angular 前端框架全面分析 Angular 是一个由 Google 维护的开源前端框架。它最早在 2010 年发布&#xff0c;最初版本称为 AngularJS。2016 年&#xff0c;团队发布了一个完全重写的版本&#xff0c;称为 Angular 2&#xff0c;之后的版本&#xff08;如 Angular 4、Angular 5…...

VACUUM 剖析

VACUUM 剖析 为什么需要 Vacuum MVCC MVCC&#xff1a;Multi-Version Concurrency Control&#xff0c;即多版本并发控制。 PostgreSQL 使用多版本并发控制&#xff08;MVCC&#xff09;来支持高并发的事务处理&#xff0c;同时保持数据的一致性和隔离性。MVCC 是一种用于管…...

基于LangChain框架搭建知识库

基于LangChain框架搭建知识库 说明流程1.数据加载2.数据清洗3.数据切分4.获取向量5.向量库保存到本地6.向量搜索7.汇总调用 说明 本文使用openai提供的embedding模型作为框架基础模型&#xff0c;知识库的搭建目的就是为了让大模型减少幻觉出现&#xff0c;实现起来也很简单&a…...

LeetCode 1789, 6, 138

目录 1789. 员工的直属部门题目链接表要求知识点思路代码 6. Z 字形变换题目链接标签思路代码 138. 随机链表的复制题目链接标签思路代码 1789. 员工的直属部门 题目链接 1789. 员工的直属部门 表 表Employee的字段为employee_id&#xff0c;department_id和primary_flag。…...

Redis部署模式全解析:单点、主从、哨兵与集群

Redis是一个高性能的键值存储系统&#xff0c;以其丰富的数据结构和优异的读写性能而闻名。在实际应用中&#xff0c;根据业务需求的不同&#xff0c;Redis可以部署在多种模式下。本文将详细介绍Redis的四种主要部署模式&#xff1a;单点模式、主从复制模式、哨兵模式以及集群模…...

python-docx顺序读取word内容

来源How to use Python iteration to read paragraphs, tables and pictures in word&#xff1f; Issue #650 python-openxml/python-docx (github.com) from docx import Document from docx.oxml.ns import qndef iter_block_items(parent):"""生成 paren…...

kafka 集群原理设计和实现概述(一)

kafka 集群原理设计和实现概述(一) Kafka 集群的设计原理是为了实现高可用性、高吞吐量、容错性和可扩展性。以下是 Kafka 集群的设计原 理及其实现方法: 1. 分布式架构设计 Kafka 采用分布式架构,集群中的多个 Broker 共同工作,负责接收、存储和传递消息。通过将数据分布…...

three.js 第十一节 - uv坐标

// ts-nocheck // 引入three.js import * as THREE from three // 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls // 导入lil.gui import { GUI } from three/examples/jsm/libs/lil-gui.module.min.js // 导入tween import * as T…...

git从master分支创建分支

1. 切换到主分支或你想从哪里创建新分支 git checkout master 2. 创建并切换到新的本地分支 develop git checkout -b develop 3. 将新分支推送到远程存储库 git push origin develop 4. 设置本地 develop 分支跟踪远程 develop 分支 git branch --set-upstream-toorigi…...

Chromium 调试指南2024 Mac篇 - 准备工作 (一)

1.引言 Chromium是一个由Google主导开发的开源浏览器项目&#xff0c;它为Google Chrome浏览器提供了基础框架。Chromium不仅是研究和开发现代浏览器技术的重要平台&#xff0c;还为众多其他基于Chromium的浏览器&#xff08;如Microsoft Edge、Brave等&#xff09;提供了基础…...

vue登陆密码加密,java后端解密

前端 安装crypto-js npm install crypto-js加密 //引入crypto-js import CryptoJS from crypto-js;/** ---密码加密 start--- */ const SECRET_KEY CryptoJS.enc.Utf8.parse("a15q8f6s5s1a2v3s"); const SECRET_IV CryptoJS.enc.Utf8.parse("a3c6g5h4v9sss…...

npm 安装踩坑

1 网络正常&#xff0c;但是以前的老项目安装依赖一直卡住无法安装&#xff1f;哪怕切换成淘宝镜像 解决办法&#xff1a;切换成yarn (1) npm i yarn -g(2) yarn init(3) yarn install在安装的过程中发现&#xff1a; [2/4] Fetching packages... error marked11.1.0:…...

内容安全复习 6 - 白帽子安全漏洞挖掘披露的法律风险

文章目录 安全漏洞的法律概念界定安全漏洞特征白帽子安全漏洞挖掘面临的法律风险“白帽子”安全漏洞挖掘的风险根源“白帽子”的主体边界授权行为边界关键结论 安全漏洞的法律概念界定 可以被利用来破坏所在系统的网络或信息安全的缺陷或错误&#xff1b;被利用的网络缺陷、错…...

dp经典问题:爬楼梯

dp经典问题&#xff1a;爬楼梯 爬楼梯 三步问题。有个小孩正在上楼梯&#xff0c;楼梯有n阶台阶&#xff0c;小孩一次可以上1阶、2阶或3阶。实现一种方法&#xff0c;计算小孩有多少种上楼梯的方式。结果可能很大&#xff0c;你需要对结果模1000000007。 Step1: 识别问题 这…...

示例:推荐一个基于第三方QRCoder.Xaml封装的二维码显示控件

一、目的&#xff1a;基于第三方QRCoder.Xaml封装的二维码控件&#xff0c;为了方便WPF调用 二、效果如下 功能包括&#xff1a;背景色&#xff0c;前景色&#xff0c;中心图片设置和修改大小&#xff0c;二维码设置等 三、环境 VS2022 四、使用方式 1、安装nuget包&#xf…...

阿里云服务器618没想到这么便宜,买早了!

2年前&#xff0c;我买了个服务器&#xff0c;租用服务器&#xff08;ECS5&#xff09;和网络宽带&#xff08;1M&#xff09;&#xff0c;可以说是非常非常低的配置了。 当时5年的折扣力度最大&#xff0c;但是打完折后&#xff0c;价格依然要近3000多元。 最近看到阿里云618活…...

提升Python技能的七个函数式编程技巧

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 递归📝 结构化模式匹配📝 不变性📝 纯函数📝 高阶函数📝 函数组合📝 惰性求值⚓️ 相关链接 ⚓️📖 介绍 📖 在现代编程中,虽然Python并不是一门纯粹的函数式编程语言,但函数式编程(Funct…...

微型操作系统内核源码详解系列五(五):cm3下Pendsv切换任务上篇

系列一&#xff1a;微型操作系统内核源码详解系列一&#xff1a;rtos内核源码概论篇&#xff08;以freertos为例&#xff09;-CSDN博客 系列二&#xff1a;微型操作系统内核源码详解系列二&#xff1a;数据结构和对象篇&#xff08;以freertos为例&#xff09;-CSDN博客 系列…...

Django测试平台搭建学习笔记1

一安装 pip离线安装requests2.32.0所需要的依赖&#xff1a; : charset-normalizer<4,>2 (3.0.0b1) : idna<4,>2.5 (3.7) : urllib3<3,>1.21.1 (2.2.0) : certifi>2017.4.17 (2024.6.2) pip离线安装pytest8.2.0所需要的依赖&#xff1a; : iniconfig (2…...

本地离线模型搭建指南-RAG架构实现

搭建一个本地中文大语言模型&#xff08;LLM&#xff09;涉及多个关键步骤&#xff0c;从选择模型底座&#xff0c;到运行机器和框架&#xff0c;再到具体的架构实现和训练方式。以下是一个详细的指南&#xff0c;帮助你从零开始构建和运行一个中文大语言模型。 本地离线模型搭…...

【IPython 使用技巧整理】

IPython 使用技巧整理 IPython 是一个交互式 Python 解释器&#xff0c;比标准 Python 解释器提供了更加强大的功能和更友好的使用体验。它为数据科学、机器学习和科学计算提供了强大的工具&#xff0c;是 Python 开发人员不可或缺的工具之一。本文将深入探讨 IPython 的各种使…...

什么是孪生素数猜想

什么是孪生素数猜想 素数p与素数p2有无穷多对 孪生素数的公式&#xff08;详见百度百科&#xff1a;孪生素数公式&#xff09; 利用素数的判定法则&#xff0c;可以得到以下的结论&#xff1a;“若自然数q与q2都不能被任何不大于的素数 整除&#xff0c;则q与q 2都是素数”…...

Python学习笔记16:进阶篇(五)异常处理

异常 在编程中&#xff0c;异常是指程序运行过程中发生的意外事件&#xff0c;这些事件通常中断了正常的指令流程。它们可能是由于错误的输入数据、资源不足、非法操作或其他未预料到的情况引起的。Python中&#xff0c;当遇到这类情况时&#xff0c;会抛出一个异常对象&#…...

Mac 安装依赖后依旧报错 ModuleNotFoundError: No module named ‘Crypto‘

ModuleNotFoundError: No module named ‘Crypto’ 解决办法 pip uninstall pycryptodome pip uninstall pycrypto pip uninstall crypto pip install pycrypto...

【07】持久化-数据库选择和设计

1. 数据库选择 在比特币原始论文中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。Bitcoin Core ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是 LevelDB。 我们将要使用的是BoltDB。Bolt DB是一个纯键值存储的 Go 数据库。没有具体的数据…...

压力测试

1.什么是压力测试 压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内&#xff0c;做到心中有数 使用压力测试&#xff0c;我们有希望找到很多种用其他测试方法更难发现的错误&#…...

C语言| 数组元素的删除

同数组元素的插入差不多。 数组元素的插入&#xff0c;是先移动要插入元素位置后面的所有元素&#xff0c;再插入新元素&#xff0c;长度1。 C语言| 数组的插入-CSDN博客 数组元素的删除&#xff0c;是先删除元素&#xff0c;再把后面的元素往前移动一位&#xff0c;而本程序…...

QListView、QTableView或QTreeView截取滚动区域(截长图)

本文以QTreeView为例,理论上继承自QAbstractScrollArea的类都支持本文所述的方法。 一.效果 一共5个文件夹,每个文件文件夹下有5个文件,先把文件夹展开,然后截图。将滚动条拖到居中位置,是为了证明截图对滚动条无影响 下面是截的图 二.原理 将滚动区域的viewport设置为…...

论文《Tree Decomposed Graph Neural Network》笔记

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

控制下属很简单,用好这3大管人绝招,再跳的刺头也不敢造次

控制下属很简单&#xff0c;用好这3大管人绝招&#xff0c;再跳的刺头也不敢造次 第一招&#xff1a;给压力 很多团队中的员工都是自己不带脑子工作&#xff0c;遇事就喜欢请示领导&#xff0c;让领导拿方案、拿决策。 还有一些人&#xff0c;推一下&#xff0c;他才动一下&a…...

2.APP测试-安卓adb抓取日志

1.打开手机的开发者模式&#xff0c;打开USB调试 &#xff08;1&#xff09;小米手机打开开发者模式&#xff1a; 【设置】-【我的设备】-【全部参数信息】-快速多次点击【OS版本】-进入开发者模式 &#xff08;2&#xff09;连接手机和电脑&#xff0c;手机打开USB调试 【设置…...

云计算【第一阶段(21)】Linux引导过程与服务控制

目录 一、linux操作系统引导过程 1.1、开机自检 1.2、MBR引导 1.3、GRUB菜单 1.4、加载 Linux 内核 1.5、init进程初始化 1.6、简述总结 1.7、初始化进程centos 6和7的区别 二、排除启动类故障 2.1、修复MBR扇区故障 2.1.1、 实验 2.2、修复grub引导故障 2.2.1、实…...

RabbitMQ的WorkQueues模型

WorkQueues模型 Work queues&#xff0c;任务模型。简单来说就是让多个消费者绑定到一个队列&#xff0c;共同消费队列中的消息。 当消息处理比较耗时的时候&#xff0c;可能生产消息的速度会远远大于消息的消费速度。长此以往&#xff0c;消息就会堆积越来越多&#xff0c;…...

Python入门-基本数据类型-字符串类型及其操作

字符串类型存储的数据是字符串&#xff0c;字符串是一个由字符构成的序列。Python字符串是不可变的 不支持动态修改。本节将对字符串进行简单介绍&#xff0c;包括字符串的定义方式、格式化、索引、切片 拼接、重复和成员归属等。 1.字符串的定义方式 1.1单行字符串 单行字符…...

数据库原理之数据库基本概念

目录 前言 基本概念 数据库完整性 前言 今天我们来看看数据库的基本概念&#xff0c;帮助大家对数据库有一点点最基本的了解 基本概念 4个基本概念 数据data&#xff1a;描述事物的符号&#xff0c;数据库中存储的基本对象。 数据库Database&#xff1a;长期存储在计算机…...

2024年6月26日 (周三) 叶子游戏新闻

老板键工具来唤去: 它可以为常用程序自定义快捷键&#xff0c;实现一键唤起、一键隐藏的 Windows 工具&#xff0c;并且支持窗口动态绑定快捷键&#xff08;无需设置自动实现&#xff09;。 土豆录屏: 免费、无录制时长限制、无水印的录屏软件 《Granblue Fantasy Versus: Risi…...

算法 —— 双指针

目录 移动零 复写零 快乐数 盛最多水的容器 有效三角形的个数 查找总价格为目标值的两个商品 三数之和 四数之和 移动零 下图以样例1为例&#xff0c;看下图如何做到保证非零元素相对顺序前提下&#xff0c;移动零元素。 代码实现如下&#xff1a; class Solution {…...

最长续航708公里阿尔法S5足以让Model3汗颜?

近日,极狐阿尔法S5已开启预售,预售价格分别为19.98万元、21.98万元,这款定位中型轿车的纯电是否有让同级竞品——Model 3汗颜的实力呢?据了解,阿尔法S5以电动性能著称,新车采用了豪华纯电车型中才会见到的同步+异步双电机组合,总功率高达390kW,总扭矩达到690Nm,百公里…...

从29W降至17W!成豪华车“价格屠夫”,190ps配四驱,月销仅401

国内汽车市场,说到豪车那么德系奔驰绝对榜上有名,作为一线豪华品牌,奔驰曾经不管是口碑还是销量都非常棒,为广大车迷上市了很多经典的标杆车型,可以毫不夸张的说,曾经只要开着一辆奔驰车回村,即便是最紧凑型的A级车,也能带来很高的回头率,地位瞬间就不一样了,可是随着…...

五菱高管发文“明年更卷”,消费者:车市越卷,我越幸福

日前,上汽通用五菱品牌事业部副总经理周钘在社交平台上发文称,“2024年初至今,宝骏停掉了所有的市场费用。企业认为如果产品、市场、渠道三者节奏都不对则是‘白费’”,“虽然今年行业确实卷,明年会更卷,但我们所有准备”。周钘从车企的角度,说出了车市竞争的残酷。不仅…...

型格/飞度無限MUGEN版上市售价9.78万元起

3月7日,广汽本田無限MUGEN热血双核在亚洲规模最大的改装车展“2024深圳国际定制改装汽车展览会”上市。型格無限MUGEN版售价15.79万元起,飞度無限MUGEN版售价9.78万元起。型格/飞度無限MUGEN版是市面少有的量产官改神车,上路合规,配齐玩车迷梦寐以求的無限MUGEN定制套件,每…...

云原生架构内涵_3.主要架构模式

云原生架构有非常多的架构模式&#xff0c;这里列举一些对应用收益更大的主要架构模式&#xff0c;如服务化架构模式、Mesh化架构模式、Serverless模式、存储计算分离模式、分布式事务模式、可观测架构、事件驱动架构等。 1.服务化架构模式 服务化架构是云时代构建云原生应用的…...

线程安全 - 笔记

1 程序a调用c.so,程序b也调用c.so c.so加载两次吗? 在这种情况下,通常 c.so 不会被加载两次。 当一个程序调用一个共享对象文件(.so)时,操作系统的动态链接器将该共享对象映射到进程的虚拟内存空间中。后续由不同程序或者同一个程序调用相同的共享对象,都不会导致共享…...