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

Obtenga el subdocumento más reciente de Array

Podrías abordar esto de un par de maneras diferentes. Por supuesto, varían según el enfoque y el rendimiento, y creo que hay algunas consideraciones más importantes que debe tener en cuenta en su diseño. Lo más notable aquí es la "necesidad" de datos de "revisiones" en el patrón de uso de su aplicación real.

Consulta a través de agregado

En cuanto al punto principal de obtener el "último elemento de la matriz interna", entonces realmente debería usar un .aggregate() operación para hacer esto:

function getProject(req,projectId) {

  return new Promise((resolve,reject) => {
    Project.aggregate([
      { "$match": { "project_id": projectId } },
      { "$addFields": {
        "uploaded_files": {
          "$map": {
            "input": "$uploaded_files",
            "as": "f",
            "in": {
              "latest": {
                "$arrayElemAt": [
                  "$$f.history",
                  -1
                ]
              },
              "_id": "$$f._id",
              "display_name": "$$f.display_name"
            }
          }
        }
      }},
      { "$lookup": {
        "from": "owner_collection",
        "localField": "owner",
        "foreignField": "_id",
        "as": "owner"
      }},
      { "$unwind": "$uploaded_files" },
      { "$lookup": {
         "from": "files_collection",
         "localField": "uploaded_files.latest.file",
         "foreignField": "_id",
         "as": "uploaded_files.latest.file"
      }},
      { "$group": {
        "_id": "$_id",
        "project_id": { "$first": "$project_id" },
        "updated_at": { "$first": "$updated_at" },
        "created_at": { "$first": "$created_at" },
        "owner" : { "$first": { "$arrayElemAt": [ "$owner", 0 ] } },
        "name":  { "$first": "$name" },
        "uploaded_files": {
          "$push": {
            "latest": { "$arrayElemAt": [ "$$uploaded_files", 0 ] },
            "_id": "$$uploaded_files._id",
            "display_name": "$$uploaded_files.display_name"
          }
        }
      }}
    ])
    .then(result => {
      if (result.length === 0)
        reject(new createError.NotFound(req.path));
      resolve(result[0])
    })
    .catch(reject)
  })
}

Dado que esta es una declaración de agregación donde también podemos hacer las "uniones" en el "servidor" en lugar de hacer solicitudes adicionales (que es lo que .populate() realmente lo hace aquí) usando $lookup , me estoy tomando cierta libertad con los nombres reales de las colecciones ya que su esquema no está incluido en la pregunta. Está bien, ya que no te diste cuenta de que, de hecho, podías hacerlo de esta manera.

Por supuesto, los nombres de colección "reales" son requeridos por el servidor, que no tiene concepto del esquema definido del "lado de la aplicación". Hay cosas que puede hacer para su conveniencia aquí, pero hablaremos de eso más adelante.

También debe tener en cuenta que dependiendo de dónde projectId en realidad proviene, a diferencia de los métodos regulares de mangosta como .find() el $match requerirá realmente "transmitir" a un ObjectId si el valor de entrada es de hecho una "cadena". Mongoose no puede aplicar "tipos de esquema" en una canalización de agregación, por lo que es posible que deba hacerlo usted mismo, especialmente si projectId vino de un parámetro de solicitud:

  { "$match": { "project_id": Schema.Types.ObjectId(projectId) } },

La parte básica aquí es donde usamos $map para iterar a través de todos los "uploaded_files" entradas, y luego simplemente extraiga la "más reciente" del "history" matriz con $arrayElemAt usando el índice "último", que es -1 .

Eso debería ser razonable ya que lo más probable es que la "revisión más reciente" sea de hecho la "última" entrada de matriz. Podríamos adaptar esto para buscar el "más grande", aplicando $max como condición para $filter . Entonces esa etapa de la canalización se convierte en:

     { "$addFields": {
        "uploaded_files": {
          "$map": {
            "input": "$uploaded_files",
            "as": "f",
            "in": {
              "latest": {
                "$arrayElemAt": [
                   { "$filter": {
                     "input": "$$f.history.revision",
                     "as": "h",
                     "cond": {
                       "$eq": [
                         "$$h",
                         { "$max": "$$f.history.revision" }
                       ]
                     }
                   }},
                   0
                 ]
              },
              "_id": "$$f._id",
              "display_name": "$$f.display_name"
            }
          }
        }
      }},

