Lors de l'utilisation de la $in
clause MongoDB , l'ordre des documents renvoyés correspond-il toujours à l'ordre de l'argument du tableau?
Lors de l'utilisation de la $in
clause MongoDB , l'ordre des documents renvoyés correspond-il toujours à l'ordre de l'argument du tableau?
Réponses:
Comme indiqué, l'ordre des arguments dans le tableau d'une clause $ in ne reflète pas l'ordre dans lequel les documents sont récupérés. Ce sera bien sûr l'ordre naturel ou par l'ordre d'index sélectionné comme indiqué.
Si vous devez conserver cet ordre, vous avez essentiellement deux options.
Supposons donc que vous compariez les valeurs de _id
dans vos documents avec un tableau qui va être transmis à $in
as [ 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 } }
])
Ce serait donc le formulaire élargi. Ce qui se passe essentiellement ici, c'est que tout comme le tableau de valeurs vous est transmis, $in
construisez également une $cond
instruction "imbriquée" pour tester les valeurs et attribuer un poids approprié. Comme cette valeur de «poids» reflète l'ordre des éléments dans le tableau, vous pouvez ensuite passer cette valeur à une étape de tri afin d'obtenir vos résultats dans l'ordre requis.
Bien sûr, vous "construisez" l'instruction pipeline dans le code, un peu 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 trop important pour votre sensibilité, vous pouvez faire la même chose en utilisant mapReduce, qui semble plus simple mais fonctionnera probablement 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 «clés» émises sont dans «l'ordre d'index» de la façon dont elles se produisent dans le tableau d'entrée.
Donc, ce sont essentiellement vos façons de maintenir l'ordre d'une liste d'entrée à une $in
condition où vous avez déjà cette liste dans un ordre déterminé.
Une autre façon d'utiliser la requête d'agrégation applicable uniquement pour la version MongoDB> = 3.4 -
Le mérite revient à ce joli article de 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 du post 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 vers 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é.
En gros, l' addFields
opérateur ajoute un nouveau order
champ à chaque document lorsqu'il le trouve et ce order
champ représente l'ordre d'origine de notre tableau que nous avons fourni. Ensuite, nous trions simplement les documents en fonction de ce champ.
Si vous ne souhaitez pas utiliser aggregate
, une autre solution consiste à utiliser find
, puis à trier les résultats de la documentation côté client en utilisant array#sort
:
Si les $in
valeurs sont des types primitifs comme les nombres, vous pouvez utiliser une approche comme:
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 $in
valeurs sont des types non primitifs tels que ObjectId
s, une autre approche est nécessaire pour indexOf
comparer par référence dans ce cas.
Si vous utilisez Node.js 4.x +, vous pouvez utiliser Array#findIndex
et ObjectID#equals
gérer cela en modifiant la sort
fonction 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 des traits de soulignement / 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); });
});
Document#equals
pour comparer avec le _id
champ du doc . Mis à jour pour rendre la _id
comparaison explicite. Merci d'avoir posé la question.
Semblable à la solution de JonnyHK , vous pouvez réorganiser les documents renvoyés par find
votre client (si votre client est en JavaScript) avec une combinaison de map
et la Array.prototype.find
fonction 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 deObjectId
map
rappel pour simplifier votre code.Un moyen simple de classer le résultat après que mongo a renvoyé le tableau consiste à créer un objet avec id comme clés, puis à mapper les _id donnés pour renvoyer un tableau correctement ordonné.
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
}
Je sais que cette question est liée au framework Mongoose JS, mais la duplication est générique, donc j'espère que publier une solution Python (PyMongo) est bien 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
Je sais que c'est un ancien thread, 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 avec un format 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});
};
Comment convertir mongo ObjectId .toString sans inclure le wrapper 'ObjectId ()' - juste la valeur?
Vous pouvez garantir la commande avec $ ou clause.
Alors utilisez $or: [ _ids.map(_id => ({_id}))]
plutôt.
$or
solution de contournement n'a pas fonctionné depuis la v2.6 .
Il s'agit d'une solution de code une fois les résultats récupérés à partir de Mongo. Utilisation d'une carte pour stocker l'index, puis permutation des 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++
}
}
}