
Mongoose Timestamps 完全指南createdAt/updatedAt 的自动管理、底层原理与实战配置【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseMongoose 的timestamps选项为 Schema 自动添加createdAt文档创建时间与updatedAt文档最后修改时间两个Date类型字段并贯穿save()、updateOne()、updateMany()、findOneAndUpdate()、update()、replaceOne()、bulkWrite()等全部写入路径。读完本文你将掌握如何开启并自定义时间戳字段名、如何按操作粒度启用/禁用时间戳、子文档时间戳的语义差异、$setOnInsert/$set的底层实现机制以及如何通过currentTime、overwriteImmutable等高级选项修正或自定义时间戳值。快速上手开启 TimestampsMongoose 的 Schema 支持timestamps选项。设置为timestamps: true后Mongoose 会自动向 Schema 追加两个Date类型属性createdAt该文档被创建的时间updatedAt该文档最后一次被更新的时间Mongoose 会在文档首次插入时设置createdAt并在你通过save()、updateOne()、updateMany()、findOneAndUpdate()、update()、replaceOne()或bulkWrite()更新文档时自动刷新updatedAtconst userSchema new Schema({ name: String }, { timestamps: true }); const User mongoose.model(User, userSchema); let doc await User.create({ name: test }); console.log(doc.createdAt); // 2022-02-26T16:37:48.244Z console.log(doc.updatedAt); // 2022-02-26T16:37:48.244Z doc.name test2; await doc.save(); console.log(doc.createdAt); // 2022-02-26T16:37:48.244Z保持不变 console.log(doc.updatedAt); // 2022-02-26T16:37:48.307Z已刷新 doc await User.findOneAndUpdate({ _id: doc._id }, { name: test3 }, { returnDocument: after }); console.log(doc.createdAt); // 2022-02-26T16:37:48.244Z保持不变 console.log(doc.updatedAt); // 2022-02-26T16:37:48.366Z已刷新从源码角度看timestamps配置解析位于 lib/helpers/timestamps/setupTimestamps.js它通过schema.add()把createdAt默认带immutable: true和updatedAt两个字段注入 Schema并注册两套内置中间件——pre(save)钩子与查询类钩子findOneAndReplace、findOneAndUpdate、replaceOne、update、updateOne、updateMany从而实现文档保存与查询更新两条路径的时间戳自动维护。createdAt 不可变、updatedAt 默认覆盖createdAt属性是**不可变immutable**的而updatedAt则默认会被 Mongoose 覆盖任何用户手动指定的值let doc await User.create({ name: test }); console.log(doc.createdAt); // 2022-02-26T17:08:13.930Z console.log(doc.updatedAt); // 2022-02-26T17:08:13.930Z doc.name test2; doc.createdAt new Date(0); doc.updatedAt new Date(0); await doc.save(); // Mongoose 阻止了修改 createdAt并用自己的值覆盖了 updatedAt // 忽略了手动设置它们的尝试。 console.log(doc.createdAt); // 2022-02-26T17:08:13.930Z console.log(doc.updatedAt); // 2022-02-26T17:08:13.991Z // Mongoose 同样会在 findOneAndUpdate()、updateMany() 等查询操作中 // 阻止修改 createdAt 并覆盖 updatedAt // 但 **replaceOne() 和 findOneAndReplace() 除外**。 doc await User.findOneAndUpdate( { _id: doc._id }, { name: test3, createdAt: new Date(0), updatedAt: new Date(0) }, { returnDocument: after } ); console.log(doc.createdAt); // 2022-02-26T17:08:13.930Z console.log(doc.updatedAt); // 2022-02-26T17:08:14.008ZcreatedAt的不可变标记在 lib/helpers/timestamps/setupTimestamps.js 中实现默认值为immutable: true。查询更新时对createdAt的清除逻辑在 lib/helpers/update/applyTimestampsToUpdate.js只要用户更新里携带了createdAt就会从顶层、$set中删除仅当overwriteImmutable为 true 时才保留用户值。注意replace 操作会连 createdAt 一起覆盖请牢记replaceOne()和findOneAndReplace()会覆盖除_id外的所有属性包括createdAt这类不可变属性。调用replaceOne()或findOneAndReplace()会更新createdAt时间戳如下所示// findOneAndReplace() 和 replaceOne() 在 replacement 中未指定 timestamps 时 // 会把 createdAt 和 updatedAt 都设置为当前时间。 doc await User.findOneAndReplace( { _id: doc._id }, { name: test3 }, { returnDocument: after } ); console.log(doc.createdAt); // 2022-02-26T17:08:14.008Z console.log(doc.updatedAt); // 2022-02-26T17:08:14.008Z // findOneAndReplace() 和 replaceOne() 在 replacement 中指定了 timestamps 时 // 会把 createdAt 和 updatedAt 设置为 replacement 中的值。 doc await User.findOneAndReplace( { _id: doc._id }, { name: test3, createdAt: new Date(2022-06-01), updatedAt: new Date(2022-06-01) }, { returnDocument: after } ); console.log(doc.createdAt); // 2022-06-01T00:00:00.000Z console.log(doc.updatedAt); // 2022-06-01T00:00:00.000Z对应实现中setupTimestamps.js维护了一个replaceOps集合replaceOne、findOneAndReplace在 lib/helpers/timestamps/setupTimestamps.js 中被单独识别applyTimestampsToUpdate.js 的isReplace分支会保证 replacement 中缺失的createdAt/updatedAt都被补上当前时间——这正是 replace 语义「整体替换」的体现。自定义时间戳字段名Alternate Property Names为便于叙述文档中统一使用createdAt和updatedAt但你完全可以重命名这两个属性const userSchema new Schema({ name: String }, { timestamps: { createdAt: created_at, // 使用 created_at 存储创建时间 updatedAt: updated_at // 使用 updated_at 存储最后更新时间 } });从handleTimestampOptionlib/helpers/schema/handleTimestampOption.js的解析逻辑可见timestamps选项支持三种形态布尔值true/false整体开关、{ createdAt: xxx, updatedAt: yyy }字符串重命名以及{ createdAt: true, updatedAt: false }这种按字段启停的布尔对象。按操作禁用 Timestampssave()、updateOne()、updateMany()、findOneAndUpdate()、update()、replaceOne()和bulkWrite()都支持timestamps选项。将timestamps: false传入即可跳过该次操作的时间戳设置let doc await User.create({ name: test }); console.log(doc.createdAt); // 2022-02-26T23:28:54.264Z console.log(doc.updatedAt); // 2022-02-26T23:28:54.264Z doc.name test2; // 设置 timestamps: false 告诉 Mongoose 在本次 save() 中跳过更新 updatedAt await doc.save({ timestamps: false }); console.log(doc.updatedAt); // 2022-02-26T23:28:54.264Z未变化 // 同样地在查询上设置 timestamps: false 会让 Mongoose 跳过更新 updatedAt doc await User.findOneAndUpdate({ _id: doc._id }, { name: test3 }, { returnDocument: after, timestamps: false }); console.log(doc.updatedAt); // 2022-02-26T23:28:54.264Z未变化 // 下面演示如何在 bulkWrite() 中禁用时间戳 await User.bulkWrite([{ updateOne: { filter: { _id: doc._id }, update: { name: test4 }, timestamps: false } }]); doc await User.findOne({ _id: doc._id }); console.log(doc.updatedAt); // 2022-02-26T23:28:54.264Z未变化精细控制分别控制 createdAt 与 updatedAt你还可以把timestamps选项设置为对象分别配置createdAt和updatedAt。例如下面的代码中Mongoose 会在save()时设置createdAt但跳过updatedAtconst doc new User({ name: test }); // 告诉 Mongoose 设置 createdAt但跳过 updatedAt。 await doc.save({ timestamps: { createdAt: true, updatedAt: false } }); console.log(doc.createdAt); // 2022-02-26T23:32:12.478Z console.log(doc.updatedAt); // undefined在源码层面查询路径的按字段启停由 applyTimestampsToUpdate.js 中的skipCreatedAt/skipUpdatedAt控制文档路径则由 setDocumentTimestamps.js 读取saveOptions.timestamps判断。手动修正时间戳禁用时间戳也意味着你可以自行设置时间戳。例如假设你需要修正某个文档的createdAt或updatedAt属性可以通过设置timestamps: false并自行指定createdAt来实现let doc await User.create({ name: test }); // 要更新 updatedAt执行带 timestamps: false 且 updatedAt 为期望值的 // findOneAndUpdate() 即可 doc await User.findOneAndUpdate({ _id: doc._id }, { updatedAt: new Date(0) }, { returnDocument: after, timestamps: false }); console.log(doc.updatedAt); // 1970-01-01T00:00:00.000Z // 要更新 createdAt还需要设置 strict: false因为 createdAt 是不可变属性 doc await User.findOneAndUpdate({ _id: doc._id }, { createdAt: new Date(0) }, { returnDocument: after, timestamps: false, strict: false }); console.log(doc.createdAt); // 1970-01-01T00:00:00.000Z子文档Subdocument上的 TimestampsMongoose 也支持在子文档上设置时间戳。需要注意子文档的createdAt和updatedAt表示的是子文档本身的创建与更新时间而不是顶层文档的时间。覆盖overwrite一个子文档也会同时覆盖其createdAtconst roleSchema new Schema({ value: String }, { timestamps: true }); const userSchema new Schema({ name: String, roles: [roleSchema] }); const doc await User.create({ name: test, roles: [{ value: admin }] }); console.log(doc.roles[0].createdAt); // 2022-02-27T00:22:53.836Z console.log(doc.roles[0].updatedAt); // 2022-02-27T00:22:53.836Z // 覆盖子文档也会覆盖 createdAt 和 updatedAt doc.roles[0] { value: root }; await doc.save(); console.log(doc.roles[0].createdAt); // 2022-02-27T00:22:53.902Z console.log(doc.roles[0].updatedAt); // 2022-02-27T00:22:53.902Z // 但修改子文档会保留 createdAt仅更新 updatedAt doc.roles[0].value admin; await doc.save(); console.log(doc.roles[0].createdAt); // 2022-02-27T00:22:53.902Z保持不变 console.log(doc.roles[0].updatedAt); // 2022-02-27T00:22:53.909Z已刷新查询更新路径上的子文档时间戳由 lib/helpers/update/applyTimestampsToChildren.js 递归处理它会遍历$push、$addToSet、$set、$setOnInsert以及普通字段更新凡是命中开启时间戳的 DocumentArray 或 SingleNested 路径都会为其补上createdAt/updatedAt并且只在父文档确实被修改时刷新子文档updatedAt对应测试见 test/timestamps.test.js 中的 gh-9357 用例。底层实现$setOnInsert 与 $set对于带时间戳的查询Mongoose 会向每个更新查询注入两个属性把updatedAt加入$set把createdAt加入$setOnInsert例如运行下面的代码mongoose.set(debug, true); const userSchema new Schema({ name: String }, { timestamps: true }); const User mongoose.model(User, userSchema); await User.findOneAndUpdate({}, { name: test });你会看到 Mongoose 调试模式输出如下Mongoose: users.findOneAndUpdate({}, { $setOnInsert: { createdAt: new Date(Sun, 27 Feb 2022 00:26:27 GMT) }, $set: { updatedAt: new Date(Sun, 27 Feb 2022 00:26:27 GMT), name: test }}, {...})注意createdAt被放入$setOnInsertupdatedAt被放入$set。MongoDB 的$setOnInsert操作符仅在插入upsert新文档时才应用该更新。因此举例来说如果你想只在创建新文档时设置updatedAt可以禁用updatedAt时间戳并自行设置await User.findOneAndUpdate({}, { $setOnInsert: { updatedAt: new Date() } }, { timestamps: { createdAt: true, updatedAt: false } });这一$set/$setOnInsert的分工在 lib/helpers/update/applyTimestampsToUpdate.js 中清晰可见updatedAt无条件进入updates.$set[updatedAt]第 52-75 行而createdAt仅在 upsert 场景下进入$setOnInsert第 121-124 行。另外当更新使用聚合管道数组形式时Mongoose 也会向管道追加一个{ $set: { updatedAt: now } }阶段第 44-51 行。更新已存在文档的时间戳如果你需要禁用 Mongoose 的时间戳、并使用updateOne()或findOneAndUpdate()把文档时间戳更新为其他值需要做两件事将timestamps选项设置为false防止 Mongoose 自动设置updatedAt将overwriteImmutable设置为true允许覆盖默认不可变的createdAt属性。const createdAt new Date(2011-06-01); // 将文档的 createdAt 更新为自定义值。 // 正常情况下 Mongoose 会阻止该操作因为 createdAt 是不可变的。 await Model.updateOne({ _id: doc._id }, { createdAt }, { overwriteImmutable: true, timestamps: false }); doc await Model.collection.findOne({ _id: doc._id }); doc.createdAt.valueOf() createdAt.valueOf(); // true结合源码看overwriteImmutable的作用点正是 applyTimestampsToUpdate.js当overwriteImmutable为 true 且用户提供了createdAt时用户的createdAt会被保留并移入$set而不是像默认行为那样被删除。进阶自定义 currentTime 时间源除了默认的Date时间源Mongoose 还允许通过timestamps.currentTime函数完全自定义时间戳的值。这在需要秒级时间戳、Unix 时间戳配合Number类型字段等场景下非常实用// 使用 Number 字段 currentTime 生成 Unix 秒级时间戳 const schema new Schema({ createdAt: Number, updatedAt: Number, name: String }, { timestamps: { currentTime: () 42 } }); const Model db.model(Test, schema); const doc await Model.create({ name: test }); // doc.createdAt 42, doc.updatedAt 42均为 Number 类型currentTime在 lib/helpers/timestamps/setupTimestamps.js 中被提取并同时作用于save路径setDocumentTimestamps与查询更新路径_setTimestampsOnUpdate中的this.model.base.now()被替换为currentTime()。该能力由测试 test/timestamps.test.js 覆盖包括配合getter把 Unix 秒转为Date对象的用法。小结与适用场景默认行为timestamps: true自动添加不可变的createdAt与可覆盖的updatedAt两者在文档新建时相同此后updatedAt随每次写入刷新字段重命名用对象形式{ createdAt: created_at, updatedAt: updated_at }可自定义存储字段名按操作控制save()、各查询方法与bulkWrite()均支持timestamps: false或{ createdAt: true, updatedAt: false }粒度的启停replace 特例replaceOne()/findOneAndReplace()会连同不可变的createdAt一并覆盖修正历史数据timestamps: falseoverwriteImmutable: true可合法改写createdAt自定义时间源timestamps.currentTime可接管时间戳的取值逻辑适用于秒级/数值型时间戳需求。相关实现可继续深入阅读lib/helpers/timestamps/setupTimestamps.js中间件与字段注入、lib/helpers/timestamps/setDocumentTimestamps.jssave 路径、lib/helpers/update/applyTimestampsToUpdate.js查询路径、lib/helpers/update/applyTimestampsToChildren.js子文档递归完整行为验证可参考 test/timestamps.test.js。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考