MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Obtenir le sous-document le plus récent de Array

Vous pouvez résoudre ce problème de différentes manières. Ils varient bien sûr en fonction de l'approche et des performances, et je pense qu'il y a des considérations plus importantes que vous devez prendre en compte dans votre conception. Plus particulièrement, voici le "besoin" de données de "révisions" dans le modèle d'utilisation de votre application actuelle.

Requête via agrégat

En ce qui concerne le point le plus important d'obtenir le "dernier élément du tableau interne", alors vous devriez vraiment utiliser un .aggregate() opération pour ce faire :

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

Puisqu'il s'agit d'une instruction d'agrégation où nous pouvons également faire les "jointures" sur le "serveur" au lieu de faire des requêtes supplémentaires (ce qui est ce que .populate() fait réellement ici) en utilisant $lookup , je prends une certaine liberté avec les noms de collection réels puisque votre schéma n'est pas inclus dans la question. Ce n'est pas grave, puisque vous ne saviez pas que vous pouviez en fait le faire de cette façon.

Bien sûr, les noms de collection "réels" sont requis par le serveur, qui n'a aucun concept du schéma défini "côté application". Il y a des choses que vous pouvez faire pour plus de commodité ici, mais nous en reparlerons plus tard.

Vous devez également noter que selon l'endroit où projectId vient réellement de, alors contrairement aux méthodes de mangouste régulières telles que .find() le $match nécessitera en fait un "casting" vers un ObjectId si la valeur d'entrée est en fait une "chaîne". Mongoose ne peut pas appliquer de "types de schéma" dans un pipeline d'agrégation, vous devrez donc peut-être le faire vous-même, surtout si projectId provient d'un paramètre de requête :

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

La partie de base ici est celle où nous utilisons $map pour parcourir tous les "uploaded_files" entrées, puis extrayez simplement le "dernier" de l'"history" tableau avec $arrayElemAt en utilisant le "dernier" index, qui est -1 .

Cela devrait être raisonnable car il est fort probable que la "révision la plus récente" soit en fait la "dernière" entrée du tableau. Nous pourrions adapter cela pour rechercher le "plus grand", en appliquant $max comme condition pour $filter . Ainsi, cette étape du pipeline devient :

     { "$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"
            }
          }
        }
      }},

Ce qui est plus ou moins la même chose, sauf que nous faisons la comparaison avec le $max value, et renvoie uniquement "one" entrée du tableau faisant de l'index à renvoyer du tableau "filtré" la "première" position, ou 0 indice.

Comme pour les autres techniques générales sur l'utilisation de $lookup à la place de .populate() , voir mon entrée sur "Querying after populate in Mongoose" qui parle un peu plus des choses qui peuvent être optimisées en adoptant cette approche.

Requête via remplissage

Bien sûr, nous pouvons également faire (même si ce n'est pas aussi efficace) le même type d'opération en utilisant .populate() appels et manipulation des tableaux résultants :

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)

Où bien sûr vous renvoyez réellement "tous" les éléments de "history" , mais nous appliquons simplement un .map() pour invoquer le .slice() sur ces éléments pour obtenir à nouveau le dernier élément du tableau pour chacun.

Un peu plus de surcharge puisque tout l'historique est renvoyé, et le .populate() les appels sont des demandes supplémentaires, mais ils obtiennent les mêmes résultats finaux.

Un point de conception

Le principal problème que je vois ici est que vous avez même un tableau "historique" dans le contenu. Ce n'est pas vraiment une bonne idée puisque vous devez faire les choses comme ci-dessus afin de ne retourner que l'article pertinent que vous voulez.

Donc, en tant que "point de conception", je ne ferais pas cela. Mais à la place, je "séparerais" l'historique des éléments dans tous les cas. En gardant les documents "intégrés", je conserverais "l'historique" dans un tableau séparé, et ne conserverais que la "dernière" révision avec le contenu réel :

{
    "_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
    }

}

Vous pouvez maintenir cela simplement en définissant $set l'entrée appropriée et en utilisant $push sur "l'historique" en une seule opération :

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

Avec le tableau séparé, vous pouvez simplement interroger et toujours obtenir le dernier, et supprimer "l'historique" jusqu'à ce que vous souhaitiez réellement faire cette requête :

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

En règle générale, je ne me préoccuperais tout simplement pas du tout du numéro de "révision". En gardant une grande partie de la même structure, vous n'en avez pas vraiment besoin lors de "l'ajout" à un tableau puisque le "dernier" est toujours le "dernier". Cela est également vrai pour la modification de la structure, où encore une fois, le "dernier" sera toujours la dernière entrée pour le fichier téléchargé donné.

Essayer de maintenir un tel index "artificiel" est semé d'embûches et ruine la plupart du temps tout changement d'opérations "atomiques" comme indiqué dans le .update() exemple ici, puisque vous avez besoin de connaître une valeur "compteur" afin de fournir le dernier numéro de révision, et donc de "lire" cela quelque part.