先回填取货选择再收紧规则

MySQLBeginner
立即练习

介绍

商店取货列已经扩展,旧版默认值为 counter。历史订单仍需应用已批准选择。之后已有新订单到达:一笔省略新列,另一笔明确选择 garden。大范围回填可能覆盖合法的新选择。

你将交付 backfill.sql 和 tighten.sql,应用精确的已批准历史映射,并保留两个客户端契约。前置知识为上一实验概念,以及基本 SELECT/WHERE 和 UPDATE;这里会讲解 JOIN、聚合、临时守卫和事务模式。本独立 Terminal 单元有自己的扩展表、数据及专用 MySQL。上一 VM 的文件、连接和进度都不会延续。

只为已批准历史 ID 更新 orders.pickup_code,并收紧该列的类型、默认值、可空性和引用规则。保留原始订单头、商品关联、较晚取货选择、审批及取货点表、演示表和预备工具。只允许添加下文练习订单 301 和 302。请求在受控维护窗口中顺序执行,没有竞争的审批写入方。这不实现在线迁移、分布式协调或 DDL 自动回滚。作者估计时长包含阅读和常见错误,没有初学者计时数据。

只应用完整且已批准的历史选择

本步骤中,你将验证业务映射,保存事务性回填,并证明无效映射不会改变任何订单值。

从 Terminal 开始,进入本独立工作区:

cd /home/labex/project/pickup-desk
./shop status
./shop map
./shop points

历史集合为 legacy_orders,包含 ID 101、102、103。已批准映射指定 101 为 north、102 为 counter、103 为 garden。原始总金额仍为 2100、3400、0。两笔 Sam 订单不能按顾客姓名联接。较晚的 ID 201 和 202 不在历史集合中:201 保持 counter,202 保持 garden。它们的商品明细也必须保留。

统计每个历史标识的已批准行数。COUNT(*) 统计行数。GROUP BY order_id 按 ID 分组;HAVING 在统计后筛选组。<> 表示不等于。此查询返回重复或其他行数不等于一的组:

./db -e "SELECT order_id,COUNT(*) FROM approved_pickups GROUP BY order_id HAVING COUNT(*)<>1;"

预期结果为空。但它无法发现完全缺失的 ID,因为缺失行不会形成组。LEFT JOIN 保留左侧每行,没有匹配时右侧字段为 NULL。l 和 m 是简短表别名;ON 按精确订单 ID 建立关联。IS NULL 选出缺失的右侧匹配:

./db -e "SELECT l.order_id FROM legacy_orders l LEFT JOIN approved_pickups m ON m.order_id=l.order_id WHERE m.order_id IS NULL;"

还要求每个映射代码存在于 pickup_points,每个映射 ID 属于 legacy_orders。在这项关系检查中,NULL 代码也视为不匹配:

./db -e "SELECT m.order_id,m.pickup_code FROM approved_pickups m LEFT JOIN pickup_points p ON p.code=m.pickup_code WHERE p.code IS NULL;"
./db -e "SELECT m.order_id FROM approved_pickups m LEFT JOIN legacy_orders l ON l.order_id=m.order_id WHERE l.order_id IS NULL;"

四个查询都应为空。预备实际映射有效且只读。空审计输出是有用依据,但可复用 SQL 文件也必须拒绝错误映射,不能盲目更新其中有效子集。

临时表属于一个数据库连接,连接关闭时消失。这个小型守卫只允许存储整数零:CHECK(errors=0) 拒绝非零计数,NOT NULL 拒绝缺失计数。INSERT ... SELECT COUNT(*) 插入违规数量。任何守卫插入失败,原生批处理都会在业务 UPDATE 前停止并关闭连接,待提交 InnoDB 数据变更会回滚。ENGINE=InnoDB 为临时表选择 MySQL 支持事务的存储引擎。临时表本身不是可回滚的永久表结构变更。每个 apply 或 trial 都开始新连接,因此无需复用之前的临时表。

使用上一实验介绍的带引号 here-document,保存完整文件。括号内名为 duplicates 的派生查询,把非单行组变成可由外层 COUNT 统计的行。UPDATE JOIN 只改变具有精确已批准 ID 的订单,绝不会为所有订单赋默认值。只有守卫成功后,COMMIT 才使数据变更永久保存:

cat > backfill.sql <<'SQL'
CREATE TEMPORARY TABLE map_guard (
    errors INT NOT NULL CHECK(errors=0)
) ENGINE=InnoDB;
START TRANSACTION;
INSERT INTO map_guard
SELECT COUNT(*) FROM legacy_orders l
LEFT JOIN approved_pickups m ON m.order_id=l.order_id
WHERE m.order_id IS NULL;
INSERT INTO map_guard
SELECT COUNT(*) FROM (
    SELECT order_id FROM approved_pickups
    GROUP BY order_id HAVING COUNT(*)<>1
) duplicates;
INSERT INTO map_guard
SELECT COUNT(*) FROM approved_pickups m
LEFT JOIN pickup_points p ON p.code=m.pickup_code
WHERE p.code IS NULL;
INSERT INTO map_guard
SELECT COUNT(*) FROM approved_pickups m
LEFT JOIN legacy_orders l ON l.order_id=m.order_id
WHERE l.order_id IS NULL;
UPDATE orders o
JOIN approved_pickups m ON m.order_id=o.order_id
SET o.pickup_code=m.pickup_code;
COMMIT;
SQL

