实际开发中数据库表之间往往存在关联关系(如用户与订单、订单与商品),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标签确保关联数据不重复; - 避免查询无关字段,减少数据传输量。
到此这篇关于mybatis的关联查询实现(一对一、一对多、多对多)的文章就介绍到这了,更多相关mybatis 关联查询内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论