如何在 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