sql >> Databáze >  >> NoSQL >> MongoDB

Získejte nejnovější dílčí dokument z Array

Můžete to řešit několika různými způsoby. Liší se samozřejmě přístupem a výkonem a myslím, že je třeba vzít v úvahu několik větších úvah, které je třeba vzít v úvahu při návrhu. Nejpozoruhodnější je zde „potřeba“ dat „revizí“ ve vzoru použití vaší skutečné aplikace.

Dotaz prostřednictvím agregátu

Pokud jde o hlavní bod získání „posledního prvku z vnitřního pole“, měli byste skutečně používat .aggregate() operace, která to provede:

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

Protože se jedná o agregační příkaz, kde můžeme také provádět „připojení“ na „serveru“, na rozdíl od vytváření dalších požadavků (což je to, co .populate() skutečně dělá zde ) pomocí $lookup , Dovoluji si trochu povolit skutečné názvy kolekcí, protože vaše schéma není v otázce zahrnuto. To je v pořádku, protože jste si neuvědomili, že byste to ve skutečnosti mohli udělat tímto způsobem.

Server samozřejmě vyžaduje „skutečné“ názvy kolekcí, které nemají žádnou koncepci definovaného schématu „na straně aplikace“. Zde jsou věci, které můžete pro pohodlí udělat, ale o tom později.

Měli byste také poznamenat, že v závislosti na tom, kde je projectId ve skutečnosti pochází, na rozdíl od běžných metod mongoose, jako je .find() $match bude skutečně vyžadovat "přetypování" na ObjectId pokud je vstupní hodnota ve skutečnosti "řetězec". Mongoose nemůže použít „typy schémat“ v agregačním kanálu, takže to možná budete muset udělat sami, zvláště pokud projectId pochází z parametru požadavku:

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

V základní části zde používáme $map iterovat všechny "uploaded_files" záznamy a poté jednoduše extrahujte "nejnovější" z "history" pole s $arrayElemAt pomocí "posledního" indexu, což je -1 .

To by mělo být rozumné, protože je nejpravděpodobnější, že „nejnovější revize“ je ve skutečnosti „poslední“ položka pole. Mohli bychom to upravit tak, abychom hledali „největší“, použitím $max jako podmínku $filter . Takže fáze potrubí se stává:

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

Což je víceméně totéž, až na to, že provedeme srovnání s $max hodnotu a vrátí pouze "jedna" záznam z pole způsobí, že index vrátí z "filtrovaného" pole "první" pozici nebo 0 index.

Pokud jde o další obecné techniky používání $lookup místo .populate() , viz můj příspěvek na "Dotazování po naplnění v Mongoose" který mluví trochu více o věcech, které lze optimalizovat při použití tohoto přístupu.

Dotaz přes vyplnění

Také samozřejmě můžeme (i když ne tak efektivně) provádět stejný druh operací pomocí .populate() volá a manipuluje s výslednými poli:

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)

Kde samozřejmě ve skutečnosti vracíte "všechny" položky z "history" , ale jednoduše použijeme .map() k vyvolání .slice() na těchto prvcích znovu získat poslední prvek pole pro každý.

Trochu více režie, protože se vrací veškerá historie a .populate() volání jsou další požadavky, ale mají stejné konečné výsledky.

Návrh

Hlavní problém, který zde vidím, je, že v obsahu máte dokonce pole „historie“. To ve skutečnosti není skvělý nápad, protože musíte udělat věci jako výše, abyste mohli vrátit pouze relevantní položku, kterou chcete.

Takže jako "bod designu" bych to nedělal. Ale místo toho bych "oddělil" historii od položek ve všech případech. Při zachování „vložených“ dokumentů bych ponechal „historii“ v samostatném poli a ponechal pouze „nejnovější“ revizi se skutečným obsahem:

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

}

Můžete to jednoduše udržovat nastavením $set relevantní záznam a pomocí $push na "historii" v jedné operaci:

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

S odděleným polem pak můžete jednoduše dotazovat a vždy získat nejnovější a zahodit "historii" až do doby, kdy skutečně chcete provést tento požadavek:

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

Jako obecný případ bych se s číslem "revize" prostě vůbec neobtěžoval. Při zachování velké části stejné struktury ji při „připojování“ k poli ve skutečnosti nepotřebujete, protože „nejnovější“ je vždy „poslední“. To platí i pro změnu struktury, kde opět "nejnovější" bude vždy poslední záznam pro daný nahraný soubor.

Snaha udržovat takový "umělý" index je plná problémů a většinou ničí jakoukoli změnu "atomových" operací, jak je ukázáno v .update() například zde, protože potřebujete znát hodnotu "počítadla", abyste mohli zadat poslední číslo revize, a proto ji musíte odněkud "číst".




  1. Rozdvojený proces:Selhání během dodávky zásuvky:Přerušená trubka

  2. Umožňuje Redis pouze reprezentaci řetězce, ale ne číselnou hodnotu

  3. Jak na to:Indexujte naskenované soubory PDF v měřítku s použitím méně než 50 řádků kódu

  4. Regulární výraz Spring data mongodb repository