$in
子句时,返回的文档顺序是否总是对应于数组参数的顺序?如前所述,$in子句的数组参数中的参数顺序不反映检索文档的顺序。当然,检索顺序将是自然顺序或按选择的索引顺序排序。
如果您需要保留此顺序,则基本上有两个选项。
假设您要匹配文档中_id
的值,并将其数组作为$in
参数传递,例如[ 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;
}
}
)
这基本上依赖于发出的“key”值在输入数组中出现的“索引顺序”。
因此,在您已经确定了列表顺序的情况下,这些基本上是维护输入列表顺序到$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
,那么在这种情况下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,使用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
方法来比较文档的 _id
字段。我已经更新代码,使得 _id
比较更加明确。感谢您的提问。 - JohnnyHK当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
}
和JonnyHK的解决方案类似,如果您的客户端使用的是JavaScript,您可以使用map
和EcmaScript 2015中的Array.prototype.find
函数组合来重新排序从find
返回的文档:
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
回调中完成它以简化您的代码。find
对于数组中的每个元素(来自外部的map
)都要遍历数组。这是非常低效的,因为可以使用查找表的O(n)解决方案。 - curran我知道这个问题与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
对于任何新手,这是一个简短而优雅的解决方案,可保留在2021年和使用MongoDb 3.6(已测试)中出现的这种情况的顺序:
const idList = ['123', '124', '125']
const out = await db
.collection('YourCollection')
.aggregate([
// Change uuid to your `id` field
{ $match: { uuid: { $in: idList } } },
{
$project: {
uuid: 1,
date: 1,
someOtherFieldToPreserve: 1,
// Addding this new field called index
index: {
// If we want index to start from 1, add an dummy value to the beggining of the idList array
$indexOfArray: [[0, ...idList], '$uuid'],
// Otherwise if 0,1,2 is fine just use this line
// $indexOfArray: [idList, '$uuid'],
},
},
},
// And finally sort the output by our index
{ $sort: { index: 1 } },
])
$project
操作符中必须投影一些其他字段,我的意思是,你不能只投影订单。 - David Corral我知道这是一个旧的线程,但如果你只是返回数组中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});
};
您可以使用$or子句来保证顺序。
因此,请改用$or: [ _ids.map(_id => ({_id}))]
。