Lo cual es más o menos lo mismo, excepto que hacemos la comparación con $max valor y devolver solo "uno" entrada de la matriz que hace que el índice regrese de la matriz "filtrada" a la "primera" posición, o 0 índice.

En cuanto a otras técnicas generales sobre el uso de $lookup en lugar de .populate() , vea mi entrada en "Consultando después de llenar en Mongoose" que habla un poco más sobre las cosas que se pueden optimizar al adoptar este enfoque.

Consulta a través de poblar

También, por supuesto, podemos hacer (aunque no tan eficientemente) el mismo tipo de operación usando .populate() llamadas y manipular las matrices resultantes:

Project.findOne({ "project_id": projectId })
  .populate(populateQuery)
  .lean()
  .then(project => {
    if (project === null) 
      reject(new createError.NotFound(req.path));

      project.uploaded_files = project.uploaded_files.map( f => ({
        latest: f.history.slice(-1)[0],
        _id: f._id,
        display_name: f.display_name
      }));

     resolve(project);
  })
  .catch(reject)

Donde, por supuesto, en realidad está devolviendo "todos" los elementos de "history" , pero simplemente aplicamos un .map() para invocar el .slice() en esos elementos para volver a obtener el último elemento de matriz para cada uno.

Un poco más de sobrecarga ya que se devuelve todo el historial y .populate() las llamadas son solicitudes adicionales, pero obtienen los mismos resultados finales.

Un punto de diseño

Sin embargo, el principal problema que veo aquí es que incluso tiene una matriz de "historial" dentro del contenido. Esta no es realmente una gran idea, ya que debe hacer cosas como las anteriores para devolver solo el artículo relevante que desea.

Entonces, como un "punto de diseño", no haría esto. Pero en cambio, "separaría" el historial de los elementos en todos los casos. Siguiendo con los documentos "incrustados", mantendría el "historial" en una matriz separada y solo mantendría la revisión "más reciente" con el contenido real:

{
    "_id" : ObjectId("5935a41f12f3fac949a5f925"),
    "project_id" : 13,
    "updated_at" : ISODate("2017-07-02T22:11:43.426Z"),
    "created_at" : ISODate("2017-06-05T18:34:07.150Z"),
    "owner" : ObjectId("591eea4439e1ce33b47e73c3"),
    "name" : "Demo project",
    "uploaded_files" : [ 
        {
            "latest" : { 
                {
                    "file" : ObjectId("59596f9fb6c89a031019bcae"),
                    "revision" : 1
                }
            },
            "_id" : ObjectId("59596f9fb6c89a031019bcaf"),
            "display_name" : "Example filename.txt"
        }
    ]
    "file_history": [
      { 
        "_id": ObjectId("59596f9fb6c89a031019bcaf"),
        "file": ObjectId("59596f9fb6c89a031019bcae"),
        "revision": 0
    },
    { 
        "_id": ObjectId("59596f9fb6c89a031019bcaf"),
        "file": ObjectId("59596f9fb6c89a031019bcae"),
        "revision": 1
    }

}

Puede mantener esto simplemente configurando $set la entrada relevante y usando $push en la "historia" en una sola operación:

.update(
  { "project_id": projectId, "uploaded_files._id": fileId }
  { 
    "$set": {
      "uploaded_files.$.latest": { 
        "file": revisionId,
        "revision": revisionNum
      }
    },
    "$push": {
      "file_history": {
        "_id": fileId,
        "file": revisionId,
        "revision": revisionNum
      }
    }
  }
)

Con la matriz separada, simplemente puede consultar y obtener siempre la última, y ​​descartar el "historial" hasta el momento en que realmente desee realizar esa solicitud:

Project.findOne({ "project_id": projectId })
  .select('-file_history')      // The '-' here removes the field from results
  .populate(populateQuery)

Sin embargo, como caso general, simplemente no me molestaría en absoluto con el número de "revisión". Manteniendo gran parte de la misma estructura, realmente no la necesita cuando "agrega" a una matriz, ya que el "último" es siempre el "último". Esto también se aplica al cambiar la estructura, donde de nuevo la "más reciente" siempre será la última entrada para el archivo cargado dado.

Tratar de mantener un índice tan "artificial" está lleno de problemas y, en su mayoría, arruina cualquier cambio de las operaciones "atómicas", como se muestra en .update() ejemplo aquí, ya que necesita conocer un valor de "contador" para proporcionar el último número de revisión y, por lo tanto, necesita "leer" eso desde algún lugar.