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

`update` が適用された **後** のドキュメントを返すには、`new` オプションを `true` に設定する必要があります。

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

アトミック更新

インデックスのないアップサートを除いて、`findOneAndUpdate()` はアトミックです。つまり、アップサートを実行している場合 **を除き**、MongoDB がドキュメントを見つけてからドキュメントを更新するまでの間に、ドキュメントが変更されないことを前提とすることができます。

たとえば、`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` オプションを使用すると、`findOneAndUpdate()` を検索とアップサート操作として使用できます。アップサートは、`filter` に一致するドキュメントが見つかった場合は、通常の `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()` の結果を変換します。更新されたドキュメントを返します。そのため、ドキュメントがアップサートされたかどうかを確認するのが困難になります。更新されたドキュメントを取得し、MongoDB が同じ操作で新しいドキュメントをアップサートしたかどうかを確認するには、`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 })
);

Mongoose は、`__t` (デフォルトのディスクリミネーターキー) が設定されている場合、`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