Czy w przypadku korzystania z $in
klauzuli MongoDB kolejność zwracanych dokumentów zawsze odpowiada kolejności argumentów tablicy?
mongodb
mongoose
mapreduce
mongodb-query
aggregation-framework
user2066880
źródło
źródło
Odpowiedzi:
Jak zauważono, kolejność argumentów w tablicy klauzuli $ in nie odzwierciedla kolejności, w jakiej dokumenty są pobierane. Będzie to oczywiście naturalna kolejność lub wybrana kolejność indeksu, jak pokazano.
Jeśli chcesz zachować tę kolejność, zasadniczo masz dwie opcje.
Powiedzmy więc, że dopasowujesz wartości
_id
w swoich dokumentach z tablicą, która zostanie przekazana do$in
as[ 4, 2, 8 ]
.Podejdź za pomocą Aggregate
var list = [ 4, 2, 8 ]; db.collection.aggregate([ // Match the selected documents by "_id" { "$match": { "_id": { "$in": [ 4, 2, 8 ] }, }, // Project a "weight" to each document { "$project": { "weight": { "$cond": [ { "$eq": [ "$_id", 4 ] }, 1, { "$cond": [ { "$eq": [ "$_id", 2 ] }, 2, 3 ]} ]} }}, // Sort the results { "$sort": { "weight": 1 } } ])
Więc to byłaby rozszerzona forma. Zasadniczo dzieje się tutaj to, że tak jak tablica wartości jest przekazywana do
$in
Ciebie, również konstruujesz „zagnieżdżony”$cond
instrukcję, aby przetestować wartości i przypisać odpowiednią wagę. Ponieważ ta wartość „wagi” odzwierciedla kolejność elementów w tablicy, możesz następnie przekazać tę wartość do etapu sortowania, aby uzyskać wyniki w wymaganej kolejności.Oczywiście faktycznie „budujesz” instrukcję potoku w kodzie, podobnie jak to:
var list = [ 4, 2, 8 ]; var stack = []; for (var i = list.length - 1; i > 0; i--) { var rec = { "$cond": [ { "$eq": [ "$_id", list[i-1] ] }, i ] }; if ( stack.length == 0 ) { rec["$cond"].push( i+1 ); } else { var lval = stack.pop(); rec["$cond"].push( lval ); } stack.push( rec ); } var pipeline = [ { "$match": { "_id": { "$in": list } }}, { "$project": { "weight": stack[0] }}, { "$sort": { "weight": 1 } } ]; db.collection.aggregate( pipeline );
Podejdź za pomocą mapReduce
Oczywiście, jeśli to wszystko wydaje się mocne dla twojej wrażliwości, możesz zrobić to samo za pomocą mapReduce, które wygląda na prostsze, ale prawdopodobnie będzie działać nieco wolniej.
var list = [ 4, 2, 8 ]; db.collection.mapReduce( function () { var order = inputs.indexOf(this._id); emit( order, { doc: this } ); }, function() {}, { "out": { "inline": 1 }, "query": { "_id": { "$in": list } }, "scope": { "inputs": list } , "finalize": function (key, value) { return value.doc; } } )
Zasadniczo polega to na tym, że emitowane wartości „klucza” są w „kolejności indeksów”, w jakiej występują w tablicy wejściowej.
Więc to są w istocie sposoby na utrzymanie kolejności listy wejściowej do
$in
stanu, w którym masz już tę listę w określonej kolejności.źródło
Inny sposób użycia kwerendy agregacji ma zastosowanie tylko do wersji MongoDB> = 3.4 -
To zasługa tego miłego wpisu na blogu .
Przykładowe dokumenty do pobrania w tej kolejności -
var order = [ "David", "Charlie", "Tess" ];
Zapytanie -
var query = [ {$match: {name: {$in: order}}}, {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}}, {$sort: {"__order": 1}} ]; var result = db.users.aggregate(query);
Kolejny cytat z postu wyjaśniający zastosowane operatory agregacji -
Zasadniczo
addFields
operator dołącza noweorder
pole do każdego dokumentu, gdy je znajdzie, a toorder
pole reprezentuje oryginalną kolejność podanej przez nas tablicy. Następnie po prostu sortujemy dokumenty na podstawie tego pola.źródło
Jeśli nie chcesz używać
aggregate
, innym rozwiązaniem jest użycie,find
a następnie posortowanie wyników dokumentu po stronie klienta za pomocąarray#sort
:Jeśli
$in
wartości są typami pierwotnymi, takimi jak liczby, możesz użyć podejścia takiego:var ids = [4, 2, 8, 1, 9, 3, 5, 6]; MyModel.find({ _id: { $in: ids } }).exec(function(err, docs) { docs.sort(function(a, b) { // Sort docs by the order of their _id values in ids. return ids.indexOf(a._id) - ids.indexOf(b._id); }); });
Jeśli
$in
wartości nie są typami pierwotnymi, takimi jakObjectId
s, wymagane jest inne podejście, ponieważindexOf
w tym przypadku porównuje się przez odniesienie.Jeśli używasz Node.js 4.x +, możesz użyć
Array#findIndex
i,ObjectID#equals
aby sobie z tym poradzić, zmieniającsort
funkcję na:docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - ids.findIndex(id => b._id.equals(id)));
Lub z dowolną wersją Node.js, z podkreśleniem / lodash
findIndex
:docs.sort(function (a, b) { return _.findIndex(ids, function (id) { return a._id.equals(id); }) - _.findIndex(ids, function (id) { return b._id.equals(id); }); });
źródło
Document#equals
do porównania z_id
polem doc . Zaktualizowano, aby_id
porównanie było wyraźne. Dzięki, że pytasz.Podobnie jak w rozwiązaniu JonnyHK , możesz zmienić kolejność dokumentów zwróconych
find
w swoim kliencie (jeśli twój klient jest w JavaScript) za pomocą kombinacjimap
iArray.prototype.find
funkcji w EcmaScript 2015:Collection.find({ _id: { $in: idArray } }).toArray(function(err, res) { var orderedResults = idArray.map(function(id) { return res.find(function(document) { return document._id.equals(id); }); }); });
Kilka uwag:
idArray
Jest tablicąObjectId
map
wywołaniu zwrotnym, aby uprościć swój kod.źródło
find
przechodzi przez tablicę dla każdego elementu tablicy (od zewnętrznejmap
). Jest to strasznie nieefektywne, ponieważ istnieje rozwiązanie O (n) korzystające z tabeli przeglądowej.Wiem, że to pytanie jest związane z frameworkiem Mongoose JS, ale zduplikowany jest ogólny, więc mam nadzieję, że opublikowanie tutaj rozwiązania w języku Python (PyMongo) jest w porządku.
things = list(db.things.find({'_id': {'$in': id_array}})) things.sort(key=lambda thing: id_array.index(thing['_id'])) # things are now sorted according to id_array order
źródło
Łatwym sposobem uporządkowania wyniku po zwróceniu tablicy przez mongo jest utworzenie obiektu o identyfikatorze jako kluczy, a następnie odwzorowanie podanych _id w celu zwrócenia tablicy, która jest poprawnie uporządkowana.
async function batchUsers(Users, keys) { const unorderedUsers = await Users.find({_id: {$in: keys}}).toArray() let obj = {} unorderedUsers.forEach(x => obj[x._id]=x) const ordered = keys.map(key => obj[key]) return ordered }
źródło
Zawsze? Nigdy. Kolejność jest zawsze taka sama: nieokreślona (prawdopodobnie fizyczna kolejność, w jakiej przechowywane są dokumenty). Chyba że to posortujesz.
źródło
$natural
porządek normalny, który jest raczej logiczny niż fizycznyWiem, że to stary wątek, ale jeśli po prostu zwracasz wartość identyfikatora w tablicy, być może będziesz musiał wybrać tę składnię. Ponieważ nie mogłem uzyskać wartości indexOf pasującej do formatu mongo ObjectId.
obj.map = function() { for(var i = 0; i < inputs.length; i++){ if(this._id.equals(inputs[i])) { var order = i; } } emit(order, {doc: this}); };
Jak przekonwertować mongo ObjectId .toString bez dołączania opakowania „ObjectId ()” - tylko wartość?
źródło
Możesz zagwarantować zamówienie za pomocą $ lub klauzuli.
Więc użyj
$or: [ _ids.map(_id => ({_id}))]
zamiast tego.źródło
$or
obejście nie działa od wersji 2.6 .Jest to rozwiązanie kodowe po pobraniu wyników z Mongo. Używanie mapy do przechowywania indeksu, a następnie zamiana wartości.
catDetails := make([]CategoryDetail, 0) err = sess.DB(mdb).C("category"). Find(bson.M{ "_id": bson.M{"$in": path}, "is_active": 1, "name": bson.M{"$ne": ""}, "url.path": bson.M{"$exists": true, "$ne": ""}, }). Select( bson.M{ "is_active": 1, "name": 1, "url.path": 1, }).All(&catDetails) if err != nil{ return } categoryOrderMap := make(map[int]int) for index, v := range catDetails { categoryOrderMap[v.Id] = index } counter := 0 for i := 0; counter < len(categoryOrderMap); i++ { if catId := int(path[i].(float64)); catId > 0 { fmt.Println("cat", catId) if swapIndex, exists := categoryOrderMap[catId]; exists { if counter != swapIndex { catDetails[swapIndex], catDetails[counter] = catDetails[counter], catDetails[swapIndex] categoryOrderMap[catId] = counter categoryOrderMap[catDetails[swapIndex].Id] = swapIndex } counter++ } } }
źródło