TL;DR
- 场景:关系型数据库中一对多关系的建模,以及使用MyBatis框架实现用户-订单跨表查询
- 结论:一对多关系通过外键约束(uid引用用户表主键)实现,MyBatis通过resultMap的collection标签将子表订单映射为父表用户的List属性
- 产出:完整的建表SQL(带ON DELETE CASCADE)、LEFT JOIN查询语句、UserMapper.xml配置、WzkUser实体类代码,可直接移植到项目

版本矩阵
| 功能 | 版本/年份 | 状态 | 说明 |
|---|---|---|---|
| MyBatis ORM框架 | 3.5.x (2024) | ✅ 已验证 | 本文代码基于3.5.x版本 |
| MySQL数据库 | 8.0+ | ✅ 已验证 | 支持InnoDB引擎与AUTO_INCREMENT |
| 外键约束 | MySQL 5.0+ | ✅ 已验证 | FOREIGN KEY + REFERENCES |
| ON DELETE CASCADE | MySQL 5.0+ | ✅ 已验证 | 级联删除外键关联记录 |
| resultMap collection | MyBatis 3.0+ | ✅ 已验证 | 一对多嵌套结果映射 |
| LEFT JOIN | SQL标准 | ✅ 已验证 | 保留左表所有记录 |
文章正文
基本介绍
在数据库设计中,一对多(One-to-Many)的关系是最常见的关系模型之一。它通常用于表示一个实体(表中的一条记录)可以与另一个实体的多条记录相关联的情况。
一对多关系表示一个实体(父表中的一条记录)可以关联到多个实体(子表中的多条记录),而子表中的每条记录只能与父表中的一条记录关联。
通俗理解:一个用户可以下多个订单,但每个订单只属于一个用户。这就是典型的一对多关系。
一对多优点
- 数据清晰,关系明确:表结构直观反映了现实世界中的层级关系,易于理解和维护。
- 数据完整性可以通过外键约束强制维护:数据库层面的约束确保不会出现"孤儿记录"。
- 查询灵活,通过关联表可以获取丰富的数据 :支持
LEFT JOIN、INNER JOIN等多种查询方式,灵活获取关联数据。 - 减少数据冗余:相比将所有数据放在一张表中,一对多设计避免了大量重复存储。
一对多缺点
- 对于复杂查询,可能涉及多次连接(JOIN),性能稍差:当数据量较大时,多表 JOIN 可能成为性能瓶颈。
- 数据模型耦合较紧,当表结构发生变更时,影响范围较大:修改主表或子表结构时,需要同步调整关联逻辑。
- 需要额外维护外键索引:外键字段若不建索引,在大数据量下查询性能会显著下降。
查询模型
用户表和订单表的关系为:一个用户有多个订单,一个订单只属于一个用户,一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单。
实现方式
数据库约束
外键约束(FOREIGN KEY)确保子表的外键值必须是主表中存在的主键值。
级联操作
ON DELETE CASCADE:如果删除主表中的记录,子表中的相关记录也会被删除。 ON UPDATE CASCADE:如果主表中的主键被更新,子表中的外键也会相应更新。
对应特点
- 单向性:一侧是"一个",另一侧是"多个"。
- 关联约束:多的一侧的每条记录只能关联到一的一侧的一条记录,但一的一侧可以关联多条记录。
- 典型场景:用户与订单(一个用户可以有多个订单)、学校与学生(一所学校可以有多个学生)。
创建表
虽然表之前已经创建出来了,但这里补充建表 SQL 供参考:
sql
-- 用户表(一的一方)
CREATE TABLE wzk_user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL,
birthday DATE
);
-- 订单表(多的一方)
CREATE TABLE wzk_orders (
id INT PRIMARY KEY AUTO_INCREMENT,
uid INT NOT NULL,
ordertime DATETIME,
total DECIMAL(10, 2),
FOREIGN KEY (uid) REFERENCES wzk_user(id) ON DELETE CASCADE
);
关键点 :订单表中的 uid 字段作为外键,引用用户表的主键 id,从而建立一对多关系。
插入数据
数据之前已经写入,这里补充插入示例供参考:
sql
-- 插入用户数据
INSERT INTO wzk_user (id, username, password, birthday) VALUES
(1, 'wzk', 'icu', '2024-11-11'),
(2, 'wzk2', 'icu2', '2024-11-11');
-- 插入订单数据(关联用户)
INSERT INTO wzk_orders (id, uid, ordertime, total) VALUES
(1, 1, '2024-11-11 10:00:00', 100.00),
(2, 1, '2024-11-11 14:30:00', 200.00),
(3, 2, '2024-11-10 09:00:00', 150.00);
可以看到,用户 wzk(id=1)有两个订单,用户 wzk2(id=2)有一个订单,完美体现了一对多关系。
查询语句
sql
select *, o.id oid from wzk_user u left join wzk_orders o on u.id = o.uid;
执行结果如下所示:

