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) {
}
)
NB 는 .collection.name
여기에 실제 모델에 할당으로 MongoDB를 수집의 실제 이름 인 "문자열"로 평가한다. mongoose는 기본적으로 컬렉션 이름을 "복수화" $lookup
하고 인수로 실제 MongoDB 컬렉션 이름을 필요로하기 때문에 (서버 작업이므로), 컬렉션 이름을 직접 "하드 코딩"하는 대신 mongoose 코드에서 사용하는 편리한 트릭입니다. .
$filter
원하지 않는 항목을 제거하기 위해 배열을 사용할 수도 있지만 , 이는 an 및 조건이 뒤 따르는 특수 조건에 대한 집계 파이프 라인 최적화 로 인해 실제로 가장 효율적인 형식 입니다.$lookup
$unwind
$match
이로 인해 실제로 3 개의 파이프 라인 단계가 하나로 통합됩니다.
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
이것은 실제 작업이 "먼저 조인 할 컬렉션을 필터링"한 다음 결과를 반환하고 배열을 "풀기"하므로 매우 최적입니다. 두 가지 방법이 모두 사용되므로 결과가 BSON 제한 인 16MB를 깨뜨리지 않습니다. 이는 클라이언트에없는 제약입니다.
유일한 문제는 특히 배열로 결과를 원할 때 어떤면에서 "반 직관적"으로 보이지만 $group
원래 문서 양식으로 재구성되므로 여기에 해당됩니다.
또한 현재로서는 $lookup
서버가 사용하는 것과 동일한 최종 구문 으로 실제로 작성할 수 없다는 것도 안타깝습니다 . IMHO, 이것은 수정해야 할 감독입니다. 그러나 현재로서는 시퀀스를 사용하는 것만으로도 효과가 있으며 최상의 성능과 확장 성을 갖춘 가장 실행 가능한 옵션입니다.
부록-MongoDB 3.6 이상
여기에 표시된 패턴 은 다른 단계가로 롤링되는 방식으로 인해 상당히 최적화 되었지만 $lookup
일반적으로 두 단계 모두에 내재 된 "LEFT JOIN" $lookup
과 populate()
의 "최적의" 사용으로 인해 무효화 된다는 점에서 하나의 실패가 있습니다. $unwind
여기에 빈 배열을 보존하지 않습니다. preserveNullAndEmptyArrays
옵션을 추가 할 수 있지만 이는 위에서 설명한 "최적화 된" 시퀀스를 무효화 하고 기본적으로 최적화에서 일반적으로 결합되는 세 단계를 모두 그대로 둡니다.
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
이것이 훨씬 동일하도록 허용하고 "많은" 이 전체 사용에서 더 강력합니다.
작업 예
다음은 모델에서 정적 메서드를 사용하는 예입니다. 정적 메서드가 구현되면 호출은 다음과 같이됩니다.
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
또는 좀 더 현대적으로 발전하는 것은 다음과 같습니다.
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
.populate()
구조에서 와 매우 유사 하지만 실제로는 서버에서 조인을 대신 수행합니다. 완전성을 위해 여기서 사용하면 반환 된 데이터를 부모 및 자식 사례에 따라 몽구스 문서 인스턴스로 다시 캐스팅합니다.
매우 사소하고 조정하기 쉽고 대부분의 일반적인 경우 그대로 사용하기 쉽습니다.
NB 여기서 async를 사용 하는 것은 동봉 된 예제를 간단히 실행하기위한 것입니다. 실제 구현에는 이러한 종속성이 없습니다.
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(
[
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
(callback) =>
async.waterfall(
[
(callback) =>
ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
callback),
(tags, callback) =>
Item.create({ "title": "Something","description": "An item",
"tags": tags },callback)
],
callback
),
(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
추가 종속성없이 Node 8.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);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
const tags = await ItemTag.create(
["movies", "funny"].map(tagName =>({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
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);
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);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
const tags = await ItemTag.insertMany(
["movies", "funny"].map(tagName => ({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
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()
}
})()