もちろん、利用可能なMongoDBのバージョンに応じて、いくつかのアプローチがあります。これらは、$lookup
のさまざまな使用法によって異なります。 .populate()
でオブジェクトマニピュレーションを有効にするまで .lean()
による結果 。
セクションを注意深くお読みいただき、実装ソリューションを検討する際にすべてが思ったとおりになっていない可能性があることに注意してください。
MongoDB 3.6、「ネストされた」$ lookup
MongoDB 3.6では、$lookup
オペレーターは、pipeline
を含める追加機能を取得します 「ローカル」キー値を「外部」キー値に単純に結合するのではなく、式は、基本的に各$lookup
を実行できることを意味します。 これらのパイプライン式内に「ネストされた」ものとして
Venue.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
{ "$lookup": {
"from": Review.collection.name,
"let": { "reviews": "$reviews" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
{ "$lookup": {
"from": Comment.collection.name,
"let": { "comments": "$comments" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
{ "$lookup": {
"from": Author.collection.name,
"let": { "author": "$author" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
{ "$addFields": {
"isFollower": {
"$in": [
mongoose.Types.ObjectId(req.user.id),
"$followers"
]
}
}}
],
"as": "author"
}},
{ "$addFields": {
"author": { "$arrayElemAt": [ "$author", 0 ] }
}}
],
"as": "comments"
}},
{ "$sort": { "createdAt": -1 } }
],
"as": "reviews"
}},
])
元のパイプラインの観点から見ると、これは非常に強力な場合があります。実際には、コンテンツを"reviews"
に追加することしか認識していません。 配列とそれに続く各「ネストされた」パイプライン式も、結合からの「内部」要素のみを認識します。
これは強力であり、すべてのフィールドパスがネストレベルに関連しているため、いくつかの点で少し明確になる可能性がありますが、BSON構造でインデントがクリープし始めます。また、配列と一致しているかどうかを認識する必要があります。または構造をトラバースする際の特異値。
"comments"
内に見られるように、ここでは「作成者プロパティのフラット化」なども実行できることに注意してください。 配列エントリ。すべての$lookup
ターゲット出力は「配列」の場合がありますが、「サブパイプライン」内で、その単一要素配列を単一の値に再形成できます。
標準のMongoDB$lookup
「サーバーに参加」を維持したまま、実際には$lookup
で実行できます。 、ただし、中間処理が必要です。これは、$unwind
を使用して配列を分解する長年のアプローチです。 $group
を使用します アレイを再構築する段階:
Venue.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
{ "$lookup": {
"from": Review.collection.name,
"localField": "reviews",
"foreignField": "_id",
"as": "reviews"
}},
{ "$unwind": "$reviews" },
{ "$lookup": {
"from": Comment.collection.name,
"localField": "reviews.comments",
"foreignField": "_id",
"as": "reviews.comments",
}},
{ "$unwind": "$reviews.comments" },
{ "$lookup": {
"from": Author.collection.name,
"localField": "reviews.comments.author",
"foreignField": "_id",
"as": "reviews.comments.author"
}},
{ "$unwind": "$reviews.comments.author" },
{ "$addFields": {
"reviews.comments.author.isFollower": {
"$in": [
mongoose.Types.ObjectId(req.user.id),
"$reviews.comments.author.followers"
]
}
}},
{ "$group": {
"_id": {
"_id": "$_id",
"reviewId": "$review._id"
},
"name": { "$first": "$name" },
"addedBy": { "$first": "$addedBy" },
"review": {
"$first": {
"_id": "$review._id",
"createdAt": "$review.createdAt",
"venue": "$review.venue",
"author": "$review.author",
"content": "$review.content"
}
},
"comments": { "$push": "$reviews.comments" }
}},
{ "$sort": { "_id._id": 1, "review.createdAt": -1 } },
{ "$group": {
"_id": "$_id._id",
"name": { "$first": "$name" },
"addedBy": { "$first": "$addedBy" },
"reviews": {
"$push": {
"_id": "$review._id",
"venue": "$review.venue",
"author": "$review.author",
"content": "$review.content",
"comments": "$comments"
}
}
}}
])
これは、最初に考えるほど困難ではなく、$lookup
の単純なパターンに従います。 および$unwind
各アレイを進めていくと。
"author"
もちろん、詳細は特異であるため、「巻き戻し」が終わったら、そのままにしておき、フィールドを追加して、配列に「ロールバック」するプロセスを開始します。
2つしかありません 元のVenue
に再構築するレベル ドキュメントなので、最初の詳細レベルはReview
によるものです "comments"
を再構築します 配列。必要なのは$push
することだけです "$reviews.comments"
のパス これらを収集するために、"$reviews._id"
である限り フィールドは「grouping_id」にあります。保持する必要がある他のすべてのものは、他のすべてのフィールドです。これらすべてを_id
に入れることができます 同様に、または$first
を使用できます 。
これで、$group
はあと1つだけになります。 Venue
に戻るためのステージ 自体。今回のグループ化キーは"$_id"
です。 もちろん、$first
を使用して会場自体のすべてのプロパティを使用します 残りの"$review"
$push
を使用して配列に戻る詳細 。もちろん、"$comments"
前の$group
からの出力 "review.comments"
になります パス。
単一のドキュメントとその関係に取り組んでいるので、これはそれほど悪くはありません。 $unwind
パイプライン演算子は一般的に パフォーマンスの問題ですが、この使用法のコンテキストでは、それほど大きな影響を与えることはないはずです。
データはまだ「サーバーに参加」しているため、まだ 他の残りの選択肢よりもはるかに少ないトラフィック。
JavaScript操作
もちろん、ここでのもう1つのケースは、サーバー自体のデータを変更する代わりに、実際に結果を操作することです。 ほとんど データへの「追加」はおそらくクライアントで最も適切に処理されるため、このアプローチを支持する場合があります。
もちろん、populate()
の使用に関する問題 「のように見える」 はるかに単純化されたプロセスであり、実際には参加しない とにかく。すべてのpopulate()
実際には"非表示" 複数を送信する基本的なプロセス データベースにクエリを実行し、非同期処理によって結果を待機します。
つまり、「外観」 参加の結果は、実際にはサーバーへの複数のリクエストと「クライアント側の操作」の結果です。 配列内に詳細を埋め込むためのデータの一覧。
したがって、その明確な警告は別として、 パフォーマンス特性は、サーバーの$lookup
と同等にはほど遠いことです。 、もちろん、もう1つの注意点は、結果の「マングースドキュメント」は、実際にはさらに操作される可能性のあるプレーンなJavaScriptオブジェクトではないということです。
したがって、このアプローチを採用するには、.lean()
を追加する必要があります Document
の代わりに「プレーンJavaScriptオブジェクト」を返すようにmongooseに指示するための実行前のクエリへのメソッド モデルにアタッチされたスキーマメソッドでキャストされるタイプ。もちろん、結果のデータは、関連するモデル自体に関連付けられる「インスタンスメソッド」にアクセスできなくなることに注意してください。
let venue = await Venue.findOne({ _id: id.id })
.populate({
path: 'reviews',
options: { sort: { createdAt: -1 } },
populate: [
{ path: 'comments', populate: [{ path: 'author' }] }
]
})
.lean();
今venue
はプレーンオブジェクトであるため、必要に応じて簡単に処理および調整できます。
venue.reviews = venue.reviews.map( r =>
({
...r,
comments: r.comments.map( c =>
({
...c,
author: {
...c.author,
isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
}
})
)
})
);
したがって、実際には、followers
が表示されるレベルまで、内部配列のそれぞれを循環するだけです。 author
内の配列 詳細。次に、ObjectId
と比較できます。 最初に.map()
を使用した後にその配列に格納された値 req.user.id
と比較するための「文字列」値を返します これも文字列です(そうでない場合は、.toString()
も追加します その上で)、JavaScriptコードを介してこの方法でこれらの値を比較する方が一般的に簡単であるため。
繰り返しになりますが、「シンプルに見える」ことを強調する必要がありますが、これらの追加のクエリとサーバーとクライアント間の転送には処理に多くの時間がかかるため、実際にはシステムパフォーマンスのために避けたいものです。リクエストのオーバーヘッドが原因でさえ、これはホスティングプロバイダー間のトランスポートの実際のコストになります。
概要
これらは基本的に、実際に「複数のクエリ」を実行する「独自のローリング」を除いて、実行できるアプローチです。 .populate()
というヘルパーを使用する代わりに、自分でデータベースに追加します です。
次に、入力出力を使用して、.lean()
を適用する限り、他のデータ構造と同じように結果のデータを簡単に操作できます。 返されたマングースドキュメントからプレーンオブジェクトデータを変換または抽出するクエリに変換します。
集約的なアプローチははるかに複雑に見えますが、「たくさん」があります。 サーバー上でこの作業を行うことのその他の利点。より大きな結果セットを並べ替えたり、さらにフィルタリングするために計算を実行したりできます。もちろん、「単一の応答」を取得できます。 「単一のリクエスト」 サーバーに対して行われ、すべて追加のオーバーヘッドはありません。
パイプライン自体が、スキーマにすでに格納されている属性に基づいて単純に構築できることは、完全に議論の余地があります。したがって、添付されたスキーマに基づいてこの「構築」を実行する独自のメソッドを作成することはそれほど難しくありません。
もちろん長期的には$lookup
がより良い解決策ですが、もちろん、ここにリストされているものから単にコピーするだけではない場合は、最初のコーディングにもう少し作業を加える必要があります;)