Java 实战:拆分复杂 SQL,代码分层组装数据(微服务 / 多表联查场景)
2026/6/26 22:13:54 网站建设 项目流程

目录

一、业务背景(真实业务场景)

两种实现对比

二、数据库表结构(简化版)

三、分层架构说明

四、完整代码实战

1. 底层 DO 实体(对应单表)

OrderDO

OrderItemDO

UserDO、GoodsDO、LogisticsDO 省略(字段和表一一对应)

2. 聚合 VO(前端最终返回对象,复杂组装结果)

3. Mapper 层:全部单表简单查询(无任何 JOIN)

OrderMapper

OrderItemMapper

UserMapper

GoodsMapper、LogisticsMapper 同理,只提供批量 ID 查询接口

4. Service 核心:代码层面组装数据(核心实战逻辑)

五、Controller 调用层

六、核心技术要点(生产环境必看)

1. 为什么拆分 SQL 比多表 JOIN 更好?

2. 避坑关键点(防止 N+1 查询灾难)

3. 内存组装三大工具

4. 性能优化补充方案

七、适用 & 不适用场景

适合拆分 SQL 代码组装

不适合(直接写简单 JOIN 即可)

八、对比巨型 JOIN SQL 反面示例(不推荐)

九、扩展进阶:MyBatis-Plus 简化批量查询


一、业务背景(真实业务场景)

需求:电商订单详情页,需要一次性返回:

  1. 订单主表t_order:订单基础信息
  2. 订单商品明细表t_order_item:多件商品
  3. 用户表t_user:下单人昵称、手机号
  4. 商品表t_goods:商品名称、图片、单价
  5. 物流表t_order_logistics:快递单号、收货地址

两种实现对比

  1. 传统写法(致命问题):写一张超级大LEFT JOIN多表 SQL,几十行关联,分页慢、索引失效、维护困难、联表锁表、扩展字段就要改 SQL;
  2. 推荐方案单表简单 SQL 分开查询,Java 内存组装 VO,解耦、易优化、分库分表友好、可加缓存。

二、数据库表结构(简化版)

sql

-- 订单主表 CREATE TABLE t_order ( order_id BIGINT PRIMARY KEY, user_id BIGINT, order_no VARCHAR(32), total_amount DECIMAL(10,2), create_time DATETIME ); -- 订单商品明细(一对多:1订单N商品) CREATE TABLE t_order_item ( id BIGINT PRIMARY KEY, order_id BIGINT, goods_id BIGINT, buy_num INT, item_amount DECIMAL(10,2) ); -- 用户表 CREATE TABLE t_user ( user_id BIGINT PRIMARY KEY, nick_name VARCHAR(50), phone VARCHAR(11) ); -- 商品表 CREATE TABLE t_goods ( goods_id BIGINT PRIMARY KEY, goods_name VARCHAR(100), cover_img VARCHAR(255), price DECIMAL(10,2) ); -- 物流表(一对一:1订单1物流) CREATE TABLE t_order_logistics ( logistics_id BIGINT PRIMARY KEY, order_id BIGINT, express_no VARCHAR(32), receive_addr VARCHAR(200) );

三、分层架构说明

Controller → Service → Mapper VO(前端聚合实体) ← 代码组装 ← 多个单表DO

核心思想:只做单表简单查询,所有关联、分组、匹配逻辑交给 Java 代码

四、完整代码实战

1. 底层 DO 实体(对应单表)

OrderDO
@Data public class OrderDO { private Long orderId; private Long userId; private String orderNo; private BigDecimal totalAmount; private LocalDateTime createTime; }
OrderItemDO
@Data public class OrderItemDO { private Long id; private Long orderId; private Long goodsId; private Integer buyNum; private BigDecimal itemAmount; }
UserDO、GoodsDO、LogisticsDO 省略(字段和表一一对应)

2. 聚合 VO(前端最终返回对象,复杂组装结果)

