mybatis-plus 多表关联查询

快速概览 — 常见实现方式(按推荐顺序)

  1. Mapper XML + resultMap(推荐):灵活、可映射嵌套对象/集合,适合复杂 join。
  2. Mapper 注解(@Select + @Results):适合简单 join、小 SQL。
  3. DTO 投影(把 select 的列 alias 到 DTO 字段):简单高效,避免实体污染。
  4. MyBatis 的 <collection> / <association>(子集合/关联对象):处理一对多/一对一的映射。
  5. 第三方“join wrapper” 插件 / 自建 SQL 构造器:如果你想用链式 API 构造 join(但需引入/评估插件)。
  6. 避免用 QueryWrapper 做复杂 join:QueryWrapper 原生不支持语义化 join,虽可用 .apply() 强行拼 SQL,但可维护性差。

核心概念与要点

  • 别用 SELECT *:join 时列会重名,且返回比需要多,最好写明确列并用 alias。
  • 用 DTO 做投影:实体类一般映射单表,join 返回的组合结构最好映射到 DTO(嵌套对象或展平字段)。
  • 别忽略分页的 count 查询:MyBatis-Plus 的分页插件一般能自动拦截 IPage 参数并生成分页/count,但复杂 SQL(group by、distinct、多表复杂条件)可能需要自定义 count。
  • 注意 N+1 问题:用 <collection select=”…”> 会触发额外查询(单表 select per parent),join 一次性取出则不会,但要做好 row 去重 / 聚合。
  • 列别名与 resultMap 映射:join 后列名冲突用 alias,并在 resultMap 用 column 指定对应字段。
  • 性能:join 时注意索引、避免大表全表扫描,count 查询开销常是瓶颈。

详细示例(常用场景:Order + User + Product)

1) 实体与 DTO(示例)

// Order.java (数据库表 orders)
public class Order {
  private Long id;
  private Long userId;
  private Long productId;
  private BigDecimal amount;
  // getters/setters
}

// User.java
public class User {
  private Long id;
  private String name;
  // ...
}

// Product.java
public class Product {
  private Long id;
  private String name;
  // ...
}

// OrderDTO.java — 我们将返回嵌套结构
public class OrderDTO {
  private Long id;
  private BigDecimal amount;
  private User user;        // 嵌套
  private Product product;  // 嵌套
  // getters/setters
}

2) Mapper 接口(自定义方法,配合 IPage 支持分页)

public interface OrderMapper extends BaseMapper<Order> {
    // MyBatis-Plus 的分页拦截器会识别 IPage 参数并做分页(一般)
    IPage<OrderDTO> selectOrderPageWithUserProduct(IPage<?> page, @Param("status") Integer status);
}

3) Mapper XML(推荐做法:明确列、用 resultMap 映射嵌套对象)

<mapper namespace="com.example.mapper.OrderMapper">

  <resultMap id="OrderWithUserProductMap" type="com.example.dto.OrderDTO">
    <id column="order_id" property="id"/>
    <result column="amount" property="amount"/>
    <association property="user" javaType="com.example.entity.User">
      <id column="user_id" property="id"/>
      <result column="user_name" property="name"/>
    </association>
    <association property="product" javaType="com.example.entity.Product">
      <id column="product_id" property="id"/>
      <result column="product_name" property="name"/>
    </association>
  </resultMap>

  <select id="selectOrderPageWithUserProduct" resultMap="OrderWithUserProductMap">
    SELECT
      o.id        AS order_id,
      o.amount    AS amount,
      u.id        AS user_id,
      u.name      AS user_name,
      p.id        AS product_id,
      p.name      AS product_name
    FROM orders o
    LEFT JOIN users u ON o.user_id = u.id
    LEFT JOIN products p ON o.product_id = p.id
    WHERE o.status = #{status}
    <!-- 如果使用 MyBatis-Plus 分页插件,这里不需要手写 limit,插件会自动注入 -->
  </select>

</mapper>

调用:

IPage<OrderDTO> page = new Page<>(1, 10);
IPage<OrderDTO> result = orderMapper.selectOrderPageWithUserProduct(page, 1);

