it-swarm.com.de

Gewährleistet die $ in-Klausel von MongoDB

Stimmt die Reihenfolge der zurückgegebenen Dokumente bei der Verwendung der MongoDB-Klausel $in immer mit der Reihenfolge des Array-Arguments überein?

65
user2066880

Wie bereits erwähnt, gibt die Reihenfolge der Argumente im Array einer $ in-Klausel nicht die Reihenfolge wieder, in der die Dokumente abgerufen werden. Das ist natürlich die natürliche Reihenfolge oder wie gezeigt die ausgewählte Indexreihenfolge.

Wenn Sie diese Reihenfolge beibehalten möchten, haben Sie grundsätzlich zwei Möglichkeiten.

Nehmen wir also an, Sie stimmen mit den Werten von _id in Ihren Dokumenten mit einem Array überein, das als $in an [ 4, 2, 8 ] übergeben wird.

Ansatz mit Aggregat


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

])

Das wäre also die erweiterte Form. Grundsätzlich geschieht hier Folgendes: Sobald das Wertefeld an $in übergeben wird, erstellen Sie auch eine "verschachtelte" $cond-Anweisung, um die Werte zu testen und eine entsprechende Gewichtung zuzuweisen. Da der Wert "weight" die Reihenfolge der Elemente im Array widerspiegelt, können Sie diesen Wert an eine Sortierstufe übergeben, um die Ergebnisse in der erforderlichen Reihenfolge zu erhalten.

Natürlich "bauen" Sie die Pipeline-Anweisung tatsächlich im Code, ähnlich wie folgt:

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

Ansatz mit mapReduce


Wenn dies alles für Ihre Empfindungen recht heftig erscheint, können Sie dasselbe mit mapReduce tun, was einfacher aussieht, aber wahrscheinlich etwas langsamer läuft.

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

Und das hängt im Wesentlichen davon ab, dass die ausgegebenen "Schlüssel" -Werte in der "Indexreihenfolge" der Reihenfolge im Eingabefeld liegen.


Dies sind im Wesentlichen Ihre Möglichkeiten, um die Reihenfolge einer Eingabeliste in einer $in-Bedingung zu erhalten, in der Sie diese Liste bereits in einer bestimmten Reihenfolge haben.

65
Neil Lunn

Wenn Sie aggregate nicht verwenden möchten, verwenden Sie find und sortieren Sie dann die doc-Ergebnisse mit array#sort auf Clientseite.

Wenn die $in-Werte primitive Typen wie Zahlen sind, können Sie einen Ansatz wie den folgenden verwenden:

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

Wenn die $in-Werte nicht primitive Typen wie ObjectIds sind, ist ein anderer Ansatz erforderlich, da indexOf in diesem Fall durch Verweis verglichen wird.

Wenn Sie Node.js 4.x + verwenden, können Sie mit Array#findIndex und ObjectID#equals umgehen, indem Sie die sort-Funktion folgendermaßen ändern:

docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - 
                    ids.findIndex(id => b._id.equals(id)));

Oder mit jeder Node.js-Version, mit Unterstrich/lodash's 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); });
});
19
JohnnyHK

Eine andere Möglichkeit, die Aggregationsabfrage zu verwenden, gilt nur für MongoDB Version 3.4.

Die Anerkennung geht an diesen Nizza Blogbeitrag .

Beispieldokumente, die in dieser Reihenfolge abgerufen werden sollen - 

var order = [ "David", "Charlie", "Tess" ];

Die Abfrage -

var query = [
             {$match: {name: {$in: order}}},
             {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
             {$sort: {"__order": 1}}
            ];

var result = db.users.aggregate(query);

Ein weiteres Zitat aus dem Beitrag, in dem diese Aggregationsoperatoren erläutert werden: 

Die Phase "$ addFields" ist neu in 3.4 und ermöglicht Ihnen, neue Felder in vorhandene Dokumente zu "projizieren", ohne alle anderen vorhandenen Felder zu kennen. Der neue Ausdruck "$ indexOfArray" gibt die Position eines bestimmten Elements in einem gegebenen Array zurück.

Grundsätzlich hängt der Operator addToSet an jedes Dokument ein neues order-Feld an, wenn es gefunden wird. Dieses order-Feld repräsentiert die ursprüngliche Reihenfolge unseres Arrays. Dann sortieren wir die Dokumente einfach nach diesem Feld.

17
Jyotman Singh

Ähnlich wie bei der Lösung von JonnyHK können Sie die von find in Ihrem Client zurückgegebenen Dokumente (wenn Ihr Client in JavaScript ist) mit einer Kombination aus map und der Array.prototype.find-Funktion in EcmaScript 2015 neu anordnen:

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

});

Ein paar Anmerkungen:

  • Der obige Code verwendet den Mongo Node-Treiber und not Mongoose
  • Die idArray ist ein Array von ObjectId
  • Ich habe die Leistung dieser Methode im Vergleich zur Sortierung nicht getestet, aber wenn Sie jeden zurückgegebenen Artikel bearbeiten müssen (was ziemlich häufig vorkommt), können Sie dies im map-Callback tun, um Ihren Code zu vereinfachen.
4
tebs1200

Ich weiß, dass diese Frage mit dem Mongoose JS-Framework zusammenhängt, aber das duplizierte ist generisch, also hoffe ich, dass es gut ist, eine Python-Lösung (PyMongo) zu posten.

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
2

Immer? Noch nie. Die Reihenfolge ist immer dieselbe: undefined (wahrscheinlich die physische Reihenfolge, in der Dokumente gespeichert werden). Es sei denn, Sie sortieren es.

2
freakish

Ich weiß, dass dies ein alter Thread ist, aber wenn Sie nur den Wert der Id im Array zurückgeben, müssen Sie sich möglicherweise für diese Syntax entscheiden. Da ich den indexOf-Wert scheinbar nicht mit einem Mongo-ObjectId-Format vergleichen konnte. 

  obj.map = function() {
    for(var i = 0; i < inputs.length; i++){
      if(this._id.equals(inputs[i])) {
        var order = i;
      }
    }
    emit(order, {doc: this});
  };

Wie konvertiert man Mongo ObjectId .toString, ohne 'ObjectId ()' - Wrapper einzubeziehen - nur den Wert?

1
NoobSter

Dies ist eine Codelösung, nachdem die Ergebnisse von Mongo abgerufen wurden. Verwenden einer Karte zum Speichern von Indexen und Austauschen von Werten.

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++
        }
    }
}
0
Prateek

Eine einfache Möglichkeit, das Ergebnis zu bestellen, nachdem der Mongo das Array zurückgegeben hat, besteht darin, ein Objekt mit der ID als Schlüssel zu erstellen und dann die angegebenen _IDs zuzuordnen, um ein Array mit korrekter Reihenfolge zurückzugeben.

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
}
0
Arne Jenssen

Sie können eine Bestellung mit $ oder Klausel garantieren.

Verwenden Sie also stattdessen $or: [ _ids.map(_id => ({_id}))].

0
d3t5411