快速概览 — 常见实现方式(按推荐顺序)
- Mapper XML + resultMap(推荐):灵活、可映射嵌套对象/集合,适合复杂 join。
- Mapper 注解(@Select + @Results):适合简单 join、小 SQL。
- DTO 投影(把 select 的列 alias 到 DTO 字段):简单高效,避免实体污染。
- MyBatis 的 <collection> / <association>(子集合/关联对象):处理一对多/一对一的映射。
- 第三方“join wrapper” 插件 / 自建 SQL 构造器:如果你想用链式 API 构造 join(但需引入/评估插件)。
- 避免用 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)
- 列重名 → 给每列做 AS 别名,并在 resultMap 中用 column=”别名”。
- mapping 为空/为 null → 检查 alias 是否一致、javaType 是否正确、是否缺少 getter/setter。
- 分页返回条数异常 → 检查分页插件是否生效(配置拦截器)、是否需要自定义 count SQL。
- N+1 性能问题 → 如果你用 <collection select=”…”>,评估是否能改成单次 join + 程序端去重聚合,或使用 batch loading。
- 调试 SQL → 开启 MyBatis/数据库日志,在 DB 客户端粘贴 SQL 验证返回结果是否符合预期。
- 大字段/重复字段 → 避免 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
















暂无评论内容