sql >> Base de Datos >  >> NoSQL >> MongoDB

$buscar varios niveles sin $descansar?

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

Le pido que lea las secciones detenidamente y tenga en cuenta que es posible 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 $lookup el operador obtiene la capacidad adicional de incluir un pipeline expresión en lugar de simplemente unir un valor clave "local" a "extranjero", lo que esto significa es que esencialmente puede hacer cada $lookup como "anidado" dentro de estas expresiones de canalización

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 puede ver desde la perspectiva de la canalización original, en realidad solo sabe agregar contenido a las "reviews" matriz y luego cada expresión de canalización "anidada" subsiguiente también solo ve sus elementos "internos" de la unión.

Es poderoso 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 comienza a arrastrarse la sangría en la estructura BSON, y debe saber si está haciendo coincidir 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 dentro de los "comments" entradas de matriz. Todo $lookup la salida de destino puede ser una "matriz", pero dentro de una "subcanalización" podemos remodelar esa matriz de un solo elemento en un solo valor.

Búsqueda $ MongoDB estándar

Aún manteniendo "unirse 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 de $group etapas para reconstruir arreglos:

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 a medida que avanza en cada matriz.

El "author" El detalle, por supuesto, es singular, por lo que una vez que se "desenrolla", simplemente desea dejarlo así, agregar el campo y comenzar el proceso de "revertir" en las matrices.

Solo hay dos niveles para reconstruir de nuevo al Venue original documento, por lo que el primer nivel de detalle es por Review para reconstruir los "comments" formación. Todo lo que necesitas es $push la ruta de "$reviews.comments" para recopilarlos, y siempre que "$reviews._id" El campo está en "grouping _id", las únicas otras cosas que necesita conservar son todos los demás campos. Puede poner todo esto en el _id también, o puede usar $first .

Con eso hecho, solo hay un $group más escenario para volver a Venue sí mismo. Esta vez la clave de agrupación es "$_id" por supuesto, con todas las propiedades del lugar usando $first y el resto "$review" detalles que regresan a una matriz con $push . Por supuesto, los "$comments" salida del $group anterior se convierte en "review.comments" camino.

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

Dado que los datos todavía se "unen en el servidor", hay todavía 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 propio servidor, en realidad manipulas el resultado. En la mayoría casos, estaría a favor de este enfoque ya que cualquier "agregado" 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 UN ÚNICO de cualquier manera. Todo populate() en realidad es "hide" 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.

Así que la "apariencia" de una unió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.

Aparte de esa advertencia clara que las características de rendimiento no están ni cerca de estar a la par con un servidor $lookup , la otra advertencia es, por supuesto, que los "Documentos de mangosta" en el resultado no son en realidad objetos JavaScript simples sujetos a una mayor manipulación.

Entonces, para adoptar este enfoque, debe agregar .lean() método a la consulta antes de la ejecución, para indicarle a mongoose que devuelva "objetos de JavaScript sin formato" en lugar de Document tipos que se emiten con métodos de esquema adjuntos al modelo. Teniendo en cuenta, 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 propios 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 venue es un objeto simple, podemos simplemente procesarlo y ajustarlo 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
        }
      })
    )
  })
);

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

Nuevamente, 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 proveedores de alojamiento.

Resumen

Esos son básicamente los enfoques que puede tomar, aparte de "hacer los suyos propios", en los que realmente realiza las "consultas múltiples" a la base de datos usted mismo en lugar de usar el ayudante que .populate() es.

Usando la salida de llenado, 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 sin formato de los documentos mangosta devueltos.

Si bien los enfoques agregados parecen mucho más complicados, hay "muchos" 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 filtrar más y, por supuesto, se obtiene una "respuesta única" a una "solicitud única" hecho al servidor, todo sin sobrecarga adicional.

Es totalmente discutible que las propias canalizaciones podrían simplemente construirse en función de los atributos ya almacenados en el esquema. Así que escribir su propio método para realizar esta "construcción" basada en el esquema adjunto no debería ser demasiado difícil.

A largo plazo, por supuesto, $lookup es la mejor solución, pero probablemente necesitará trabajar un poco más en la codificación inicial, si por supuesto no simplemente copia de lo que se enumera aquí;)