Как сгруппировать данные в mongo каждый x документ? [Дубликат]

Большинство ответов здесь дают полезные советы, когда у вас есть одна операция async, но иногда это возникает, когда вам нужно выполнить асинхронную операцию для каждой записи в массиве или другом списке подобная структура. Искушение состоит в том, чтобы сделать это:

// WRONG
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log(results); // E.g., using them, returning them, etc.

Пример:

// WRONG
var theArray = [1, 2, 3];
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log("Results:", results); // E.g., using them, returning them, etc.

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
  max-height: 100% !important;
}

Причина, 't работа заключается в том, что обратные вызовы из doSomethingAsync еще не запущены к тому моменту, когда вы пытаетесь использовать результаты.

Итак, если у вас есть массив (или список какого-то типа) и хотите выполнять асинхронные операции для каждой записи, у вас есть два варианта: выполнять операции параллельно (перекрывающиеся) или последовательно (последовательно последовательно).

Параллельный

Вы могут запускать все из них и отслеживать количество обратных вызовов, которые вы ожидаете, а затем использовать результаты, когда вы получили много обратных вызовов:

var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", results); // E.g., using the results
        }
    });
});

Пример:

var theArray = [1, 2, 3];
var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", results); // E.g., using the results
        }
    });
});

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
  max-height: 100% !important;
}

(Мы могли бы покончить с expecting и просто использовать results.length === theArray.length, но это оставляет нам открытым возможность того, что theArray изменяется, пока вызовы выдающиеся ...)

Обратите внимание, как мы используем index из forEach, чтобы сохранить результат в results в том же положении, что и запись это относится, даже если результаты a (поскольку асинхронные вызовы не обязательно завершаются в том порядке, в котором они были запущены).

Но что, если вам нужно вернуть те результаты от функции? Как указывали другие ответы, вы не можете; вы должны принять вашу функцию и вызвать обратный вызов (или вернуть Promise ). Вот вариант обратного вызова:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

Пример:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith([1, 2, 3], function(results) {
    console.log("Results:", results);
});

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
  max-height: 100% !important;
}

Или вот версия, возвращающая Promise вместо:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Конечно, если doSomethingAsync передал нам ошибки, мы использовали бы reject, чтобы отклонить обещание, когда мы получили сообщение об ошибке.)

Пример:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", results);
});

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
  max-height: 100% !important;
}

(Или, альтернативно, вы можете сделать обертку для doSomethingAsync, который возвращает обещание, а затем сделайте следующее ...)

Если doSomethingAsync дает вам Promise , вы можете использовать Promise.all :

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry, function(result) {
            results.push(result);
        });
    }));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Пример:

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry, function(result) {
            results.push(result);
        });
    }));
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", results);
});

function doSomethingAsync(value) {
    console.log("Starting async operation for " + value);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log("Completing async operation for " + value);
            resolve(value * 2);
        }, Math.floor(Math.random() * 200));
    });
}
.as-console-wrapper {
  max-height: 100% !important;
}

Обратите внимание, что Promise.all решает свое обещание с помощью массива результатов всех обещаний, которые вы даете ему, когда все они разрешены, или отвергает его обещание, когда первый обещаний, которые вы ему даете, отклоняет.

Серия

Предположим, вы не хотите, чтобы операции были параллельны? Если вы хотите запускать их один за другим, вам нужно дождаться завершения каждой операции до начала следующего. Вот пример функции, которая делает это, и вызывает обратный вызов с результатом:

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

(Поскольку мы выполняем работу последовательно, мы можем просто использовать results.push(result), так как мы знаю, что мы не получим результаты не в порядке. В приведенном выше примере мы могли бы использовать results[index] = result;, но в некоторых из следующих примеров у нас нет индекса для использования.)

Пример:

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith([1, 2, 3], function(results) {
    console.log("Results:", results);
});

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
  max-height: 100% !important;
}

(Или снова создайте обертку для doSomethingAsync, которая дает вам обещание и выполните ниже ...)

Если doSomethingAsync дает вам обещание, если вы можете использовать синтаксис ES2017 + (возможно, с транспилером, например Babel ), вы можете использовать функцию async с помощью for-of и await :

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Пример:

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", results);
});

function doSomethingAsync(value) {
    console.log("Starting async operation for " + value);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log("Completing async operation for " + value);
            resolve(value * 2);
        }, Math.floor(Math.random() * 200));
    });
}
.as-console-wrapper {
  max-height: 100% !important;
}

Если вы не можете использовать синтаксис ES2017 + (пока), вы можете использовать вариацию на Обещают уменьшить шаблон (это сложнее, чем обычное сокращение Promise, потому что мы не передаем результат от одного к другому, а вместо gat

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Пример:

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", results);
});

function doSomethingAsync(value) {
    console.log("Starting async operation for " + value);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log("Completing async operation for " + value);
            resolve(value * 2);
        }, Math.floor(Math.random() * 200));
    });
}
.as-console-wrapper {
  max-height: 100% !important;
}

.. , который менее громоздк с функциями стрелок ES2015 + :

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Пример:

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", results);
});

function doSomethingAsync(value) {
    console.log("Starting async operation for " + value);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log("Completing async operation for " + value);
            resolve(value * 2);
        }, Math.floor(Math.random() * 200));
    });
}
.as-console-wrapper {
  max-height: 100% !important;
}

26
задан Neil Lunn 8 November 2014 в 07:31
поделиться

4 ответа

Мне нравится другой ответ здесь, и в основном для использования математики даты вместо операторов даты агрегации, которые, хотя полезно, также могут быть немного неясными.

Единственное, что я хочу добавить здесь, это то, что вы также можете вернуть объект Date из структуры агрегации таким подходом, в отличие от «числовой» метки времени. Это лишь небольшая дополнительная математика на тех же принципах, используя $add :

