在实际开发中,一对多关系是非常常见的数据关联场景,比如用户与订单、部门与员工等。MyBatis作为主流的持久层框架,提供了灵活的一对多嵌套查询实现方案。本文将围绕给定的代码案例,从SQL99式关联查询 和**分布式查询(分步查询)**两个核心角度,深入讲解一对多嵌套查询的实现逻辑、核心要点及适用场景,帮助大家精准掌握两种方案的实战应用。
先明确核心业务场景:本文案例围绕「用户-订单」一对多关系展开,一个用户可以拥有多个订单,需通过嵌套查询获取用户信息及关联的订单列表。先熟悉核心实体与DAO层定义,为后续讲解奠定基础。
一、核心基础:实体与DAO层核心代码解析
在讲解嵌套查询前,先明确数据载体(实体类)和数据访问入口(DAO接口)的核心定义,这是MyBatis查询的基础。
1.1 实体类定义
核心实体为Users(用户)和Order(订单),通过Users中的List<Order> orders属性体现一对多关系:
java
public class Users {
private Integer id;
private String username;
private String password;
private String realName;
// 一对多关联:一个用户拥有多个订单
private List<Order> orders;
// 省略getter、setter和toString方法
}
// 订单实体(案例中未完整给出,核心属性如下)
public class Order {
private Integer id;
private String order_number; // 订单号
private Double total_price; // 总价
private Integer status; // 订单状态
private Integer user_id; // 关联用户ID
// 省略getter、setter和toString方法
}
关键要点:一对多关系的核心是「主实体包含从实体的集合」,这里Users作为主实体,通过orders集合承载关联的订单数据。
1.2 DAO层接口定义
DAO接口OrdersDao定义了两个核心查询方法,分别对应两种嵌套查询方案:
java
public interface OrdersDao {
// 方案1:SQL99式关联查询,一次性查询用户及关联订单
List<Users> findUsersOrders();
// 方案2:分布式查询(分步查询),先查用户,再按需查订单
List<Users> findUsers();
}
二、方案一:SQL99式关联查询(一次性关联)
SQL99式查询是MyBatis中实现一对多嵌套查询的「一次性方案」,核心思路是通过SQL99标准的LEFT JOIN(左连接)将主表(users)和从表(orders)关联,一次性查询出所有数据,再通过resultMap定义数据映射规则,将查询结果封装到主实体的集合属性中。
2.1 Mapper核心配置
XML
<mapper namespace="dao.OrdersDao">
<!-- 查询语句:SQL99式LEFT JOIN关联users和orders -->
<select id="findUsersOrders" resultMap="UserOrders">
select users.*,orders.order_number,total_price,status
from users
left join orders on users.id = orders.user_id
</select>
<!-- 结果映射:定义用户与订单的一对多映射规则 -->
<resultMap id="UserOrders" type="entity.Users"><!-- 1. 映射用户自身属性 -->
<result property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="realName" column="realname"/>
<!-- 2. 一对多嵌套映射:通过collection标签映射订单集合 -->
<collection property="orders" ofType="entity.Order">
<result property="id" column="id"/>
<result property="order_number" column="order_number"/>
<result property="total_price" column="total_price"/>
<result property="status" column="status"/>
<result property="user_id" column="user_id"/>
</collection>
</resultMap>
</mapper>
2.2 核心逻辑拆解
2.2.1 SQL99式关联查询语句解析
核心SQL语句采用SQL99标准的LEFT JOIN实现关联,优势是语法规范、可读性强,支持多种关联类型(内连接、左连接、右连接等):
sql
select users.*,orders.order_number,total_price,status
from users
left join orders on users.id = orders.user_id
关键说明:
-
LEFT JOIN:保证即使用户没有订单(orders表无对应记录),也能查询出该用户信息,订单集合为空(符合业务中「查询所有用户及他们的订单」的需求); -
关联条件
users.id = orders.user_id:通过用户ID和订单表中的用户ID外键关联,这是一对多关系的核心关联依据; -
查询字段:
users.*查询所有用户字段,orders.order_number,total_price,status查询订单核心字段,避免冗余字段查询。
2.2.2 resultMap一对多映射核心:collection标签
MyBatis中,一对多嵌套映射的核心是collection标签,用于将查询结果中的「多条从表记录」封装到主实体的「集合属性」中,核心属性说明:
-
property="orders":对应主实体Users中的集合属性名orders,指定将从表数据封装到哪个集合; -
ofType="entity.Order":指定集合中元素的类型(从实体类型),即订单实体Order; -
子标签
result:定义订单实体属性与查询结果列的映射关系(property为实体属性名,column为SQL查询结果的列名)。
注意点:案例中用户表和订单表的主键都为id,SQL查询结果中会出现两个id列,但MyBatis会根据resultMap的映射顺序和属性类型自动区分,不会出现混淆(用户的id映射到Users.id,订单的id映射到Order.id)。
2.2.3 测试方法与执行结果
测试方法通过调用findUsersOrders()方法查询数据并打印:
java
@Test
public void findUsersOrders(){
List<Users> usersOrders = mapper.findUsersOrders();
for (Users usersOrder:usersOrders){
System.out.println(usersOrder.toString());
}
}
执行结果示例(简化):
结果说明:用户「张三」有2个订单,订单集合被正确封装;用户「李四」没有订单,订单集合为空(因使用LEFT JOIN)。
2.3 方案一优势与局限性
优势:
-
效率高(一次数据库连接):仅需执行一次SQL查询,减少数据库连接次数,适合数据量不大的场景;
-
逻辑简单:SQL关联查询+resultMap映射,配置简洁,易理解和维护;
-
SQL99标准兼容:支持多种关联类型,适配不同业务需求(如内连接查询有订单的用户)。
局限性:
-
数据冗余风险:当主表记录较多、从表关联记录量大时,主表字段(如username、password)会被重复查询(每条订单记录对应一条主表记录),增加数据传输量;
-
灵活性差:无论是否需要从表数据,都会一次性查询,不适合「按需加载」场景(如仅需查询用户列表,偶尔需要订单数据)。
三、方案二:分布式查询(分步查询/按需加载)
分布式查询(也叫分步查询)是MyBatis中实现一对多嵌套查询的「按需加载方案」,核心思路是将查询拆分为两步:① 先查询主表(users)数据,获取所有用户信息;② 再通过主表的主键(user_id),按需查询从表(orders)数据,最终将从表数据封装到主实体的集合属性中。这种方案支持「延迟加载」(懒加载),即只有当访问主实体的集合属性时,才会执行第二步查询。
3.1 Mapper核心配置
XML
<select id="findUsers" resultMap="UserOrders1">
select * from users
</select>
<resultMap id="UserOrders1" type="entity.Users">
<result property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="realName" column="realname"/>
<collection property="orders" ofType="entity.Order" column="id" select="findOrdersById"/>
</resultMap>
<select id="findOrdersById" resultType="entity.Order">
select * from orders where user_id = #{id}
</select>
3.2 核心逻辑拆解
3.2.1 分步查询的两步核心流程
-
第一步:查询主表(users):执行
findUsers()方法,执行SQLselect * from users,获取所有用户记录,此时订单集合orders未加载(若开启延迟加载); -
第二步:按需查询从表(orders):当程序访问某个用户的
orders集合时(如user.getOrders()),MyBatis会自动执行findOrdersById()方法,通过第一步查询到的用户id(通过column="id"传递),执行SQLselect * from orders where user_id = #{id},查询该用户的所有订单,再封装到orders集合中。
3.2.2 collection标签分步查询核心属性
分步查询的核心是collection标签的三个关键属性,缺一不可:
-
property="orders":同方案一,对应主实体的集合属性名; -
ofType="entity.Order":同方案一,指定集合元素类型; -
column="id":指定将主表的哪个字段作为参数传递给第二步查询(这里将用户id传递给findOrdersById的#{id}); -
select="findOrdersById":指定第二步查询的SQL语句ID(即当前mapper中定义的findOrdersById查询)。
3.2.3 测试方法与执行结果
测试方法调用findUsers()方法查询用户列表并打印:
java
@Test
public void findUsers(){
List<Users> users = mapper.findUsers();
for (Users user:users){
System.out.println(user.toString());
}
}
执行结果(开启延迟加载时):
-
当仅打印用户信息,不访问
orders集合时,仅执行第一步SQL(查询users),第二步SQL(查询orders)不执行; -
当打印
user.toString()时(toString方法中包含orders),会触发第二步SQL,查询每个用户的订单,最终结果与方案一一致。
执行SQL日志示例(开启日志打印):
3.3 方案二优势与局限性
优势:
-
灵活性高(支持延迟加载):按需加载从表数据,避免查询不需要的冗余数据,适合「主表数据量大、从表数据按需使用」的场景;
-
数据无冗余:主表和从表数据分开查询,主表字段不会重复传输,减少数据传输量;
-
可复用性强:第二步查询
findOrdersById可单独复用(如其他场景需要根据用户ID查询订单)。
局限性:
-
数据库连接次数多:若查询N个用户,且都需要加载订单,会执行1(主表)+N(从表)次SQL,增加数据库连接开销,适合用户数量较少的场景;
-
配置稍复杂:需要拆分SQL并配置分步映射,相比方案一多一步配置;
-
不支持复杂关联条件:分步查询的关联条件仅能通过单一字段(如user_id)传递,复杂关联场景适配性差。
四、两种方案核心对比与选型建议
4.1 核心对比表
| 对比维度 | SQL99式关联查询(方案一) | 分布式查询(方案二) |
|---|---|---|
| 查询次数 | 1次(一次性查询) | 1+N次(分步查询,N为用户数) |
| 数据冗余 | 主表字段可能重复(冗余) | 无冗余(按需加载) |
| 灵活性 | 低(无法按需加载) | 高(支持延迟加载) |
| 配置复杂度 | 简单(单SQL+单resultMap) | 稍复杂(多SQL+分步映射) |
| 适用数据量 | 主从表数据量均较小 | 主表数据量大,从表数据按需使用 |
| 关联条件复杂度 | 支持复杂关联(多字段关联) | 仅支持简单关联(单字段传递) |
4.2 选型建议
-
优先选方案一(SQL99式关联查询):当主从表数据量不大、需要一次性获取完整数据(如用户详情页需展示所有订单)、关联条件复杂时,选择该方案,兼顾效率和简洁性;
-
优先选方案二(分布式查询):当主表数据量大(如查询所有用户列表)、从表数据无需全部加载(如仅部分用户需要展示订单)、需要复用从表查询逻辑时,选择该方案,兼顾灵活性和数据冗余控制;
-
特殊优化:若方案二存在N+1查询问题(用户数量多导致多次从表查询),可通过MyBatis的
fetchSize属性或批量查询优化,减少数据库连接次数。
五、核心总结
MyBatis一对多嵌套查询的两种核心方案,本质是「一次性加载」与「按需加载」的权衡:
-
SQL99式关联查询:基于SQL99标准的关联语法,一次性查询所有数据,配置简单、效率高,适合小数据量、完整数据需求场景;
-
分布式查询:基于分步查询逻辑,支持延迟加载,数据无冗余、灵活性高,适合大数据量、按需加载场景。
实际开发中,需根据业务数据量、查询需求(是否按需加载)、关联条件复杂度选择合适的方案。同时,无论哪种方案,核心都是通过collection标签定义一对多映射规则,确保从表数据能正确封装到主实体的集合属性中。希望通过本文的拆解,能帮助大家精准掌握两种方案的实战应用,避开开发中的常见坑!