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

$przeglądać wiele poziomów bez $unwind?

Istnieje oczywiście kilka podejść w zależności od dostępnej wersji MongoDB. Różnią się one od różnych zastosowań $lookup aż do włączenia manipulacji obiektami w .populate() wynik za pomocą .lean() .

Proszę o uważne przeczytanie sekcji i pamiętaj, że podczas rozważania rozwiązania wdrożeniowego wszystko może nie wyglądać tak, jak się wydaje.

MongoDB 3.6, „zagnieżdżone” wyszukiwanie

W MongoDB 3.6 $lookup operator otrzymuje dodatkową możliwość dołączenia pipeline wyrażenie w przeciwieństwie do zwykłego łączenia wartości klucza „lokalnego” z „obcym”, co oznacza, że ​​możesz zasadniczo wykonać każdy $lookup jako "zagnieżdżone" w tych wyrażeniach potoku

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "let": { "reviews": "$reviews" },
    "pipeline": [
       { "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
       { "$lookup": {
         "from": Comment.collection.name,
         "let": { "comments": "$comments" },
         "pipeline": [
           { "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
           { "$lookup": {
             "from": Author.collection.name,
             "let": { "author": "$author" },
             "pipeline": [
               { "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
               { "$addFields": {
                 "isFollower": { 
                   "$in": [ 
                     mongoose.Types.ObjectId(req.user.id),
                     "$followers"
                   ]
                 }
               }}
             ],
             "as": "author"
           }},
           { "$addFields": { 
             "author": { "$arrayElemAt": [ "$author", 0 ] }
           }}
         ],
         "as": "comments"
       }},
       { "$sort": { "createdAt": -1 } }
     ],
     "as": "reviews"
  }},
 ])

To może być naprawdę potężne, jak widać z perspektywy oryginalnego potoku, tak naprawdę wie tylko o dodawaniu treści do "reviews" array, a następnie każde kolejne "zagnieżdżone" wyrażenie potoku również widzi tylko "wewnętrzne" elementy z połączenia.

Jest potężny i pod pewnymi względami może być nieco jaśniejszy, ponieważ wszystkie ścieżki pól odnoszą się do poziomu zagnieżdżenia, ale zaczyna to pełzanie wcięć w strukturze BSON i musisz być świadomy, czy pasujesz do tablic lub pojedyncze wartości podczas przemierzania struktury.

Zauważ, że możemy tutaj również zrobić takie rzeczy, jak „spłaszczenie właściwości autora”, jak widać w "comments" wpisy tablicy. Wszystkie $lookup docelowy wynik może być „tablicą”, ale w „podpotoku” możemy przekształcić tę tablicę pojedynczego elementu w pojedynczą wartość.

Standardowa wyszukiwarka MongoDB

Nadal zachowując „dołącz na serwerze”, możesz to zrobić za pomocą $lookup , ale wymaga to tylko przetwarzania pośredniego. To jest dawne podejście do dekonstruowania tablicy za pomocą $unwind i użycie $group etapy przebudowy tablic:

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "localField": "reviews",
    "foreignField": "_id",
    "as": "reviews"
  }},
  { "$unwind": "$reviews" },
  { "$lookup": {
    "from": Comment.collection.name,
    "localField": "reviews.comments",
    "foreignField": "_id",
    "as": "reviews.comments",
  }},
  { "$unwind": "$reviews.comments" },
  { "$lookup": {
    "from": Author.collection.name,
    "localField": "reviews.comments.author",
    "foreignField": "_id",
    "as": "reviews.comments.author"
  }},
  { "$unwind": "$reviews.comments.author" },
  { "$addFields": {
    "reviews.comments.author.isFollower": {
      "$in": [ 
        mongoose.Types.ObjectId(req.user.id), 
        "$reviews.comments.author.followers"
      ]
    }
  }},
  { "$group": {
    "_id": { 
      "_id": "$_id",
      "reviewId": "$review._id"
    },
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "review": {
      "$first": {
        "_id": "$review._id",
        "createdAt": "$review.createdAt",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content"
      }
    },
    "comments": { "$push": "$reviews.comments" }
  }},
  { "$sort": { "_id._id": 1, "review.createdAt": -1 } },
  { "$group": {
    "_id": "$_id._id",
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "reviews": {
      "$push": {
        "_id": "$review._id",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content",
        "comments": "$comments"
      }
    }
  }}
])

To naprawdę nie jest tak zniechęcające, jak mogłoby się wydawać na początku i jest zgodne z prostym wzorcem $lookup i $unwind w miarę przechodzenia przez każdą tablicę.

"author" szczegóły są oczywiście pojedyncze, więc po „rozwinięciu” po prostu chcesz je pozostawić w ten sposób, dodaj pola i rozpocznij proces „wycofywania” do tablic.

Są tylko dwa poziomy do odtworzenia z powrotem do oryginalnego Venue dokument, więc pierwszy poziom szczegółowości to Review odbudować "comments" szyk. Wszystko, czego potrzebujesz, to $push ścieżka "$reviews.comments" w celu ich zebrania i o ile "$reviews._id" pole znajduje się w "grouping _id" jedyne inne rzeczy, które musisz zachować, to wszystkie inne pola. Możesz umieścić je wszystkie w _id lub możesz użyć $first .

Po wykonaniu tych czynności pozostała tylko jedna $group etap, aby wrócić do Venue samo. Tym razem klucz grupujący to "$_id" oczywiście ze wszystkimi właściwościami samego lokalu za pomocą $first a pozostałe "$review" szczegóły wracają do tablicy za pomocą $push . Oczywiście "$comments" wyjście z poprzedniej $group staje się "review.comments" ścieżka.

