在数据库设计中,一对多关系是最常见的关联模式之一。比如一个部门对应多个员工、一篇博客对应多条评论。MyBatis作为Java生态中最流行的ORM框架,提供了三种处理这种关系的实现方式:
实际开发中最常用的是前两种方式。以电商系统为例,订单(Order)与订单项(OrderItem)就是典型的一对多关系。下面通过这个案例演示具体实现。
提示:在决定使用嵌套结果还是嵌套查询前,需要评估数据量和性能需求。嵌套结果适合关联数据量小的场景,嵌套查询则更适合大数据量下的分页查询。
首先定义领域模型,注意一对多关系在Java中的表示方式:
java复制// 订单实体
public class Order {
private Long id;
private String orderNo;
private Date createTime;
// 一对多关联
private List<OrderItem> items;
// getters/setters...
}
// 订单项实体
public class OrderItem {
private Long id;
private Long orderId;
private String productName;
private BigDecimal price;
// getters/setters...
}
关键点在于<resultMap>中的<collection>标签配置:
xml复制<resultMap id="orderWithItemsResultMap" type="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="createTime" column="create_time"/>
<!-- 一对多映射配置 -->
<collection property="items" ofType="OrderItem">
<id property="id" column="item_id"/>
<result property="orderId" column="order_id"/>
<result property="productName" column="product_name"/>
<result property="price" column="price"/>
</collection>
</resultMap>
需要编写联表查询SQL并注意列别名避免冲突:
xml复制<select id="findOrderWithItems" resultMap="orderWithItemsResultMap">
SELECT
o.id as order_id,
o.order_no,
o.create_time,
i.id as item_id,
i.product_name,
i.price
FROM
orders o
LEFT JOIN
order_items i ON o.id = i.order_id
WHERE
o.id = #{id}
</select>
注意:必须为所有同名列设置别名,否则MyBatis在映射时会出现列名冲突。建议采用"表名_字段名"的别名规范。
当订单数据量很大时,更适合使用嵌套查询方式:
xml复制<resultMap id="orderWithItemsNestedMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="createTime" column="create_time"/>
<!-- 通过select属性指定嵌套查询 -->
<collection
property="items"
column="id"
ofType="OrderItem"
select="findItemsByOrderId"/>
</resultMap>
<select id="findOrderWithItemsNested" resultMap="orderWithItemsNestedMap">
SELECT * FROM orders WHERE id = #{id}
</select>
<select id="findItemsByOrderId" resultType="OrderItem">
SELECT * FROM order_items WHERE order_id = #{orderId}
</select>
结合MyBatis的懒加载机制可以优化性能:
xml复制<!-- 全局开启懒加载 -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!-- 或针对特定collection配置 -->
<collection
property="items"
column="id"
ofType="OrderItem"
select="findItemsByOrderId"
fetchType="lazy"/>
| 对比维度 | 嵌套结果映射 | 嵌套查询映射 |
|---|---|---|
| 数据库查询次数 | 1次 | N+1次(N为关联记录数) |
| 网络IO开销 | 单次传输大数据包 | 多次传输小数据包 |
| 内存消耗 | 较高(一次性加载所有数据) | 较低(按需加载) |
| 适用场景 | 关联数据量小、需要立即使用 | 大数据量、需要分页或延迟加载 |
一对多分页查询时需要注意:
xml复制<!-- 错误做法:直接对主表分页会导致数据缺失 -->
<select id="findOrdersWithItems" resultMap="orderWithItemsResultMap">
SELECT o.*, i.*
FROM orders o
LEFT JOIN order_items i ON o.id = i.order_id
LIMIT #{offset}, #{pageSize} <!-- 这里分页不准确 -->
</select>
<!-- 正确做法:先分页查询主表ID,再联表查询 -->
<select id="findOrdersWithItems" resultMap="orderWithItemsResultMap">
SELECT o.*, i.*
FROM (
SELECT id FROM orders
ORDER BY create_time DESC
LIMIT #{offset}, #{pageSize}
) temp
JOIN orders o ON temp.id = o.id
LEFT JOIN order_items i ON o.id = i.order_id
</select>
合理使用二级缓存减少查询压力:
xml复制<cache eviction="LRU" flushInterval="60000" size="512"/>
<resultMap id="orderWithItemsResultMap" type="Order">
<!-- ...其他配置... -->
<collection property="items" ... />
</resultMap>
重要提示:缓存关联对象时,确保所有关联实体都实现了Serializable接口,否则会抛出序列化异常。
症状:查询返回数据但关联集合为空
检查清单:
<collection>的property名称与实体类字段名完全一致(区分大小写)ofType指定的是集合元素的完整类名N+1查询问题解决方案:
@BatchSize注解批量加载关联对象java复制@BatchSize(size = 10)
public class Order {
private List<OrderItem> items;
}
xml复制<settings>
<setting name="defaultExecutorType" value="BATCH"/>
</settings>
处理多层嵌套关联时(如Order→OrderItem→Product),建议:
<association>和<collection>组合映射<sql>片段提高可读性xml复制<sql id="orderColumns">o.id as order_id, o.order_no</sql>
<sql id="itemColumns">i.id as item_id, i.product_name</sql>
<select id="findComplexOrder" resultMap="complexOrderMap">
SELECT
<include refid="orderColumns"/>,
<include refid="itemColumns"/>,
p.name as product_name
FROM orders o
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
</select>
在实际项目中,我通常会根据业务场景混合使用多种映射方式。对于核心业务流程使用嵌套结果映射保证性能,对于管理后台等复杂查询场景则采用嵌套查询提高灵活性。特别注意在微服务架构下,跨服务的一对多关系建议通过API调用实现而非数据库关联。