@Data public class OrderDetailVO { // 订单基础 private Long orderId; private String orderNo; private BigDecimal totalAmount; private LocalDateTime createTime; // 用户信息 一对一 private String userNick; private String userPhone; // 物流信息 一对一 private String expressNo; private String receiveAddr; // 商品明细列表 一对多 private List<OrderItemVO> itemList; @Data public static class OrderItemVO { private Long goodsId; private String goodsName; private String coverImg; private BigDecimal price; private Integer buyNum; private BigDecimal itemAmount; } }

3. Mapper 层:全部单表简单查询(无任何 JOIN)

OrderMapper
@Mapper public interface OrderMapper { // 根据订单ID查订单 OrderDO selectById(Long orderId); // 批量查订单(批量场景用) List<OrderDO> selectBatchIds(@Param("orderIds") List<Long> orderIds); }
OrderItemMapper
@Mapper public interface OrderItemMapper { // 根据订单ID集合批量查明细 List<OrderItemDO> selectByOrderIds(@Param("orderIds") List<Long> orderIds); }
UserMapper
@Mapper public interface UserMapper { List<UserDO> selectBatchIds(@Param("userIds") List<Long> userIds); }
GoodsMapper、LogisticsMapper 同理,只提供批量 ID 查询接口

关键点:全部使用 IN 批量查询,杜绝循环单条查数据库(N+1 问题)

4. Service 核心:代码层面组装数据(核心实战逻辑)

@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderItemMapper itemMapper; @Autowired private UserMapper userMapper; @Autowired private GoodsMapper goodsMapper; @Autowired private LogisticsMapper logisticsMapper; /** * 查询单个订单详情(拆分多SQL,内存组装) */ @Override public OrderDetailVO getOrderDetail(Long orderId) { // ====================== 步骤1:单表查询各层基础数据 ====================== // 1. 查询订单主数据 OrderDO orderDO = orderMapper.selectById(orderId); if (orderDO == null) { return null; } Long userId = orderDO.getUserId(); // 2. 查询当前订单所有商品明细 List<OrderItemDO> itemDOList = itemMapper.selectByOrderIds(List.of(orderId)); // 3. 收集明细里所有商品ID,批量查商品信息 List<Long> goodsIdList = itemDOList.stream() .map(OrderItemDO::getGoodsId) .distinct() .collect(Collectors.toList()); List<GoodsDO> goodsDOList = goodsMapper.selectBatchIds(goodsIdList); // 4. 批量查询用户、物流 UserDO userDO = userMapper.selectBatchIds(List.of(userId)).get(0); List<OrderLogisticsDO> logisticsList = logisticsMapper.selectByOrderIds(List.of(orderId)); OrderLogisticsDO logisticsDO = logisticsList.isEmpty() ? null : logisticsList.get(0); // ====================== 步骤2:把DB数据转Map,方便内存快速匹配(核心优化) ====================== Map<Long, GoodsDO> goodsMap = goodsDOList.stream() .collect(Collectors.toMap(GoodsDO::getGoodsId, g -> g)); // ====================== 步骤3:逐层组装VO ====================== OrderDetailVO vo = new OrderDetailVO(); // 填充订单基础 vo.setOrderId(orderDO.getOrderId()); vo.setOrderNo(orderDO.getOrderNo()); vo.setTotalAmount(orderDO.getTotalAmount()); vo.setCreateTime(orderDO.getCreateTime()); // 填充用户信息 vo.setUserNick(userDO.getNickName()); vo.setUserPhone(userDO.getPhone()); // 填充物流 if (logisticsDO != null) { vo.setExpressNo(logisticsDO.getExpressNo()); vo.setReceiveAddr(logisticsDO.getReceiveAddr()); } // 组装一对多商品明细(核心:循环明细,从goodsMap匹配商品数据) List<OrderDetailVO.OrderItemVO> itemVOList = new ArrayList<>(); for (OrderItemDO itemDO : itemDOList) { OrderDetailVO.OrderItemVO itemVO = new OrderDetailVO.OrderItemVO(); itemVO.setGoodsId(itemDO.getGoodsId()); itemVO.setBuyNum(itemDO.getBuyNum()); itemVO.setItemAmount(itemDO.getItemAmount()); // 从内存Map匹配商品信息,无需查库 GoodsDO goods = goodsMap.get(itemDO.getGoodsId()); if (goods != null) { itemVO.setGoodsName(goods.getGoodsName()); itemVO.setCoverImg(goods.getCoverImg()); itemVO.setPrice(goods.getPrice()); } itemVOList.add(itemVO); } vo.setItemList(itemVOList); return vo; } /** * 扩展:批量查询多个订单(更能体现拆分SQL优势,避免多表联查分页卡死) */ @Override public List<OrderDetailVO> listOrderBatch(List<Long> orderIdList) { // 1. 批量查所有订单 List<OrderDO> orderDOList = orderMapper.selectBatchIds(orderIdList); if (CollUtil.isEmpty(orderDOList)) { return Collections.emptyList(); } // 2. 批量查所有订单明细、物流 List<OrderItemDO> allItemList = itemMapper.selectByOrderIds(orderIdList); List<OrderLogisticsDO> allLogisticsList = logisticsMapper.selectByOrderIds(orderIdList); // 3. 收集所有用户ID、商品ID,一次性批量查询 List<Long> userIdList = orderDOList.stream() .map(OrderDO::getUserId) .distinct() .collect(Collectors.toList()); List<UserDO> userDOList = userMapper.selectBatchIds(userIdList); List<Long> goodsIdList = allItemList.stream() .map(OrderItemDO::getGoodsId) .distinct() .collect(Collectors.toList()); List<GoodsDO> goodsDOList = goodsMapper.selectBatchIds(goodsIdList); // ====================== 内存分组、转Map,O(1)匹配 ====================== // 用户Map key:userId Map<Long, UserDO> userMap = userDOList.stream() .collect(Collectors.toMap(UserDO::getUserId, u -> u)); // 商品Map key:goodsId Map<Long, GoodsDO> goodsMap = goodsDOList.stream() .collect(Collectors.toMap(GoodsDO::getGoodsId, g -> g)); // 明细按orderId分组(一对多核心分组) Map<Long, List<OrderItemDO>> itemGroupByOrderId = allItemList.stream() .collect(Collectors.groupingBy(OrderItemDO::getOrderId)); // 物流按orderId分组(一对一) Map<Long, OrderLogisticsDO> logisticsMap = allLogisticsList.stream() .collect(Collectors.toMap(OrderLogisticsDO::getOrderId, l -> l)); // ====================== 循环组装每一个订单VO ====================== List<OrderDetailVO> result = new ArrayList<>(); for (OrderDO order : orderDOList) { OrderDetailVO vo = new OrderDetailVO(); vo.setOrderId(order.getOrderId()); vo.setOrderNo(order.getOrderNo()); vo.setTotalAmount(order.getTotalAmount()); vo.setCreateTime(order.getCreateTime()); // 用户 UserDO user = userMap.get(order.getUserId()); if (user != null) { vo.setUserNick(user.getNickName()); vo.setUserPhone(user.getPhone()); } // 物流 OrderLogisticsDO logistics = logisticsMap.get(order.getOrderId()); if (logistics != null) { vo.setExpressNo(logistics.getExpressNo()); vo.setReceiveAddr(logistics.getReceiveAddr()); } // 商品明细列表 List<OrderItemDO> itemDOs = itemGroupByOrderId.getOrDefault(order.getOrderId(), Collections.emptyList()); List<OrderDetailVO.OrderItemVO> itemVOs = new ArrayList<>(); for (OrderItemDO item : itemDOs) { OrderDetailVO.OrderItemVO itemVO = new OrderDetailVO.OrderItemVO(); itemVO.setGoodsId(item.getGoodsId()); itemVO.setBuyNum(item.getBuyNum()); itemVO.setItemAmount(item.getItemAmount()); GoodsDO goods = goodsMap.get(item.getGoodsId()); if (goods != null) { itemVO.setGoodsName(goods.getGoodsName()); itemVO.setCoverImg(goods.getCoverImg()); itemVO.setPrice(goods.getPrice()); } itemVOs.add(itemVO); } vo.setItemList(itemVOs); result.add(vo); } return result; } }

五、Controller 调用层

@RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @GetMapping("/detail/{orderId}") public Result<OrderDetailVO> detail(@PathVariable Long orderId) { OrderDetailVO vo = orderService.getOrderDetail(orderId); return Result.success(vo); } @GetMapping("/batch") public Result<List<OrderDetailVO>> batch(@RequestParam List<Long> orderIds) { List<OrderDetailVO> list = orderService.listOrderBatch(orderIds); return Result.success(list); } }

六、核心技术要点(生产环境必看)

1. 为什么拆分 SQL 比多表 JOIN 更好?