Praca nad jednym dokumentem i jego relacjami nie jest taka zła. $unwind Operator potoku może ogólnie może być problemem z wydajnością, ale w kontekście tego użycia nie powinno to mieć tak dużego wpływu.

Ponieważ dane są nadal „łączone na serwerze”, nadal znacznie mniejszy ruch niż inne pozostałe alternatywy.

Manipulacja JavaScriptem

Oczywiście innym przypadkiem jest to, że zamiast zmieniać dane na samym serwerze, faktycznie manipulujesz wynikiem. W większości Przypadki Opowiedziałbym się za tym podejściem, ponieważ wszelkie „dodatki” do danych są prawdopodobnie najlepiej obsługiwane przez klienta.

Oczywiście problem z użyciem populate() jest to, chociaż może „wyglądać” znacznie uproszczony proces, w rzeczywistości NIE JEST DOŁĄCZANIEM w jakikolwiek sposób. Wszystkie populate() tak naprawdę to „ukryj” podstawowy proces składania wielu zapytania do bazy danych, a następnie oczekiwanie na wyniki poprzez obsługę asynchroniczną.

A więc „wygląd” złączenia jest w rzeczywistości wynikiem wielu żądań do serwera, a następnie wykonania „manipulacji po stronie klienta” danych do osadzenia szczegółów w tablicach.

Więc poza tym wyraźnym ostrzeżeniem że charakterystyka wydajności nie jest nawet zbliżona do serwera $lookup , innym zastrzeżeniem jest oczywiście to, że "dokumenty mangusty" w wyniku nie są w rzeczywistości zwykłymi obiektami JavaScript podlegającymi dalszej manipulacji.

Aby zastosować takie podejście, musisz dodać .lean() metody do zapytania przed wykonaniem, aby nakazać manguście zwracanie "zwykłych obiektów JavaScript" zamiast Document typy, które są rzutowane za pomocą metod schematu dołączonych do modelu. Zauważając oczywiście, że wynikowe dane nie mają już dostępu do żadnych „metod instancji”, które w przeciwnym razie byłyby powiązane z samymi powiązanymi modelami:

let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();

Teraz venue jest zwykłym obiektem, możemy go po prostu przetworzyć i dostosować w razie potrzeby:

venue.reviews = venue.reviews.map( r => 
  ({
    ...r,
    comments: r.comments.map( c =>
      ({
        ...c,
        author: {
          ...c.author,
          isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
        }
      })
    )
  })
);

Więc tak naprawdę jest to tylko kwestia przechodzenia przez każdą z wewnętrznych tablic w dół do poziomu, na którym można zobaczyć followers tablica w author Detale. Porównanie można następnie przeprowadzić z ObjectId wartości przechowywane w tej tablicy po pierwszym użyciu .map() aby zwrócić wartości "string" do porównania z req.user.id który jest również ciągiem (jeśli tak nie jest, dodaj również .toString() na tym ), ponieważ ogólnie łatwiej jest porównać te wartości w ten sposób za pomocą kodu JavaScript.

Ponownie muszę podkreślić, że „wygląda to prosto”, ale w rzeczywistości jest to coś, czego naprawdę chcesz uniknąć dla wydajności systemu, ponieważ te dodatkowe zapytania i transfer między serwerem a klientem kosztują dużo czasu przetwarzania a nawet z powodu narzutu na żądanie zwiększa to rzeczywiste koszty transportu między dostawcami usług hostingowych.

Podsumowanie

To są w zasadzie twoje podejścia, które możesz zastosować, z wyjątkiem „swojego własnego”, gdzie faktycznie wykonujesz „wiele zapytań” do bazy danych samodzielnie, zamiast korzystać z pomocnika, który .populate() jest.

Używając danych wyjściowych wypełniania, możesz po prostu manipulować danymi w wyniku, tak jak każdą inną strukturą danych, o ile zastosujesz .lean() do zapytania, aby przekonwertować lub w inny sposób wyodrębnić zwykłe dane obiektu ze zwróconych dokumentów mangusty.

Chociaż podejścia zbiorcze wyglądają na znacznie bardziej zaangażowane, jest „dużo” więcej korzyści z wykonywania tej pracy na serwerze. Większe zestawy wyników można sortować, można wykonywać obliczenia w celu dalszego filtrowania i oczywiście otrzymujesz „pojedynczą odpowiedź” na „pojedyncze żądanie” na serwerze, wszystko bez dodatkowych kosztów.

Jest całkowicie dyskusyjne, że same potoki można po prostu skonstruować na podstawie atrybutów już przechowywanych w schemacie. Dlatego napisanie własnej metody wykonywania tej „konstrukcji” na podstawie załączonego schematu nie powinno być zbyt trudne.

W dłuższej perspektywie oczywiście $lookup jest lepszym rozwiązaniem, ale prawdopodobnie będziesz musiał włożyć trochę więcej pracy w początkowe kodowanie, jeśli oczywiście nie po prostu kopiujesz z tego, co jest tutaj wymienione;)




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. MongoDB $dayOfWeek

  2. Jak zaktualizować poddokument w mongodb

  3. Jak preprecyzować wyeksportowane dokumenty MongoDB w mongoexport

  4. Mongoose, zaktualizuj wartości w tablicy obiektów

  5. E:Nie można znaleźć pakietu mongodb-org