MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

insertMany gestisce gli errori duplicati

Ebbene, in realtà, MongoDB di "predefinito" non creerà dati duplicati in cui è coinvolta una "chiave univoca", di cui _id ( alias da mangusta come id , ma ignorato da insertMany() quindi devi stare attento ), ma c'è una storia molto più ampia in questo di cui devi essere davvero consapevole .

Il problema di base qui è che sia l'implementazione "mangusta" di insertMany() così come il driver sottostante sono attualmente un po' "storditi" per usare un eufemismo. Questa è una piccola incoerenza nel modo in cui il driver passa la risposta all'errore nelle operazioni "bulk" e questo è in realtà aggravato da "mangusta" che non "cerca nel posto giusto" per le informazioni sull'errore effettivo.

La parte "rapida" che ti manca è l'aggiunta di { ordered: false } all'operazione "Bulk" di cui .insertMany() avvolge semplicemente una chiamata a. L'impostazione di questa opzione garantisce che il "batch" di richieste venga effettivamente inviato "completamente" e non interrompa l'esecuzione quando si verifica un errore.

Ma poiché "mangusta" non gestisce questo molto bene (né il driver "coerentemente"), in realtà dobbiamo cercare possibili "errori" nella "risposta" piuttosto che il risultato "errore" del callback sottostante.

A titolo di dimostrazione:

const mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug',true);

const uri = 'mongodb://localhost/test',
      options = { useMongoClient: true };

const songSchema = new Schema({
  _id: Number,
  name: String
});

const Song = mongoose.model('Song', songSchema);

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

let docs = [
  { _id: 1, name: "something" },
  { _id: 2, name: "something else" },
  { _id: 2, name: "something else entirely" },
  { _id: 3, name: "another thing" }
];

mongoose.connect(uri,options)
  .then( () => Song.remove() )
  .then( () =>
    new Promise((resolve,reject) =>
      Song.collection.insertMany(docs,{ ordered: false },function(err,result) {
        if (result.hasWriteErrors()) {
          // Log something just for the sake of it
          console.log('Has Write Errors:');
          log(result.getWriteErrors());

          // Check to see if something else other than a duplicate key, and throw
          if (result.getWriteErrors().some( error => error.code != 11000 ))
            reject(err);
        }
        resolve(result);    // Otherwise resolve
      })
    )
  )
  .then( results => { log(results); return true; } )
  .then( () => Song.find() )
  .then( songs => { log(songs); mongoose.disconnect() })
  .catch( err => { console.error(err); mongoose.disconnect(); } );

O forse un po' più bello dato che l'attuale LTS node.js ha async/await :

const mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug',true);

const uri = 'mongodb://localhost/test',
      options = { useMongoClient: true };

const songSchema = new Schema({
  _id: Number,
  name: String
});

const Song = mongoose.model('Song', songSchema);

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

let docs = [
  { _id: 1, name: "something" },
  { _id: 2, name: "something else" },
  { _id: 2, name: "something else entirely" },
  { _id: 3, name: "another thing" }
];

(async function() {

  try {
    const conn = await mongoose.connect(uri,options);

    await Song.remove();

    let results = await new Promise((resolve,reject) => {
      Song.collection.insertMany(docs,{ ordered: false },function(err,result) {
        if (result.hasWriteErrors()) {
          // Log something just for the sake of it
          console.log('Has Write Errors:');
          log(result.getWriteErrors());

          // Check to see if something else other than a duplicate key, then throw
          if (result.getWriteErrors().some( error => error.code != 11000 ))
            reject(err);
        }
        resolve(result);    // Otherwise resolve

      });
    });

    log(results);

    let songs = await Song.find();
    log(songs);

  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }


})()

In ogni caso, ottieni lo stesso risultato mostrando che le scritture sono entrambe continuate e che "ignoriamo" rispettosamente gli errori relativi a una "chiave duplicata" o altrimenti noti come codice di errore 11000 . La "gestione sicura" è che ci aspettiamo tali errori e li scartiamo cercando la presenza di "altri errori" a cui potremmo semplicemente voler prestare attenzione. Vediamo anche che il resto del codice continua ed elenca tutti i documenti effettivamente inseriti eseguendo un successivo .find() chiama:

Mongoose: songs.remove({}, {})
Mongoose: songs.insertMany([ { _id: 1, name: 'something' }, { _id: 2, name: 'something else' }, { _id: 2, name: 'something else entirely' }, { _id: 3, name: 'another thing' } ], { ordered: false })
Has Write Errors:
[
  {
    "code": 11000,
    "index": 2,
    "errmsg": "E11000 duplicate key error collection: test.songs index: _id_ dup key: { : 2 }",
    "op": {
      "_id": 2,
      "name": "something else entirely"
    }
  }
]
{
  "ok": 1,
  "writeErrors": [
    {
      "code": 11000,
      "index": 2,
      "errmsg": "E11000 duplicate key error collection: test.songs index: _id_ dup key: { : 2 }",
      "op": {
        "_id": 2,
        "name": "something else entirely"
      }
    }
  ],
  "writeConcernErrors": [],
  "insertedIds": [
    {
      "index": 0,
      "_id": 1
    },
    {
      "index": 1,
      "_id": 2
    },
    {
      "index": 2,
      "_id": 2
    },
    {
      "index": 3,
      "_id": 3
    }
  ],
  "nInserted": 3,
  "nUpserted": 0,
  "nMatched": 0,
  "nModified": 0,
  "nRemoved": 0,
  "upserted": [],
  "lastOp": {
    "ts": "6485492726828630028",
    "t": 23
  }
}
Mongoose: songs.find({}, { fields: {} })
[
  {
    "_id": 1,
    "name": "something"
  },
  {
    "_id": 2,
    "name": "something else"
  },
  {
    "_id": 3,
    "name": "another thing"
  }
]

