$ buscar varios niveles sin $ desenrollar?

Tengo las siguientes colecciones

colecciones de lugar

{ "_id" : ObjectId("5acdb8f65ea63a27c1facf86"), "name" : "ASA College - Manhattan Campus", "addedBy" : ObjectId("5ac8ba3582c2345af70d4658"), "reviews" : [ ObjectId("5acdb8f65ea63a27c1facf8b"), ObjectId("5ad8288ccdd9241781dce698") ] } 

revisa colecciones

 { "_id" : ObjectId("5acdb8f65ea63a27c1facf8b"), "createdAt" : ISODate("2018-04-07T12:31:49.503Z"), "venue" : ObjectId("5acdb8f65ea63a27c1facf86"), "author" : ObjectId("5ac8ba3582c2345af70d4658"), "content" : "nice place", "comments" : [ ObjectId("5ad87113882d445c5cbc92c8") ], } 

colecciones de comentarios

 { "_id" : ObjectId("5ad87113882d445c5cbc92c8"), "author" : ObjectId("5ac8ba3582c2345af70d4658"), "comment" : "dcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsf", "review" : ObjectId("5acdb8f65ea63a27c1facf8b"), "__v" : 0 } 

colecciones de autor

 { "_id" : ObjectId("5ac8ba3582c2345af70d4658"), "firstName" : "Bruce", "lastName" : "Wayne", "email" : "bruce@linkites.com", "followers" : [ObjectId("5ac8b91482c2345af70d4650")] } 

Ahora mi consulta de llenado siguiente funciona bien

  const venues = await Venue.findOne({ _id: id.id }) .populate({ path: 'reviews', options: { sort: { createdAt: -1 } }, populate: [ { path: 'author' }, { path: 'comments', populate: [{ path: 'author' }] } ] }) 

Pero quiero lograrlo con la consulta de $lookup pero divide el lugar cuando estoy haciendo ‘$ unwind’ para las revisiones … Quiero reseñas en la misma matriz (como poblar) y en el mismo orden …

Deseo lograr la siguiente consulta con $lookup porque el autor tiene un campo de seguidores, así que necesito enviar el campo ” isFollow haciendo $project que no se puede hacer utilizando ” populate

 $project: { isFollow: { $in: [mongoose.Types.ObjectId(req.user.id), '$followers'] } } 

Hay un par de enfoques, por supuesto, dependiendo de su versión MongoDB disponible. Estos varían desde diferentes usos de $lookup hasta la manipulación de objetos en el resultado de .populate() través de .lean() .

Le pido que lea las secciones detenidamente y tenga en cuenta que puede que no todo sea lo que parece al considerar su solución de implementación.

MongoDB 3.6, búsqueda $ “anidada”

Con MongoDB 3.6 el operador de $lookup obtiene la capacidad adicional de incluir una expresión de pipeline en lugar de simplemente unir un valor de clave “local” a “extranjero”, lo que esto significa es que básicamente puede hacer cada $lookup como “anidada” dentro de estas canalizaciones expresiones

 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" }}, ]) 

Esto puede ser bastante poderoso, como se ve desde la perspectiva de la canalización original, solo sabe sobre cómo agregar contenido a la matriz de "reviews" y luego cada expresión de canalizaciones “anidadas” subsiguiente solo ve que son elementos “internos” de la unión

Es potente y, en algunos aspectos, puede ser un poco más claro ya que todas las rutas de campo son relativas al nivel de anidamiento, pero inicia ese desplazamiento de indentación en la estructura BSON, y es necesario saber si se está haciendo coincidir con las matrices. o valores singulares al atravesar la estructura.

Tenga en cuenta que también podemos hacer cosas aquí como “aplanar la propiedad del autor” como se ve en las entradas de la matriz "comments" . Todos los resultados $lookup pueden ser una “matriz”, pero dentro de una “sub-línea” podemos modificar esa matriz de un solo elemento en un solo valor.

Búsqueda estándar de $ MongoDB

Aún manteniendo el “join en el servidor”, puedes hacerlo con $lookup , pero solo requiere un procesamiento intermedio. Este es el enfoque de larga data con la deconstrucción de una matriz con $unwind y el uso $group etapas de $group para reconstruir matrices:

 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" } } }} ]) 

Esto realmente no es tan desalentador como podrías pensar al principio y sigue un patrón simple de $lookup y $unwind medida que avanzas en cada matriz.

El detalle del "author" por supuesto, es singular, por lo que una vez que se “desenrolla” simplemente desea dejarlo de esa manera, hacer la adición de campo y comenzar el proceso de “retroceso” en las matrices.

Solo hay dos niveles para reconstruir de nuevo al documento original de Venue , por lo que el primer nivel de detalle es Review para reconstruir el conjunto de "comments" . Todo lo que necesita hacer es $push la ruta de "$reviews.comments" para recostackrlos, y siempre que el campo "$reviews._id" esté en la “agrupación _id”, las únicas otras cosas que debe mantener son todos los otros campos Puede poner todos estos en el _id también, o puede usar $first .

