当前位置: 首页 > news >正文

MyBatis之关联查询

MyBatis之关联查询

    • 一、关联查询的基本概念
      • 1.1 数据库表关联关系
      • 1.2 MyBatis关联查询的核心
    • 二、一对一关联查询
      • 2.1 数据库表设计
      • 2.2 实体类设计
      • 2.3 一对一查询实现
        • 2.3.1 方式1:连接查询(推荐)
        • 2.3.2 方式2:嵌套查询
        • 2.3.3 两种方式对比
    • 三、一对多关联查询
      • 3.1 数据库表设计
      • 3.2 实体类设计
      • 3.3 一对多查询实现
    • 四、多对多关联查询
      • 4.1 数据库表设计
      • 4.2 实体类设计
      • 4.3 多对多查询实现
    • 五、关联查询的优化与最佳实践
      • 5.1 避免N+1查询问题
      • 5.2 合理使用别名避免字段冲突
      • 5.3 按需查询关联数据
      • 5.4 延迟加载(按需加载关联数据)
    • 六、常见问题与避坑指南
      • 6.1 关联对象为null(映射失败)
      • 6.2 集合数据重复(一条数据被多次映射)
      • 6.3 嵌套查询参数传递错误

实际开发中数据库表之间往往存在关联关系(如用户与订单、订单与商品),MyBatis的关联查询用于处理这些关系,将多表数据映射为Java对象的关联关系,相比JDBC手动处理结果集拼接,MyBatis通过resultMapassociationcollection标签,能自动完成关联数据的映射。本文我将系统讲解MyBatis关联查询的核心实现,包括一对一、一对多、多对多关系,并结合实例解析查询方式与优化技巧。

一、关联查询的基本概念

1.1 数据库表关联关系

数据库表的关联关系主要有三种:

  • 一对一:A表一条记录对应B表一条记录(如用户与身份证,一个用户对应一个身份证);
  • 一对多:A表一条记录对应B表多条记录(如用户与订单,一个用户可有多笔订单);
  • 多对多:A表多条记录对应B表多条记录(如学生与课程,一个学生可选多门课程,一门课程可有多个学生),通常通过中间表实现。

1.2 MyBatis关联查询的核心

MyBatis通过resultMap实现关联查询,核心标签:

  • association:映射一对一关系(如用户对象中包含一个身份证对象);
  • collection:映射一对多多对多关系(如用户对象中包含一个订单列表)。

关联查询有两种实现方式:

  • 嵌套查询:先查询主表数据,再根据主表字段查询关联表(多轮查询);
  • 连接查询:通过JOIN语句一次性查询多表数据(单轮查询)。

后续将通过案例详细对比这两种方式的优缺点。

二、一对一关联查询

以“用户(user)与身份证(id_card)”为例,一个用户对应一个身份证,实现一对一关联查询。

2.1 数据库表设计

