O que está faltando aqui é que
$lookup
produz um "array" no campo de saída especificado por as
em seus argumentos. Este é o conceito geral de "relações" do MongoDB, em que uma "relação" entre documentos é representada como uma "subpropriedade" que está "dentro" do próprio documento, sendo singular ou um "array" para muitos. Como o MongoDB é "sem esquema", a presunção geral de
$lookup
é que você quer dizer "muitos" e o resultado é, portanto, "sempre" uma matriz. Então, procurando o "mesmo resultado que no SQL", você precisa $unwind
esse array após o $lookup
. Se é "um" ou "muitos" não tem importância, pois ainda é "sempre" uma matriz: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 } }
])
Aqui você precisa observar as outras coisas que estão faltando na tradução:
A sequência é "importante"
Os pipelines de agregação são mais "concisos" que o SQL. Na verdade, eles são mais bem considerados como "uma sequência de etapas" aplicado à fonte de dados para agrupar e transformar os dados. O melhor análogo a isso são as instruções de linha de comando "piped", como:
ps -ef | grep mongod | grep -v grep | awk '{ print $1 }'
Onde o "pipe"
|
pode ser considerado como um "pipeline stage" em um "pipeline" de agregação do MongoDB. Como tal, queremos
$match
para filtrar coisas da coleção "source" como nossa primeira operação. E isso geralmente é uma boa prática, pois remove quaisquer documentos que não atendam às condições exigidas de outras condições. Assim como o que está acontecendo em nosso exemplo "command line pipe", onde levamos "input" e depois "pipe" para um grep
para "remover" ou "filtrar". Os caminhos são importantes
Onde a próxima coisa que você faz aqui é "juntar-se" via
$lookup
. O resultado é um "array" dos itens do "from"
argumento de coleção correspondido pelos campos fornecidos para saída no "as"
"caminho de campo" como uma "matriz". A nomenclatura escolhida aqui é importante, pois agora o "documento" da coleção de origem considera que todos os itens da "junção" agora existem naquele caminho determinado. Para facilitar isso, uso o mesmo nome de "coleção" que o "join" para o novo "caminho".
Então, a partir do primeiro "join", a saída é
"tb2"
e que conterá todos os resultados dessa coleção. Há também uma coisa importante a ser observada com a seguinte sequência de $unwind
e então $match
, sobre como o MongoDB realmente processa a consulta. Certas sequências "realmente" importam
Como "parece" existem "três" estágios de pipeline, sendo
$lookup
então $unwind
e então $match
. Mas, de fato, o MongoDB realmente faz outra coisa, o que é demonstrado na saída de { "explain": true }
adicionado ao .aggregate()
comando: {
"$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
}
}
}
},
Então, além do primeiro ponto de aplicação de "sequência", onde você precisa colocar o
$match
declarações onde eles são necessários e fazem o "mais bem", isso realmente se torna "realmente importante" com o conceito de "junções". A coisa a notar aqui é que nossas sequências de $lookup
então $unwind
e então $match
, na verdade são processados pelo MongoDB como apenas o $lookup
estágios, com as outras operações "enroladas" em um estágio de pipeline para cada um. Esta é uma distinção importante para outras formas de "filtrar" os resultados obtidos pelo
$lookup
. Como neste caso, as condições reais de "consulta" no "join" de $match
são executados na coleção para unir "antes" que os resultados sejam retornados ao pai. Isso em combinação com
$unwind
(que é traduzido em unwinding
) como mostrado acima é como o MongoDB realmente lida com a possibilidade de que a "junção" possa resultar na produção de uma matriz de conteúdo no documento de origem que faz com que ele exceda o limite de 16 MB BSON. Isso só aconteceria nos casos em que o resultado que está sendo unido é muito grande, mas a mesma vantagem está em onde o "filtro" é realmente aplicado, estando na coleção de destino "antes" de os resultados serem retornados. É esse tipo de manipulação que melhor "se correlaciona" com o mesmo comportamento de um SQL JOIN. Portanto, também é a maneira mais eficaz de obter resultados de um
$lookup
onde há outras condições a serem aplicadas ao JOIN além de simplesmente o "local" de valores de chave "estrangeiros". Observe também que a outra mudança de comportamento é do que é essencialmente um LEFT JOIN realizado por
$lookup
onde o documento "origem" sempre seria retido, independentemente da presença de um documento correspondente na coleção "destino". Em vez disso, o $unwind
adiciona a isso "descartando" quaisquer resultados da "fonte" que não tenham nada correspondente do "destino" pelas condições adicionais em $match
.
Na verdade, eles são descartados de antemão devido aopreserveNullAndEmptyArrays: false
implícito que está incluído e descartaria qualquer coisa em que as chaves "local" e "estrangeira" nem sequer correspondessem entre as duas coleções. Isso é bom para esse tipo específico de consulta, pois a "junção" destina-se ao "igual" desses valores.
Concluir
Como observado anteriormente, o MongoDB geralmente trata "relações" de maneira muito diferente de como você usaria um "Banco de Dados Relacional" ou RDBMS. O conceito geral de "relações" é de fato "incorporar" os dados, seja como uma única propriedade ou como um array.
Você pode realmente desejar essa saída, o que também é parte do motivo pelo qual isso sem o
$unwind
sequencia aqui a saída de $lookup
é na verdade uma "matriz". No entanto, usando $unwind
neste contexto é realmente a coisa mais eficaz a fazer, além de garantir que os dados "juntados" não façam com que o limite de BSON mencionado seja excedido como resultado dessa "junção". Se você realmente quer arrays de saída, então a melhor coisa a fazer aqui seria usar o
$group
estágio de pipeline e possivelmente como vários estágios para "normalizar" e "desfazer os resultados" de $unwind
{ "$group": {
"_id": "$_id",
"tb1_field": { "$first": "$tb1_field" },
"tb1_another": { "$first": "$tb1_another" },
"tb3": { "$push": "$tb3" }
}}
Onde você, de fato, para este caso, listaria todos os campos necessários de
"tb1"
por seus nomes de propriedade usando $first
para manter apenas a "primeira" ocorrência (repetida essencialmente pelos resultados de "tb2"
e "tb3"
unwound ) e então $push
o "detalhe" de "tb3"
em um "array" para representar a relação com "tb1"
. Mas a forma geral do pipeline de agregação, conforme fornecido, é a representação exata de como os resultados seriam obtidos do SQL original, que é uma saída "desnormalizada" como resultado da "junção". Se você deseja "normalizar" os resultados novamente depois disso, é com você.