MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Pobierz najnowszy dokument podrzędny z Array

Możesz rozwiązać ten problem na kilka różnych sposobów. Różnią się one oczywiście podejściem i wydajnością, i myślę, że należy wziąć pod uwagę kilka większych kwestii, które należy uwzględnić w swoim projekcie. W szczególności jest to „potrzeba” danych „wersji” we wzorcu użytkowania Twojej rzeczywistej aplikacji.

Zapytanie przez agregat

Jeśli chodzi o najważniejszy punkt pobierania „ostatniego elementu z tablicy wewnętrznej”, naprawdę powinieneś używać .aggregate() operacja, aby to zrobić:

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

Ponieważ jest to instrukcja agregacji, w której możemy również wykonać "dołączenia" na "serwerze", w przeciwieństwie do tworzenia dodatkowych żądań (co jest tym, co .populate() faktycznie robi tutaj ), używając $lookup , pozwalam sobie na trochę luzu z rzeczywistymi nazwami kolekcji, ponieważ twój schemat nie jest uwzględniony w pytaniu. W porządku, ponieważ nie zdawałeś sobie sprawy, że możesz to zrobić w ten sposób.

Oczywiście "rzeczywiste" nazwy kolekcji są wymagane przez serwer, który nie ma pojęcia o zdefiniowanym schemacie "strony aplikacji". Są tu rzeczy, które możesz zrobić dla wygody, ale o tym później.

Należy również pamiętać, że w zależności od tego, gdzie projectId faktycznie pochodzi, to w przeciwieństwie do zwykłych metod mangusty, takich jak .find() $match będzie wymagać faktycznie "przesyłania" na ObjectId jeśli wartość wejściowa jest w rzeczywistości „ciągiem”. Mongoose nie może zastosować „typów schematu” w potoku agregacji, więc może być konieczne zrobienie tego samodzielnie, zwłaszcza jeśli projectId pochodzi z parametru żądania:

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

Podstawowa część to miejsce, w którym używamy $map aby przejść przez wszystkie "przesłane_pliki" wpisy, a następnie po prostu wyodrębnij „najnowsze” z „historii” tablica z $arrayElemAt używając indeksu „ostatniego”, którym jest -1 .

Powinno to być rozsądne, ponieważ najprawdopodobniej „najnowsza wersja” jest w rzeczywistości „ostatnią” pozycją tablicy. Możemy to dostosować, aby szukać „największych”, stosując $maks jako warunek $filter . Tak więc etap potoku staje się:

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

To mniej więcej to samo, z wyjątkiem tego, że dokonujemy porównania z $maks wartość i zwróć tylko "jeden" wpis z tablicy powodujący powrót indeksu z "filtrowanej" tablicy na "pierwszą" pozycję lub 0 indeks.

Jeśli chodzi o inne ogólne techniki korzystania z $lookup zamiast .populate() , zobacz mój wpis na temat "Zapytania po wypełnieniu w Mongoose" który mówi nieco więcej o rzeczach, które można zoptymalizować przy takim podejściu.

Zapytanie przez wypełnienie

Oczywiście możemy wykonać (choć nie tak wydajnie) ten sam rodzaj operacji za pomocą .populate() wywołania i manipulowanie wynikowymi tablicami:

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)

Gdzie oczywiście faktycznie zwracasz "wszystkie" elementy z "historii" , ale po prostu stosujemy .map () aby wywołać .slice() na tych elementach, aby ponownie uzyskać ostatni element tablicy dla każdego.

Trochę więcej, ponieważ zwracana jest cała historia, a .populate() połączenia są dodatkowymi prośbami, ale dają takie same wyniki końcowe.

Punkt projektowania

Jednak głównym problemem, który tu widzę, jest to, że masz nawet tablicę „historii” w treści. To naprawdę nie jest świetny pomysł, ponieważ musisz zrobić rzeczy takie jak powyżej, aby zwrócić tylko odpowiedni przedmiot, który chcesz.

Więc jako „punkt projektowy” nie zrobiłbym tego. Ale zamiast tego we wszystkich przypadkach „oddzieliłbym” historię od przedmiotów. Trzymając się „osadzone” dokumenty, zachowałbym „historię” w osobnej tablicy i zachowałbym tylko „najnowszą” wersję z rzeczywistą zawartością:

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

}

Możesz to po prostu utrzymać, ustawiając $set odpowiedni wpis i przy użyciu $push na "historii" w jednej operacji:

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

Mając oddzieloną tablicę, możesz po prostu wykonać zapytanie i zawsze pobrać ostatnią i odrzucić „historię” do czasu, gdy rzeczywiście chcesz wykonać to żądanie:

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

W ogólnym przypadku po prostu nie zawracałbym sobie głowy numerem „wersji”. Zachowując w dużej mierze tę samą strukturę, tak naprawdę nie potrzebujesz jej podczas „dodawania” do tablicy, ponieważ „najnowszy” jest zawsze „ostatni”. Dotyczy to również zmiany struktury, gdzie ponownie „najnowszy” zawsze będzie ostatnim wpisem dla danego przesłanego pliku.

Próba utrzymania takiego „sztucznego” indeksu jest najeżona problemami i najczęściej rujnuje wszelkie zmiany „atomowych” operacji, jak pokazano w .update() na przykład tutaj, ponieważ musisz znać wartość „licznika”, aby podać najnowszy numer wersji, a zatem musisz ją „odczytać” skądś.




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Jaka byłaby motywacja do integracji mongodb z solr

  2. Kiedy należy używać bazy danych NoSQL zamiast relacyjnej bazy danych? Czy można używać obu w tej samej witrynie?

  3. Meteor - collection.find() zawsze zwraca wszystkie pola

  4. mongodb, replikuje się i błąd:{ $err :not master and slaveOk=false, kod :13435 }

  5. Agregacja Mongo a Java dla pętli i wydajności