(13)复杂查询 - 教程

场景说明

  • 多对一(Many-to-One):多个 Order 属于一个 User → Order → User
  • 一对多(One-to-Many):一个 User 有多个 Order → User → List

✅ 一、多对一(Many-to-One)—— 嵌套查询写法

1. Java 实体

public class Order {
private Long id;
private String product;
private User user; // 多对一:每个订单属于一个用户
}
public class User {
private Long id;
private String name;
}

2. Mapper XML(嵌套查询)

<resultMap id="OrderWithUserResultMap" type="Order">
  <id property="id" column="id"/>
  <result property="product" column="product"/>
  <!-- 嵌套查询:通过 userId 去查 User -->
      <association property="user"
      javaType="User"
      select="com.example.mapper.UserMapper.selectById"
      column="user_id" />
  </resultMap>
    <select id="selectOrdersWithUser" resultMap="OrderWithUserResultMap">
    SELECT id, product, user_id FROM order
  </select>

逐行解释

1. <resultMap id="OrderWithUserResultMap" type="Order">

定义一个叫 OrderWithUserResultMap 的映射规则,目标类型是 Order 类。

2. <id property="id" column="id"/>
> 把 SQL 查询结果中的 `id` 列 → 赋值给 `Order` 对象的 `id` 属性。MyBatis 要求主键必须用  标签映射,不能用 
3. <result property="product" column="product"/>

product 列 → 赋值给 Order.product

4. 关键部分
<association property="user"
  javaType="User"
  select="com.example.mapper.UserMapper.selectById"
  column="user_id" />

这四行的意思是:

属性含义
property="user"当前 Order 对象里有个字段叫 user(类型是 User
javaType="User"明确告诉 MyBatis 这个字段的类型是 User
select="..."去调用另一个 Mapper 方法UserMapper.selectById 来查用户
column="user_id"把当前行的 user_id,作为参数传给上面那个方法

执行过程(重点!)

假设 order 表有 2 条数据:

id | product | user_id
1  | iPhone  | 100
2  | iPad    | 200

当你调用:

List<Order> orders = orderMapper.selectOrdersWithUser();

MyBatis 会这样执行:

  1. 先执行主查询

    SELECT id, product, user_id FROM order

    得到两行结果:(1, "iPhone", 100)(2, "iPad", 200)

  2. 对每一行,再发起一次子查询

    • 第一行:调用 UserMapper.selectById(100)

      SELECT * FROM user WHERE id = 100
    • 第二行:调用 UserMapper.selectById(200)

      SELECT * FROM user WHERE id = 200
  3. 组装对象

    Order order1 = new Order();
    order1.setId(1);
    order1.setProduct("iPhone");
    order1.setUser( userFromDB1 ); // ← 从子查询拿到

⚠️ 问题来了:这就是 N+1 查询

  • 主查询:1 次
  • 子查询:N 次(N = 订单数量)
  • 总共:1 + N 条 SQL

如果查 1000 个订单 → 执行 1001 条 SQL → 数据库压力巨大!


✅ 正确做法(大厂推荐):用 JOIN 一次性查完

<resultMap id="OrderWithUserResultMap" type="Order">
  <id property="id" column="order_id"/>
  <result property="product" column="product"/>
  <!-- 直接映射关联对象,不发起新查询 -->
      <association property="user" javaType="User">
      <id property="id" column="user_id"/>
      <result property="name" column="user_name"/>
    </association>
  </resultMap>
    <select id="selectOrdersWithUser" resultMap="OrderWithUserResultMap">
    SELECT
    o.id AS order_id,
    o.product,
    u.id AS user_id,
    u.name AS user_name
    FROM order o
    LEFT JOIN user u ON o.user_id = u.id
  </select>

✅ 只执行 1 条 SQL,性能好一万倍!
相当于在对象的每个值都拿出来继续映射了,而不用再一次次查询分别封装,因为只写了一句SQL,这两个信息通过JOIN 一次性查完


总结

你看到的写法实际含义是否推荐
<association select="...">每行都发起一次新查询(N+1)❌ 仅用于查单条记录
<association> + JOIN一次查完所有数据✅ 列表查询必须用这个

记住

  • select="..." 是“懒加载式”查关联对象 → 慎用
  • 想高性能?永远优先用 JOIN + 嵌套结果映射
    column="user_id" 会作为参数传给 UserMapper.selectById(Long id)

✅ 二、一对多(One-to-many)—— 嵌套查询写法

1. Java 实体

public class User {
private Long id;
private String name;
private List<Order> orders; // 一对多:用户有多个订单
  }

2. Mapper XML

<resultMap id="UserWithOrdersResultMap" type="User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <!-- 嵌套查询:通过 user.id 去查所有订单 -->
      <collection property="orders"
      ofType="Order"
      select="com.example.mapper.OrderMapper.selectByUserId"
      column="id" />
  </resultMap>
    <select id="selectUsersWithOrders" resultMap="UserWithOrdersResultMap">
    SELECT id, name FROM user
  </select>

column="id" 会作为参数传给 OrderMapper.selectByUserId(Long userId)

对比两个标签:
嵌套查询:

    

⚠️ 三、嵌套查询的致命问题:N+1 查询(跟上面一样)

  • 查 10 个用户 → 执行 1 次主查询 + 10 次子查询(查订单)→ 共 11 条 SQL
  • 数据量大时,数据库压力爆炸,响应慢

❌ 这是大厂 Code Review 直接打回的写法!


✅ 四、大厂推荐方案:嵌套结果(JOIN + collection/association)

一对多示例(User + Orders):

<resultMap id="UserWithOrdersResultMap" type="User">
  <id property="id" column="user_id"/>
  <result property="name" column="user_name"/>
    <collection property="orders" ofType="Order">
    <id property="id" column="order_id"/>
    <result property="product" column="product"/>
  </collection>
</resultMap>
  <select id="selectUsersWithOrders" resultMap="UserWithOrdersResultMap">
  SELECT
  u.id AS user_id,
  u.name AS user_name,
  o.id AS order_id,
  o.product
  FROM user u
  LEFT JOIN order o ON u.id = o.user_id
  ORDER BY u.id
</select>

只执行 1 条 SQL,MyBatis 自动去重 + 组装 List
必须加 ORDER BY 主表主键,否则去重可能出错


本质上是两个复杂有什么问题?

本质上查询多次是因为调用了另外一条sql,而通过一条sql 加上join就可以解决,剩下的就是数据的映射问题。

五、什么时候可以用嵌套查询?

场景是否可用
数据量极小(如查 1 个用户及其配置)✅ 可用
列表页(查 N 条主记录 + 关联数据)❌ 禁止!用 JOIN
关联对象非常大(避免 JOIN 膨胀)⚠️ 谨慎,考虑分两次查

✅ 六、总结(背下来)

方式优点缺点推荐度
嵌套查询(select)写法简单N+1 性能灾难❌ 仅限单条查询
嵌套结果(JOIN)1 条 SQL,高效SQL 稍复杂大厂标准做法

口诀
“列表用 JOIN,单查可嵌套;
N+1 是红线,上线必被打回。”

如果你用的是 MyBatis-Plus,它也提供了 @TableField(exist = false) + 手动组装的方式,但底层原理相同。

posted @ 2026-01-09 09:53  gccbuaa  阅读(15)  评论(0)    收藏  举报