如何在 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: truereturnOriginal 選項的存在是為了與 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 會將 filterupdate 組合在一起插入一個文件,如下所示。

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 參數。但是,您可以透過將 overwriteDiscriminatorKey 選項設定為 true,來告訴 Mongoose 允許更新辨別器鍵,如下所示。

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