创建类
实体类已经创建了,这里跳过。但是需要进行修改。
WzkUser
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WzkUser {
private int id;
private String username;
private String password;
private Date birthday;
private List<WzkOrder> orderList;
}
对应的截图如下所示:

UserMapper
java
public interface UserMapper {
List<WzkUser> findAll();
}
对应的截图如下所示:

UserMapper.xml
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="icu.wzk.mapper.UserMapper">
<resultMap id="userMap" type="icu.wzk.model.WzkUser">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<collection property="orderList" ofType="icu.wzk.model.WzkOrder">
<result column="oid" property="id"></result>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select *, o.id oid from wzk_user u left join wzk_orders o on u.id = o.uid;
</select>
</mapper>
对应的截图如下所示:

sqlMapConfig.xml
现在注意,记得修改 sqlMapConfig 文件。
xml
<mappers>
<mapper resource="mapper.xml"/>
<mapper resource="OrderMapper.xml"/>
<mapper resource="UserMapper.xml"/>
</mappers>
对应的截图如下所示:

编写代码
java
public class WzkIcu09 {
public static void main(String[] args) throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<WzkUser> dataList = userMapper.findAll();
dataList.forEach(System.out::println);
sqlSession.close();
}
}
编写的代码截图如下所示:

运行结果
控制台运行结果如下所示:
shell
24/11/12 17:12:41 DEBUG UserMapper.findAll: <== Total: 3
WzkUser(id=1, username=wzk, password=icu, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=[WzkOrder(id=1, ordertime=Mon Nov 11 00:00:00 CST 2024, total=100.0, user=null), WzkOrder(id=2, ordertime=Mon Nov 11 00:00:00 CST 2024, total=200.0, user=null)])
WzkUser(id=2, username=wzk2, password=icu2, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=[WzkOrder(id=3, ordertime=Sun Nov 10 00:00:00 CST 2024, total=150.0, user=null)])
对应的控制台如下所示:

优化与注意事项
索引优化
为外键字段添加索引以提高查询性能。
数据完整性
外键约束确保数据一致性,但在高并发场景下可能降低性能,因此可以选择通过程序逻辑维护。
设计扩展性
考虑未来是否会转变为多对多关系(例如:一个订单包含多个商品),在设计时预留扩展空间。
暂时小结
总结来说,一对多模型是关系型数据库中最基本、最常用的关系之一,它清晰地表达了实体间的层级关系。通过合理设计表结构、优化查询和索引,可以高效管理和操作这些数据关系。
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| 查询结果 orderList 为空 | LEFT JOIN 写成 INNER JOIN,左表有记录但右表无关联时丢失 | 检查 SQL 是否使用了 LEFT JOIN |
确保使用 LEFT JOIN 保留左表所有记录 |
| collection 中子对象属性为空 | resultMap 中 column 与 SQL 返回列名不匹配 | 检查 collection 内 result column 与 SELECT 语句列别名 | 确保 SELECT 中使用 o.id oid 别名,collection 中用 oid |
| 外键约束报错 "Cannot add or update" | 子表插入的 uid 值在父表中不存在 | 插入前先确认父表是否有对应 id | 先插入父表记录,或检查插入顺序 |
| 删除父表记录后子表成为孤儿记录 | 未设置 ON DELETE CASCADE | 检查建表语句的外键定义 | 添加 ON DELETE CASCADE 级联删除 |
| 多表 JOIN 查询性能极慢 | 大数据量下未给外键字段建索引 | 执行 EXPLAIN 查看查询计划 | 为 wzk_orders.uid 列添加索引 CREATE INDEX idx_uid ON wzk_orders(uid) |
| MyBatis 映射的 orderList 中 user 字段非 null | 这是正常现象,WzkOrder 对象的 user 属性在本次查询中未映射 | 不影响业务逻辑,orderList 已正确包含订单数据 | 如需避免混淆,可在 WzkOrder 类中将 user 标记为 transient |
| 一对多查询 N+1 问题 | 循环查询导致性能问题 | 观察 SQL 日志中是否存在多次查询 | 使用 LEFT JOIN 一次性查询,或配置 MyBatis 延迟加载 |
作者:武子康的个人博客