web-dev-qa-db-fra.com

Upsert en vrac dans MongoDB en utilisant mangouste

Existe-t-il une option pour effectuer des upserts en masse avec de la mangouste? Donc, fondamentalement, avoir un tableau et insérer chaque élément s'il n'existe pas ou le mettre à jour s'il existe? (J'utilise les droits de douane _ids)

Lorsque j'utilise .insert, MongoDB renvoie une erreur E11000 pour les clés dupliquées (qui doivent être mises à jour). Insérer plusieurs nouveaux documents fonctionne bien cependant: 

var Users = self.db.collection('Users');

Users.insert(data, function(err){
            if (err) {
                callback(err);
            }
            else {
                callback(null);
            }
        });

Utiliser .save renvoie une erreur indiquant que le paramètre doit être un document unique:

Users.save(data, function(err){
   ...
}

Cette réponse suggère qu’il n’existe pas d’option de ce type, mais elle est spécifique à C # et a également déjà 3 ans. Alors je me demandais s'il y avait une option pour le faire en utilisant de la mangouste?

Je vous remercie!

26
user3122267

Pas dans "mangouste" en particulier, ou du moins pas encore au moment de l'écriture. À partir de la version 2.6, le shell MongoDB utilise en fait le "API d'opérations en bloc" "sous le capot" pour ainsi dire pour toutes les méthodes d'assistance générale. Dans son implémentation, il essaie d'abord de le faire, et si une ancienne version du serveur est détectée, il y a un "repli" sur l'implémentation héritée.

Actuellement, toutes les méthodes de mangouste utilisent l'implémentation "héritée" ou la réponse d'écriture préoccupante et les méthodes héritées de base. Cependant, il existe un accesseur .collection issu de tout modèle de mangouste donné qui accède essentiellement à "l'objet de collection" à partir du "pilote natif du nœud" sous-jacent sur lequel mangouste est implémenté:

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

 });

La principale difficulté réside dans le fait que les "méthodes mangoustes" sont conscientes du fait qu'une connexion peut ne pas encore être établie et de la "file d'attente" jusqu'à ce que cela soit terminé. Le pilote natif dans lequel vous "creusez" ne fait pas cette distinction.

Vous devez donc vraiment savoir que la connexion est établie d’une manière ou d’une autre. Mais vous pouvez utiliser les méthodes de pilote natif tant que vous faites attention à ce que vous faites.

22
Neil Lunn

Vous n'avez pas besoin de gérer la limite (1000) comme le suggère @ neil-lunn. Mongoose le fait déjà. J'ai utilisé sa grande réponse comme base pour cette implémentation complète basée sur Promise et cet exemple:

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

// });

Cela a l'avantage de fermer la connexion quand c'est fait, d'afficher les erreurs si cela vous importe, mais de les ignorer sinon (les rappels d'erreur dans Promises sont facultatifs.) C'est aussi très rapide. Je laisse juste ça ici pour partager mes découvertes. Vous pouvez supprimer le commentaire de la substance eztv si vous souhaitez enregistrer tous les shows eztv dans une base de données, à titre d'exemple.

18
konsumer

J'ai publié un plugin pour Mongoose qui expose une méthode statique upsertMany pour effectuer des opérations upsert en bloc avec une interface de promesse.

Un avantage supplémentaire de l'utilisation de ce plug-in par rapport à l'initialisation de votre propre opération en bloc sur la collection sous-jacente est que ce plug-in convertit vos données en premier dans le modèle Mongoose, puis en objets simples avant l'upsert. Cela garantit que la validation du schéma Mongoose est appliquée et que les données sont dépeuplées et adaptées pour une insertion brute.

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

J'espère que ça aide!

3
Adam Reis

Si vous ne voyez pas les méthodes en bloc dans votre base de données db.collection, c'est-à-dire que vous obtenez une erreur sous l'effet de La variable xxx n'a pas de méthode: initializeOrderedBulkOp ()

Essayez de mettre à jour votre version de mangouste. Apparemment, les anciennes versions de mangouste ne passent pas par toutes les méthodes mongo db.collection sous-jacentes. 

npm installez la mangouste

s'en est occupé pour moi.

1
zstew

Je devais y parvenir récemment tout en stockant des produits dans mon application de commerce électronique. Mon base de données était utilisée jusqu'à expiration car je devais insérer 10 000 articles toutes les 4 heures. Une option pour moi était de définir socketTimeoutMS et connectTimeoutMS en mangouste lors de la connexion à la base de données, mais je me sentais plutôt mal et je ne voulais pas manipuler les valeurs par défaut du délai de connexion. Je constate également que la solution de @neil lunn adopte une approche de synchronisation simple consistant à prendre un module à l’intérieur de la boucle for. Voici une version asynchrone de la mienne qui, je crois, fait beaucoup mieux le travail.

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')
                    }
                }
            }
        )
    }
}
0
PirateApp