从0开始学习Java+AI知识点总结-20.web实战(多表查询)
一、多表关系设计:数据关联的核心逻辑
在数据库设计中,表与表之间的关系是根据业务场景确定的,常见的多表关系分为三种:一对多(多对一)、一对一和多对多。理解这些关系是实现复杂业务功能的基础。
1.1 一对多(多对一)关系
场景:部门与员工的关系(一个部门包含多个员工,一个员工仅属于一个部门)。
实现方式:在 "多" 的一方(员工表)中添加外键字段,关联 "一" 的一方(部门表)的主键。
案例:部门表与员工表设计
-- 部门表(一的一方) CREATE TABLE dept ( id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', name varchar(10) NOT NULL UNIQUE COMMENT '部门名称', create_time datetime COMMENT '创建时间', update_time datetime COMMENT '修改时间' ) COMMENT '部门表'; -- 员工表(多的一方) CREATE TABLE emp ( id int unsigned primary key auto_increment comment '主键ID', name varchar(10) not null comment '姓名', gender tinyint unsigned not null comment '性别(1:男,2:女)', dept_id int unsigned comment '关联部门ID', -- 外键,关联dept表的id entry_date date comment '入职日期', create_time datetime comment '创建时间', update_time datetime comment '修改时间' ) comment '员工表'; |
核心逻辑:通过emp.dept_id关联dept.id,确保员工与部门的归属关系。
1.2 一对一关系
场景:用户与身份证信息的关系(一个用户对应一个身份证,一个身份证仅属于一个用户)。
用途:单表拆分,将高频查询字段与低频字段分离,提升查询效率。
实现方式:在任意一方添加外键字段关联另一方的主键,并设置外键为唯一(UNIQUE)。
案例:用户表与身份证表设计
-- 用户基本信息表 CREATE TABLE tb_user ( id int unsigned primary key auto_increment comment '主键ID', name varchar(10) not null comment '姓名', phone char(11) comment '手机号' ) comment '用户基本信息表'; -- 用户身份证表(一对一关联) CREATE TABLE tb_user_card ( id int unsigned primary key auto_increment comment '主键ID', idcard char(18) not null comment '身份证号', issued varchar(20) not null comment '签发机关', user_id int unsigned not null unique comment '关联用户ID', -- 外键+唯一约束 constraint fk_user_id foreign key (user_id) references tb_user(id) ) comment '用户身份证表'; |
核心逻辑:tb_user_card.user_id关联tb_user.id,且user_id设为唯一,确保一对一映射。
1.3 多对多关系
场景:学生与课程的关系(一个学生可选多门课程,一门课程可被多个学生选择)。
实现方式:新增中间表,通过两个外键分别关联双方主键。
案例:学生表、课程表与中间表设计
-- 学生表 CREATE TABLE tb_student ( id int auto_increment primary key comment '主键ID', name varchar(10) comment '姓名', no varchar(10) comment '学号' ) comment '学生表'; -- 课程表 CREATE TABLE tb_course ( id int auto_increment primary key comment '主键ID', name varchar(10) comment '课程名称' ) comment '课程表'; -- 中间表(关联学生与课程) CREATE TABLE tb_student_course ( id int auto_increment primary key comment '主键', student_id int not null comment '学生ID', course_id int not null comment '课程ID', constraint fk_student_id foreign key (student_id) references tb_student(id), constraint fk_course_id foreign key (course_id) references tb_course(id) ) comment '学生课程关联表'; |
核心逻辑:中间表tb_student_course通过student_id和course_id分别关联学生表和课程表,实现多对多关系。
1.4 外键约束:数据一致性的保障
外键约束用于保证多表数据的一致性和完整性,但在实际开发中需权衡使用:
- 物理外键:通过foreign key关键字定义,数据库层面强制关联,缺点是影响增删改效率,不适合分布式场景。
- 逻辑外键:仅在业务代码中维护关联关系(如查询时通过dept_id过滤),企业开发中更常用,避免数据库性能瓶颈。
建议:非强一致性场景优先使用逻辑外键,通过代码逻辑保证数据关联。
二、多表查询:从关联数据中精准取数
多表查询是获取跨表关联数据的核心操作,需解决笛卡尔积问题并选择合适的查询方式。
2.1 笛卡尔积与关联条件
多表查询时若不添加关联条件,会产生笛卡尔积(两表所有记录的组合),导致数据冗余。需通过关联条件过滤无效数据:
-- 错误:产生笛卡尔积 select * from emp, dept; -- 正确:添加关联条件 select * from emp e, dept d where e.dept_id = d.id; -- 消除无效组合 |
2.2 内连接:查询交集数据
内连接仅返回两表中满足关联条件的记录(交集部分),语法有两种:
隐式内连接(常用)
-- 查询员工姓名及所属部门名称 select e.name as emp_name, d.name as dept_name from emp e, dept d where e.dept_id = d.id; |
显式内连接(推荐,可读性更高)
select e.name as emp_name, d.name as dept_name from emp e inner join dept d on e.dept_id = d.id; |
适用场景:需同时存在关联数据的场景(如查询有部门的员工)。
2.3 外连接:保留主表全部数据
外连接用于查询主表所有记录,即使关联表中无匹配数据,分为左外连接和右外连接。
左外连接
以左表为主表,返回左表所有记录及右表匹配记录(无匹配则为null):
-- 查询所有员工及所属部门(包括无部门的员工) select e.name as emp_name, d.name as dept_name from emp e left join dept d on e.dept_id = d.id; |
右外连接
以右表为主表,返回右表所有记录及左表匹配记录:
-- 查询所有部门及下属员工(包括无员工的部门) select d.name as dept_name, e.name as emp_name from emp e right join dept d on e.dept_id = d.id; |
技巧:左外连接和右外连接可互换,只需调整表的顺序。
2.4 子查询:嵌套查询的灵活应用
子查询是嵌套在主查询中的select语句,按返回结果可分为四类:
1. 标量子查询(返回单个值)
用于 where 条件中,作为比较值:
-- 查询最早入职的员工信息 select * from emp where entry_date = (select min(entry_date) from emp); |
2. 列子查询(返回单列多行)
常用in或not in判断是否在结果集中:
-- 查询"教研部"和"咨询部"的员工 select * from emp where dept_id in (select id from dept where name in ('教研部', '咨询部')); |
3. 行子查询(返回单行多列)
用于匹配多行多列的条件:
-- 查询与"李忠"薪资和职位相同的员工 select * from emp where (salary, job) = (select salary, job from emp where name = '李忠'); |
4. 表子查询(返回多行多列)
作为临时表参与主查询:
-- 查询2006年后入职的员工及部门信息 select e.*, d.name from (select * from emp where entry_date > '2006-01-01') e left join dept d on e.dept_id = d.id; |
优势:将复杂查询拆分为简单步骤,逻辑更清晰。
三、分页查询:高效展示大量数据
当数据量较大时,分页查询是必备功能,通过限制每页条数提升性能和用户体验。
3.1 原始分页实现
通过limit关键字实现,需计算起始索引和总记录数:
核心公式
- 起始索引 = (当前页码 - 1) × 每页条数
- 总页数 = 总记录数 ÷ 每页条数(向上取整)
代码实现
// 1. 计算总记录数 Long total = empMapper.count(); // 2. 计算起始索引 Integer start = (page - 1) * pageSize; // 3. 查询当前页数据 List<Emp> empList = empMapper.list(start, pageSize); // 4. 封装结果 return new PageResult(total, empList); |
对应的 Mapper 接口:
-- 统计总记录数 select count(*) from emp e left join dept d on e.dept_id = d.id; -- 查询分页数据 select e.*, d.name as dept_name from emp e left join dept d on e.dept_id = d.id limit #{start}, #{pageSize}; |
3.2 PageHelper 插件:简化分页操作
PageHelper 是 MyBatis 的分页插件,可自动完成分页逻辑,无需手动计算索引。
步骤 1:引入依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> |
步骤 2:代码实现
@Override public PageResult page(Integer page, Integer pageSize) { // 设置分页参数 PageHelper.startPage(page, pageSize); // 执行查询(无需手动加limit) List<Emp> empList = empMapper.list(); // 解析分页结果 Page<Emp> pageResult = (Page<Emp>) empList; return new PageResult(pageResult.getTotal(), pageResult.getResult()); } |
实现原理
PageHelper 通过拦截 SQL,自动添加count(*)查询总记录数,并在原 SQL 后拼接limit语句,简化开发。
注意事项:
- SQL 语句结尾不要加分号(;),否则插件可能无法正确解析。
- 仅对紧跟PageHelper.startPage()的第一条 SQL 生效。
四、条件分页与动态 SQL:灵活应对查询需求
实际场景中,用户常需通过多条件筛选数据(如按姓名、性别、日期范围),动态 SQL 可根据条件自动拼接 SQL 语句。
4.1 条件参数封装
当查询参数较多时,建议用实体类封装参数,提升代码可读性:
@Data public class EmpQueryParam { private Integer page = 1; // 默认页码 private Integer pageSize = 10; // 默认每页条数 private String name; // 姓名模糊查询 private Integer gender; // 性别(1:男,2:女) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate begin; // 入职开始日期 @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate end; // 入职结束日期 } |
4.2 动态 SQL 实现
使用 MyBatis 的<if>和<where>标签动态拼接条件:
<select id="list" resultType="com.example.pojo.Emp"> select e.*, d.name as dept_name from emp e left join dept d on e.dept_id = d.id <where> <!-- 姓名模糊查询:仅当name不为空时拼接 --> <if test="name != null and name != ''"> and e.name like concat('%', #{name}, '%') </if> <!-- 性别筛选:仅当gender不为空时拼接 --> <if test="gender != null"> and e.gender = #{gender} </if> <!-- 日期范围查询:仅当begin和end都不为空时拼接 --> <if test="begin != null and end != null"> and e.entry_date between #{begin} and #{end} </if> </where> order by e.update_time desc </select> |
标签作用:
- <if>:条件判断,true 则拼接 SQL 片段。
- <where>:自动添加where关键字,并去除多余的and/or。
4.3 完整条件分页流程
- Controller 接收参数:
@GetMapping("/emps") public Result page(EmpQueryParam param) { PageResult result = empService.page(param); return Result.success(result); } |
- Service 层实现:
public PageResult page(EmpQueryParam param) { PageHelper.startPage(param.getPage(), param.getPageSize()); List<Emp> empList = empMapper.list(param); // 传入条件参数 Page<Emp> page = (Page<Emp>) empList; return new PageResult(page.getTotal(), page.getResult()); } |
- Mapper 接口:
List<Emp> list(EmpQueryParam param); |
五、实战总结与最佳实践
- 多表关系设计:
- 一对多:在多的一方加外键(如emp.dept_id)。
- 一对一:外键 + 唯一约束(如tb_user_card.user_id)。
- 多对多:通过中间表关联(如tb_student_course)。
- 查询方式选择:
- 简单关联查询用内连接 / 外连接。
- 复杂条件查询用子查询或表连接 + 动态 SQL。
- 分页优化:
- 小数据量用原始limit分页。
- 大数据量用 PageHelper 插件,配合索引提升性能。
- 动态 SQL 技巧:
- 用<if>判断条件,<where>处理关键字。
- 参数较多时封装实体类,避免方法参数冗余。
通过本文的知识点,你可以轻松实现员工管理系统中的多表关联、分页查询和条件筛选功能。这些技术在电商、CRM 等各类系统中均有广泛应用,掌握后能显著提升后端开发效率。收藏本文,关注后续实战进阶内容,带你深入更多 Java 后端核心技术!