Ao usar a $in
cláusula do MongoDB , a ordem dos documentos retornados sempre corresponde à ordem do argumento da matriz?
mongodb
mongoose
mapreduce
mongodb-query
aggregation-framework
user2066880
fonte
fonte
Respostas:
Conforme observado, a ordem dos argumentos na matriz de uma cláusula $ in não reflete a ordem de como os documentos são recuperados. Essa, é claro, será a ordem natural ou pela ordem do índice selecionado, conforme mostrado.
Se você precisar preservar esse pedido, terá basicamente duas opções.
Então, digamos que você estava combinando os valores de
_id
em seus documentos com um array que será passado para o$in
as[ 4, 2, 8 ]
.Abordagem usando agregado
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 } } ])
Então essa seria a forma expandida. O que basicamente acontece aqui é que, assim como a matriz de valores é passada para
$in
você, você também constrói uma$cond
instrução "aninhada" para testar os valores e atribuir um peso apropriado. Como esse valor de "peso" reflete a ordem dos elementos na matriz, você pode então passar esse valor para um estágio de classificação para obter seus resultados na ordem necessária.É claro que você realmente "constrói" a instrução do pipeline no código, assim:
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 );
Abordagem usando mapReduce
Claro, se tudo isso parecer pesado para sua sensibilidade, você pode fazer a mesma coisa usando mapReduce, que parece mais simples, mas provavelmente será executado um pouco mais lento.
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 isso basicamente depende dos valores de "chave" emitidos estarem na "ordem do índice" de como eles ocorrem na matriz de entrada.
Então, essas são essencialmente suas maneiras de manter a ordem de uma lista de entrada para uma
$in
condição em que você já tem essa lista em uma determinada ordem.fonte
Outra maneira de usar a consulta de agregação aplicável apenas para MongoDB versão> = 3.4 -
O crédito vai para esta bela postagem no blog .
Documentos de exemplo a serem buscados neste pedido -
var order = [ "David", "Charlie", "Tess" ];
A pergunta -
var query = [ {$match: {name: {$in: order}}}, {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}}, {$sort: {"__order": 1}} ]; var result = db.users.aggregate(query);
Outra citação do post explicando esses operadores de agregação usados -
Basicamente, o
addFields
operador anexa um novoorder
campo a cada documento quando o encontra e esseorder
campo representa a ordem original do nosso array que fornecemos. Em seguida, simplesmente classificamos os documentos com base neste campo.fonte
Se você não quiser usar
aggregate
, outra solução é usarfind
e classificar os resultados do documento do lado do cliente usandoarray#sort
:Se os
$in
valores são tipos primitivos, como números, você pode usar uma abordagem como: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 os
$in
valores forem tipos não primitivos comoObjectId
s, outra abordagem é necessária comoindexOf
comparação por referência nesse caso.Se você estiver usando o Node.js 4.x +, pode usar
Array#findIndex
eObjectID#equals
para lidar com isso alterando asort
função para:docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - ids.findIndex(id => b._id.equals(id)));
Ou com qualquer versão do Node.js, com sublinhado / 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); }); });
fonte
Document#equals
para comparar com o_id
campo do doc . Atualizado para tornar a_id
comparação explícita. Obrigado por perguntar.Semelhante à solução JonnyHK , você pode reordenar os documentos retornados de
find
seu cliente (se seu cliente estiver em JavaScript) com uma combinação demap
e aArray.prototype.find
função em 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); }); }); });
Algumas notas:
idArray
é uma matriz deObjectId
map
retorno de chamada para simplificar seu código.fonte
Uma maneira fácil de ordenar o resultado após o mongo retornar o array é fazer um objeto com id como chaves e, em seguida, mapear os _id's fornecidos para retornar um array que esteja corretamente ordenado.
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 }
fonte
Eu sei que esta questão está relacionada ao framework JS Mongoose, mas o duplicado é genérico, então espero que postar uma solução Python (PyMongo) esteja bem aqui.
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
fonte
Sempre? Nunca. A ordem é sempre a mesma: indefinida (provavelmente a ordem física em que os documentos são armazenados). A menos que você classifique.
fonte
$natural
ordem normalmente, o que é lógico ao invés de físicoEu sei que este é um thread antigo, mas se você estiver apenas retornando o valor do Id na matriz, pode ser necessário optar por esta sintaxe. Como não consegui obter o valor indexOf para corresponder a um 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}); };
Como converter mongo ObjectId .toString sem incluir o wrapper 'ObjectId ()' - apenas o Value?
fonte
Você pode garantir o pedido com a cláusula $ or.
Portanto, use em seu
$or: [ _ids.map(_id => ({_id}))]
lugar.fonte
$or
solução alternativa não funcionou desde a v2.6 .Esta é uma solução de código depois que os resultados são recuperados do Mongo. Usando um mapa para armazenar o índice e, em seguida, trocando os valores.
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++ } } }
fonte