sql >> Database teknologi >  >> NoSQL >> MongoDB

Hent det seneste underdokument fra Array

Du kan tackle dette på et par forskellige måder. De varierer selvfølgelig på tilgang og ydeevne, og jeg tror, ​​at der er nogle større overvejelser, du skal gøre dig til dit design. Mest bemærkelsesværdigt her er "behovet" for "revisioner" data i brugsmønsteret for din faktiske applikation.

Forespørgsel via aggregat

Hvad angår det vigtigste punkt med at få det "sidste element fra det indre array", så burde du virkelig bruge en .aggregate() handling for at gøre dette:

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

Da dette er en aggregeringssætning, hvor vi også kan lave "joins" på "serveren" i modsætning til at lave yderligere anmodninger (som er hvad .populate() faktisk gør her ) ved at bruge $lookup , Jeg tager en vis frihed med de faktiske samlingsnavne, da dit skema ikke er inkluderet i spørgsmålet. Det er okay, da du ikke var klar over, at du faktisk kunne gøre det på denne måde.

Selvfølgelig kræves de "faktiske" samlingsnavne af serveren, som ikke har noget begreb om det "applikationsside" definerede skema. Der er ting, du kan gøre for nemheds skyld her, men mere om det senere.

Du skal også bemærke, at afhængigt af hvor projectId faktisk kommer fra, så i modsætning til almindelige mongoose metoder såsom .find() $match vil kræve faktisk "casting" til et ObjectId hvis inputværdien i virkeligheden er en "streng". Mongoose kan ikke anvende "skematyper" i en aggregeringspipeline, så du skal muligvis gøre dette selv, især hvis projectId kom fra en anmodningsparameter:

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

Den grundlæggende del her er, hvor vi bruger $map at gentage alle "uploaded_files" indgange, og udtræk derefter blot det "seneste" fra "historien" array med $arrayElemAt ved at bruge det "sidste" indeks, som er -1 .

Det burde være rimeligt, da det er mest sandsynligt, at den "seneste revision" faktisk er den "sidste" array-indgang. Vi kunne tilpasse dette til at lede efter det "største" ved at anvende $max som en betingelse for $filter . Så det pipelinestadium bliver:

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

Hvilket er mere eller mindre det samme, bortset fra at vi sammenligner med $max værdi, og returner kun "én" indtastning fra arrayet, hvilket får indekset til at returnere fra det "filtrerede" array til den "første" position eller 0 indeks.

Med hensyn til andre generelle teknikker til brug af $lookup i stedet for .populate() , se mit indlæg om "Forespørger efter populate in Mongoose" som fortæller lidt mere om ting, der kan optimeres, når man tager denne tilgang.

Forespørgsel via udfyld

Selvfølgelig kan vi også udføre (selvom ikke så effektivt) den samme slags operation ved hjælp af .populate() kalder og manipulerer 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)

Hvor du selvfølgelig returnerer "alle" varer fra "historie" , men vi anvender blot en .map () for at påkalde .slice() på disse elementer for igen at få det sidste array-element for hver.

Lidt mere overhead, da al historikken returneres, og .populate() opkald er yderligere anmodninger, men det giver de samme slutresultater.

Et designpunkt

Det største problem, jeg ser her, er dog, at du endda har en "historie"-array i indholdet. Dette er egentlig ikke en god idé, da du skal gøre ting som ovenfor for kun at returnere den relevante vare, du ønsker.

Så som et "point of design", ville jeg ikke gøre dette. Men i stedet ville jeg "adskille" historikken fra genstandene i alle tilfælde. Ved at holde med "indlejrede" dokumenter ville jeg beholde "historikken" i et separat array og kun beholde den "seneste" revision med det faktiske indhold:

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

}

Du kan vedligeholde dette ved blot at indstille $set den relevante post og brug af $push på "historikken" i den ene operation:

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

Med arrayet adskilt, så kan du simpelthen forespørge og altid få det seneste og kassere "historikken" indtil det tidspunkt, du rent faktisk ønsker at fremsætte denne anmodning:

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

Som en generel sag ville jeg simpelthen ikke bekymre mig om "revision"-nummeret overhovedet. Hvis du beholder meget af den samme struktur, har du ikke rigtig brug for det, når du "tilføjer" til et array, da det "seneste" altid er det "sidste". Dette gælder også for at ændre strukturen, hvor det "seneste" igen altid vil være den sidste post for den givne uploadede fil.

At forsøge at opretholde et sådant "kunstigt" indeks er fyldt med problemer og ødelægger for det meste enhver ændring af "atomare" operationer som vist i .update() eksempel her, da du skal kende en "tæller"-værdi for at kunne levere det seneste revisionsnummer, og derfor skal "læse" det et sted fra.




  1. Meget mærkeligt Mongoose/MongoDB-problem, når du kører MEAN-websted på Openshift

  2. Mongo kompleks sortering?

  3. Hvordan får jeg adgang til indlejrede json-objekter i en Pandas DataFrame?

  4. Hvordan kan jeg køre MongoDB som en Windows-tjeneste?