При использовании предложения MongoDB $in
всегда ли порядок возвращаемых документов соответствует порядку аргумента массива?
При использовании предложения MongoDB $in
всегда ли порядок возвращаемых документов соответствует порядку аргумента массива?
Ответы:
Как уже отмечалось, порядок аргументов в массиве предложения $ 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, который выглядит проще, но, вероятно, будет работать несколько медленнее.
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» появился в версии 3.4 и позволяет «$ project» добавлять новые поля в существующие документы, не зная всех остальных существующих полей. Новое выражение «$ indexOfArray» возвращает позицию определенного элемента в данном массиве.
Обычно 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++
}
}
}