При использовании предложения MongoDB $in
всегда ли порядок возвращаемых документов соответствует порядку аргумента массива?
mongodb
mongoose
mapreduce
mongodb-query
aggregation-framework
user2066880
источник
источник
Ответы:
Как уже отмечалось, порядок аргументов в массиве предложения $ in не отражает порядок получения документов. Это, конечно, будет естественный порядок или выбранный порядок индекса, как показано.
Если вам нужно сохранить этот порядок, у вас есть два варианта.
Итак, предположим, что вы сравнивали значения
_id
в своих документах с массивом, который будет передан в$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 } } ])
Итак, это будет расширенная форма. В основном здесь происходит то, что, как только массив значений передается
$in
вам, вы также создаете "вложенный"$cond
оператор для проверки значений и присвоения соответствующего веса. Поскольку это значение «веса» отражает порядок элементов в массиве, вы можете передать это значение на этап сортировки, чтобы получить результаты в требуемом порядке.Конечно, вы на самом деле «строите» оператор конвейера в коде, примерно так:
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 );
Подход с использованием mapReduce
Конечно, если все это кажется вам изрядным для вашей чувствительности, вы можете сделать то же самое с помощью mapReduce, который выглядит проще, но, вероятно, будет работать несколько медленнее.
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; } } )
И это в основном зависит от того, что излучаемые «ключевые» значения находятся в «порядковом индексе» того, как они встречаются во входном массиве.
Таким образом, это, по сути, ваши способы поддержания порядка входного списка до
$in
состояния, при котором у вас уже есть этот список в определенном порядке.источник
Другой способ использования запроса агрегации применим только для MongoDB версии> = 3.4 -
Благодарность за это хорошее сообщение в блоге .
Примеры документов, которые нужно получить в этом порядке -
var order = [ "David", "Charlie", "Tess" ];
Запрос -
var query = [ {$match: {name: {$in: order}}}, {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}}, {$sort: {"__order": 1}} ]; var result = db.users.aggregate(query);
Еще одна цитата из сообщения, объясняющего эти используемые операторы агрегации -
Обычно
addFields
оператор добавляет новоеorder
поле к каждому документу, когда он его находит, и этоorder
поле представляет исходный порядок предоставленного нами массива. Затем мы просто сортируем документы по этому полю.источник
Если вы не хотите использовать
aggregate
, другое решение - использовать,find
а затем отсортировать результаты документа на стороне клиента, используяarray#sort
:Если
$in
значения являются примитивными типами, например числами, вы можете использовать такой подход: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); }); });
Если
$in
значения не являются примитивными типами, такими какObjectId
s,indexOf
в этом случае требуется другой подход, сравниваемый по ссылке.Если вы используете Node.js 4.x +, вы можете использовать
Array#findIndex
и,ObjectID#equals
чтобы справиться с этим, изменивsort
функцию на:docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - ids.findIndex(id => b._id.equals(id)));
Или с любой версией Node.js с подчеркиванием / 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
для сравнения с_id
полем документа. Обновлено, чтобы сделать_id
сравнение явным. Спасибо за вопрос.Подобно решению JonnyHK , вы можете изменить порядок документов, возвращаемых
find
в вашем клиенте (если ваш клиент использует JavaScript), с помощью комбинацииmap
иArray.prototype.find
функции в 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); }); }); });
Пара замечаний:
idArray
массивObjectId
map
обратном вызове, чтобы упростить свой код.источник
Простой способ упорядочить результат после того, как mongo вернет массив, - создать объект с идентификатором в качестве ключей, а затем сопоставить данные _id, чтобы вернуть массив, который правильно упорядочен.
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 }
источник
Я знаю, что этот вопрос связан с фреймворком Mongoose JS, но дублированный вариант является общим, поэтому я надеюсь, что публикация решения Python (PyMongo) здесь подойдет.
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
источник
Всегда? Никогда. Порядок всегда один и тот же: не определен (вероятно, физический порядок, в котором хранятся документы). Если только вы его не отсортируете.
источник
$natural
порядок обычно логический, а не физическийЯ знаю, что это старый поток, но если вы просто возвращаете значение Id в массиве, вам, возможно, придется выбрать этот синтаксис. Поскольку я не мог получить значение indexOf, соответствующее формату 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}); };
Как преобразовать mongo ObjectId .toString без включения оболочки ObjectId () - только значение?
источник
Вы можете гарантировать заказ с помощью пункта $ или.
Так что используйте
$or: [ _ids.map(_id => ({_id}))]
вместо этого.источник
$or
Обходной путь не работал с тех пор v2.6 .Это кодовое решение после получения результатов из Mongo. Использование карты для хранения индекса с последующей заменой значений.
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++ } } }
источник