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?
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 ]
.
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 );
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é.
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 ObjectId
s, 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); });
});
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.
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:
idArray
est un tableau de ObjectId
map
pour simplifier votre code.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
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.
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});
};
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++
}
}
}
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
}
Vous pouvez garantir la commande avec $ ou clause.
Utilisez donc $or: [ _ids.map(_id => ({_id}))]
à la place.