これには、いくつかの異なる方法で取り組むことができます。もちろん、アプローチやパフォーマンスによって異なりますが、設計にはもっと大きな考慮事項があると思います。最も注目すべき点は、実際のアプリケーションの使用パターンにおける「改訂」データの「必要性」です。
集計によるクエリ
「内部配列から最後の要素」を取得するための最も重要なポイントについては、実際には .aggregate()
これを行うための操作:
function getProject(req,projectId) {
return new Promise((resolve,reject) => {
Project.aggregate([
{ "$match": { "project_id": projectId } },
{ "$addFields": {
"uploaded_files": {
"$map": {
"input": "$uploaded_files",
"as": "f",
"in": {
"latest": {
"$arrayElemAt": [
"$$f.history",
-1
]
},
"_id": "$$f._id",
"display_name": "$$f.display_name"
}
}
}
}},
{ "$lookup": {
"from": "owner_collection",
"localField": "owner",
"foreignField": "_id",
"as": "owner"
}},
{ "$unwind": "$uploaded_files" },
{ "$lookup": {
"from": "files_collection",
"localField": "uploaded_files.latest.file",
"foreignField": "_id",
"as": "uploaded_files.latest.file"
}},
{ "$group": {
"_id": "$_id",
"project_id": { "$first": "$project_id" },
"updated_at": { "$first": "$updated_at" },
"created_at": { "$first": "$created_at" },
"owner" : { "$first": { "$arrayElemAt": [ "$owner", 0 ] } },
"name": { "$first": "$name" },
"uploaded_files": {
"$push": {
"latest": { "$arrayElemAt": [ "$$uploaded_files", 0 ] },
"_id": "$$uploaded_files._id",
"display_name": "$$uploaded_files.display_name"
}
}
}}
])
.then(result => {
if (result.length === 0)
reject(new createError.NotFound(req.path));
resolve(result[0])
})
.catch(reject)
})
}
これは、追加のリクエスト( .populate()
)を行うのではなく、「サーバー」で「結合」を実行できる集約ステートメントであるためです。 実際にここで行います) $ lookup
、スキーマが質問に含まれていないため、実際のコレクション名を自由に使用しています。実際にこの方法で実行できることに気付いていなかったので、それで問題ありません。
もちろん、「実際の」コレクション名はサーバーに必要です。サーバーには、「アプリケーション側」で定義されたスキーマの概念がありません。ここでは便利な方法がありますが、それについては後で詳しく説明します。
projectId
の場所によっては注意が必要です。 .find()
などの通常のマングースメソッドとは異なり、実際には $ match
実際にはObjectId
に「キャスト」する必要があります 入力値が実際に「文字列」である場合。 Mongooseは集約パイプラインに「スキーマタイプ」を適用できないため、特に projectId
の場合は、これを自分で行う必要がある場合があります。 リクエストパラメータから来ました:
{ "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
ここでの基本的な部分は、 $ map
>
すべての"uploaded_files"
を反復処理します エントリを入力し、 "history"
から「最新」を抽出します。 $ arrayElemAt
の配列
-1
である「最後の」インデックスを使用する 。
「最新のリビジョン」が実際には「最後の」配列エントリである可能性が最も高いため、これは合理的です。 $ max
$ filter
の条件として
。そのため、パイプラインステージは次のようになります。
{ "$addFields": {
"uploaded_files": {
"$map": {
"input": "$uploaded_files",
"as": "f",
"in": {
"latest": {
"$arrayElemAt": [
{ "$filter": {
"input": "$$f.history.revision",
"as": "h",
"cond": {
"$eq": [
"$$h",
{ "$max": "$$f.history.revision" }
]
}
}},
0
]
},
"_id": "$$f._id",
"display_name": "$$f.display_name"
}
}
}
}},
と比較することを除いて、これはほぼ同じです。 $ max
値を返し、 "one"のみを返します 「フィルタリングされた」配列から「最初の」位置、つまり 0
を返すインデックスを作成する配列からのエントリ インデックス。
$ lookup
の使用に関するその他の一般的なテクニックについて
.populate()
の代わりに 、「マングースにデータを入力した後のクエリ」
のエントリを参照してください これは、このアプローチを取るときに最適化できることについてもう少し話します。
入力によるクエリ
もちろん、 .populate()
を使用して、同じ種類の操作を実行できます(効率的ではありませんが)。 呼び出しと結果の配列の操作:
Project.findOne({ "project_id": projectId })
.populate(populateQuery)
.lean()
.then(project => {
if (project === null)
reject(new createError.NotFound(req.path));
project.uploaded_files = project.uploaded_files.map( f => ({
latest: f.history.slice(-1)[0],
_id: f._id,
display_name: f.display_name
}));
resolve(project);
})
.catch(reject)
もちろん、実際には "history"
からアイテムの「すべて」を返します。 、ただし、.mapを適用するだけです。 ()
.slice()<を呼び出す/ code>
これらの要素に対して、それぞれの最後の配列要素を再度取得します。
すべての履歴が返され、 .populate()
が返されるため、オーバーヘッドが少し増えます。 呼び出しは追加のリクエストですが、同じ最終結果が得られます。
設計のポイント
ここで私が目にする主な問題は、コンテンツ内に「履歴」配列さえあることです。必要な関連アイテムのみを返品するには、上記のようなことを行う必要があるため、これはあまり良いアイデアではありません。
ですから、「デザインのポイント」として、私はこれをしません。しかし、代わりに、すべての場合にアイテムから履歴を「分離」します。 「埋め込まれた」ドキュメントを維持しながら、「履歴」を別の配列に保持し、「最新の」リビジョンのみを実際のコンテンツとともに保持します。
{
"_id" : ObjectId("5935a41f12f3fac949a5f925"),
"project_id" : 13,
"updated_at" : ISODate("2017-07-02T22:11:43.426Z"),
"created_at" : ISODate("2017-06-05T18:34:07.150Z"),
"owner" : ObjectId("591eea4439e1ce33b47e73c3"),
"name" : "Demo project",
"uploaded_files" : [
{
"latest" : {
{
"file" : ObjectId("59596f9fb6c89a031019bcae"),
"revision" : 1
}
},
"_id" : ObjectId("59596f9fb6c89a031019bcaf"),
"display_name" : "Example filename.txt"
}
]
"file_history": [
{
"_id": ObjectId("59596f9fb6c89a031019bcaf"),
"file": ObjectId("59596f9fb6c89a031019bcae"),
"revision": 0
},
{
"_id": ObjectId("59596f9fb6c89a031019bcaf"),
"file": ObjectId("59596f9fb6c89a031019bcae"),
"revision": 1
}
}
これは、 $ set
を設定するだけで維持できます。
関連するエントリと $ push
の使用
1回の操作での「履歴」について:
.update(
{ "project_id": projectId, "uploaded_files._id": fileId }
{
"$set": {
"uploaded_files.$.latest": {
"file": revisionId,
"revision": revisionNum
}
},
"$push": {
"file_history": {
"_id": fileId,
"file": revisionId,
"revision": revisionNum
}
}
}
)
配列を分離すると、クエリを実行して常に最新のものを取得し、実際にそのリクエストを行いたいときまで「履歴」を破棄できます。
Project.findOne({ "project_id": projectId })
.select('-file_history') // The '-' here removes the field from results
.populate(populateQuery)
一般的なケースとして、私は単に「改訂」番号をまったく気にしないでしょう。 「最新」は常に「最後」であるため、配列に「追加」するときに、同じ構造の多くを維持することは実際には必要ありません。これは、構造を変更する場合にも当てはまります。ここでも、「最新」が常に、指定されたアップロードファイルの最後のエントリになります。
このような「人工」インデックスを維持しようとすると問題が発生し、 .update()
に示すように「アトミック」操作の変更がほとんど台無しになります。 ここでの例は、最新のリビジョン番号を提供するために「カウンター」値を知る必要があるため、どこかからそれを「読み取る」必要があるためです。