3.2を超える最新のMongoDBでは、$lookup
を使用できます。 .populate()
の代わりとして ほとんどの場合。これには、.populate()
とは対照的に、実際に「サーバー上で」参加するという利点もあります。 これは実際には「エミュレート」するための「複数のクエリ」です 参加します。
したがって、.populate()
ない リレーショナルデータベースがどのようにそれを行うかという意味で、実際には「参加」します。 $lookup
一方、演算子は実際にサーバー上で作業を行い、 "LEFT JOIN"に多かれ少なかれ類似しています。 :
Item.aggregate(
[
{ "$lookup": {
"from": ItemTags.collection.name,
"localField": "tags",
"foreignField": "_id",
"as": "tags"
}},
{ "$unwind": "$tags" },
{ "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },
{ "$group": {
"_id": "$_id",
"dateCreated": { "$first": "$dateCreated" },
"title": { "$first": "$title" },
"description": { "$first": "$description" },
"tags": { "$push": "$tags" }
}}
],
function(err, result) {
// "tags" is now filtered by condition and "joined"
}
)
N.B。
.collection.name
ここでは、モデルに割り当てられたMongoDBコレクションの実際の名前である「文字列」に実際に評価されます。マングースはデフォルトでコレクション名を「複数化」するため、$lookup
引数として実際のMongoDBコレクション名が必要です(サーバー操作であるため)。これは、コレクション名を直接「ハードコーディング」するのではなく、mongooseコードで使用する便利なトリックです。
$filter
を使用することもできます 不要なアイテムを削除する配列では、$lookup
の特別な条件に対する集約パイプラインの最適化により、これが実際に最も効率的な形式になります。 その後に$unwind
の両方が続きます および$match
状態。
これにより、実際には3つのパイプラインステージが1つにまとめられます。
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
これは、実際の操作が「最初に結合するようにコレクションをフィルタリング」し、次に結果を返し、配列を「巻き戻す」ため、非常に最適です。両方の方法が採用されているため、結果はBSONの制限である16MBを超えません。これは、クライアントにはない制約です。
唯一の問題は、特に配列の結果が必要な場合に、いくつかの点で「直感に反する」ように見えることですが、それが$group
です。 元のドキュメント形式に再構築されるため、ここにあります。
また、現時点では実際に$lookup
を記述できないことも残念です。 サーバーが使用するのと同じ最終的な構文で。私見、これは修正すべき見落としです。ただし、現時点では、シーケンスを使用するだけで機能し、最高のパフォーマンスとスケーラビリティを備えた最も実行可能なオプションです。
補遺-MongoDB3.6以降
ここに示されているパターンはかなり最適化されています 他のステージが$lookup
に組み込まれる方法が原因です 、通常は両方の$lookup
に固有の「LEFTJOIN」という点で1つ失敗しています。 およびpopulate()
のアクション 「最適」によって否定されます $unwind
の使用法 ここでは、空の配列は保持されません。 preserveNullAndEmptyArrays
を追加できます オプションですが、これは「最適化された」を無効にします 上記のシーケンスでは、基本的に3つのステージすべてがそのまま残り、通常は最適化で組み合わされます。
MongoDB 3.6は、「より表現力豊かな」で拡張されます。 $lookup
の形式 「サブパイプライン」式を許可します。これは、「LEFT JOIN」を保持するという目標を達成するだけでなく、最適なクエリを使用して、返される結果を減らし、構文を大幅に簡素化することができます。
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
$expr
宣言された「ローカル」値を「外部」値と一致させるために使用されるのは、実際には、MongoDBが元の$lookup
で「内部的に」実行することです。 構文。この形式で表現することにより、最初の$match
を調整できます。 「サブパイプライン」内での表現。
実際、真の「集約パイプライン」として、$lookup
のレベルを「ネスト」するなど、この「サブパイプライン」式内で集約パイプラインを使用して実行できるほぼすべてのことを実行できます。 他の関連コレクションへ。
さらなる使用法は、ここでの質問の範囲を少し超えていますが、「ネストされた母集団」に関連して、$lookup
の新しい使用パターンです。 これをほぼ同じにすることができ、 "lot" 完全に使用することでより強力になります。
実例
以下に、モデルで静的メソッドを使用する例を示します。その静的メソッドが実装されると、呼び出しは単純に次のようになります。
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
または、もう少し現代的なものに拡張すると、次のようになります。
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
.populate()
と非常によく似ています 構造的には、実際には代わりにサーバーで結合を実行しています。完全を期すために、ここでの使用法は、親と子の両方のケースに従って、返されたデータをマングースドキュメントインスタンスにキャストバックします。
かなり簡単で、適応するのも簡単で、ほとんどの一般的なケースのようにそのまま使用することもできます。
N.B ここでの非同期の使用は、同封の例を実行するための簡潔さのためです。実際の実装には、この依存関係はありません。
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost/looktest');
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
dateCreated: { type: Date, default: Date.now },
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});
itemSchema.statics.lookup = function(opt,callback) {
let rel =
mongoose.model(this.schema.path(opt.path).caster.options.ref);
let group = { "$group": { } };
this.schema.eachPath(p =>
group.$group[p] = (p === "_id") ? "$_id" :
(p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": opt.path,
"localField": opt.path,
"foreignField": "_id"
}},
{ "$unwind": `$${opt.path}` },
{ "$match": opt.query },
group
];
this.aggregate(pipeline,(err,result) => {
if (err) callback(err);
result = result.map(m => {
m[opt.path] = m[opt.path].map(r => rel(r));
return this(m);
});
callback(err,result);
});
}
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
function log(body) {
console.log(JSON.stringify(body, undefined, 2))
}
async.series(
[
// Clean data
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
// Create tags and items
(callback) =>
async.waterfall(
[
(callback) =>
ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
callback),
(tags, callback) =>
Item.create({ "title": "Something","description": "An item",
"tags": tags },callback)
],
callback
),
// Query with our static
(callback) =>
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
],
(err,results) => {
if (err) throw err;
let result = results.pop();
log(result);
mongoose.disconnect();
}
)
または、async/await
を使用したNode8.x以降のもう少し最新の機能 追加の依存関係はありません:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
dateCreated: { type: Date, default: Date.now },
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});
itemSchema.statics.lookup = function(opt) {
let rel =
mongoose.model(this.schema.path(opt.path).caster.options.ref);
let group = { "$group": { } };
this.schema.eachPath(p =>
group.$group[p] = (p === "_id") ? "$_id" :
(p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": opt.path,
"localField": opt.path,
"foreignField": "_id"
}},
{ "$unwind": `$${opt.path}` },
{ "$match": opt.query },
group
];
return this.aggregate(pipeline).exec().then(r => r.map(m =>
this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })
));
}
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
const log = body => console.log(JSON.stringify(body, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
// Clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Create tags and items
const tags = await ItemTag.create(
["movies", "funny"].map(tagName =>({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
// Query with our static
const result = (await Item.lookup({
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
})).pop();
log(result);
mongoose.disconnect();
} catch (e) {
console.error(e);
} finally {
process.exit()
}
})()
また、MongoDB 3.6以降では、$unwind
がなくても および$group
建物:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
},{ timestamps: true });
itemSchema.statics.lookup = function({ path, query }) {
let rel =
mongoose.model(this.schema.path(path).caster.options.ref);
// MongoDB 3.6 and up $lookup with sub-pipeline
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": path,
"let": { [path]: `$${path}` },
"pipeline": [
{ "$match": {
...query,
"$expr": { "$in": [ "$_id", `$$${path}` ] }
}}
]
}}
];
return this.aggregate(pipeline).exec().then(r => r.map(m =>
this({ ...m, [path]: m[path].map(r => rel(r)) })
));
};
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
const log = body => console.log(JSON.stringify(body, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
// Clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Create tags and items
const tags = await ItemTag.insertMany(
["movies", "funny"].map(tagName => ({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
// Query with our static
let result = (await Item.lookup({
path: 'tags',
query: { 'tagName': { '$in': [ 'funny', 'politics' ] } }
})).pop();
log(result);
await mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()