介绍
FIELDNOTE 出租小型设备套件。接手的 MongoDB 商品目录中,既有可用的库存数量,也有旧字符串和不确定值。已知数量为零表示套件不可用;数量缺失则表示店铺不知道。把两者都改成零,会掩盖数据问题。你的任务是区分这些情况,并保留不可靠记录,供后续供应商审核。
你将保存可复用分类策略和隔离脚本,在从活跃集合移除记录前保留完整原始文档。你需要掌握基本终端导航,并熟悉 MongoDB 文档、嵌套字段、find 和普通更新。这里使用的 JavaScript 结构、模式规则和 BSON 细节会在使用前讲解。MongoDB、其 shell 和合成商品目录数据均已准备好。全程使用 Terminal。时长为作者估计,包含阅读和普通错误处理。
在 /home/labex/project/catalog-desk 下编写 policy.js 和 quarantine.js。保留每个原始 ID、全部原始字段和类型、无关扩展信息、集合标识及提供的工具。本实验中,有效和可修复记录保持不变。不要删除重建集合,也不要覆盖已有隔离原件。只操作本课程商品目录。这是在没有并发写入方的受控维护窗口中执行的工作;记录复制和删除是独立操作,不是覆盖整个商品目录的事务。
读取并分类接手的商品目录
本步骤中,你将读取一条普通可用记录,区分存储的数量类型,并保存明确分类策略。从 Terminal 开始。cd 切换目录;进入错误目录时,也可以用此绝对路径返回:
cd /home/labex/project/catalog-desk
./catalog 是当前目录中提供的数据库连接执行工具;./ 选择本地可执行文件。其 show 命令读取实际存储文档。给它一个文档 ID:
./catalog show KIT-101
查找 name: "Tripod"、stock.rack: "A",以及包含 "$numberInt": "12" 的 stock.available。输出使用规范 Extended JSON,即带明确 BSON 类型标签的文本表示。BSON 是 MongoDB 存储文档的格式。$numberInt 标识 32 位整数,$numberLong 标识 64 位整数。显示输出中的标签描述已存储数字,不是你应添加的普通嵌套库存字段。
店铺接受这两种整数类型,数量范围为 0 到 50,包含端点。原始 stock.rack 和 supplier 字段具有实用信息,必须保留。_id 标识记录。两条都叫 Tripod 的记录仍是不同记录。
检查服务器中每个数量的实际类型:
./catalog types
此读取工具在 stock.available 上使用 MongoDB 聚合 $type 表达式。其 actualType 列区分 int、long、string、null、missing、array 和 double。double 是浮点 BSON 类型;即使 Double 值 12 看起来是整数,也不符合本店只接受整数类型的契约。missing 表示字段不存在,null 表示字段存在但明确为空值。[12] 这样的数组不是一个标量数量。
读取完整商品目录,将类型与原始记录对应起来:
./catalog show
编写查询前使用这张判断表:
| 存储数量 | 本实验中的处理 | 原因 |
|---|---|---|
| Int32 12 或 Long 0 | 保留为可用记录 | 接受的标量整数,包括已知零值 |
字符串 "12" 或 "0" |
保留,等待批准的修复 | 约定范围内的规范十进制表示 |
"12 kits"、"012" 或 "51" |
隔离 | 含义未知、表示未经批准或超出范围 |
| Null、缺失、数组或 Double 12 | 隔离 | 不满足约定的标量整数契约 |
可修复字符串必须只含 0 到 50 的规范 ASCII 数字表示:没有正负号、空格、前导零、小数点或单位后缀。这是对这些接手数据的明确约定,不是承诺每个像数字的字符串都可安全转换。隔离表示保留完整原件供审核,不表示编造替代数量。
后续命令都留在此 Terminal 和目录。文件名输错时,ls 列出实际本地名称。修正拼写后重跑;文件错误不是数据库拒绝,也无需重置环境。
保存分类策略
MongoDB 选择器是描述哪些记录匹配的文档。$jsonSchema 检查文档形状和 BSON 类型。required 指定必须存在的字段,本身不要求字段值非空。properties 描述每个字段,bsonType 限制 BSON 类型,minimum/maximum 包含边界值。嵌套 stock 规则单独要求 available。未提及字段仍允许存在,因此 rack、supplier 和后续扩展都能保留。
这里使用 JSON Schema 的标量类型规则。普通查询谓词,如 {"stock.available": {$type: "int"}},可以匹配包含整数的数组,却不能证明字段本身是商品目录要求的标量数量。
你将为 MongoDB shell mongosh 编写 JavaScript。const 为值命名;= 赋初始值,花括号组成对象,方括号组成数组,逗号分隔条目。带引号的文本是字符串,不带引号的 0 和 50 是数字。分号结束语句。schema 描述完整可接受记录,valid 将它包装为 MongoDB 选择器。
批准字符串策略采用相同形状,只改变数量规则。JSON.stringify 将这个普通策略对象转成 JSON 文本,JSON.parse 读成独立副本;随后改变副本,不影响原策略。此复制方式用于普通策略,不用于保留已存储 BSON 记录。stringSchema.properties.stock.properties.available 访问要替换的嵌套规则。
pattern 是允许文本的正则表达式。^ 锚定开头,\z 表示字符串绝对末尾;| 分隔候选项,[1-9] 允许 1 到 9 的单个数字,[1-4][0-9] 允许 10 到 49 的两位数。加上 0 和 50,这些候选项恰好覆盖批准范围。JavaScript 字符串使用反斜杠转义,因此源代码写 \\z,向 MongoDB 传递 \z。绝对末尾锚点也拒绝结尾换行;普通 $ 末尾锚点可能允许在该换行之前匹配。$nor: [valid, repairable] 选择两个选择器都不匹配的记录。
用 here-document 创建策略文件。cat > policy.js 将下文写入文件并替换旧内容。带引号的 <<'POLICY' 分隔标记使 shell 原样保留美元符号和其他 JavaScript 文本。以独占一行的 POLICY 结束。复制完整代码块:
cat > policy.js <<'POLICY'
const schema = {
bsonType: "object",
required: ["_id", "name", "stock"],
properties: {
_id: { bsonType: "string", minLength: 1 },
name: { bsonType: "string", minLength: 1 },
stock: {
bsonType: "object",
required: ["available"],
properties: {
available: { bsonType: ["int", "long"], minimum: 0, maximum: 50 }
}
}
}
};
const valid = { $jsonSchema: schema };
const stringSchema = JSON.parse(JSON.stringify(schema));
stringSchema.properties.stock.properties.available = {
bsonType: "string",
pattern: "^(0|[1-9]|[1-4][0-9]|50)\\z"
};
const repairable = { $jsonSchema: stringSchema };
const bad = { $nor: [valid, repairable] };
POLICY
提供的分类工具以只读数据库身份原样执行保存文件,再在真实 MongoDB 查询中使用你的 valid、repairable 和 bad 选择器:
./catalog classify policy.js
检查实际文档分组,不只看数量。valid 应包含 KIT-101 和 KIT-102,包括原始 Long 零值。repairable 应包含 KIT-103 和 KIT-104,原始字符串不变。outside 应包含 KIT-105 到 KIT-111,包括原始 null、缺失字段、数组和 Double。KIT-105 上完整的供应商审核扩展信息也必须仍可见。
出现 JavaScript 语法错误时,文件尚不是可用策略。重新执行完整 here-document 替换损坏文件,再运行分类工具。语法错误指出文件或表达式问题;数据库连接不可用是另一种错误,不表示分类没有匹配记录。分类工具没有修复或移除任何数据。
移除不确定记录前保留原件
本步骤中,你将保存隔离脚本,观察首次复制后的真实中断,再原样重跑脚本完成工作。从 Terminal 开始,工作目录为 /home/labex/project/catalog-desk。
每个隔离文档具有原始 _id,包含完整已存储记录的 original,以及 reason: "outside-approved-catalog-contract"。原因说明为什么需要供应商审核,不声称库存为零。原始数量可能缺失、为 null、数组或其他类型,因此只复制显示值或名称会丢失证据。
还需注意一个边界:读取时,shell 可能将已存储 BSON 数字转成普通 JavaScript 数字。保存一个看似整数的 JavaScript 数字,可能生成 Int32,即使原来是 Double。对每次原件复制和比较,使用第三个 find/findOne 参数 {promoteValues: false, bsonRegExp: true},保留 BSON 数值包装对象和正则值,而不转换为对应 JavaScript 值。第二个参数 {} 是空投影,保留全部字段。EJSON.stringify(value, {relaxed: false}) 从保留的类型生成规范 Extended JSON,因此比较时 Double 12 和 Int32 12 仍不同。
脚本循环一次读出范围外分组,再分别处理每条记录。sort({_id: 1}) 给出易读的 ID 升序,toArray() 收集结果。for (const doc of ...) 为每个结果重复执行循环体内的语句。upsert 按原始 ID 查找隔离记录;upsert: true 在不存在时创建。$setOnInsert 只在创建时写入原件和原因,已有副本保持不动。findOne 回读存储副本。
if 检查条件。!== 表示不相等,|| 表示任一条件成立。原件或原因不同必须触发 throw Error(...),使脚本在删除该源记录前以非零状态停止。deleteOne 只删除验证过的源 ID。此前记录可能已完成;这些是独立操作,不是整个批次全成或全败。没有并发写入方时,每次删除前成功进行完整比较,提供了此处使用的重试路径。
写入脚本。QUARANTINE here-document 使用此前相同的原样写入语法:
cat > quarantine.js <<'QUARANTINE'
const typed = { promoteValues: false, bsonRegExp: true };
for (const doc of db.equipment.find(bad, {}, typed).sort({_id: 1}).toArray()) {
db.quarantine.updateOne(
{_id: doc._id},
{$setOnInsert: {original: doc, reason: "outside-approved-catalog-contract"}},
{upsert: true}
);
const saved = db.quarantine.findOne({_id: doc._id}, {}, typed);
if (EJSON.stringify(saved.original, {relaxed: false}) !== EJSON.stringify(doc, {relaxed: false}) ||
saved.reason !== "outside-approved-catalog-contract") {
throw Error("Existing quarantine copy differs; keep source and investigate");
}
db.equipment.deleteOne({_id: doc._id});
}
QUARANTINE
先使用提供的 copy-only 连接。这是真实数据库身份,允许读取和复制,但不允许删除。执行工具在一次 shell 调用中先原样运行 policy.js,再运行 quarantine.js。脚本首次尝试删除时,预期出现原生授权错误并非零退出:
./catalog copy-only policy.js quarantine.js
消息包含 not authorized,以及对 equipment 的 delete 命令。在拒绝前,复制已成功。读取两处真实记录:
./catalog archive KIT-105
./catalog show KIT-105
KIT-105 现在同时存在于两处。隔离原件必须包含库存文本和完整 extension,包括数字、日期和 ObjectId 类型标签。活跃源记录仍完整。因此命令失败并不意味着此前写入都已撤销。
使用课程普通写入连接重新运行完全相同的保存文件:
./catalog run policy.js quarantine.js
成功脚本不要求输出。先验证并保留已有首份副本,再移除对应源记录。其余不确定记录依次复制、验证并移除。读取实际结果:
./catalog show
./catalog archive
活跃商品目录应只含未改变的 KIT-101 到 KIT-104,包括等待修复的两个批准字符串。隔离区应含 KIT-105 到 KIT-111,具有完整原始文档和约定原因。特别是 KIT-109 的原始 Double 仍显示 $numberDouble,KIT-107 原件仍没有 available 字段。
再次运行迁移:
./catalog run policy.js quarantine.js
./catalog archive
已没有范围外记录需要移动,因此保存原件不变。这是幂等重放:重复已完成操作,不改变业务结果。复制后的中断也可恢复,因为归档键阻止重复副本,比较会拒绝不一致的已有副本。出现此冲突错误时,保留源和旧归档调查;不要通过覆盖归档或删除源来“修复”。脚本不保证读取与删除之间发生并发编辑时的安全,也不能回滚整个批次。
总结
你用可复用 MongoDB 策略区分了可用整数数量、明确批准的字符串和不确定记录,再在删除活跃源记录前保留完整原件及其类型,观察真实部分失败,并恢复运行相同保存脚本。商品目录现在保留可用和可修复记录,隔离区则保留供应商审核证据。转换和后续写入校验是下一个独立单元的内容。