Allora perché questo processo? Il motivo è che la chiamata sottostante restituisce effettivamente sia err e result come mostrato nell'implementazione della callback, ma c'è un'incoerenza in ciò che viene restituito. Il motivo principale per farlo è che vedi effettivamente il "risultato", che non solo ha il risultato dell'operazione riuscita, ma anche il messaggio di errore.

Insieme alle informazioni sull'errore c'è il nInserted: 3 indicando quanti del "lotto" sono stati effettivamente scritti. Puoi praticamente ignorare gli insertedIds qui poiché questo particolare test ha comportato effettivamente la fornitura di _id valori. Nel caso in cui una proprietà diversa avesse il vincolo "unico" che ha causato l'errore, gli unici valori qui sarebbero quelli delle scritture effettivamente riuscite. Un po' fuorviante, ma facile da testare e vedere di persona.

Come affermato, il problema è l'"incoerenza" che può essere dimostrata con un altro esempio ( async/await solo per brevità dell'elenco):

const mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug',true);

const uri = 'mongodb://localhost/test',
      options = { useMongoClient: true };

const songSchema = new Schema({
  _id: Number,
  name: String
});

const Song = mongoose.model('Song', songSchema);

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

let docs = [
  { _id: 1, name: "something" },
  { _id: 2, name: "something else" },
  { _id: 2, name: "something else entirely" },
  { _id: 3, name: "another thing" },
  { _id: 4, name: "different thing" },
  //{ _id: 4, name: "different thing again" }
];

(async function() {

  try {
    const conn = await mongoose.connect(uri,options);

    await Song.remove();

    try {
      let results = await Song.insertMany(docs,{ ordered: false });
      console.log('what? no result!');
      log(results);   // not going to get here
    } catch(e) {
      // Log something for the sake of it
      console.log('Has write Errors:');

      // Check to see if something else other than a duplicate key, then throw
      // Branching because MongoError is not consistent
      if (e.hasOwnProperty('writeErrors')) {
        log(e.writeErrors);
        if(e.writeErrors.some( error => error.code !== 11000 ))
          throw e;
      } else if (e.code !== 11000) {
        throw e;
      } else {
        log(e);
      }

    }

    let songs = await Song.find();
    log(songs);

  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }


})()

Tutto più o meno la stessa cosa, ma presta attenzione a come l'errore viene registrato qui:

Has write Errors:
{
  "code": 11000,
  "index": 2,
  "errmsg": "E11000 duplicate key error collection: test.songs index: _id_ dup key: { : 2 }",
  "op": {
    "__v": 0,
    "_id": 2,
    "name": "something else entirely"
  }
}

Nota che non ci sono informazioni di "successo", anche se otteniamo la stessa continuazione dell'elenco eseguendo il successivo .find() e ottenere l'output. Questo perché l'implementazione agisce solo sull'"errore lanciato" in caso di rifiuto e non passa mai attraverso il result effettivo parte. Quindi, anche se abbiamo chiesto ordered: false , non otteniamo le informazioni su ciò che è stato completato a meno che non eseguiamo il wrapping del callback e implementiamo la logica noi stessi, come mostrato negli elenchi iniziali.

L'altra importante "incoerenza" si verifica quando c'è "più di un errore". Quindi decommentando il valore aggiuntivo per _id: 4 ci dà:

Has write Errors:
[
  {
    "code": 11000,
    "index": 2,
    "errmsg": "E11000 duplicate key error collection: test.songs index: _id_ dup key: { : 2 }",
    "op": {
      "__v": 0,
      "_id": 2,
      "name": "something else entirely"
    }
  },
  {
    "code": 11000,
    "index": 5,
    "errmsg": "E11000 duplicate key error collection: test.songs index: _id_ dup key: { : 4 }",
    "op": {
      "__v": 0,
      "_id": 4,
      "name": "different thing again"
    }
  }
]

Qui puoi vedere il codice "ramificato" sulla presenza di e.writeErrors , che non esiste quando ce n'è uno errore. Al contrario, la precedente response l'oggetto ha entrambi hasWriteErrors() e getWriteErrors() metodi, indipendentemente dalla presenza di qualsiasi errore. Quindi questa è l'interfaccia più coerente e il motivo per cui dovresti usarla invece di ispezionare err risposta da solo.

Correzioni del driver MongoDB 3.x

Questo comportamento è stato effettivamente corretto nella prossima versione 3.x del driver che dovrebbe coincidere con la versione del server MongoDB 3.6. Il comportamento cambia in quanto err la risposta è più simile al result standard , ma ovviamente classificato come BulkWriteError risposta invece di MongoError quale è attualmente.

Fino a quando non viene rilasciato (e ovviamente fino a quando la dipendenza e le modifiche non vengono propagate all'implementazione "mangusta"), la linea d'azione consigliata è essere consapevoli che le informazioni utili sono nel result e non il err . In effetti, il tuo codice probabilmente dovrebbe cercare hasErrors() nel result e poi fallback per controllare err inoltre, per far fronte alla modifica da implementare nel driver.

Nota degli autori: Gran parte di questo contenuto e della relativa lettura in realtà ha già una risposta qui su Function insertMany() non ordinato:modo corretto per ottenere sia gli errori che il risultato? e il driver nativo MongoDB Node.js ingoia silenziosamente bulkWrite eccezione. Ma ripetendo ed elaborando qui fino a quando non viene finalmente capito alle persone che questo è il modo in cui gestisci le eccezioni nell'attuale implementazione del driver. E funziona davvero, quando guardi nel posto giusto e scrivi il tuo codice per gestirlo di conseguenza.