介绍
REPAIR CIRCLE 举办动手维修工作坊。它的报名工作台已经能够记录报名并显示真实名单,但数据库尚不能拒绝重复报名或无效引用。你的目标是在保持工作台可用的同时,让数据库强制维护报名关系。
你需要掌握基本的终端导航,以及 MySQL SELECT、INSERT 和 UPDATE 的基础知识。本实验会讲解约束、用于可回滚试验的事务,以及 ALTER TABLE。MySQL 和合成的工作坊数据已准备好。全程使用 Terminal;提供的客户端会连接本课程的 MySQL 数据库。
你可以添加要求的报名记录,并对 registrations 作必要的结构修改,将 SQL 保存到 rules.sql。保留原有 ID、人员、场次名称、报名人数、备注和提供的客户端。同一个人可以参加不同场次;不同的人可以同名。本项目不限制容量,也不处理并发名额分配。
使用工作台并发现重复报名
本步骤中,你将记录一次正常报名,读取它的真实关系,并演示缺失的规则,同时不留下错误记录。从 Terminal 开始。cd 用于切换目录;如果进入了错误目录,再用这个绝对路径返回:
cd /home/labex/project/sign-up-desk
./desk 是当前目录中提供的报名客户端。./ 表示当前目录。roster 读取真实的 MySQL 表,不会显示预先计算好的答案:
./desk roster
预期有两条原始报名记录。报名 501 将人员 101(Mara Lin)与场次 11(Lamp Repair)关联,人数为 1,备注为 Window seat。报名 502 将人员 102(Alex Kim)与场次 12(Bike Patch)关联,人数为 2,备注为 Bring partner。保留这些原始关系和值。
一项新请求要求人员 104(Sam Reed)报名场次 11,人数为 1。book 按顺序接收人员 ID、活动 ID 和报名人数。保存这次合法报名:
./desk book 104 11 1
./desk roster
第三条报名属于 Sam,而非 Mara,尽管两人都参加 Lamp Repair。客户端会返回数据库保存的 ID。你无需选择或记住这个新 ID。
./db 打开本课程的 MySQL 客户端连接。请使用它,不要使用镜像中与本课程无关的 mysql 命令。--table 要求以易读表格输出;-e 执行引号中的 SQL 并关闭连接。引号将空格和分号保留在同一个参数中。读取两个父表:
./db --table -e "SELECT id,name FROM participants ORDER BY id; SELECT id,title FROM sessions ORDER BY id;"
人员 102 和 103 都叫 Alex Kim,但他们的 ID 不同,因此是不同的人。姓名不能作为报名的唯一标识。三个表具有以下关系:
| 表 | 标识 | 含义 |
|---|---|---|
participants |
id |
一个人,姓名可能与他人相同 |
sessions |
id |
一个工作坊场次 |
registrations |
id,以及人员和场次引用 |
某人在某场次的一组报名 |
业务规定每个 (participant_id,session_id) 组合只能有一条报名,人数必须为正,且父表中必须存在对应 ID。已有报名必须阻止删除其引用的人员或场次 ID,也必须阻止更改该 ID。未被使用的场次仍可按需删除。
保护表结构前,先测试当前缺失的防重复规则。事务将同一个连接上的数据变更组织在一起。START TRANSACTION 开始试验;ROLLBACK 撤销其中的 INSERT,而不是保存它。下面这条命令会刻意尝试为 Mara 再报名一次 Lamp Repair,统计该组合的记录数,然后回滚。跨行的引号内容仍组成一个 shell 参数:
./db --table -e "
START TRANSACTION;
INSERT INTO registrations(participant_id,session_id,seats) VALUES(101,11,1);
SELECT COUNT(*) AS matching_registrations FROM registrations WHERE participant_id=101 AND session_id=11;
ROLLBACK;"
计数为 2,说明数据库接受了重复记录。这是真实的规则缺陷,不是连接故障。试验使用的是 INSERT,而不是 ALTER。回滚会完整保留 Mara 的原有报名和 Sam 的新请求:
./desk roster
此时应该仍有三条报名记录。如果普通命令中的路径写错,用绝对路径 cd 返回。使用 ls 列出本目录的文件。不要为了修复输入错误而删除人员、清空报名表或重置数据库。
强制维护关系并保留合法报名
本步骤中,你将保存一次表结构变更,将其应用到现有表,并证明合法报名仍可进行,而无效请求会被拒绝。从同一个 Terminal 目录开始。
约束是数据库对写入强制执行的规则,也适用于 ./desk 之外的客户端写入。组合 UNIQUE 约束会拒绝重复组合,同时允许同一个人参加其他场次。FOREIGN KEY 将引用列关联到父表主键。只要报名引用着该父记录,RESTRICT 就会拒绝删除它或更改其 ID;它不会自动删除报名。
CHECK (seats > 0) 拒绝零和负数。SQL NULL 表示缺失值;CHECK 的结果为未知时,并不会仅凭这一检查拒绝 NULL。因此,人数和两个引用列还必须设置 NOT NULL。MODIFY 重新声明列类型并添加这一要求。提供的三个表都已使用 InnoDB,这个存储引擎能够强制维护这些关系。
ALTER TABLE 修改现有表。MySQL 会隐式提交表结构变更:不要把这个 ALTER 放进试验事务,并指望 ROLLBACK 能撤销它。我们对已有的合法记录使用一条 ALTER 语句,保留它们的 ID 和备注。
rules.sql 中只保存针对 registrations 的普通 ALTER TABLE 语句,表名不带数据库名限定;USE、shell 命令和对其他对象的修改都不属于此文件的允许范围。不要添加触发器或修改父表定义。在 Linux shell 中,> 替换目标文件,<<'SQL' 开始按字面保留内容的 here-document。粘贴完整代码块,包括末尾独占一行的 SQL。不要在 MySQL 提示符下输入这条 cat 命令:
cat > rules.sql <<'SQL'
ALTER TABLE registrations
MODIFY participant_id INT NOT NULL,
MODIFY session_id INT NOT NULL,
MODIFY seats INT NOT NULL,
ADD CONSTRAINT one_person_event UNIQUE (participant_id, session_id),
ADD CONSTRAINT known_person FOREIGN KEY (participant_id)
REFERENCES participants(id) ON DELETE RESTRICT ON UPDATE RESTRICT,
ADD CONSTRAINT known_event FOREIGN KEY (session_id)
REFERENCES sessions(id) ON DELETE RESTRICT ON UPDATE RESTRICT,
ADD CONSTRAINT positive_group CHECK (seats > 0);
SQL
如果 shell 一直等待,输入缺失的最后一行 SQL。如果文件内容输错,重新执行整个代码块以替换它。MySQL 错误信息会用约束名称标识这些规则;名称的具体拼写并不是业务成果。
< 将保存的文件作为客户端的 SQL 输入。只应用一次:
./db < rules.sql
成功时预期没有输出。如果文件名错误,使用 ls,修正路径后重试。如果这一条 ALTER 有语法错误,MySQL 会拒绝它;修正保存的完整代码块后再重试。ALTER 一旦成功,就不要再次应用:约束已经存在。第二次执行出现重复约束错误,并不意味着数据丢失。
读取实际定义。SHOW CREATE TABLE 报告当前表的定义;\G 将结果纵向显示,方便阅读完整定义:
./db -e "SHOW CREATE TABLE registrations\G"
查找三个设置为 NOT NULL 的引用或数量列、由两列组成的 UNIQUE 键、两个 FOREIGN KEY 关系,以及要求人数为正的 CHECK。名称或格式可以不同,但关系必须一致。
现在用普通客户端重复提交那条重复请求。预期此命令失败,数据库拒绝信息中包含 ERROR 1062。它不能再创建一行:
./desk book 101 11 1
这是重复键导致的拒绝,不是服务器断开连接。名单仍然可用:
./desk roster
在真实工作台上,用一个临时数据事务证明两种允许的身份组合。Mara 可以参加 Bike Patch,另一位 Alex Kim(103)也可以与 Alex 102 参加同一场次。大于 1 的正报名人数同样合法:
./db --table -e "
START TRANSACTION;
INSERT INTO registrations(participant_id,session_id,seats) VALUES(101,12,3),(103,12,5);
SELECT participant_id,session_id,seats FROM registrations ORDER BY participant_id,session_id;
ROLLBACK;"
试验显示五行,各自有独立标识。回滚只删除本次新增的两条试验记录,不会撤销此前的表结构变更,也不会撤销 Sam 已提交的请求。
直接检查三种有代表性的拒绝写入。下面每条命令都预期失败;这些命令没有需要恢复的事务,也没有需要撤销的成功变更。MySQL 会拒绝各自的无效语句。提供的父表中不存在 9999:
./db -e "INSERT INTO registrations(participant_id,session_id,seats) VALUES(9999,11,1);"
预期出现 ERROR 1452,表示引用了不存在的父记录。
./db -e "INSERT INTO registrations(participant_id,session_id,seats) VALUES(103,11,NULL);"
预期出现 ERROR 1048,表示缺少人数。将 NULL 换成 0 会违反 CHECK,也不能成为有效报名。
./db -e "DELETE FROM sessions WHERE id=11;"
预期出现 ERROR 1451:Lamp Repair 仍有报名,因此该场次会保留。这说明为什么删除父记录或级联删除报名是错误的捷径。
最后,通过可回滚试验证明没有报名的场次仍可删除:
./db --table -e "
START TRANSACTION;
DELETE FROM sessions WHERE id=13;
SELECT id,title FROM sessions ORDER BY id;
ROLLBACK;"
该事务中只显示场次 11 和 12。回滚会恢复未被使用的场次。检查最终交付状态:
./db --table -e "SELECT id,title FROM sessions ORDER BY id;"
./desk roster
三个原有场次全部保留,真实报名恰好有三条:两条原有记录和 Sam 的新请求。保留 rules.sql,以便在另一个兼容工作台上实施相同结构变更。它是一次性的表结构迁移,不是可重复执行而保持相同结果的导入脚本。默认连接会强制检查外键;关闭检查不属于本任务范围。约束保护的是普通应用写入,不能阻止管理员刻意禁用规则。
总结
你在保持报名工作台可用的同时,将它的关系规则落实为真实的 MySQL 约束。你保留了原有报名和备注,允许合法的同名人员与跨场次组合,拒绝重复记录和无效引用,并防止被引用的记录遭到删除。



