MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

insertMany Gérer les erreurs en double

Eh bien en réalité, MongoDB par "défaut" ne créera pas de données en double lorsqu'il y a une "clé unique" impliquée, dont _id ( aliasé par mangouste comme id , mais ignoré par insertMany() vous devez donc être prudent), mais il y a une histoire beaucoup plus vaste à cela dont vous vraiment devez être conscient .

Le problème de base ici est que l'implémentation "mongoose" de insertMany() ainsi que le pilote sous-jacent sont actuellement un peu "ennuyés" pour le moins. Cela étant, il y a un peu d'incohérence dans la façon dont le pilote transmet la réponse d'erreur dans les opérations "en masse" et cela est en fait aggravé par "la mangouste" qui ne "regarde pas vraiment au bon endroit" pour les informations d'erreur réelles.

La partie "rapide" qu'il vous manque est l'ajout de { ordered: false } à l'opération "Bulk" dont .insertMany() enveloppe simplement un appel à. Ce paramètre garantit que le "lot" de requêtes est réellement soumis "complètement" et n'arrête pas l'exécution lorsqu'une erreur se produit.

Mais comme "mongoose" ne gère pas très bien cela (et le pilote non plus "de manière cohérente"), nous devons en fait rechercher d'éventuelles "erreurs" dans la "réponse" plutôt que le résultat "d'erreur" du rappel sous-jacent.

A titre de démonstration :

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

Ou peut-être un peu mieux puisque le node.js LTS actuel a 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();
  }


})()

Dans tous les cas, vous obtenez le même résultat montrant que les écritures sont toutes les deux poursuivies et que nous "ignorons" respectueusement les erreurs liées à une "clé en double" ou autrement appelées code d'erreur 11000 . La "gestion sûre" consiste à s'attendre à de telles erreurs et à les éliminer tout en recherchant la présence d'"autres erreurs" auxquelles nous pourrions simplement vouloir prêter attention. Nous voyons également le reste du code continuer et répertorier tous les documents réellement insérés en exécutant un .find() suivant appeler :

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"
  }
]

Alors pourquoi ce procédé ? La raison étant que l'appel sous-jacent renvoie en fait à la fois le err et result comme indiqué dans l'implémentation du rappel, mais il y a une incohérence dans ce qui est renvoyé. La principale raison de le faire est que vous voyiez réellement le "résultat", qui contient non seulement le résultat de l'opération réussie, mais également le message d'erreur.

Avec les informations d'erreur est le nInserted: 3 indiquant combien de "lots" ont été réellement écrits. Vous pouvez pratiquement ignorer les insertedIds ici puisque ce test particulier impliquait en fait de fournir _id valeurs. Dans le cas où une propriété différente avait la contrainte "unique" qui a provoqué l'erreur, les seules valeurs ici seraient celles des écritures réussies réelles. Un peu trompeur, mais facile à tester et à voir par vous-même.

Comme indiqué, le hic est "l'incohérence" qui peut être démontrée avec un autre exemple ( async/await uniquement pour la brièveté de la liste):

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


})()

C'est à peu près la même chose, mais faites attention à la façon dont l'erreur est enregistrée ici :

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"
  }
}

Notez qu'il n'y a pas d'informations de "succès", même si nous obtenons la même suite de la liste en faisant le .find() suivant et obtenir la sortie. En effet, l'implémentation n'agit que sur "l'erreur lancée" dans le rejet et ne passe jamais par le result réel partie. Donc, même si nous avons demandé ordered: false , nous n'obtenons pas d'informations sur ce qui a été effectué, sauf si nous encapsulons le rappel et implémentons la logique nous-mêmes, comme indiqué dans les listes initiales.

L'autre "incohérence" importante se produit lorsqu'il y a "plus d'une erreur". Décommentez donc la valeur supplémentaire pour _id: 4 nous donne :

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"
    }
  }
]

Ici vous pouvez voir le code "ramifié" sur la présence de e.writeErrors , qui n'existe pas lorsqu'il y en a un Erreur. En revanche, la précédente response l'objet a à la fois le hasWriteErrors() et getWriteErrors() méthodes, indépendamment de toute erreur étant présente à tous. C'est donc l'interface la plus cohérente et la raison pour laquelle vous devriez l'utiliser au lieu d'inspecter le err réponse seule.

Corrections du pilote MongoDB 3.x

Ce comportement est en fait corrigé dans la prochaine version 3.x du pilote qui est censée coïncider avec la version du serveur MongoDB 3.6. Le comportement change dans la mesure où le err la réponse est plus proche du result standard , mais bien sûr classé comme une BulkWriteError réponse au lieu de MongoError ce qu'il est actuellement.

Jusqu'à ce que cela soit publié (et bien sûr jusqu'à ce que la dépendance et les modifications soient propagées à l'implémentation "mongoose"), la ligne de conduite recommandée est de savoir que les informations utiles se trouvent dans le result et non le err . En fait, votre code devrait probablement rechercher hasErrors() dans le result puis se replier pour vérifier err ainsi, afin de répondre au changement à implémenter dans le pilote.

Note des auteurs : Une grande partie de ce contenu et de la lecture connexe est en fait déjà répondue ici sur Function insertMany() unordered:bonne façon d'obtenir à la fois les erreurs et le résultat? et le pilote natif MongoDB Node.js avale silencieusement bulkWrite exception. Mais répéter et élaborer ici jusqu'à ce que les gens comprennent enfin que c'est ainsi que vous gérez les exceptions dans l'implémentation actuelle du pilote. Et cela fonctionne réellement, lorsque vous regardez au bon endroit et écrivez votre code pour le gérer en conséquence.