web-dev-qa-db-fra.com

La clause $ in de MongoDB garantit-elle l'ordre

Lors de l'utilisation de la clause $in de MongoDB, l'ordre des documents retournés correspond-il toujours à l'ordre des arguments du tableau?

65
user2066880

Comme indiqué précédemment, l'ordre des arguments dans le tableau d'une clause $ in ne reflète pas l'ordre dans lequel les documents sont extraits. Ce sera bien sûr l'ordre naturel ou l'ordre d'index sélectionné, comme indiqué.

Si vous devez conserver cet ordre, vous avez essentiellement deux options.

Supposons donc que vous correspondiez aux valeurs de _id dans vos documents avec un tableau qui sera transmis au $in en tant que [ 4, 2, 8 ].

Approche utilisant l'agrégat


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

])

Donc, ce serait la forme développée. Ce qui se passe fondamentalement ici est que, tout comme le tableau de valeurs est passé à $in, vous construisez également une instruction "imbriquée" $cond pour tester les valeurs et affecter un poids approprié. Comme cette valeur "pondérée" reflète l'ordre des éléments dans le tableau, vous pouvez ensuite transmettre cette valeur à une étape de tri afin d'obtenir vos résultats dans l'ordre requis.

Bien sûr, vous "construisez" en fait l'instruction code dans le code, comme ceci:

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

Approche en utilisant mapReduce


Bien sûr, si tout cela semble lourd pour votre sensibilité, vous pouvez faire la même chose en utilisant mapReduce, qui a l'air plus simple mais qui risque de tourner un peu plus lentement.

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

Et cela repose essentiellement sur le fait que les valeurs de "clé" émises sont dans "l'ordre d'index" de la façon dont elles apparaissent dans le tableau en entrée.


Il s’agit donc essentiellement de votre façon de maintenir l’ordre d’une liste d’entrée à une condition $in dans laquelle vous avez déjà cette liste dans un ordre déterminé.

65
Neil Lunn

Si vous ne souhaitez pas utiliser aggregate, une autre solution consiste à utiliser find, puis à trier les résultats de la documentation côté client à l'aide de array#sort :

Si les valeurs $in sont des types primitifs tels que des nombres, vous pouvez utiliser une approche telle que:

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

Si les valeurs $in sont des types non primitifs tels que ObjectIds, une autre approche est requise, car indexOf compare par référence dans ce cas.

Si vous utilisez Node.js 4.x +, vous pouvez utiliser Array#findIndex et ObjectID#equals pour gérer cela en modifiant la fonction sort en:

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

Ou avec n'importe quelle version de Node.js, avec underscore/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

Une autre méthode utilisant la requête d'agrégation uniquement applicable à la version MongoDB> 3.4

Le mérite revient à cet article de Nice blog. .

Exemples de documents à récupérer dans cet ordre - 

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

La requête -

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

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

Une autre citation de la poste expliquant ces opérateurs d'agrégation utilisés - 

L'étape "$ addFields" est nouvelle dans la version 3.4 et vous permet de "projeter" de nouveaux champs dans des documents existants sans connaître tous les autres champs existants. La nouvelle expression "$ indexOfArray" renvoie la position d'un élément particulier dans un tableau donné.

Fondamentalement, l'opérateur addToSet ajoute un nouveau champ order à chaque document lorsqu'il le trouve et ce champ order représente la commande d'origine de notre tableau que nous avons fourni. Ensuite, nous trions simplement les documents en fonction de ce champ.

17
Jyotman Singh

Comme pour la solution JonnyHK , vous pouvez réorganiser les documents renvoyés depuis find dans votre client (si votre client est en JavaScript) avec une combinaison de map et de la fonction Array.prototype.find dans 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);
        });
    });

});

Quelques notes:

  • Le code ci-dessus utilise le pilote Mongo Node et not Mongoose
  • idArray est un tableau de ObjectId
  • Je n'ai pas testé les performances de cette méthode par rapport au tri, mais si vous devez manipuler chaque élément renvoyé (ce qui est assez commun), vous pouvez le faire dans le rappel map pour simplifier votre code.
4
tebs1200

Je sais que cette question est liée au framework Mongoose JS, mais le dupliqué est générique. J'espère donc que l'affichage d'une solution Python (PyMongo) convient parfaitement ici.

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
Dennis Golomazov

Toujours? Jamais. L'ordre est toujours le même: indéfini (probablement l'ordre physique dans lequel les documents sont stockés). Sauf si vous le triez.

2
freakish

Je sais que c'est un ancien fil de discussion, mais si vous ne renvoyez que la valeur de l'ID dans le tableau, vous devrez peut-être opter pour cette syntaxe. Comme je ne pouvais pas sembler obtenir la valeur indexOf pour correspondre à un format ObjectId Mongo. 

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

Comment convertir mongo ObjectId .toString sans inclure l'encapsuleur 'ObjectId ()' - juste la valeur?

1
NoobSter

Ceci est une solution de code une fois que les résultats ont été extraits de Mongo. Utiliser une carte pour stocker l’index, puis échanger les valeurs.

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

Un moyen simple de classer le résultat après le retour du tableau par mongo consiste à créer un objet avec id en tant que clé, puis à le mapper sur le _id donné pour renvoyer un tableau correctement commandé.

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

Vous pouvez garantir la commande avec $ ou clause.

Utilisez donc $or: [ _ids.map(_id => ({_id}))] à la place.

0
d3t5411