Това, което ви липсва тук, е този $lookup
произвежда "масив" в изходното поле, определено от as
в неговите аргументи. Това е общата концепция на MongoDB „връзки“, тъй като „връзка“ между документи се представя като „подсвойство“, което е „в рамките“ на самия документ, като е или единично, или „масив“ за много.
Тъй като MongoDB е "безсхемен", общата презумпция за $lookup
е, че имате предвид "много" и следователно резултатът е "винаги" масив. Така че търсейки "същия резултат като в SQL", трябва да $unwind
този масив след $lookup
. Дали е "един" или "много" няма значение, тъй като все още е "винаги" масив:
db.getCollection.('tb1').aggregate([
// Filter conditions from the source collection
{ "$match": { "status": { "$ne": "closed" } }},
// Do the first join
{ "$lookup": {
"from": "tb2",
"localField": "id",
"foreignField": "profileId",
"as": "tb2"
}},
// $unwind the array to denormalize
{ "$unwind": "$tb2" },
// Then match on the condtion for tb2
{ "$match": { "tb2.profile_type": "agent" } },
// join the second additional collection
{ "$lookup": {
"from": "tb3",
"localField": "tb2.id",
"foreignField": "id",
"as": "tb3"
}},
// $unwind again to de-normalize
{ "$unwind": "$tb3" },
// Now filter the condition on tb3
{ "$match": { "tb3.status": 0 } },
// Project only wanted fields. In this case, exclude "tb2"
{ "$project": { "tb2": 0 } }
])
Тук трябва да отбележите другите неща, които липсват в превода:
Последователността е „важна“
Конвейерите за агрегиране са по-„кратко изразителни“ от SQL. Всъщност те се считат най-добре като „последователност от стъпки“ прилага се към източника на данни, за да се съпоставят и трансформират данните. Най-добрият аналог на това са "тръбните" инструкции от командния ред, като:
ps -ef | grep mongod | grep -v grep | awk '{ print $1 }'
Където е "тръбата" |
може да се разглежда като "тръбопроводен етап" в агрегация на MongoDB "тръбопровод".
Като такива искаме да $match
за да филтрираме нещата от колекцията "източник" като нашата първа операция. И това като цяло е добра практика, тъй като премахва всички документи, които не отговарят на изискваните условия, от допълнителни условия. Точно като това, което се случва в нашия пример за "команден ред", където приемаме "вход", след което "pipe" към grep
за „премахване“ или „филтриране“.
Пътищата имат значение
Следващото нещо, което правите тук, е да се присъедините чрез $lookup
. Резултатът е "масив" от елементите от "from"
аргумент за колекция, съвпадащ с предоставените полета за извеждане в "as"
"път на полето" като "масив".
Избраното тук име е важно, тъй като сега "документът" от изходната колекция счита, че всички елементи от "присъединяване" сега съществуват на този даден път. За да направя това лесно, използвам същото име на "колекция" като "join" за новия "път".
Така, като се започне от първото "присъединяване" изходът е до "tb2"
и това ще съдържа всички резултати от тази колекция. Също така трябва да се отбележи важно нещо със следната последователност от $unwind
и след това $match
, относно това как MongoDB всъщност обработва заявката.
Определени последователности "наистина" имат значение
Тъй като "изглежда" има "три" етапа на конвейера, като $lookup
след това $unwind
и след това $match
. Но на практика MongoDB наистина прави нещо друго, което се демонстрира в изхода на { "explain": true }
добавен към .aggregate()
команда:
{
"$lookup" : {
"from" : "tb2",
"as" : "tb2",
"localField" : "id",
"foreignField" : "profileId",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"profile_type" : {
"$eq" : "agent"
}
}
}
},
{
"$lookup" : {
"from" : "tb3",
"as" : "tb3",
"localField" : "tb2.id",
"foreignField" : "id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"status" : {
"$eq" : 0.0
}
}
}
},
Така че освен първата точка от "последователността", която се прилага, където трябва да поставите $match
изявления там, където са необходими и правят „най-доброто“, това всъщност става „наистина важно“ с концепцията за „съединяване“. Тук трябва да се отбележи, че нашите последователности от $lookup
след това $unwind
и след това $match
, всъщност се обработват от MongoDB само като $lookup
етапи, като другите операции са "навити" в един етап на конвейера за всеки.
Това е важно разграничение спрямо другите начини за "филтриране" на резултатите, получени чрез $lookup
. Тъй като в този случай действителните условия на "заявка" за "join" от $match
се изпълняват върху колекцията, за да се присъединят "преди" резултатите да бъдат върнати на родителя.
Това в комбинация с $unwind
(което се превежда на unwinding
), както е показано по-горе, е как MongoDB всъщност се справя с възможността „присъединяването“ да доведе до създаване на масив от съдържание в изходния документ, което го кара да надвиши ограничението от 16MB BSON. Това би се случило само в случаите, когато резултатът, към който се присъединява, е много голям, но същото предимство е в това, когато „филтърът“ действително се прилага, като е в целевата колекция „преди“ резултатите да бъдат върнати.
Това е този вид обработка, която най-добре "корелира" със същото поведение като SQL JOIN. Следователно това е и най-ефективният начин за получаване на резултати от $lookup
където има други условия, които да се прилагат към JOIN, освен просто "локалните" на "чужди" ключови стойности.
Също така имайте предвид, че другата промяна в поведението е от това, което по същество е LEFT JOIN, извършено от $lookup
където "изходният" документ винаги ще се запазва независимо от наличието на съвпадащ документ в "целевата" колекция. Вместо това $unwind
добавя към това чрез "отхвърляне" на всички резултати от "източника", които не са имали нищо съвпадащо с "целта" от допълнителните условия в $match
.
Всъщност те дори се изхвърлят предварително поради подразбиращия се
preserveNullAndEmptyArrays: false
което е включено и би отхвърлило всичко, където "местните" и "чуждите" ключове дори не съвпадат между двете колекции. Това е добро нещо за този конкретен тип заявка, тъй като „join“ е предназначено за „равно“ на тези стойности.
Заключение
Както беше отбелязано по-горе, MongoDB обикновено третира "връзките" много по-различно от това как бихте използвали "Релационна база данни" или RDBMS. Общата концепция за „отношения“ всъщност е „вграждане“ на данните или като единично свойство, или като масив.
Всъщност може да желаете такъв изход, което също е част от причината, поради която това без $unwind
подредете тук изхода на $lookup
всъщност е "масив". Въпреки това с помощта на $unwind
в този контекст всъщност е най-ефективното нещо, което трябва да се направи, както и даването на гаранция, че „присъединените“ данни всъщност не причиняват превишаване на гореспоменатото ограничение на BSON в резултат на това „присъединяване“.
Ако всъщност искате масиви от изходни данни, тогава най-доброто нещо, което можете да направите тук, би било да използвате $group
етап на конвейера и вероятно като няколко етапа, за да се "нормализират" и "отменят резултатите" на $unwind
{ "$group": {
"_id": "$_id",
"tb1_field": { "$first": "$tb1_field" },
"tb1_another": { "$first": "$tb1_another" },
"tb3": { "$push": "$tb3" }
}}
Където всъщност бихте посочили за този случай всички полета, които сте изисквали от "tb1"
чрез имената на свойствата им с помощта на $first
да запази само "първото" появяване (по същество се повтаря от резултатите от "tb2"
и "tb3"
unwound ) и след това $push
"подробностите" от "tb3"
в "масив", за да представи връзката към "tb1"
.
Но общата форма на конвейера за агрегиране, както е дадена, е точното представяне на това как ще бъдат получени резултатите от оригиналния SQL, който е "денормализиран" изход в резултат на "съединяването". Дали искате да "нормализирате" резултатите отново след това зависи от вас.