Quando si utilizza la $in
clausola di MongoDB , l'ordine dei documenti restituiti corrisponde sempre all'ordine dell'argomento dell'array?
Quando si utilizza la $in
clausola di MongoDB , l'ordine dei documenti restituiti corrisponde sempre all'ordine dell'argomento dell'array?
Risposte:
Come notato, l'ordine degli argomenti nella matrice di una clausola $ in non riflette l'ordine di come i documenti vengono recuperati. Quello ovviamente sarà l'ordine naturale o dall'ordine dell'indice selezionato come mostrato.
Se hai bisogno di preservare questo ordine, hai fondamentalmente due opzioni.
Quindi diciamo che stavi facendo corrispondere i valori di _id
nei tuoi documenti con un array che verrà passato a $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 } }
])
Quindi quella sarebbe la forma espansa. Quello che fondamentalmente accade qui è che così come viene passato l'array di valori, $in
costruisci anche un "annidato"$cond
un'istruzione per testare i valori e assegnare un peso appropriato. Poiché il valore di "peso" riflette l'ordine degli elementi nell'array, è quindi possibile passare tale valore a una fase di ordinamento per ottenere i risultati nell'ordine richiesto.
Ovviamente in realtà "costruisci" l'istruzione pipeline nel codice, in modo molto simile a questo:
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 );
Ovviamente se tutto questo sembra pesante per la tua sensibilità, puoi fare la stessa cosa usando mapReduce, che sembra più semplice ma probabilmente funzionerà un po 'più lentamente.
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;
}
}
)
E questo fondamentalmente si basa sul fatto che i valori "chiave" emessi siano nell '"ordine di indice" di come si presentano nell'array di input.
Quindi questi sono essenzialmente i tuoi modi per mantenere l'ordine di un elenco di input a una $in
condizione in cui hai già quell'elenco in un determinato ordine.
Un altro modo di utilizzare la query di aggregazione applicabile solo per MongoDB versione> = 3.4 -
Il merito va a questo bel post sul blog .
Documenti di esempio da recuperare in questo ordine:
var order = [ "David", "Charlie", "Tess" ];
La domanda -
var query = [
{$match: {name: {$in: order}}},
{$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
{$sort: {"__order": 1}}
];
var result = db.users.aggregate(query);
Un'altra citazione dal post che spiega questi operatori di aggregazione utilizzati -
Lo stadio "$ addFields" è nuovo in 3.4 e ti permette di "$ proiettare" nuovi campi su documenti esistenti senza conoscere tutti gli altri campi esistenti. La nuova espressione "$ indexOfArray" restituisce la posizione di un particolare elemento in un dato array.
Fondamentalmente l' addFields
operatore aggiunge un nuovo order
campo a ogni documento quando lo trova e questo order
campo rappresenta l'ordine originale del nostro array che abbiamo fornito. Quindi ordiniamo semplicemente i documenti in base a questo campo.
Se non si desidera utilizzare aggregate
, un'altra soluzione è utilizzare find
e quindi ordinare i risultati del documento lato client utilizzando array#sort
:
Se i $in
valori sono tipi primitivi come i numeri, puoi utilizzare un approccio come:
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);
});
});
Se i $in
valori sono tipi non primitivi come ObjectId
s, indexOf
in questo caso è necessario un altro approccio per i confronti per riferimento.
Se stai usando Node.js 4.x +, puoi usare Array#findIndex
e ObjectID#equals
per gestirlo cambiando la sort
funzione in:
docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) -
ids.findIndex(id => b._id.equals(id)));
O con qualsiasi versione di Node.js, con underscore / 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
per confrontare con il _id
campo del doc . Aggiornato per rendere _id
esplicito il confronto. Grazie per avermelo chiesto.
Simile alla soluzione di JonnyHK , puoi riordinare i documenti restituiti dal find
tuo client (se il tuo client è in JavaScript) con una combinazione di map
e la Array.prototype.find
funzione in 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);
});
});
});
Un paio di note:
idArray
è un array diObjectId
map
callback per semplificare il tuo codice.find
attraversa l'array per ogni elemento dell'array (dall'esterno map
). Questo è orribilmente inefficiente, poiché esiste una soluzione O (n) che utilizza una tabella di ricerca.
So che questa domanda è correlata al framework Mongoose JS, ma quella duplicata è generica, quindi spero che pubblicare una soluzione Python (PyMongo) vada bene qui.
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
Un modo semplice per ordinare il risultato dopo che mongo ha restituito l'array è creare un oggetto con id come chiavi e quindi mappare gli _id forniti per restituire un array ordinato correttamente.
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
}
So che questo è un vecchio thread, ma se stai solo restituendo il valore dell'ID nell'array, potresti dover optare per questa sintassi. Poiché non riuscivo a ottenere il valore indexOf per abbinarlo a un formato 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});
};
Come convertire mongo ObjectId .toString senza includere il wrapper 'ObjectId ()' - solo il valore?
Puoi garantire l'ordine con $ o clausola.
Quindi usa $or: [ _ids.map(_id => ({_id}))]
invece.
$or
soluzione alternativa non ha funzionato dalla v2.6 .
Questa è una soluzione di codice dopo che i risultati sono stati recuperati da Mongo. Utilizzo di una mappa per memorizzare l'indice e quindi scambiare i valori.
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++
}
}
}