  1. 性能可控
    • 多表 LEFT JOIN:数据量大时笛卡尔积、索引失效、分页limit扫描全表;
    • 分单表查询:每张表 SQL 简单,索引高效,可单独给单表加缓存(Redis 缓存商品、用户)。
  2. 扩展性强
    • 新增表关联(优惠券、发票)只需要新增一次批量查询,不用重构巨型 SQL;
    • 适配分库分表:订单、商品可能分不同库,跨库不能 JOIN,只能代码组装。
  3. 维护简单
    • 单表 SQL 短小,MyBatis 容易维护;复杂 JOIN 几十行,改字段极易漏关联条件。
  4. 灵活加工数据内存中可以自由排序、过滤、计算、拼接,SQL 很难实现复杂业务计算。

2. 避坑关键点(防止 N+1 查询灾难)

❌ 错误写法:循环订单 ID,每一条订单单独查明细、商品、用户(循环查库,百万数据直接打垮 DB) ✅ 标准规范:全部使用批量 IN 查询,一次性取出全量数据,内存 Map 匹配

3. 内存组装三大工具

  1. Collectors.toMap:一对一快速匹配(用户、物流、商品)
  2. Collectors.groupingBy:一对多分组(订单明细按订单 ID 分组)
  3. Stream 流式处理:数据清洗、去重、提取 ID 集合

4. 性能优化补充方案

