MyBatis之关联查询
实际开发中数据库表之间往往存在关联关系(如用户与订单、订单与商品),MyBatis的关联查询用于处理这些关系,将多表数据映射为Java对象的关联关系,相比JDBC手动处理结果集拼接,MyBatis通过resultMap
的association
和collection
标签,能自动完成关联数据的映射。本文我将系统讲解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_id
FROM user u
LEFT JOIN id_card ic ON u.id = ic.user_id
WHERE u.id = #{id}
</select>
核心说明:
association
的property
:对应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>
核心说明:
association
的select
:指定查询关联对象的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, -- 关联用户ID
PRIMARY 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_id
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
核心说明:
collection
的property
:对应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_name
FROM student s
LEFT JOIN student_course sc ON s.id = sc.student_id
LEFT JOIN course c ON sc.course_id = c.id
WHERE 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 合理使用别名避免字段冲突
多表查询时,若主表与关联表有同名字段(如id
、name
),需通过别名区分,否则映射结果会被覆盖。
-- 错误:未用别名,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
,但数据库存在关联数据。
原因:
resultMap
中column
与SQL查询的字段名不匹配(如SQL用card_id
,resultMap
写column="id"
);- 表连接条件错误(如
JOIN
条件不正确,导致关联数据未查询到); - 关联表无匹配数据(正常情况,如用户未绑定身份证,
idCard
为null
)。
解决方案:
- 检查
resultMap
的column
是否与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关联查询通过resultMap
的association
和collection
标签,实现了多表数据到Java对象关联关系的映射,核心要点如下:
- 标签选择:
- 一对一用
association
(映射单个对象);- 一对多/多对多用
collection
(映射集合)。
- 查询方式:
- 优先用连接查询(单轮
JOIN
查询,无N+1问题,性能好);- 嵌套查询仅用于“关联数据按需加载”场景(需注意N+1问题)。
- 优化技巧:
- 用别名区分同名字段,避免映射冲突;
- 配置
id
标签确保关联数据不重复;- 避免查询无关字段,减少数据传输量。
若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