使用MongoDB的$in
子句时,返回文档的顺序是否始终与数组参数的顺序相对应?
Answers:
如前所述,$ 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
为确定的顺序的一种方式。
使用聚合查询的另一种方法仅适用于> = 3.4的MongoDB版本-
功劳归功于这篇不错的博客文章。
按此顺序获取示例文档-
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
,则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版本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); });
});
与JonnyHK的解决方案类似,您可以find
结合使用EcmaScript 2015中的map
和Array.prototype.find
函数,对从客户端(如果客户端使用JavaScript)返回的文档进行重新排序:
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为键的对象,然后映射给定的_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
我知道这是一个旧线程,但是如果您只是返回数组中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检索结果后,这是一种代码解决方案。使用映射存储索引,然后交换值。
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++
}
}
}