db.collection.aggregate([
    { "$group": {
        "_id": {
            "$add": [
                { "$subtract": [
                    { "$subtract": [ "$current_date", new Date(0) ] },
                    { "$mod": [ 
                        { "$subtract": [ "$current_date", new Date(0) ] },
                        1000 * 60 * 15
                    ]}
                ] },
                new Date(0)
            ]
        },
        "count": { "$sum": 1 }
    }}
])

Контексты Date(0) в JavaScript здесь представляют ту же самую «эпоху» в более короткая форма, так как 0 миллисекунд от эпохи - эпоха. Но главное, что когда «добавление» к другому объекту даты BSON выполняется с числовым идентификатором, тогда обратное к описанному условию истинно, и конечный результат на самом деле теперь является Date.

Все драйверы вернут родной тип Date на свой язык с помощью этого подхода.

14
ответ дан Blakes Seven 27 August 2018 в 17:21
поделиться

Существует несколько способов сделать это.

Первый из них - Операторы агрегирования даты , которые позволяют вам анализировать значения «даты» в документах. В частности, для «группировки» в качестве основного намерения:

db.collection.aggregate([
  { "$group": {
    "_id": {
      "year": { "$year": "$created_at" },
      "dayOfYear": { "$dayOfYear": "$created_at" },
      "hour": { "$hour": "$created_at" },
      "interval": {
        "$subtract": [ 
          { "$minute": "$created_at" },
          { "$mod": [{ "$minute": "$created_at"}, 15] }
        ]
      }
    }},
    "count": { "$sum": 1 }
  }}
])

Второй способ заключается в использовании небольшого трюка, когда объект даты вычитается (или другая прямая математическая операция) из другого объекта даты, тогда результатом является числовое значение, представляющее миллисекунду времени между двумя объектами. Таким образом, просто используя дату эпохи, вы получаете представление в миллисекундах эпохи. Затем используйте математику даты для интервала:

db.collection.aggregate([
    { "$group": {
        "_id": {
            "$subtract": [
                { "$subtract": [ "$created_at", new Date("1970-01-01") ] },
                { "$mod": [ 
                    { "$subtract": [ "$created_at", new Date("1970-01-01") ] },
                    1000 * 60 * 15
                ]}
            ]
        },
        "count": { "$sum": 1 }
    }}
])

. Это зависит от того, какой формат вывода вы хотите для интервала группировки. Оба они в основном представляют одно и то же и имеют достаточные данные для повторной конструкции в качестве объекта «даты» в вашем коде.

Вы можете поместить все что угодно в разделе «оператор группировки» после группировки _id. Я просто использую основной пример «подсчета» вместо любого реального заявления от себя относительно того, что вы действительно хотите сделать.


MongoDB 4.x и Upwards

Были некоторые дополнения к операторам агрегирования даты с момента написания оригинала, но из MongoDB 4.0 будет фактическое «реальное литье типов» в отличие от основных математических приемов, сделанных здесь с преобразованием даты BSON.

Для Например, мы можем использовать $toLong и $toDate в качестве новых помощников:

db.collection.aggregate([
  { "$group": {
    "_id": {
      "$toDate": {
        "$subtract": [
          { "$toLong": "$created_at" },
          { "$mod": [ { "$toLong": "$created_at" }, 1000 * 60 * 15 ] }
        ]
      }
    },
    "count": { "$sum": 1 }
  }}
])

Это немного короче и не требует определения внешняя BSON Дата для значения «epoch» как константа при определении конвейера, поэтому она вполне согласуется для всех языковых реализаций.

Это всего лишь два из «вспомогательных» методов преобразования типов, которые все привязываются к метод $convert , который является «более длинной» формой реализации, позволяющей выполнять пользовательскую обработку на null или ошибку в преобразовании.

Это возможно даже при таком литье получить информацию о Date из ObjectId первичного ключа, так как это было бы надежным источником даты создания:

db.collection.aggregate([
  { "$group": {
    "_id": {
      "$toDate": {
        "$subtract": [
          { "$toLong": { "$toDate": "$_id" }  },
          { "$mod": [ { "$toLong": { "$toDate": "_id" } }, 1000 * 60 * 15 ] }
        ]
      }
    },
    "count": { "$sum": 1 }
  }}
])

Таким образом, «типы литья» с таким преобразованием могут быть довольно мощным инструментом.

79
ответ дан Neil Lunn 27 August 2018 в 17:21
поделиться

Еще один полезный способ:

db.collection.aggregate([
  {$group: {
    _id: { 
      overallTime: { 
        $dateToString: { format: "%Y-%m-%dT%H", date: "$created_at" } 
      },
      interval: { $trunc: { $divide: [{ $minute: "$created_at" }, 15 ]}}
    },
  }},
])

И проще в секундах min , hour , day :

var format = "%Y-%m-%dT%H:%M"; // 1 min
var format = "%Y-%m-%dT%H"; // 1 hour
var format = "%Y-%m-%d"; // 1 day

db.collection.aggregate([
  {$group: {
    _id: { $dateToString: { format: format, date: "$created_at" } },
  }},
])
3
ответ дан Sergey Reutskiy 27 August 2018 в 17:21
поделиться

Немного красивее для mongodb.version () 3.0

db.collection.aggregate([
    {$match: {created_at:{$exists:1}}},
    {$group: {
        _id: {$add:[
            {$dayOfYear: "$created_at" },
            {$multiply: [{$year: "$created_at"}, 1000]}
        ]},
        count: {$sum: 1 }
    }},
    {$sort:{_id:-1}}
])
7
ответ дан Stierlitz 27 August 2018 в 17:21
поделиться
Другие вопросы по тегам:

Похожие вопросы: