Чи гарантує замовлення $ MongoDB в пункті гарантування


Відповіді:


78

Як зазначалося, порядок аргументів у масиві речення $ in не відображає порядок отримання документів. Звичайно, це буде природний порядок або вибраний порядок індексу, як показано.

Якщо вам потрібно зберегти цей порядок, то в основному у вас є два варіанти.

Отже, скажімо, ви зіставляли значення значень _idу своїх документах із масивом, який буде передано в файл $inas [ 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умови, коли ви вже маєте цей список у визначеному порядку.


2
Чудова відповідь. Для тих , хто потребує в ній, а CoffeeScript версії тут
Лоуренс Джонс

1
@NeilLunn Я спробував підхід, використовуючи агрегат, але я отримую ідентифікатори та вагу. Чи знаєте ви, як отримати повідомлення (об'єкт)?
Хуанхо Лайнес Рече

1
@NeilLunn я насправді робив (це тут stackoverflow.com/questions/27525235/… ) Але єдиний коментар посилався тут, хоча я перевірив це перед тим, як розмістити своє запитання. Ви можете мені там допомогти? Дякую!
Хуанхо Лайнес Рече

1
знаю, що це старе, але я витратив багато часу на налагодження, чому inputs.indexOf () не збігався з this._id. Якщо ви просто повертаєте значення ідентифікатора об’єкта, можливо, вам доведеться вибрати цей синтаксис: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (входи [i])) {var order = i; }} emit (замовлення, {doc: this}); };
NoobSter

1
Ви можете використовувати "$ addFields" замість "$ project", якщо хочете мати також усі оригінальні поля
Джодо

39

Інший спосіб використання запиту агрегації, застосовний лише до 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, і він дозволяє "$ проектувати" нові поля до існуючих документів, не знаючи всіх інших існуючих полів. Новий вираз "$ indexOfArray" повертає позицію певного елемента в заданому масиві.

В основному addFieldsоператор додає нове orderполе до кожного документа, коли знаходить його, і це orderполе представляє початковий порядок нашого масиву, який ми надали. Потім ми просто сортуємо документи на основі цього поля.


чи є спосіб збереження масиву замовлення як змінної у запиті, тому у нас немає цього масивного запиту того самого масиву двічі, якщо масив великий?
Ethan SK

25

Якщо ви не хочете використовувати 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значення є непримітивними типами типу ObjectIds, потрібен інший підхід для 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); });
});

як функція рівності знає, щоб порівняти властивість id із id 'return a.equals (id);', оскільки a містить усі властивості, повернені для цієї моделі?
lboyel

1
@lboyel Я не мав на увазі, що він повинен бути таким розумним :-), але це спрацювало, оскільки він використовував Mongoose's Document#equalsдля порівняння з _idполем документа. Оновлено, щоб зробити _idпорівняння явним. Спасибі за запитання.
JohnnyHK

6

Подібно до рішення 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);
        });
    });

});

Пара приміток:

  • У наведеному вище коді використовується драйвер Mongo Node, а не Mongoose
  • Це idArrayмасивObjectId
  • Я не тестував ефективність цього методу проти сортування, але якщо вам потрібно маніпулювати кожним повернутим елементом (що досить часто), ви можете зробити це у mapзворотному виклику, щоб спростити свій код.

5

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

1
Це робить саме те, що мені потрібно, і набагато простіше, ніж коментар зверху.
Дірбро

@dyarbrough це рішення працює лише для запитів, які отримують усі документи (без обмеження або пропуску). Верхній коментар є більш складним, але працює для кожного сценарію.
marian2js

4

Я знаю, що це питання пов’язане з фреймворком 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

3

Завжди? Ніколи. Порядок завжди однаковий: невизначений (можливо, фізичний порядок зберігання документів). Якщо не відсортувати.


$naturalпорядок зазвичай є логічним, а не фізичним
Sammaye,

1

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


0

Ви можете гарантувати замовлення за допомогою $ або застереження.

Тому використовуйте $or: [ _ids.map(_id => ({_id}))]замість цього.


2
$orОбхідний шлях не працював з тих пір v2.6 .
JohnnyHK

0

Це рішення коду після отримання результатів з 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++
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.