node.js mongodb mongoose mongodb-query aggregation-framework

node.js - $ buscar mĂșltiples niveles sin $ relajarse?



mongodb mongoose (1)

Tengo las siguientes colecciones

colección del lugar

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

colección de comentarios

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

colección de comentarios

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

colección de autor

{ "_id" : ObjectId("5ac8ba3582c2345af70d4658"), "firstName" : "Bruce", "lastName" : "Wayne", "email" : "[email protected]", "followers" : [ObjectId("5ac8b91482c2345af70d4650")] }

Ahora mi siguiente consulta de relleno 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 $lookup pero divide el lugar cuando estoy haciendo ''$ unwind'' a las reseñas ... Quiero reseñas en la misma matriz (como poblar) y en el mismo orden ...

Quiero 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 usando 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 habilitar la manipulación de objetos en el resultado $lookup través de $lookup .

Le pido que lea las secciones detenidamente y tenga en cuenta que todo puede no ser 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 "externo", lo que esto significa es que esencialmente puede hacer cada $lookup como "anidada" dentro de esta canalización 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 realmente bastante poderoso, como puede ver desde la perspectiva de la tubería original, realmente solo sabe sobre agregar contenido a la matriz "reviews" y luego cada expresión de tubería "anidada" posterior también solo ve sus 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 anidación, pero sí inicia ese deslizamiento de sangría en la estructura BSON, y debe saber si está haciendo coincidir con las matrices o valores singulares al atravesar la estructura.

Tenga en cuenta que aquí también podemos hacer cosas como "aplanar la propiedad del autor" como se ve en las entradas de la matriz de "comments" . Todos los resultados de destino de $lookup pueden ser una "matriz", pero dentro de una "sub-tubería" podemos cambiar la forma de esa matriz de elementos individuales en un solo valor.

Búsqueda estándar de MongoDB $

Aún manteniendo la "unión en el servidor", puede 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 $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ía pensar al principio y sigue un patrón simple de $lookup y $unwind medida que avanza 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, agregar el campo e iniciar el proceso de "retroceder" en las matrices.

Solo hay dos niveles para reconstruir el documento original de Venue , por lo que el primer nivel de detalle es por Review para reconstruir la matriz de "comments" . Todo lo que necesita es $push la ruta de "$reviews.comments" para recopilarlos, y siempre que el campo "$reviews._id" esté en el "agrupamiento _id", las únicas otras cosas que debe mantener son todos los otros campos. Puede poner todo esto en el _id también, o puede usar $first .

Una vez hecho esto, solo hay una etapa $group más para volver a Venue . Esta vez, la clave de agrupación es "$_id" por supuesto, con todas las propiedades del lugar en sí usando $first y los detalles restantes de "$review" volviendo a una matriz con $push . Por supuesto, la salida "$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 la tubería $unwind generalmente puede ser un problema de rendimiento, pero en el contexto de este uso, en realidad no debería causar tanto impacto.

Dado que los datos todavía se están "uniendo 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 realidad 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 $lookup es que, si bien puede parecer un proceso mucho más simplificado, de hecho NO ES UNA UNIÓN . Todo lo que $lookup 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 unión es en realidad el resultado de múltiples solicitudes al servidor y luego se realiza una "manipulación del lado del cliente" de los datos para incrustar los detalles en las matrices.

Por lo tanto, aparte de esa advertencia clara de que las características de rendimiento no están ni cerca de estar a la altura de una $lookup servidor, la otra advertencia es, por supuesto, que los "Documentos de mangosta" en el resultado no son realmente objetos JavaScript sujetos a manipulación adicional.

Por lo tanto, para adoptar este enfoque, debe agregar el método .lean() a la consulta antes de la ejecución, para indicar a la mangosta que devuelva "objetos JavaScript simples" en lugar de los tipos de Document que se convierten 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:

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

Una vez más, debo enfatizar 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 tiempo de procesamiento e incluso debido a la sobrecarga de la solicitud, esto se suma a los costos reales en el transporte entre los proveedores de alojamiento.

Resumen

Esos son básicamente los enfoques que puede tomar, menos "rodar los suyos" donde realmente realiza las "consultas múltiples" a la base de datos usted mismo en lugar de usar el ayudante que es $lookup .

Usando el resultado de .lean() , 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 de objetos simples de los documentos de mangosta devueltos.

Si bien los enfoques agregados se ven mucho más involucrados, hay "muchas" más ventajas de 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 al servidor, todo sin sobrecarga adicional.

Es totalmente discutible que las tuberías mismas puedan construirse simplemente en función de los 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 largo plazo, por supuesto, la $lookup es la mejor solución, pero es probable que necesite un poco más de trabajo en la codificación inicial, si, por supuesto, no solo copia de lo que se enumera aquí;)