ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

mongoose 批量写入实战:用 bulkWrite 重构你的数据初始化脚本

mongoose 批量写入实战:用 bulkWrite 重构你的数据初始化脚本 1. 数据初始化脚本为什么越写越慢如果你写过 Node.js mongoose 的数据初始化或数据迁移脚本大概率经历过这样的场景本地跑一次种子数据要等十几秒线上迁移几万条记录直接卡到超时。问题往往不在数据库本身而在写法——用for循环里套await Model.save()或者对每条记录单独调findOneAndUpdate。这种写法的本质是每处理一条数据客户端和 MongoDB 之间就要完成一次完整的网络往返。1000 条数据就是 1000 次往返哪怕单次只要 2ms累计也是 2 秒起步再加上连接池调度、序列化开销实际耗时会被放大好几倍。更麻烦的是循环里任何一条失败都可能中断整个流程你还得自己写重试和断点续传逻辑。bulkWrite解决的正是这个问题。它把多个 insert、update、delete 操作打包成一个命令一次性发给 MongoDB只走一次网络往返。官方文档里明确写了它支持insertOne、updateOne、updateMany、replaceOne、deleteOne、deleteMany六种操作而且可以在同一个数组里混用。这意味着你可以用一段代码同时完成新增缺失记录、更新已有记录、删除废弃记录三件事。这篇文章面向的是正在写数据初始化脚本、种子数据生成器或数据迁移工具的 Node.js 开发者。我会给出可直接复制的bulkWrite操作数组骨架讲清楚ordered和writeConcern该怎么配演示如何用返回的upsertedCount/modifiedCount做结果校验并顺带说说怎么借助 TaoToken 的统一 API 通道让 AI 工具帮你生成和审查这类脚本。2. 用 TaoToken 统一通道辅助生成与校验脚本写bulkWrite脚本时最容易出错的不是语法而是操作数组的结构——filter和update的字段名、upsert放的位置、$set里能不能带_id这些细节记混一次就要 debug 半天。我的做法是让 AI 工具先帮我生成骨架再自己改业务字段。这里用 TaoToken 的原因是它把多个模型的调用收敛到一个 Key 和一套 API 通道上。你不需要为每个模型单独申请 Key、单独配 base_url换模型只改一个 model 名就行。对于生成脚本 → 审查脚本 → 解释报错这种需要来回切换模型的场景省事不少。接入方式很简单在支持自定义 base_url 的 AI 工具里填上API 地址https://taotoken.net/api API Key在 https://taotoken.net/api-keys 生成如果你用的是 Claude Code 这类编码 Agent可以参考 https://taotoken.net/doc 里的配置说明把 Anthropic 风格的请求指向统一通道。想先在网页里试一下模型对bulkWrite的理解直接打开 https://taotoken.net/model-chat 对话即可。需要说明的是TaoToken 在这里扮演的是统一调用入口的角色它不替代 mongoose也不碰你的数据库。脚本最终还是在你的 Node.js 进程里跑连的还是你自己的 MongoDB。AI 工具负责帮你把操作数组写对、把报错翻译成人话仅此而已。3. 可复制的 bulkWrite 操作数组骨架先看一个完整的、可以直接改字段用的骨架。假设你在做一个用户数据的初始化脚本需求是新用户插入、老用户更新等级、已注销用户删除。const mongoose require(mongoose); const userSchema new mongoose.Schema({ uid: { type: String, unique: true }, name: String, level: { type: Number, default: 1 }, status: { type: String, default: active }, updatedAt: Date }); const User mongoose.model(User, userSchema); async function initUsers(rawList) { const operations rawList.map((item) { if (item.status deleted) { return { deleteOne: { filter: { uid: item.uid } } }; } return { updateOne: { filter: { uid: item.uid }, update: { $set: { name: item.name, level: item.level, status: item.status, updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } }, upsert: true } }; }); const result await User.bulkWrite(operations, { ordered: false, writeConcern: { w: 1 } }); return result; }这段代码有几个关键点值得展开。updateOne里同时用了$set和$setOnInsert。$set在匹配到文档时更新字段$setOnInsert只在 upsert 触发插入时生效。这样一条操作就能覆盖存在则更新、不存在则插入两种路径不用先查再判断。upsert: true是让updateOne在没有匹配文档时自动插入。注意插入的文档内容 filter字段 update里的$set/$setOnInsert字段合并。所以filter里的uid会自动成为新文档的一部分不需要在$set里重复写。ordered: false表示无序执行。MongoDB 会尽量并行处理这些操作某一条失败不会阻断后面的。对于数据初始化这种每条记录相互独立的场景无序执行能明显提速也能让尽可能多的数据落库。writeConcern: { w: 1 }表示只要主节点确认写入即可返回。初始化脚本通常不需要等副本集多数派确认w: 1能减少等待时间。如果你的场景对持久性要求高可以改成{ w: majority }但会慢一些。如果你需要显式插入而不是 upsert把操作换成insertOne{ insertOne: { document: { uid: item.uid, name: item.name, level: item.level } } }insertOne的document里如果不带_idmongoose 会自动生成。但要注意如果uid上有唯一索引重复插入会触发 E11000 错误在无序模式下这条会失败但其他继续执行。4. ordered 与 writeConcern 怎么配才不踩坑ordered这个参数看起来简单实际影响很大值得单独说清楚。当ordered: true默认值时操作按数组顺序串行执行遇到第一个错误立即停止后面的操作全部不执行。返回结果里writeErrors只有一个错误对象。这种模式适合有严格先后依赖的场景比如先删旧数据再插新数据。当ordered: false时操作可以并行执行某条失败不影响其他条。返回结果里writeErrors可能包含多个错误。这种模式适合批量独立操作也是数据初始化最常用的配置。但无序执行有个坑如果你的操作数组里同时有deleteMany和insertOne且它们的 filter 条件有重叠那么删除的文档数量会取决于执行顺序——而顺序在无序模式下是不确定的。所以混用删除和插入时要么用ordered: true保证顺序要么确保 filter 条件互不重叠。writeConcern的常用取值取值含义适用场景{ w: 1 }主节点确认即返回数据初始化、种子脚本{ w: majority }多数派节点确认生产环境关键数据迁移{ w: 0 }不等待确认日志类、可容忍丢失的数据{ w: 1, j: true }主节点写入 journal 后返回需要崩溃恢复保障还有一个容易忽略的点updateOne和replaceOne不能修改_id。如果你在$set里写了_id或者replaceOne的replacement里带了不同的_id会直接报错。filter里用_id匹配是没问题的但更新内容里不要碰它。5. 验证请求与结果用 upsertedCount / modifiedCount 做校验bulkWrite的返回值是一个BulkWriteResult对象里面有几个关键计数const result await User.bulkWrite(operations, { ordered: false }); console.log({ matchedCount: result.matchedCount, // 匹配到的文档数 modifiedCount: result.modifiedCount, // 实际被修改的文档数 upsertedCount: result.upsertedCount, // 触发插入的文档数 deletedCount: result.deletedCount, // 删除的文档数 insertedCount: result.insertedCount // insertOne 插入的文档数 });这里有个细节matchedCount和modifiedCount可能不相等。如果某条updateOne匹配到了文档但$set的值和原值完全一样MongoDB 不会真正修改它modifiedCount就不会增加。所以校验时不要用matchedCount判断更新成功了几条要用modifiedCount。一个实用的校验模式是在脚本跑完后用upsertedCount modifiedCount insertedCount和预期数量对比。const expected rawList.filter(i i.status ! deleted).length; const actual result.upsertedCount result.modifiedCount result.insertedCount; if (actual ! expected) { console.warn(预期处理 ${expected} 条实际 ${actual} 条请检查是否有重复 uid 或字段未变化); }如果ordered: false且有操作失败result上还会挂一个writeErrors数组。你可以遍历它拿到每条失败的 index 和原因if (result.hasWriteErrors result.hasWriteErrors()) { result.getWriteErrors().forEach((err) { console.error(第 ${err.index} 条操作失败${err.errmsg}); }); }注意writeErrors里的index对应的是你传入的操作数组下标不是数据本身的下标。如果你在 map 时做了过滤需要自己维护一个映射关系才能定位到具体哪条数据。6. 本篇常见错误排查E11000 duplicate key error这是最常见的报错通常出现在insertOne或upsert时唯一索引冲突。如果你用的是updateOne upsert理论上不会触发这个错误因为 upsert 会先匹配再决定插入还是更新。但如果两个操作在无序模式下并发执行且 filter 条件相同可能出现竞态导致重复插入。解决办法是确保 filter 字段上有唯一索引并且尽量让同一 key 的操作不要重复出现在数组里。Cannot use $setOnInsert with upsert: false$setOnInsert必须配合upsert: true使用。如果你写了$setOnInsert但忘了开 upsertmongoose 会直接报错。检查每个updateOne的upsert字段。MongoServerError: Unknown modifier: $setOnInsert这个报错通常是因为你把$setOnInsert写在了update对象的外层而不是和$set平级。正确结构是update: { $set: {...}, $setOnInsert: {...} }两个操作符都在update里面。bulkWrite 返回 undefined 或空对象如果你用的是旧版 mongoose5.x 以下bulkWrite的返回值结构可能和 6.x / 7.x 不同。建议升级到 mongoose 6 以上或者用result.result取原始返回值。另外如果操作数组为空bulkWrite会直接返回一个空结果不会报错但也不会有任何写入。操作数组过大导致内存问题bulkWrite本身没有硬性条数限制但一次传几万条操作会占用大量内存而且 MongoDB 对单个命令有 16MB 的大小限制。建议分批处理比如每 1000 条调一次bulkWriteconst BATCH_SIZE 1000; for (let i 0; i operations.length; i BATCH_SIZE) { const batch operations.slice(i, i BATCH_SIZE); await User.bulkWrite(batch, { ordered: false }); }分批还能让你在每批之间打印进度长任务跑起来心里有底。ordered: false 下错误被吞掉无序模式下即使有操作失败bulkWrite也不会抛异常而是把错误放在返回值的writeErrors里。如果你不主动检查会以为全部成功了。养成习惯每次bulkWrite后都检查result.hasWriteErrors()。如果你在写这类脚本时想让 AI 帮你审查操作数组的结构或者把一段报错日志翻译成具体的修复建议可以用 TaoToken 的模型对话入口 https://taotoken.net/model-chat 快速验证。需要长期在编码 Agent 里做数据脚本开发的话Coding Plan https://taotoken.net/coding-plan 提供了更稳定的调用额度。API Key 在 https://taotoken.net/api-keys 生成接入文档在 https://taotoken.net/doc配置时 base_url 填 https://taotoken.net/api 即可。
返回列表