如何在 Mongoose 中使用 `findOneAndUpdate()`
Mongoose 中的 `findOneAndUpdate()` 函数 具有多种用例。 您应该尽可能使用 `save()` 更新文档,以获得更好的 验证 和 中间件 支持。但是,在某些情况下,您需要使用 `findOneAndUpdate()`。在本教程中,您将了解如何使用 `findOneAndUpdate()`,以及何时需要使用它。
入门
顾名思义,`findOneAndUpdate()` 查找匹配给定 `filter` 的第一个文档,应用 `update`,并返回该文档。`findOneAndUpdate()` 函数具有以下签名
function findOneAndUpdate(filter, update, options) {}
默认情况下,`findOneAndUpdate()` 返回文档在应用 `update` 之前的状态。在以下示例中,`doc` 最初只有 `name` 和 `_id` 属性。`findOneAndUpdate()` 添加了 `age` 属性,但 `findOneAndUpdate()` 的结果 **不包含** `age` 属性。
const Character = mongoose.model('Character', new mongoose.Schema({
name: String,
age: Number
}));
const _id = new mongoose.Types.ObjectId('0'.repeat(24));
let doc = await Character.create({ _id, name: 'Jean-Luc Picard' });
doc; // { name: 'Jean-Luc Picard', _id: ObjectId('000000000000000000000000') }
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
// The result of `findOneAndUpdate()` is the document _before_ `update` was applied
doc = await Character.findOneAndUpdate(filter, update);
doc; // { name: 'Jean-Luc Picard', _id: ObjectId('000000000000000000000000') }
doc = await Character.findOne(filter);
doc.age; // 59
您应该将 `new` 选项设置为 `true`,以便在应用 `update` 后返回文档。
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
// `doc` is the document _after_ `update` was applied because of
// `new: true`
const doc = await Character.findOneAndUpdate(filter, update, {
new: true
});
doc.name; // 'Jean-Luc Picard'
doc.age; // 59
Mongoose 的 `findOneAndUpdate()` 与 MongoDB Node.js 驱动程序的 `findOneAndUpdate()` 略有不同,因为它返回文档本身,而不是 结果对象。
作为 `new` 选项的替代方案,您也可以使用 `returnOriginal` 选项。`returnOriginal: false` 等效于 `new: true`。`returnOriginal` 选项存在是为了与 MongoDB Node.js 驱动程序的 `findOneAndUpdate()` 保持一致,该驱动程序具有相同的选项。
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
// `doc` is the document _after_ `update` was applied because of
// `returnOriginal: false`
const doc = await Character.findOneAndUpdate(filter, update, {
returnOriginal: false
});
doc.name; // 'Jean-Luc Picard'
doc.age; // 59
原子更新
除了 未索引的 upsert 外,`findOneAndUpdate()` 是原子操作。这意味着您可以假设文档在 MongoDB 找到文档和更新文档之间不会更改,除非您正在执行 upsert。
例如,如果您使用 `save()` 更新文档,则文档在您使用 `findOne()` 加载文档和使用 `save()` 保存文档之间可能会在 MongoDB 中更改,如下所示。对于许多用例来说,`save()` 的竞争条件是一个小问题。但是,如果您需要,可以使用 `findOneAndUpdate()`(或 事务)来解决它。
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
let doc = await Character.findOne({ name: 'Jean-Luc Picard' });
// Document changed in MongoDB, but not in Mongoose
await Character.updateOne(filter, { name: 'Will Riker' });
// This will update `doc` age to `59`, even though the doc changed.
doc.age = update.age;
await doc.save();
doc = await Character.findOne();
doc.name; // Will Riker
doc.age; // 59
Upsert
使用 `upsert` 选项,您可以将 `findOneAndUpdate()` 用作查找和 upsert 操作。如果找到匹配 `filter` 的文档,upsert 的行为与正常的 `findOneAndUpdate()` 相同。但是,如果没有文档匹配 `filter`,MongoDB 将通过组合 `filter` 和 `update` 插入一个文档,如下所示。
const filter = { name: 'Will Riker' };
const update = { age: 29 };
await Character.countDocuments(filter); // 0
const doc = await Character.findOneAndUpdate(filter, update, {
new: true,
upsert: true // Make this update into an upsert
});
doc.name; // Will Riker
doc.age; // 29
`includeResultMetadata` 选项
默认情况下,Mongoose 会转换 `findOneAndUpdate()` 的结果:它返回更新后的文档。这使得难以检查文档是否被 upsert。为了获取更新后的文档并检查 MongoDB 是否在同一个操作中 upsert 了一个新文档,您可以设置 `includeResultMetadata` 标志,使 Mongoose 返回来自 MongoDB 的原始结果。
const filter = { name: 'Will Riker' };
const update = { age: 29 };
await Character.countDocuments(filter); // 0
const res = await Character.findOneAndUpdate(filter, update, {
new: true,
upsert: true,
// Return additional properties about the operation, not just the document
includeResultMetadata: true
});
res.value instanceof Character; // true
// The below property will be `false` if MongoDB upserted a new
// document, and `true` if MongoDB updated an existing object.
res.lastErrorObject.updatedExisting; // false
以下是上面示例中的 `res` 对象的样子
{ lastErrorObject:
{ n: 1,
updatedExisting: false,
upserted: 5e6a9e5ec6e44398ae2ac16a },
value:
{ _id: 5e6a9e5ec6e44398ae2ac16a,
name: 'Will Riker',
__v: 0,
age: 29 },
ok: 1 }
更新鉴别器键
默认情况下,Mongoose 阻止使用 `findOneAndUpdate()` 更新 鉴别器键。例如,假设您有以下鉴别器模型。
const eventSchema = new mongoose.Schema({ time: Date });
const Event = db.model('Event', eventSchema);
const ClickedLinkEvent = Event.discriminator(
'ClickedLink',
new mongoose.Schema({ url: String })
);
const SignedUpEvent = Event.discriminator(
'SignedUp',
new mongoose.Schema({ username: String })
);
如果设置了 `__t`,Mongoose 会从 `update` 参数中删除 `__t`(默认的鉴别器键)。这是为了防止意外更新鉴别器键;例如,如果您将不受信任的用户输入传递给 `update` 参数。但是,您可以告诉 Mongoose 允许更新鉴别器键,方法是将 `overwriteDiscriminatorKey` 选项设置为 `true`,如下所示。
let event = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
await event.save();
event = await ClickedLinkEvent.findByIdAndUpdate(
event._id,
{ __t: 'SignedUp' },
{ overwriteDiscriminatorKey: true, new: true }
);
event.__t; // 'SignedUp', updated discriminator key