Не в MongoDB $в пункте гарантия того


при использовании MongoDB $in предложение, Всегда ли порядок возвращаемых документов соответствует порядку аргумента массива?

10 64

10 ответов:

как уже отмечалось, порядок аргументов в массиве предложения $in не отражает порядок извлечения документов. Это, конечно, будет естественный порядок или выбранный порядок индекса, как показано на рисунке.

Если вам нужно сохранить этот порядок, то вы в основном есть два варианта.

Итак, предположим, что вы соответствовали по значениям _id в ваших документах с массивом, который будет передан в $in как [ 4, 2, 8 ].

подход с использованием Aggregate


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 состояние, когда у вас уже есть этот список в определенном порядке.

если вы не хотите использовать aggregate, другое решение-использовать find а затем отсортировать результаты doc на стороне клиента с помощью 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 значения являются непримитивными типами, такими как ObjectIds, другой подход требуется как indexOf сравнивает по ссылке в этом случае.

если вы используете узел.js 4.х+, вы можете использовать Array#findIndex и ObjectID#equals чтобы справиться с этим, изменив 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); });
});

другой способ использования запроса агрегации, применимого только для MongoDB verion > 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 " возвращает позицию конкретного элемента в заданном массиве.

в основном addToSet оператор добавляет новый order поле для каждого документа, когда он находит его и это order поле представляет исходный порядок нашего массива, который мы предоставили. Затем мы просто сортируем документы на основе этого поля.

аналогично JonnyHK'S решение, вы можете изменить порядок документов, возвращенных из 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);
        });
    });

});

пара замечаний:

  • приведенный выше код использует драйвер узла Mongo и не Мангуста
  • The idArray массив ObjectId
  • я не проверял производительность этого метода против сортировки, но если вам нужно манипулировать каждым возвращаемым элементом (что довольно часто), вы можете сделать это в map обратный вызов для упрощения кода.

всегда? Никогда. Порядок всегда один и тот же: неопределенный (возможно, физический порядок, в котором хранятся документы). Если только вы не разберетесь.

Я знаю, что этот вопрос связан с Мангустом JS framework, но дублировать один является общим, поэтому я надеюсь, что размещение решения 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

Я знаю, что это старый поток, но если вы просто возвращаете значение идентификатора в массиве, вам, возможно, придется выбрать этот синтаксис. Поскольку я не мог получить значение 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 ()' -- просто значение?

вы можете гарантировать заказ с $ или предложением.

чтобы использовать .

это кодовое решение после получения результатов от 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++
        }
    }
}

простой способ упорядочить Результат после того, как 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
}