sql >> Database >  >> NoSQL >> MongoDB

Haal het meest recente subdocument op uit Array

Je zou dit op verschillende manieren kunnen aanpakken. Ze variëren natuurlijk qua aanpak en prestaties, en ik denk dat er enkele grotere overwegingen zijn die je moet maken bij je ontwerp. Het meest opvallend is hier de "behoefte" aan "revisies"-gegevens in het gebruikspatroon van uw daadwerkelijke toepassing.

Query via aggregaat

Wat betreft het belangrijkste punt om het "laatste element uit de binnenste array" te krijgen, dan zou je echt een .aggregate() bewerking om dit te doen:

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

Aangezien dit een aggregatieverklaring is waar we ook de "joins" op de "server" kunnen doen, in plaats van aanvullende verzoeken te doen (dat is wat .populate() doet hier eigenlijk ) door $lookup te gebruiken , Ik neem enige vrijheid met de daadwerkelijke collectienamen, aangezien uw schema niet in de vraag is opgenomen. Dat is oké, aangezien je niet wist dat je het op deze manier kon doen.

Natuurlijk zijn de "werkelijke" collectienamen vereist door de server, die geen concept heeft van het "toepassingskant" gedefinieerde schema. Er zijn hier dingen die u voor het gemak kunt doen, maar daarover later meer.

Houd er ook rekening mee dat, afhankelijk van waar projectId eigenlijk vandaan komt, dan in tegenstelling tot reguliere mangoestmethoden zoals .find() de $match vereist daadwerkelijk "casting" naar een ObjectId als de invoerwaarde in feite een "string" is. Mongoose kan geen "schematypen" toepassen in een aggregatiepijplijn, dus u moet dit mogelijk zelf doen, vooral als projectId kwam van een verzoekparameter:

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

Het basisgedeelte hier is waar we $map om alle "uploaded_files" te doorlopen invoeren, en dan gewoon de "laatste" extraheren uit de "geschiedenis" array met $arrayElemAt met behulp van de "laatste" index, die -1 . is .

Dat zou redelijk moeten zijn, aangezien het zeer waarschijnlijk is dat de "meest recente revisie" in feite de "laatste" array-invoer is. We zouden dit kunnen aanpassen om naar de "grootste" te zoeken door $max als voorwaarde voor $filter . Dus die pijplijnfase wordt:

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

Dat is min of meer hetzelfde, behalve dat we de vergelijking maken met de $max waarde, en retourneer slechts "één" invoer uit de array waardoor de index van de "gefilterde" array de "eerste" positie retourneert, of 0 index.

Wat betreft andere algemene technieken voor het gebruik van $lookup in plaats van .populate() , zie mijn bericht op "Opvragen na invullen in Mongoose" die iets meer vertelt over dingen die kunnen worden geoptimaliseerd met deze aanpak.

Opvragen via invullen

We kunnen natuurlijk ook (hoewel niet zo efficiënt) dezelfde soort bewerking uitvoeren met .populate() aanroepen en het manipuleren van de resulterende arrays:

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)

Waar je natuurlijk "alle" items uit "geschiedenis" teruggeeft , maar we passen gewoon een .map toe () om de .slice() op die elementen om opnieuw het laatste array-element voor elk te krijgen.

Een beetje meer overhead omdat de hele geschiedenis wordt geretourneerd en de .populate() oproepen zijn aanvullende verzoeken, maar het krijgt wel dezelfde eindresultaten.

Een ontwerppunt

Het grootste probleem dat ik hier zie, is echter dat je zelfs een "geschiedenis" -array in de inhoud hebt. Dit is niet echt een goed idee, omdat je dingen zoals hierboven moet doen om alleen het relevante item dat je wilt terug te sturen.

Dus als "ontwerppunt" zou ik dit niet doen. Maar in plaats daarvan zou ik in alle gevallen de geschiedenis van de items "scheiden". Om bij "ingesloten" documenten te blijven, zou ik de "geschiedenis" in een aparte array bewaren en alleen de "laatste" revisie met de daadwerkelijke inhoud bewaren:

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

}

U kunt dit eenvoudig handhaven door $set in te stellen het relevante item en gebruik $push over de "geschiedenis" in de ene operatie:

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

Met de array gescheiden, kunt u eenvoudig een query uitvoeren en altijd de nieuwste krijgen, en de "geschiedenis" negeren totdat u dat verzoek daadwerkelijk wilt indienen:

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

In het algemeen zou ik me echter helemaal niet druk maken om het "revisienummer". Door veel van dezelfde structuur te behouden, heb je het niet echt nodig bij het "toevoegen" aan een array, omdat de "laatste" altijd de "laatste" is. Dit geldt ook voor het wijzigen van de structuur, waarbij de "laatste" altijd het laatste item is voor het opgegeven geüploade bestand.

Proberen om zo'n "kunstmatige" index te behouden is beladen met problemen, en verpest meestal elke wijziging van "atomaire" operaties zoals getoond in de .update() voorbeeld hier, omdat u een "teller"-waarde moet weten om het laatste revisienummer te kunnen leveren, en daarom moet u dat ergens vandaan "lezen".




  1. Mongorestore via stdin naar db met andere naam

  2. MongoDB - enorme hoeveelheid MongoCleaner-threads

  3. heeft mijn meteor-app zowel MONGO_OPLOG_URL als MONGO_URL nodig?

  4. Gegevens ophalen van mongoDB en Nodejs:toArray is geen functiefout