  1. 热点数据缓存:商品、用户信息存入 Redis,不用每次查库;
  2. 分页分层:先分页查订单主表,再批量查当前页关联数据,避免全表加载;
  3. 异步冗余:报表类大数据可通过 ES / 数据中台预聚合,不在线上业务做大量内存组装;
  4. 空值保护:所有 Map.get () 判空,防止空指针。

七、适用 & 不适用场景

适合拆分 SQL 代码组装

  1. 多表一对多、一对一聚合查询(订单、商品、用户、物流)
  2. 分库分表、跨库查询(禁止跨库 JOIN)
  3. 前端复杂 VO 聚合页面、列表分页
  4. 表字段频繁迭代、业务逻辑经常变更

不适合(直接写简单 JOIN 即可)

  1. 仅 2 张表简单关联,数据量很小
  2. 简单报表、不需要复杂内存计算
  3. 超高并发极简查询(为减少 IO,少量 JOIN 开销可接受)

八、对比巨型 JOIN SQL 反面示例(不推荐)

sql

-- 禁止这种写法!5表LEFT JOIN,数据量大性能爆炸 SELECT o.*,u.nick_name,u.phone,l.express_no,l.receive_addr, i.goods_id,i.buy_num,i.item_amount,g.goods_name,g.cover_img,g.price FROM t_order o LEFT JOIN t_user u ON o.user_id = u.user_id LEFT JOIN t_order_logistics l ON o.order_id = l.order_id LEFT JOIN t_order_item i ON o.order_id = i.order_id LEFT JOIN t_goods g ON i.goods_id = g.goods_id WHERE o.order_id = #{orderId}

问题:关联越多优化器越难走索引、无法单独缓存商品表、分页慢、新增一张表就要改整条 SQL。

九、扩展进阶:MyBatis-Plus 简化批量查询

项目使用 MP 时,Mapper 不用手写 XML,直接调用内置批量方法:

// 代替手写XML selectBatchIds List<OrderDO> list = orderMapper.selectBatchIds(orderIdList);

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询