MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Вземете най-новия поддокумент от Array

Можете да се справите с това по няколко различни начина. Разбира се, те варират в зависимост от подхода и производителността и мисля, че има някои по-големи съображения, които трябва да вземете във вашия дизайн. Най-вече тук е „нуждата“ от данни за „ревизии“ в модела на използване на вашето действително приложение.

Заявка чрез агрегат

Що се отнася до най-важната точка за получаване на „последния елемент от вътрешния масив“, тогава наистина трябва да използвате .aggregate() операция за извършване на това:

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

Тъй като това е оператор за агрегиране, където можем също така да правим „съединения“ на „сървъра“, вместо да правим допълнителни заявки (което е това, което .populate() всъщност прави тук ), като използвате $lookup , приемам известна свобода с действителните имена на колекции, тъй като вашата схема не е включена във въпроса. Всичко е наред, тъй като не сте осъзнавали, че всъщност можете да го направите по този начин.

Разбира се, "действителните" имена на колекции се изискват от сървъра, който няма концепция за дефинираната схема от "страна на приложението". Има неща, които можете да направите за удобство тук, но повече за това по-късно.

Трябва също да имате предвид, че в зависимост от това къде projectId всъщност идва от, тогава за разлика от обикновените мангустови методи като .find() $match ще изисква действително "кастинг" към ObjectId ако входната стойност всъщност е "низ". Mongoose не може да прилага „типове схеми“ в тръбопровод за агрегиране, така че може да се наложи да направите това сами, особено ако projectId идва от параметър на заявка:

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

Основната част тук е мястото, където използваме $map за итерация през всички "uploaded_files" записи и след това просто извлечете „най-новите“ от „history“ масив с $arrayElemAt използвайки "последния" индекс, който е -1 .

Това би трябвало да е разумно, тъй като е най-вероятно "най-скорошната ревизия" всъщност да е "последният" запис в масива. Можем да адаптираме това, за да търсим „най-големия“, като приложим $max като условие за $filter . Така този етап на тръбопровод става:

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

Което е повече или по-малко същото, освен че правим сравнението с $max стойност и връща само "one" запис от масива, който прави индекса да се върне от "филтрирания" масив на "първата" позиция или 0 индекс.

Що се отнася до други общи техники за използване на $lookup на мястото на .populate() , вижте моя запис на "Извършване на заявки след попълване в Mongoose" който говори малко повече за нещата, които могат да бъдат оптимизирани при прилагането на този подход.

Запитване чрез попълване

Също така, разбира се, можем да направим (макар и не толкова ефективно) същия вид операция, използвайки .populate() извиквания и манипулиране на получените масиви:

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)

Където, разбира се, всъщност връщате "всички" елементи от "history" , но ние просто прилагаме .map () за извикване на .slice() върху тези елементи, за да получите отново последния елемент от масива за всеки.

Малко повече разходи, тъй като се връща цялата история и .populate() обажданията са допълнителни заявки, но се получават същите крайни резултати.

Точка на дизайна

Основният проблем, който виждам тук обаче, е, че дори имате масив "история" в съдържанието. Това всъщност не е страхотна идея, тъй като трябва да направите неща като по-горе, за да върнете само съответния артикул, който искате.

Така че като „точка на дизайна“ не бих направил това. Но вместо това бих "отделил" историята от елементите във всички случаи. Запазвайки „вградените“ документи, бих запазил „историята“ в отделен масив и бих запазил само „най-новата“ версия с действителното съдържание:

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

}

Можете да поддържате това просто като зададете $set съответния запис и използване на $push върху "историята" в една операция:

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

След като масивът е разделен, тогава можете просто да правите заявки и винаги да получавате най-новите и да изхвърляте „историята“ до момента, в който наистина искате да направите тази заявка:

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

Като общ случай обаче аз просто изобщо не бих се занимавал с номера на "ревизацията". Запазването на голяма част от същата структура не ви е нужно наистина, когато "добавяте" към масив, тъй като "последното" винаги е "последно". Това важи и за промяна на структурата, където отново „най-новият“ винаги ще бъде последният запис за дадения качен файл.

Опитът да се поддържа такъв "изкуствен" индекс е изпълнен с проблеми и най-вече съсипва всяка промяна на "атомарните" операции, както е показано в .update() пример тук, тъй като трябва да знаете стойност на "брояч", за да предоставите последния номер на ревизия, и следователно трябва да "прочетете" това отнякъде.




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. TypeError:Не може да се използва оператор „in“ за търсене на „_id“ в мъжки

  2. Верижно базирано на времето сортиране и проблем с ограничаване

  3. Mongoose.aggregate(pipeline) свързва множество колекции с помощта на $unwind, $lookup, $group

  4. Как да активирам удостоверяване на MongoDB чрез Docker?

  5. MongoDB C#:Update.pullAll не премахва елементи