MyBatis一对多嵌套查询实战:SQL99式与分布式查询双视角解析

在实际开发中,一对多关系是非常常见的数据关联场景,比如用户与订单、部门与员工等。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 分步查询的两步核心流程
  1. 第一步:查询主表(users):执行findUsers()方法,执行SQLselect * from users,获取所有用户记录,此时订单集合orders未加载(若开启延迟加载);

  2. 第二步:按需查询从表(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标签定义一对多映射规则,确保从表数据能正确封装到主实体的集合属性中。希望通过本文的拆解,能帮助大家精准掌握两种方案的实战应用,避开开发中的常见坑!

相关推荐
要记得喝水1 天前
某公司WPF面试题(含答案和解析)--3
wpf
zzyzxb2 天前
WPF中Adorner和Style异同
wpf
棉晗榜2 天前
WPF锚点页面,点击跳转到指定区域
wpf
zzyzxb2 天前
Style/Setter、Template 属性、ControlTemplate 三者的关系
wpf
要记得喝水2 天前
某公司WPF面试题(含答案和解析)--2
wpf
zzyzxb2 天前
WPF中Template、Style、Adorner异同
wpf
小股虫2 天前
数据一致性保障:从理论深度到架构实践的十年沉淀
架构·wpf
廋到被风吹走3 天前
【Spring】PlatformTransactionManager详解
java·spring·wpf
源之缘-OFD先行者3 天前
全栈开发实战:WPF+FFmpeg+GIS,打造工业级雷达探测终端
ffmpeg·wpf