-- 用户表
CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT,`username` varchar(50) NOT NULL,`age` int DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 身份证表(与用户一对一关联)
CREATE TABLE `id_card` (`id` int NOT NULL AUTO_INCREMENT,`card_no` varchar(20) NOT NULL, -- 身份证号`user_id` int NOT NULL, -- 关联用户ID(外键)PRIMARY KEY (`id`),UNIQUE KEY `user_id` (`user_id`) -- 一对一:user_id唯一
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2 实体类设计

// 用户类(包含一个身份证对象)
@Data
public class User {private Integer id;private String username;private Integer age;// 一对一关联:用户包含一个身份证private IdCard idCard; 
}// 身份证类
@Data
public class IdCard {private Integer id;private String cardNo;private Integer userId;
}

2.3 一对一查询实现

2.3.1 方式1:连接查询(推荐)

通过JOIN语句一次性查询用户和身份证数据,再通过association映射关联对象。

Mapper接口

// 根据用户ID查询用户及关联的身份证
User selectUserWithIdCardById(Integer id);

Mapper XML

<!-- 定义resultMap:映射用户及身份证 -->
<resultMap id="UserWithIdCardMap" type="User"><!-- 用户表字段映射 --><id column="id" property="id"/><result column="username" property="username"/><result column="age" property="age"/><!-- 一对一关联:association映射IdCard对象 --><association property="idCard" javaType="IdCard"><id column="card_id" property="id"/> <!-- 注意:避免与user.id字段冲突 --><result column="card_no" property="cardNo"/><result column="user_id" property="userId"/></association>
</resultMap><!-- 连接查询:一次性查询用户和身份证 -->
<select id="selectUserWithIdCardById" resultMap="UserWithIdCardMap">SELECT u.id, u.username, u.age,ic.id AS card_id, ic.card_no, ic.user_idFROM user uLEFT JOIN id_card ic ON u.id = ic.user_idWHERE u.id = #{id}
</select>

核心说明

  • associationproperty:对应User类中的idCard属性;
  • javaType:指定关联对象的类型(IdCard);
  • 表连接时需通过AS为关联表字段起别名(如ic.id AS card_id),避免与主表字段(u.id)冲突。
2.3.2 方式2:嵌套查询

先查询用户数据,再通过用户ID查询身份证(分两次查询)。

步骤1:查询身份证的Mapper

// IdCardMapper接口
IdCard selectById(Integer id);
<select id="selectById" resultType="IdCard">SELECT id, card_no, user_id FROM id_card WHERE id = #{id}
</select>

步骤2:查询用户并嵌套查询身份证

<resultMap id="UserWithIdCardNestedMap" type="User"><id column="id" property="id"/><result column="username" property="username"/><result column="age" property="age"/><!-- 嵌套查询:通过select属性指定查询关联对象的方法 --><association property="idCard" javaType="IdCard"column="id" <!-- 将用户id作为参数传递给关联查询 -->select="com.example.mapper.IdCardMapper.selectByUserId"/> <!-- 关联查询的Mapper方法 -->
</resultMap><select id="selectUserWithIdCardNestedById" resultMap="UserWithIdCardNestedMap">SELECT id, username, age FROM user WHERE id = #{id}
</select>

核心说明

  • associationselect:指定查询关联对象的Mapper方法(全类名+方法名);
  • column:将主查询的id(用户ID)作为参数传递给selectByUserId方法。
2.3.3 两种方式对比
方式优点缺点适用场景
连接查询单轮查询,性能好SQL较复杂(多表JOIN)关联数据必须查询,且表数据量不大
嵌套查询SQL简单,逻辑清晰多轮查询(N+1问题),性能较差关联数据可选查询(按需加载)

三、一对多关联查询

以“用户(user)与订单(order)”为例,一个用户可有多笔订单,实现一对多关联查询。

3.1 数据库表设计

-- 订单表(与用户一对多关联)
CREATE TABLE `order` (`id` int NOT NULL AUTO_INCREMENT,`order_no` varchar(20) NOT NULL, -- 订单号`total_amount` decimal(10,2) NOT NULL, -- 总金额`user_id` int NOT NULL, -- 关联用户IDPRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3.2 实体类设计

// 用户类(包含订单列表)
@Data
public class User {private Integer id;private String username;private Integer age;// 一对多关联:用户包含多个订单private List<Order> orders; 
}// 订单类
@Data
public class Order {private Integer id;private String orderNo;private BigDecimal totalAmount;private Integer userId;
}

3.3 一对多查询实现

连接查询为例(推荐,单轮查询性能更好):

Mapper接口

// 查询用户及关联的所有订单
User selectUserWithOrdersById(Integer id);

Mapper XML

<!-- 定义resultMap:映射用户及订单列表 -->
<resultMap id="UserWithOrdersMap" type="User"><!-- 用户表字段 --><id column="id" property="id"/><result column="username" property="username"/><result column="age" property="age"/><!-- 一对多关联:collection映射订单列表 --><collection property="orders" ofType="Order"> <!-- ofType指定集合元素类型 --><id column="order_id" property="id"/> <!-- 订单ID(别名避免冲突) --><result column="order_no" property="orderNo"/><result column="total_amount" property="totalAmount"/><result column="user_id" property="userId"/></collection>
</resultMap><!-- 连接查询:用户与订单 -->
<select id="selectUserWithOrdersById" resultMap="UserWithOrdersMap">SELECT u.id, u.username, u.age,o.id AS order_id, o.order_no, o.total_amount, o.user_idFROM user uLEFT JOIN `order` o ON u.id = o.user_idWHERE u.id = #{id}
</select>

核心说明

  • collectionproperty:对应User类中的orders属性(List类型);
  • ofType:指定集合中元素的类型(Order),区别于javaType(用于指定属性类型,如List);
  • 主表与关联表的字段需通过别名区分(如o.id AS order_id),避免映射混乱。

四、多对多关联查询

以“学生(student)与课程(course)”为例,一个学生可选多门课程,一门课程可有多个学生,通过中间表student_course实现关联。

4.1 数据库表设计

-- 学生表
CREATE TABLE `student` (`id` int NOT NULL AUTO_INCREMENT,`name` varchar(50) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 课程表
CREATE TABLE `course` (`id` int NOT NULL AUTO_INCREMENT,`name` varchar(50) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 中间表(多对多关联)
CREATE TABLE `student_course` (`id` int NOT NULL AUTO_INCREMENT,`student_id` int NOT NULL,`course_id` int NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `uk_stu_course` (`student_id`,`course_id`) -- 避免重复关联
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 实体类设计

// 学生类(包含课程列表)
@Data
public class Student {private Integer id;private String name;// 多对多关联:学生包含多个课程private List<Course> courses;
}// 课程类
@Data
public class Course {private Integer id;private String name;
}

4.3 多对多查询实现

多对多查询本质是一对多的扩展(通过中间表连接),以连接查询为例:

Mapper接口

// 查询学生及所选课程
Student selectStudentWithCoursesById(Integer id);

Mapper XML

<resultMap id="StudentWithCoursesMap" type="Student"><id column="id" property="id"/><result column="name" property="name"/><!-- 多对多:collection映射课程列表 --><collection property="courses" ofType="Course"><id column="course_id" property="id"/><result column="course_name" property="name"/></collection>
</resultMap><select id="selectStudentWithCoursesById" resultMap="StudentWithCoursesMap">SELECT s.id, s.name,c.id AS course_id, c.name AS course_nameFROM student sLEFT JOIN student_course sc ON s.id = sc.student_idLEFT JOIN course c ON sc.course_id = c.idWHERE s.id = #{id}
</select>

核心说明

  • 多对多通过“主表→中间表→关联表”的JOIN实现;
  • collection标签用法与一对多相同(均映射集合),区别在于表连接逻辑。

五、关联查询的优化与最佳实践

5.1 避免N+1查询问题

N+1问题:嵌套查询时,若查询N个主表记录,会触发1次主表查询+N次关联表查询,导致性能下降。

示例:查询所有用户及其订单(嵌套查询方式):

<!-- 1次主表查询:查询所有用户 -->
<select id="selectAllUser" resultMap="UserWithOrdersNestedMap">SELECT id, username, age FROM user
</select><!-- 每个用户触发1次订单查询(若有100个用户,触发100次) -->
<collection property="orders" select="selectOrdersByUserId" column="id"/>

解决方案

  • 优先使用连接查询(单轮查询,无N+1问题);
  • 若需嵌套查询,开启MyBatis二级缓存,缓存关联查询结果;
  • 限制查询数量(如分页查询),减少关联查询次数。

5.2 合理使用别名避免字段冲突

多表查询时,若主表与关联表有同名字段(如idname),需通过别名区分,否则映射结果会被覆盖。

-- 错误:未用别名,o.id会覆盖u.id
SELECT u.id, u.name, o.id, o.name 
FROM user u JOIN order o ON u.id = o.user_id-- 正确:用别名区分
SELECT u.id AS user_id, u.name AS user_name,o.id AS order_id, o.name AS order_name

5.3 按需查询关联数据

并非所有场景都需要查询关联数据(如列表页展示用户基本信息,无需查询订单),应根据场景设计不同查询:

  • 简单查询:仅查询主表数据(无关联);
  • 详情查询:查询主表+关联数据(通过连接查询)。

5.4 延迟加载(按需加载关联数据)

MyBatis支持延迟加载(懒加载):查询主表数据时不加载关联数据,仅当访问关联属性时才触发关联查询,适合“大部分场景不需要关联数据”的场景。

开启延迟加载(在MyBatis配置文件中):

<settings><setting name="lazyLoadingEnabled" value="true"/> <!-- 全局开启延迟加载 --><setting name="aggressiveLazyLoading" value="false"/> <!-- 按需加载(访问时才加载) -->
</settings>

使用场景:详情页默认展示用户信息,点击“查看订单”按钮才加载订单数据(通过代码触发关联属性访问)。

六、常见问题与避坑指南

6.1 关联对象为null(映射失败)

问题:关联对象(如idCard)为null,但数据库存在关联数据。

原因

  • resultMapcolumn与SQL查询的字段名不匹配(如SQL用card_idresultMapcolumn="id");
  • 表连接条件错误(如JOIN条件不正确,导致关联数据未查询到);
  • 关联表无匹配数据(正常情况,如用户未绑定身份证,idCardnull)。

解决方案

  • 检查resultMapcolumn是否与SQL查询的字段(含别名)一致;
  • 单独执行SQL,确认关联数据是否被正确查询;
  • 若允许关联数据为null,无需处理(正常逻辑)。

6.2 集合数据重复(一条数据被多次映射)

问题collection映射的列表中,同一条数据被重复添加(如一个订单出现多次)。

原因

  • 未正确配置id标签:resultMap中未用id标签指定关联对象的唯一标识(如订单的id),MyBatis无法判断数据是否重复;
  • SQL查询返回重复数据(如JOIN导致主表数据被关联表数据重复)。

解决方案

  • 为关联对象配置id标签(collection内的id),指定唯一标识字段:
<collection property="orders" ofType="Order"><id column="order_id" property="id"/> <!-- 关键:指定订单唯一标识 --><!-- 其他字段 -->
</collection>
  • 优化SQL,避免返回重复数据(如使用DISTINCT或调整JOIN逻辑)。

6.3 嵌套查询参数传递错误

问题:嵌套查询时,column传递的参数不正确,导致关联查询无结果。

解决方案

  • 确保column的值与主查询返回的字段名一致:
<!-- 主查询返回字段为user_id -->
<select id="selectUser" resultMap="UserMap">SELECT id AS user_id, username FROM user
</select><!-- 嵌套查询需用主查询的字段名作为参数 -->
<association select="selectOrder" column="user_id"/> <!-- 正确:column="user_id" -->
  • 传递多个参数时,用column="{key1=col1, key2=col2}"
<association select="selectByParams" column="{id=user_id, name=user_name}"/>

总结:关联查询的核心要点
MyBatis关联查询通过resultMapassociationcollection标签,实现了多表数据到Java对象关联关系的映射,核心要点如下:

  1. 标签选择
  • 一对一用association(映射单个对象);
  • 一对多/多对多用collection(映射集合)。
  1. 查询方式
  • 优先用连接查询(单轮JOIN查询,无N+1问题,性能好);
  • 嵌套查询仅用于“关联数据按需加载”场景(需注意N+1问题)。
  1. 优化技巧
  • 用别名区分同名字段,避免映射冲突;
  • 配置id标签确保关联数据不重复;
  • 避免查询无关字段,减少数据传输量。

若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ

http://www.lryc.cn/news/592611.html

相关文章:

  • leetcode:冗余连接 II[并查集检查环][节点入度]
  • 【机器人】HOV-SG 开放词汇 | 分层3D场景图 | 语言引导机器人导航
  • vue3+vite 使用scss、sass 全局定义的变量以及使用
  • 【Linux】进程间通信(三)——共享内存和消息队列
  • 特种作业操作证(制冷空调)的考试科目有哪些?
  • Spring AI开发智能客服(Tool calling)
  • 第七章 愿景09 海波龙的坑
  • 链表算法之【链表的中间节点】
  • MSTP+VRRP+DHCP配置实验(ensp)
  • 医疗人工智能的心电图分析:创新技术与临床应用
  • 多组件Canvas ID冲突解决方案
  • Pythonday17
  • 深入理解进程地址空间:虚拟内存与进程独立性
  • 2-大语言模型—理论基础:详解Transformer架构的实现(2)
  • 专题 原型与继承完全指南
  • QT聊天项目DAY15
  • 更适合后端宝宝的前端三件套之HTML
  • GEV/POT/Markov/点过程/贝叶斯极值全解析;基于R语言的极值统计学
  • 设计模式五:桥模式(Bridge Pattern)
  • 关于在VScode中使用git的一些步骤常用命令及其常见问题:
  • Redis工具类
  • RHCE第二次作业
  • MyBatis:配置文件完成增删改查_添加
  • Java 核心工具类 API 详解(一):从 Math 到 Runtime 的实用指南
  • 谷歌浏览器Chrome的多用户配置文件功能
  • 简单易懂,基本地址变换机构
  • 高防IP能够防御CC攻击吗?它具备哪些显著优势?
  • 【easytokenizer】高性能文本 Tokenizer库 | 源码详解与编译安装
  • Java中类加载器及双亲委派机制原理
  • 2023 年 3 月青少年软编等考 C 语言八级真题解析