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