注意(分页 & count):MyBatis-Plus 的分页拦截器会在方法签名包含 IPage 时尝试自动生成 count SQL 并做分页。但当你的 SQL 超级复杂(含 group by、distinct、复杂子查询)时,自动生成的 count 可能不准确或性能差,这时你应该写一个专门的 count 查询并在业务层调用或在 mapper 中提供单独方法。


注解方式(小例子,适合短 SQL)

public interface OrderMapper extends BaseMapper<Order> {

  @Select("SELECT o.id AS order_id, o.amount, u.id AS user_id, u.name AS user_name " +
          "FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE o.id = #{id}")
  @Results({
    @Result(property="id", column="order_id", id=true),
    @Result(property="amount", column="amount"),
    @Result(property="user.id", column="user_id"),
    @Result(property="user.name", column="user_name")
  })
  OrderDTO selectOrderWithUser(Long id);
}
  • 注解方式可读性好,但 SQL 一长就不易维护。嵌套属性映射(user.id)是支持的。

一对多(Order -> OrderItem):<collection>与 join 的选择

方式 A:用 join 一次拿出(扁平化)

  • SQL 返回多行(order 字段重复),需要在 Java 端或 MyBatis resultMap 做分组(复杂)。
  • 优点:一次 SQL,避免多次查询;缺点:数据量大时重复字段传输 / 复杂映射。

方式 B:<collection select=”selectItemsByOrderId” column=”id”>(子查询)

<resultMap id="orderMap" type="Order">
  <id column="id" property="id"/>
  <result column="amount" property="amount"/>
  <collection property="items" ofType="OrderItem" select="selectItemsByOrderId" column="id"/>
</resultMap>

<select id="selectItemsByOrderId" resultType="OrderItem" parameterType="long">
  SELECT id, order_id, product_id, quantity FROM order_items WHERE order_id = #{orderId}
</select>
  • 优点:映射简单、清晰;缺点:会触发额外查询(N+1),适合 parent 数量较少的场景或子表较小场景。

常见问题 & 排查提议(Checklist)

  1. 列重名 → 给每列做 AS 别名,并在 resultMap 中用 column=”别名”。
  2. mapping 为空/为 null → 检查 alias 是否一致、javaType 是否正确、是否缺少 getter/setter。
  3. 分页返回条数异常 → 检查分页插件是否生效(配置拦截器)、是否需要自定义 count SQL。
  4. N+1 性能问题 → 如果你用 <collection select=”…”>,评估是否能改成单次 join + 程序端去重聚合,或使用 batch loading。
  5. 调试 SQL → 开启 MyBatis/数据库日志,在 DB 客户端粘贴 SQL 验证返回结果是否符合预期。
  6. 大字段/重复字段 → 避免 SELECT 大列(text/blob)或大量冗余字段。

性能优化提议

  • 只选必要列,避免 SELECT *。
  • 为 join 的连接列建立索引(如 user_id、product_id)。
  • 尽量避免在 count 上做 join(可写单独简化的 count SQL)。
  • 分页时避免 offset 很大的情况(思考 keyset pagination / 游标)。
  • 复杂报表/聚合思考用物化视图或专门的报表库/缓存

何时使用插件或链式 join(以及风险)

  • 有些社区插件提供链式 join API(看起来像 joinWrapper.leftJoin(…).eq(…)),这能用更贴近 ORM 的写法做 join,优点:书写方便;缺点:引入第三方依赖,升级/兼容需慎重,且社区插件行为可能随版本变化。
  • 个人提议:生产系统对复杂 SQL 有严格需求时,首选手写 SQL(XML/注解)+ resultMap,这样最稳定、可调优、可复用。

小结(最佳实践)

  • 复杂多表关联写在 XML(或 Provider)里,明确列与别名,返回 DTO 并用 resultMap 做嵌套/集合映射。
  • 注解适合短 SQL;集合映射要注意 N+1。
  • 分页时注意 count 查询(必要时自写 count)。
  • 性能靠索引、只选必要字段、避免重复数据传输来保障。
© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
郑成操的头像 - 宋马
评论 抢沙发

请登录后发表评论

    暂无评论内容