先在可丢弃原生数据库中测试保存文件。trial 在隔离迁移身份下原样执行文件,使用不同历史 ID 和值,报告实际 SQL 退出状态及实际保留对比。它绝不编辑实际映射或修复 SQL。错误案例中,只有失败符合预期且每个业务值都未变时,native_sql_status 才会非零而 accepted 为 true。这表示试验成功检验了拒绝,并不表示 SQL 写入成功。

./shop trial backfill.sql good
./shop trial backfill.sql missing
./shop trial backfill.sql duplicate
./shop trial backfill.sql unknown
./shop trial backfill.sql null
./shop trial backfill.sql extra

good 试验应用每个已批准选择,并保留较晚的明确取货选择。每个错误试验报告原生 CHECK 错误 3819,业务事实不变。这些情况依次移除一条审批、重复一个标识、使用未知取货点、使用 NULL,或尝试覆盖较晚订单。始终失败的文件无法通过 good 试验。如果试验未被接受,读取原生 stderr 和原因,检查 cat backfill.sql,并修正完整文件。不要修改预备工具或映射来让测试变绿。

将同一原始文件应用到有效实际映射,再读取每笔受影响和受保护订单:

./shop apply backfill.sql
./shop show 101
./shop show 102
./shop show 103
./shop show 201
./shop show 202

历史代码为 north、counter、garden。较晚的 201 保持 counter,202 保持 garden,备注及明细均完整。只有成功填上默认值的表结构,不能证明这些历史选择。使用新连接和同一未变审批映射,重复这一纯数据回填会得到相同选择;这不意味着可任意重复一次性 ALTER 文件。

遇到 ./shopp,用 ls 并修正工具名。语法错误时,检查并替换保存 SQL,再重新应用。预备批处理在首个原生错误处停止并断开连接;不要使用 --force,也不要使用守卫失败后继续执行的交互式 SOURCE 流程。此受控维护窗口不允许并发审批编辑;单独审计查询不是通用的并发迁移加锁协议。

收紧规则并保留两个结账版本

本步骤中,你将强制要求有效的非空取货引用,同时保留省略列的旧版写入和明确的新选择。

继续使用 Terminal,工作目录为 /home/labex/project/pickup-desk。已批准回填已完成;检查实际定义:

./db -e "SHOW CREATE TABLE orders;"

列仍可为空,并具有明确的 counter 默认值。NOT NULL 拒绝显式 NULL,但它本身不为旧客户端提供默认值。外键将 pickup_code 关联到实际唯一的 pickup_points.code,拒绝未知代码。RESTRICT 阻止删除或更改已使用取货点的标识。这是真实数据库规则,不是应用下拉框检查。

MODIFY COLUMN 重新声明完整目标列定义。添加 NOT NULL 时,同时保留 VARCHAR(16) 和 DEFAULT 'counter';MODIFY 中省略的属性未必保留。逗号让两个变更位于同一 ALTER 语句中。约束名标识规则,但接受等效名称。将此 DDL 放在回填事务之外:上一单元已讲解其隐式提交边界。

cat > tighten.sql <<'SQL'
ALTER TABLE orders
MODIFY COLUMN pickup_code VARCHAR(16) NOT NULL DEFAULT 'counter',
ADD CONSTRAINT order_pickup_fk FOREIGN KEY(pickup_code)
REFERENCES pickup_points(code)
ON DELETE RESTRICT ON UPDATE RESTRICT;
SQL
./shop apply tighten.sql
./db -e "SHOW COLUMNS FROM orders;"
./db -e "SHOW CREATE TABLE orders;"

读取 pickup_code:NOT NULL、默认值 counter,并引用 pickup_points(code),限制父记录变更。所有已有值保留。现在创建允许的两笔练习订单:

./shop old 301 Mina 1600 MUG 1
./shop new 302 Pia 1900 BAG 2 north
./shop show 301
./shop show 302

实际旧版行保存 counter,实际新版行保存 north。商品数量分别为 1 和 2,两个事务都已提交。旧客户端仍省略 pickup_code,并没有被悄悄改成提供该值。

接下来两次调用是故意失败,必须非零退出。new-null 提供真实 SQL NULL,而非省略字段,原生错误 1048 应拒绝它。普通新版客户端提供 unknown 时应收到原生外键错误 1452。两次使用同一个未使用 ID,因为任何一次失败都不得留下订单头或商品:

./shop new-null 399 Invalid 100 MUG 1
./shop new 399 Invalid 100 MUG 1 unknown
./shop show 399
./shop status

Show 399 的订单头及商品列表为空。三个历史选择、较晚的 201/202 和新增 301/302 保持正确。原生数据库错误不是成功的空结账,也不是连接问题。预备客户端会关闭失败事务,无需重置 VM。接着读取已有的允许订单。

最终成果是可复用回填和收紧规则 SQL,以及实际正确的订单系统。评分读取这个实际结果,并在可丢弃原生资源上测试原样保存 SQL 和当前定义,包括合法写入及禁止的父记录变更。它不会在实际商店创建订单、应用迁移或修复缺失值。

不小心重复成功的 tighten 文件时,命名外键可能已存在。检查 SHOW CREATE TABLE,保留有效定义;不要删除并重建 orders,让脚本可重复执行。保存文件仍需包含从指定上一阶段开始的正确一次性迁移。维护窗口中的结果不能证明无锁 DDL、任意部署回滚、生产持续可用或抵御管理员伪造。

总结

你验证了精确历史标识,在改变业务值前拒绝不完整或含糊映射,并应用事务性回填,不覆盖较晚客户端选择。独立的规则收紧 DDL 保留旧版默认值,同时强制执行非空、有效的取货引用。两个真实结账版本仍可工作,无效写入由数据库原生拒绝。