Массовое увеличение в MongoDB с использованием мангуста
Есть ли возможность выполнять массовые upserts с мангуста? Итак, в основном наличие массива и вставка каждого элемента, если он не существует, или обновлять его, если он существует? (Я использую таможенные _ids)
Когда я использую .insert, MongoDB возвращает ошибку E11000 для дубликатов ключей (которые необходимо обновить). Вставка нескольких новых документов отлично работает:
var Users = self.db.collection('Users');
Users.insert(data, function(err){
if (err) {
callback(err);
}
else {
callback(null);
}
});
Использование .save возвращает ошибку, что параметр должен быть единственным документом:
Users.save(data, function(err){
...
}
Этот ответ предполагает, что такого варианта нет, однако он специфичен для С#, а также уже 3 года. Поэтому мне было интересно, есть ли возможность сделать это с помощью мангуста?
Спасибо!
Ответы
Ответ 1
Не в "мангусте" конкретно или, по крайней мере, еще не написано. В оболочке MongoDB, начиная с версии 2.6, фактически используется "Массовый API операций" под капотом, как и для всех общих вспомогательных методов. В этом случае она пытается сделать это в первую очередь, и если обнаружен старый сервер версии, тогда существует "резервное копирование" унаследованной реализации.
Все методы мангуста "в настоящее время" используют "устаревшую" реализацию или реакцию на реакцию на запись и основные унаследованные методы. Но есть аксессуар .collection
от любой данной модели мангуста, которая по существу обращается к "объекту коллекции" из основного "node родного драйвера", на котором сам мангуст реализован:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var sampleSchema = new Schema({},{ "strict": false });
var Sample = mongoose.model( "Sample", sampleSchema, "sample" );
mongoose.connection.on("open", function(err,conn) {
var bulk = Sample.collection.initializeOrderedBulkOp();
var counter = 0;
// representing a long loop
for ( var x = 0; x < 100000; x++ ) {
bulk.find(/* some search */).upsert().updateOne(
/* update conditions */
});
counter++;
if ( counter % 1000 == 0 )
bulk.execute(function(err,result) {
bulk = Sample.collection.initializeOrderedBulkOp();
});
}
if ( counter % 1000 != 0 )
bulk.execute(function(err,result) {
// maybe do something with result
});
});
Основной улов, заключающийся в том, что "методы мангуста" фактически знают, что соединение фактически не может быть сделано еще и "очередь", пока это не будет завершено. Нативный драйвер, который вы "копаете", не делает этого различия.
Итак, вам действительно нужно знать, что соединение установлено каким-то образом или формой. Но вы можете использовать собственные методы драйвера, если будете осторожны с тем, что вы делаете.
Ответ 2
Вам не нужно управлять лимитом (1000), как предложил @neil-lunn. Мангуст делает это уже. Я использовал его большой ответ в качестве основы для этой полной реализации и примера на основе Promise:
var Promise = require('bluebird');
var mongoose = require('mongoose');
var Show = mongoose.model('Show', {
"id": Number,
"title": String,
"provider": {'type':String, 'default':'eztv'}
});
/**
* Atomic connect Promise - not sure if I need this, might be in mongoose already..
* @return {Priomise}
*/
function connect(uri, options){
return new Promise(function(resolve, reject){
mongoose.connect(uri, options, function(err){
if (err) return reject(err);
resolve(mongoose.connection);
});
});
}
/**
* Bulk-upsert an array of records
* @param {Array} records List of records to update
* @param {Model} Model Mongoose model to update
* @param {Object} match Database field to match
* @return {Promise} always resolves a BulkWriteResult
*/
function save(records, Model, match){
match = match || 'id';
return new Promise(function(resolve, reject){
var bulk = Model.collection.initializeUnorderedBulkOp();
records.forEach(function(record){
var query = {};
query[match] = record[match];
bulk.find(query).upsert().updateOne( record );
});
bulk.execute(function(err, bulkres){
if (err) return reject(err);
resolve(bulkres);
});
});
}
/**
* Map function for EZTV-to-Show
* @param {Object} show EZTV show
* @return {Object} Mongoose Show object
*/
function mapEZ(show){
return {
title: show.title,
id: Number(show.id),
provider: 'eztv'
};
}
// if you are not using EZTV, put shows in here
var shows = []; // giant array of {id: X, title: "X"}
// var eztv = require('eztv');
// eztv.getShows({}, function(err, shows){
// if(err) return console.log('EZ Error:', err);
// var shows = shows.map(mapEZ);
console.log('found', shows.length, 'shows.');
connect('mongodb://localhost/tv', {}).then(function(db){
save(shows, Show).then(function(bulkRes){
console.log('Bulk complete.', bulkRes);
db.close();
}, function(err){
console.log('Bulk Error:', err);
db.close();
});
}, function(err){
console.log('DB Error:', err);
});
// });
У этого есть бонус закрытия соединения, когда он был сделан, отображая любые ошибки, если вам это нравится, но игнорируя их, если нет (ошибки обратного вызова в Promises являются необязательными.) Это также очень быстро. Просто оставив это здесь, чтобы поделиться своими выводами. Вы можете раскомментировать материал eztv, если хотите сохранить все eztv-шоу в базе данных в качестве примера.
Ответ 3
Если вы не видите массовые методы в своем db.collection, т.е. вы получаете сообщение об ошибке
переменная xxx не имеет метода: initializeOrderedBulkOp()
Попробуйте обновить версию mongoose. По-видимому, старые версии mongoose не проходят через все основные методы mongo db.collection.
npm install mongoose
позаботился об этом для меня.
Ответ 4
Мне нужно было это сделать недавно, сохраняя продукты в своем приложении для электронной коммерции. Моя база данных использовалась для тайм-аута, так как мне приходилось поднимать 10000 предметов каждые 4 часа. Один из вариантов для меня заключался в том, чтобы установить socketTimeoutMS и connectTimeoutMS в mongoose при подключении к базе данных, но это сотрясало хакки, и я не хотел манипулировать значениями тайм-аута соединения в базе данных. Я также вижу, что решение @neil lunn использует простой синхронный подход для взятия модуля внутри цикла for. Вот моя асинхронная версия, которая, я считаю, делает работу намного лучше
let BATCH_SIZE = 500
Array.prototype.chunk = function (groupsize) {
var sets = [];
var chunks = this.length / groupsize;
for (var i = 0, j = 0; i < chunks; i++ , j += groupsize) {
sets[i] = this.slice(j, j + groupsize);
}
return sets;
}
function upsertDiscountedProducts(products) {
//Take the input array of products and divide it into chunks of BATCH_SIZE
let chunks = products.chunk(BATCH_SIZE), current = 0
console.log('Number of chunks ', chunks.length)
let bulk = models.Product.collection.initializeUnorderedBulkOp();
//Get the current time as timestamp
let timestamp = new Date(),
//Keep track of the number of items being looped
pendingCount = 0,
inserted = 0,
upserted = 0,
matched = 0,
modified = 0,
removed = 0,
//If atleast one upsert was performed
upsertHappened = false;
//Call the load function to get started
load()
function load() {
//If we have a chunk to process
if (current < chunks.length) {
console.log('Current value ', current)
for (let i = 0; i < chunks[current].length; i++) {
//For each item set the updated timestamp to the current time
let item = chunks[current][i]
//Set the updated timestamp on each item
item.updatedAt = timestamp;
bulk.find({ _id: item._id })
.upsert()
.updateOne({
"$set": item,
//If the item is being newly inserted, set a created timestamp on it
"$setOnInsert": {
"createdAt": timestamp
}
})
}
//Execute the bulk operation for the current chunk
bulk.execute((error, result) => {
if (error) {
console.error('Error while inserting products' + JSON.stringify(error))
next()
}
else {
//Atleast one upsert has happened
upsertHappened = true;
inserted += result.nInserted
upserted += result.nUpserted
matched += result.nMatched
modified += result.nModified
removed += result.nRemoved
//Move to the next chunk
next()
}
})
}
else {
console.log("Calling finish")
finish()
}
}
function next() {
current++;
//Reassign bulk to a new object and call load once again on the new object after incrementing chunk
bulk = models.Product.collection.initializeUnorderedBulkOp();
setTimeout(load, 0)
}
function finish() {
console.log('Inserted ', inserted + ' Upserted ', upserted, ' Matched ', matched, ' Modified ', modified, ' Removed ', removed)
//If atleast one chunk was inserted, remove all items with a 0% discount or not updated in the latest upsert
if (upsertHappened) {
console.log("Calling remove")
remove()
}
}
/**
* Remove all the items that were not updated in the recent upsert or those items with a discount of 0
*/
function remove() {
models.Product.remove(
{
"$or":
[{
"updatedAt": { "$lt": timestamp }
},
{
"discount": { "$eq": 0 }
}]
}, (error, obj) => {
if (error) {
console.log('Error while removing', JSON.stringify(error))
}
else {
if (obj.result.n === 0) {
console.log('Nothing was removed')
} else {
console.log('Removed ' + obj.result.n + ' documents')
}
}
}
)
}
}
Ответ 5
Я выпустил плагин для Mongoose, который предоставляет статический метод upsertMany
для выполнения операций массового upsert с интерфейсом обещания.
Дополнительным преимуществом использования этого плагина над инициализацией вашего собственного массового оп в базовой коллекции является то, что этот плагин сначала конвертирует ваши данные в модель Mongoose, а затем обратно к простым объектам до восстановления. Это гарантирует, что проверка схемы Mongoose применяется, и данные депопулируются и подходят для сырой вставки.
https://github.com/meanie/mongoose-upsert-many
https://www.npmjs.com/package/@meanie/mongoose-upsert-many
Надеюсь, что это поможет!