Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现

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 JOININNER 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 延迟加载

作者:武子康的个人博客

相关推荐
花椒技术2 小时前
企业内部 Agent 落地复盘:Gateway、Skill 和二次确认如何串起受控业务执行
后端·agent·ai编程
REDcker3 小时前
Linux OverlayFS详解
java·linux·运维
Royzst3 小时前
xml知识点
java·服务器·前端
我是一颗柠檬4 小时前
【MySQL全面教学】MySQL事务与ACID Day9(2026年)
数据库·后端·mysql
枕星而眠4 小时前
数据结构八大排序详解(一):四大简单排序
c语言·数据结构·c++·后端
IT_陈寒4 小时前
React useEffect闭包陷阱差点把我整失业了
前端·人工智能·后端
鱼鳞_4 小时前
苍穹外卖-Day08(缓存套餐)
java·redis·缓存
过期动态4 小时前
【LeetCode 热题 100】移动零
java·数据结构·算法·leetcode·职场和发展·rabbitmq
苍何4 小时前
爆肝两周,我把 Codex 最全实战指南开源了
后端