班级表是整条链的起点,所以它最「安全」——只被一个模块写,改坏了影响一大片。 列表一定要带「这个班多少人」,因为「删不删得掉」取决于人数。 「改成毕业」不等于「能删」——外键还指着它,软删除解决不了引用问题。
从这一节开始进业务模块,四个模块(班级、学生、成绩、查询)骨架一样: 列表 → 选中 → 编辑 → 校验 → 保存 → 刷新。这一节把骨架走一遍,后面三节就快了。 教材 20.8 分三小节:概述、技术分析、实现过程。 「实现过程」里的删除部分值得细看——它要处理「班里还有学生」这个情况。 沙箱没有 JSP,所以「页面」用「表格输出 + 提示文案」复刻, 复刻的是「用户点了之后数据怎么变」,不是「按钮长什么样」。
一、列表:两个数比一个数有用
| 列 | 从哪来 | 为什么要有 |
|---|---|---|
| 班名 | tb_class.cname |
主标识 |
| 年级 | tb_class.grade |
排序和筛选 |
| 班主任 | tb_class.teacher |
可空,新班可能没定 |
| 人数 | COUNT(tb_student.id) |
决定能不能删 |
列表页永远要带「这个班有多少人」。 因为「删不删得掉」取决于人数——
管理员点了删除才知道删不掉,体验很差。
这个人数要 LEFT JOIN 数出来,不能用 JOIN。
新班没有学生,JOIN 会让它整行消失——
管理员刚建完班,列表里就看不到,他一定会再建一遍,于是重名报错。
二、增:一个班级三件事
| 要做的事 | 注意什么 |
|---|---|
| 班名唯一 | UNIQUE 拦住重名,报错要翻译成人话 |
| 年级必填 | NOT NULL,别指望页面校验 |
| 班主任可空 | 新班可能还没定班主任 |
「报错要翻译」这件事看着小事,其实是体验的分水岭。
直接抛出 UNIQUE constraint failed 给用户看,他只会觉得系统坏了。
做法是在 DAO 里 catch 住,按错误类型返回人话。
「班级名已存在,请换一个」比一堆英文有用得多。
还有一个更细的点: 重名检查该在页面做还是数据库做?
都做。 页面做是为了「不用等一次往返就知道错了」,数据库做是为了「并发下也不会重」。
两个目的不同,不冲突。
三、改:改班名影响谁
| 改名后 | 会怎样 |
|---|---|
学生表里的 class_id |
不受影响(存的是 id) |
| 历史成绩 | 不受影响(从学生表连过去) |
| 页面上的旧截图、打印件 | 还是旧名字——这个改不了 |
这就是「存 id 不存名字」的价值。 改一次班名,一处改完,全站生效。 如果学生表里存的是班名字符串,改名要改所有学生记录,还得担心漏。 但有个例外要记住:历史表要存名字(第 19 章调岗记录那样)。 区别在于「历史要冻结当时的样子」,主数据要「跟着当前状态走」。
四、删:三种情况,三种回答
| 情况 | 能不能删 | 该给什么提示 |
|---|---|---|
| 班里 0 人 | 能删 | 确认即可 |
| 班里有人 | 删不掉 | 「还有 4 人,请先转班」 |
| 学生都改成毕业/转出 | 还是删不掉 | 「class_id 还指着这个班」 |
第三种情况是这一节最容易踩的坑。
「软删除」只解决「人还在不在名单里」,不解决「外键引用」。
class_id 还指着这个班,外键照样拦。
真要撤班,学生必须挪到另一个班(或者真删)。
所以界面上该给的提示是「这个班还有 4 人,请先处理」,
而不是「删除失败」——前者告诉他下一步做什么,后者只告诉他不行。
五、动手 Demo:班级的增删改查与三种删除结果
这一段把班级的四个动作各跑一遍,重点在第四节:三种删除情况各是什么结果。 尤其是第三种——学生都改成「转出」了,班级还是删不掉。
# ① 列表带人数(LEFT JOIN 不能换成 JOIN)
sql = '''SELECT c.id, c.cname, c.teacher, COUNT(s.id) AS 人数
FROM tb_class c
LEFT JOIN tb_student s ON s.class_id = c.id
GROUP BY c.id ORDER BY c.id'''
# ② 增:重名被 UNIQUE 拦住
try:
c.execute('INSERT INTO tb_class (cname, grade, teacher)'
' VALUES (?,?,?)', ('2026级计算机1班', '2026', '某人'))
except Exception as e:
print('重名:', e) # 要翻译成「班级已存在」
# ③ 删:三种情况
for sql in ('DELETE FROM tb_class WHERE id = ?', # 空班
'DELETE FROM tb_class WHERE id = 1'): # 有人的班
try:
print('影响', c.execute(sql, (new_id,)).rowcount, '行')
except Exception as e:
print('被拦住:', e)
实跑输出:
--------------------------------------------------------------------
班级信息设置:最基础也最容易漏
--------------------------------------------------------------------
① 四件事:查、增、改、删
--------------------------------------------------------------------
1> SELECT id, cname, grade, teacher FROM tb_class ORDER BY id
id cname grade teacher
-----------------------------------
1 2026级计算机1班 2026 刘建国
2 2026级计算机2班 2026 陈丽
3 2026级软件1班 2026 周海涛
4 2025级计算机1班 2025 刘建国
(4 行受影响)
列表页永远要带「这个班有多少人」。
因为「删不删得掉」取决于人数——管理员点删除才知道删不掉,体验很差。
1> SELECT c.id, c.cname, c.teacher, COUNT(s.id) AS 人数
FROM tb_class c
LEFT JOIN tb_student s ON s.class_id = c.id
GROUP BY c.id
ORDER BY c.id
id cname teacher 人数
----------------------------------
1 2026级计算机1班 刘建国 4
2 2026级计算机2班 陈丽 3
3 2026级软件1班 周海涛 3
4 2025级计算机1班 刘建国 2
(4 行受影响)
LEFT JOIN 不能换成 JOIN。
新班没有学生,JOIN 会让它整行消失——管理员刚建完班,列表里就看不到,他一定会再建一遍,
于是重名报错。
② 增:一个班级三件事
--------------------------------------------------------------------
| 要做的事 | 注意什么 |
|---|---|
| 班名唯一 | UNIQUE 拦住重名,报错要翻译成「班级已存在」 |
| 年级必填 | NOT NULL,别指望页面校验 |
| 班主任可空 | 新班可能还没定班主任 |
重复插入 → 被拦住:UNIQUE constraint failed: tb_class.cname
「报错要翻译」这件事看着小事,其实是体验的分水岭。
直接抛出 UNIQUE constraint failed 给用户看,他只会觉得系统坏了。
做法是在 DAO 里 catch 住,按错误类型返回人话:「班级名已存在,请换一个」比一堆英文有用得多。
③ 改:改班名要想清楚一件事
--------------------------------------------------------------------
| 改名后 | 会怎样 |
|---|---|
| 学生表里的 class_id | 不受影响(存的是 id) |
| 历史成绩 | 不受影响(从学生表连过去) |
| 页面上的旧截图、打印件 | 还是旧名字——这个改不了 |
这就是「存 id 不存名字」的价值。 改一次班名,一处改完,全站生效。
如果学生表里存的是班名字符串,改名要改所有学生记录,还得担心漏。
但有个例外要记住:历史表要存名字(第 19 章调岗记录那样)。
区别在于「历史要冻结当时的样子」,主数据要「跟着当前状态走」。
④ 删:三种情况,三种回答
--------------------------------------------------------------------
先建一个空班:「2027级试点班」,id = 5,班里 0 人
甲 删这个空班:
→ 影响 1 行,班级数 5 → 4
乙 删有人的班(id=1,四人):
→ 被拦住:FOREIGN KEY constraint failed
丙 先把学生转走,再删空出来的班(id=4,两人):
转班前,id=4 班人数:2
把两人转到 id=3 班,再删 id=4 → 影响 1 行,班级数 4 → 3
乙那条是「正常会发生的操作」——教务要撤一个班,班里还有四个学生。
正确做法不是「强行删」,是「先把学生转到别的班」,然后班级才删得掉。
注意「改成毕业/转出」不等于「能删」——class_id 还指着这个班,外键照样拦。
软删除只解决「人还在不在名单里」,不解决「外键引用」。
真要撤班,学生必须挪到另一个班(或者真删)。
所以界面上该给的提示是「这个班还有 4 人,请先处理」,
而不是「删除失败」——前者告诉他下一步做什么,后者只告诉他不行。
⑤ 一个班最多装多少人:要不要做上限
--------------------------------------------------------------------
| 做法 | 代价 |
|---|---|
| 不做上限 | 数据可能不真实,但没有功能损失 |
| CHECK 拦(需要触发器) | 复杂,且插入时才报错 |
| 业务层校验 | 简单,并发下可能超一两个 |
教材没做上限,这一版也没做。
理由是「班级人数不是硬约束」——多一个人不会让数据错,只会让统计不好看。
判断一条规则要不要硬拦,看「违反了会不会导致数据错」。会导致(成绩 120 分、学号重复)就硬拦;
不会(班级多一个人)就提示。
列表那一列「人数」不是存在表里的,是每次数出来的。
有人会想在 tb_class 里加一个 cnt 字段,每次增删学生时维护它——
这是典型的「用空间换时间,但换来了不一致」。
一旦某次漏更新,人数就永远是错的,而且没人知道什么时候错的。
重名那条报错信息是英文的。 真实代码要 catch 住再翻译,
而且要先判断「是唯一键冲突」还是「别的错」——把所有错都翻译成「已存在」,会掩盖真正的问题。
删除那一节里,「有人的班」被外键拦住,班级数没有变化。
这个「拦住了而不是删了一半」的性质,就是外键的价值。
30 秒实验
- 把
LEFT JOIN换成JOIN,先建一个空班再跑列表,看这个班还在不在。 - 把一个班的学生全部改成
state='转出',再删这个班,看能不能删。 删不掉——外键不管状态。 - 想一想:如果学校要求「班级可以归档但不能删」,该怎么设计?
加一个
state字段,列表默认只查「在用」的班,跟学生的软删除一个套路。
常见误区
- 「列表里加个
cnt字段存人数更快」。 漏更新一次就永远错,而且查不出来。 每次数一遍的成本可以忽略。 - 「报错直接显示给用户就行」。 英文报错只会让人觉得系统坏了。 要翻译成人话。
- 「学生都转出了,班就能删了」。
class_id还指着它,外键照样拦。 软删除不解决引用。
自测三问(点开对答案)
1. 列表里的人数为什么要 `LEFT JOIN`? —— **新班没有学生,`JOIN` 会让它整行消失**,**管理员会以为班没建上。** 2. 改班名为什么不用改学生表? —— **因为学生表存的是 `class_id`,不是班名。** **这就是「存 id 不存名字」的价值。** 3. 学生都改成「转出」了,班级为什么还删不掉? —— **因为 `class_id` 还指着这个班,外键引用还在。** **软删除只解决「名单」,不解决「引用」。**下一篇 20.9,学生管理:分页要稳定排序、批量导入要包事务、删除要走软删——三个坑一次讲完。
- ☐ (本篇完,下一篇 20.9 学生基本信息管理模块设计)