MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

$procure vários níveis sem $unwind?


Existem algumas abordagens, é claro, dependendo da versão disponível do MongoDB. Estes variam de usos diferentes de $lookup até habilitar a manipulação de objetos no .populate() resultado via .lean() .

Peço que você leia as seções com atenção e esteja ciente de que nem tudo pode ser o que parece ao considerar sua solução de implementação.

MongoDB 3.6, pesquisa $ "aninhada"


Com o MongoDB 3.6, o $lookup operador obtém a capacidade adicional de incluir um pipeline expressão em vez de simplesmente juntar um valor de chave "local" a "estrangeiro", o que isso significa é que você pode essencialmente fazer cada $lookup como "aninhado" dentro dessas expressões de pipeline
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"
  }},
 ])

Isso pode ser realmente muito poderoso, como você vê da perspectiva do pipeline original, ele realmente só sabe adicionar conteúdo às "reviews" array e, em seguida, cada expressão de pipeline "aninhada" subsequente também só vê seus elementos "internos" da junção.

É poderoso e, em alguns aspectos, pode ser um pouco mais claro, pois todos os caminhos de campo são relativos ao nível de aninhamento, mas ele inicia esse deslocamento de recuo na estrutura BSON e você precisa estar ciente se está correspondendo a matrizes ou valores singulares na travessia da estrutura.

Observe que também podemos fazer coisas aqui como "achatar a propriedade do autor" como visto nos "comments" entradas de matriz. Todos os $lookup a saída de destino pode ser um "array", mas dentro de um "sub-pipeline" podemos remodelar esse array de elemento único em apenas um único valor.

MongoDB $lookup padrão


Ainda mantendo o "join no servidor", você pode fazer isso com $lookup , mas leva apenas processamento intermediário. Esta é a abordagem de longa data com a desconstrução de um array com $unwind e o uso de $group etapas para reconstruir matrizes:
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"
      }
    }
  }}
])

Isso realmente não é tão assustador quanto você pode pensar a princípio e segue um padrão simples de $lookup e $unwind conforme você progride em cada array.

O "author" O detalhe é claro é singular, então uma vez que é "desenrolado" você simplesmente quer deixá-lo assim, faça a adição do campo e inicie o processo de "retorno" para os arrays.

Existem apenas dois níveis para reconstruir de volta ao Venue original documento, então o primeiro nível de detalhe é por Review para reconstruir os "comments" variedade. Tudo que você precisa é $push o caminho de "$reviews.comments" para coletá-los e contanto que o "$reviews._id" campo está no "grouping _id" as únicas outras coisas que você precisa manter são todos os outros campos. Você pode colocar tudo isso no _id também, ou você pode usar $first .

Feito isso, há apenas mais um $group palco para voltar ao Venue em si. Desta vez, a chave de agrupamento é "$_id" claro, com todas as propriedades do próprio local usando $first e o restante "$review" detalhes voltando para um array com $push . Claro que os "$comments" saída do $group anterior torna-se o "review.comments" caminho.

Trabalhar em um único documento e suas relações, isso não é tão ruim. O $unwind operador de pipeline pode geralmente ser um problema de desempenho, mas no contexto desse uso não deve causar tanto impacto.

Como os dados ainda estão sendo "ingressados ​​no servidor", ainda muito menos tráfego do que a outra alternativa restante.

Manipulação de JavaScript


Claro que o outro caso aqui é que, em vez de alterar os dados no próprio servidor, você manipula o resultado. Na maioria Em alguns casos, eu seria a favor dessa abordagem, já que quaisquer "adições" aos dados provavelmente são mais bem tratadas no cliente.

O problema, claro, de usar populate() é que, embora possa 'parecer' um processo muito mais simplificado, na verdade é NÃO UM JOIN de qualquer maneira. Todos os populate() realmente é "esconder" o processo subjacente de envio de vários consultas ao banco de dados e, em seguida, aguardando os resultados por meio de manipulação assíncrona.

Portanto, a "aparência" de uma junção é, na verdade, o resultado de várias solicitações ao servidor e, em seguida, "manipulação do lado do cliente" dos dados para incorporar os detalhes em matrizes.

Além desse aviso claro que as características de desempenho estão longe de estar no mesmo nível de um servidor $lookup , a outra ressalva é que os "documentos mangusto" no resultado não são objetos JavaScript simples sujeitos a manipulação adicional.

Portanto, para adotar essa abordagem, você precisa adicionar o .lean() método para a consulta antes da execução, para instruir o mangusto a retornar "objetos JavaScript simples" em vez de Document tipos que são convertidos com métodos de esquema anexados ao modelo. Observando, é claro, que os dados resultantes não têm mais acesso a nenhum "método de instância" que de outra forma estaria associado aos próprios modelos relacionados:
let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();

Agora venue é um objeto simples, podemos simplesmente processar e ajustar conforme necessário:
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
        }
      })
    )
  })
);

Portanto, é apenas uma questão de percorrer cada uma das matrizes internas até o nível em que você pode ver os followers array dentro do author detalhes. A comparação pode ser feita com o ObjectId valores armazenados nesse array depois de primeiro usar .map() para retornar os valores "string" para comparação com o req.user.id que também é uma string (se não for, adicione também .toString() sobre isso ), já que é mais fácil em geral comparar esses valores dessa maneira via código JavaScript.

Novamente, porém, preciso enfatizar que "parece simples", mas na verdade é o tipo de coisa que você realmente deseja evitar para o desempenho do sistema, pois essas consultas adicionais e a transferência entre o servidor e o cliente custam muito tempo de processamento e mesmo devido à sobrecarga de solicitação, isso aumenta os custos reais de transporte entre provedores de hospedagem.

Resumo


Essas são basicamente as abordagens que você pode adotar, com exceção de "fazer o seu próprio", onde você realmente executa as "várias consultas" para o banco de dados em vez de usar o auxiliar que .populate() é.

Usando a saída de preenchimento, você pode simplesmente manipular os dados no resultado como qualquer outra estrutura de dados, contanto que você aplique .lean() para a consulta para converter ou extrair os dados do objeto simples dos documentos do mangusto retornados.

Embora as abordagens agregadas pareçam muito mais complexas, existem "muito" mais vantagens em fazer este trabalho no servidor. Conjuntos de resultados maiores podem ser classificados, cálculos podem ser feitos para filtragem adicional e, claro, você obtém uma "resposta única" a uma "solicitação única" feito para o servidor, tudo sem sobrecarga adicional.

É totalmente discutível que os próprios pipelines poderiam simplesmente ser construídos com base em atributos já armazenados no esquema. Portanto, escrever seu próprio método para realizar essa "construção" com base no esquema anexado não deve ser muito difícil.

A longo prazo, é claro $lookup é a melhor solução, mas você provavelmente precisará colocar um pouco mais de trabalho na codificação inicial, se é claro que você não simplesmente copie do que está listado aqui;)