Una vez hecho esto, solo hay una etapa más de $group para regresar a Venue . Esta vez, la clave de agrupación es "$_id" por supuesto, con todas las propiedades del lugar mismo usando $first y los detalles "$review" restantes regresando a una matriz con $push . Por supuesto, el resultado "$comments" del $group anterior se convierte en la "review.comments" .

Trabajando en un solo documento y sus relaciones, esto no es realmente tan malo. El operador de canalización $unwind generalmente puede ser un problema de rendimiento, pero en el contexto de este uso no debería causar un gran impacto.

Dado que los datos aún se “unen en el servidor”, todavía hay mucho menos tráfico que la otra alternativa restante.

Manipulación de JavaScript

Por supuesto, el otro caso aquí es que en lugar de cambiar los datos en el servidor en sí, realmente manipulas el resultado. En la mayoría de los casos, estaría a favor de este enfoque ya que cualquier “adición” a los datos probablemente se maneje mejor en el cliente.

El problema, por supuesto, con el uso de populate() es que, si bien puede parecer un proceso mucho más simplificado, de hecho no es una unión de ninguna manera. Todo lo que populate() realmente hace es “ocultar” el proceso subyacente de enviar múltiples consultas a la base de datos, y luego esperar los resultados a través del manejo asíncrono.

Por lo tanto, la “apariencia” de una combinación es en realidad el resultado de múltiples solicitudes al servidor y luego hacer “manipulación del lado del cliente” de los datos para incrustar los detalles dentro de las matrices.

Por lo tanto, aparte de esa clara advertencia de que las características de rendimiento no están a la par de una $lookup servidor, la otra advertencia es que los “Documentos de mongoose” en el resultado no son en realidad objetos de JavaScript sujetos a manipulación adicional.

Entonces, para tomar este enfoque, debe agregar el método .lean() a la consulta antes de la ejecución, para instruir a mongoose a que devuelva “objetos JavaScript simples” en lugar de tipos de Document que se emiten con métodos de esquema adjuntos al modelo. . Observando, por supuesto, que los datos resultantes ya no tienen acceso a ningún “método de instancia” que de otro modo estaría asociado con los modelos relacionados en sí mismos:

 let venue = await Venue.findOne({ _id: id.id }) .populate({ path: 'reviews', options: { sort: { createdAt: -1 } }, populate: [ { path: 'comments', populate: [{ path: 'author' }] } ] }) .lean(); 

Ahora el venue es un objeto simple, simplemente podemos procesar y ajustar según sea necesario:

 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 } }) ) }) ); 

Por lo tanto, en realidad se trata solo de recorrer cada una de las matrices internas hasta el nivel donde se puede ver la matriz de followers dentro de los detalles del author . La comparación se puede hacer con los valores de ObjectId almacenados en esa matriz después de usar .map() para devolver los valores de “cadena” para comparar con el req.user.id que también es una cadena (si no lo es, entonces también agregue .toString() en eso), ya que es más fácil en general comparar estos valores de esta manera a través de código JavaScript.

De nuevo, debo recalcar que “parece simple”, pero de hecho es el tipo de cosas que realmente desea evitar para el rendimiento del sistema, ya que esas consultas adicionales y la transferencia entre el servidor y el cliente cuestan mucho en el tiempo de procesamiento. e incluso debido a la sobrecarga de solicitudes, esto se sum a los costos reales de transporte entre proveedores de alojamiento.


Resumen

Esos son básicamente sus enfoques que puede tomar, a excepción de “rodar el suyo” donde realmente realiza las “múltiples consultas” a la base de datos usted mismo en lugar de usar el asistente que es .populate() .

Utilizando la salida populate, puede simplemente manipular los datos en el resultado como cualquier otra estructura de datos, siempre que aplique .lean() a la consulta para convertir o extraer los datos del objeto simple de los documentos de mongoose devueltos.

Si bien los enfoques agregados parecen mucho más complicados, existen “muchas” más ventajas al hacer este trabajo en el servidor. Se pueden ordenar conjuntos de resultados más grandes, se pueden hacer cálculos para un mayor filtrado y, por supuesto, se obtiene una “respuesta única” a una “solicitud única” realizada en el servidor, todo sin gastos adicionales.

Es totalmente discutible que los gasoductos mismos puedan simplemente construirse sobre la base de atributos ya almacenados en el esquema. Por lo tanto, escribir su propio método para realizar esta “construcción” basado en el esquema adjunto no debería ser demasiado difícil.

A más largo plazo, $lookup es la mejor solución, pero es probable que necesite poner un poco más de trabajo en la encoding inicial, si, por supuesto, no solo copia de lo que se indica aquí;)