簡體   English   中英

使用 mongoose 在 MongoDB 中批量更新插入

[英]Bulk upsert in MongoDB using mongoose

是否有任何選項可以使用貓鼬執行批量更新插入? 所以基本上有一個數組並在每個元素不存在時插入它,如果存在則更新它? (我正在使用海關 _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){
   ...
}

這個答案表明沒有這樣的選項,但是它是特定於 C# 的,並且已經有 3 年歷史了。 所以我想知道是否有任何選項可以使用貓鼬來做到這一點?

謝謝!

具體不是“貓鼬”,或者至少在寫作時還沒有。 從 2.6 版開始,MongoDB shell 實際上在“幕后”中使用了“批量操作 API ”,因為它用於所有通用輔助方法。 在它的實現中,它首先嘗試執行此操作,如果檢測到舊版本服務器,則會“回退”到舊版實現。

所有 mongoose 方法“當前”都使用“遺留”實現或寫關注響應和基本遺留方法。 但是任何給定的.collection模型都有一個.collection訪問器,它本質上是從底層的“節點本機驅動程序”訪問“集合對象”,在該驅動程序上實現了.collection本身:

 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
        });

 });

主要的問題是“貓鼬方法”實際上意識到可能實際上還沒有建立連接並在完成之前“排隊”。 您正在“深入研究”的本機驅動程序並沒有做出這種區分。

所以你真的必須意識到連接是以某種方式或形式建立的。 但是您可以使用本機驅動程序方法,只要您小心自己在做什么。

您不需要像@neil-lunn 建議的那樣管理限制(1000)。 貓鼬已經這樣做了。 我使用他的出色回答作為這個完整的基於 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);
  });

// });

這樣做的好處是在連接完成后關閉連接,如果您關心,則顯示任何錯誤,如果不關心,則忽略它們(Promise 中的錯誤回調是可選的。)它也非常快。 只是把這個留在這里分享我的發現。 例如,如果要將所有 eztv 節目保存到數據庫中,您可以取消注釋 eztv 內容。

await Model.bulkWrite(docs.map(doc => ({
    updateOne: {
        filter: {id: doc.id},
        update: doc,
        upsert: true
    }
})))


或者更詳細:

const bulkOps = docs.map(doc => ({
    updateOne: {
        filter: {id: doc.id},
        update: doc,
        upsert: true
    }
}))

Model.bulkWrite(bulkOps)
        .then(bulkWriteOpResult => console.log('BULK update OK:', bulkWriteOpResult))
        .catch(err => console.error('BULK update error:', err))

https://stackoverflow.com/a/60330161/5318303

我已經為 Mongoose 發布了一個插件,它公開了一個靜態upsertMany方法來執行帶有承諾接口的批量 upsert 操作。

使用這個插件而不是在底層集合上初始化你自己的批量操作的另一個好處是,這個插件首先將你的數據轉換為 Mongoose 模型的數據,然后在 upsert 之前轉換回普通對象。 這可確保應用 Mongoose 模式驗證,並減少數據填充並適合原始插入。

https://github.com/meanie/mongoose-upsert-many https://www.npmjs.com/package/@meanie/mongoose-upsert-many

希望能幫助到你!

如果您沒有在 db.collection 中看到批量方法,即您收到 xxx 變量沒有方法的影響的錯誤: initializeOrderedBulkOp()

嘗試更新您的貓鼬版本。 顯然,較舊的 mongoose 版本不會通過所有底層 mongo db.collection 方法。

npm 安裝貓鼬

為我照顧它。

我最近必須在我的電子商務應用程序中存儲產品時實現這一目標。 我的數據庫曾經超時,因為我必須每 4 小時更新 10000 個項目。 對我來說,一種選擇是在連接到數據庫時在 mongoose 中設置 socketTimeoutMS 和 connectTimeoutMS,但它有點感覺很hacky,我不想操縱數據庫的連接超時默認值。 我還看到@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')
                    }
                }
            }
        )
    }
}

您可以使用貓鼬的 Model.bulkWrite()

const res = await Character.bulkWrite([
  {
    updateOne: {
      filter: { name: 'Will Riker' },
      update: { age: 29 },
      upsert: true
    }
  },
  {
    updateOne: {
      filter: { name: 'Geordi La Forge' },
      update: { age: 29 },
      upsert: true
    }
  }
]);

參考: https : //masteringjs.io/tutorials/mongoose/upsert

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM