sql >> Base de Datos >  >> NoSQL >> MongoDB

insertMany maneja errores duplicados

Bueno, de hecho, MongoDB por "predeterminado" no creará datos duplicados donde haya una "clave única" involucrada, de la cual _id (con alias de mongoose como id , pero ignorado por insertMany() por lo que debe tener cuidado), pero hay una historia mucho más grande de la que realmente debe tener conocimiento. .

El problema básico aquí es que tanto la implementación "mangosta" de insertMany() así como el controlador subyacente actualmente están un poco "borrados", por decirlo suavemente. Eso es un poco de inconsistencia en la forma en que el controlador pasa la respuesta de error en las operaciones "en masa" y esto en realidad se ve agravado por "mangosta" que realmente no "busca en el lugar correcto" la información de error real.

La parte "rápida" que te falta es la adición de { ordered: false } a la operación "en bloque" de la cual .insertMany() simplemente envuelve una llamada a. Configurar esto asegura que el "lote" de solicitudes se envíe realmente "completamente" y no detiene la ejecución cuando ocurre un error.

Pero dado que "mangoose" no maneja esto muy bien (ni el controlador "consistentemente"), en realidad necesitamos buscar posibles "errores" en la "respuesta" en lugar del resultado de "error" de la devolución de llamada subyacente.

Como demostración:

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 tal vez un poco mejor ya que LTS actual node.js tiene 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();
  }


})()

En cualquier caso, obtiene el mismo resultado que muestra que las escrituras continúan y que "ignoramos" respetuosamente los errores relacionados con una "clave duplicada" o también conocidos como código de error 11000 . El "manejo seguro" es que esperamos tales errores y los descartamos mientras buscamos la presencia de "otros errores" a los que podríamos querer prestar atención. También vemos que el resto del código continúa y enumera todos los documentos realmente insertados al ejecutar un .find() subsiguiente llamar:

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

Entonces, ¿por qué este proceso? El motivo es que la llamada subyacente en realidad devuelve tanto el err y result como se muestra en la implementación de devolución de llamada, pero hay una incoherencia en lo que se devuelve. La razón principal para hacer esto es que realmente vea el "resultado", que no solo tiene el resultado de la operación exitosa, sino también el mensaje de error.

Junto con la información del error está el nInserted: 3 indicando cuántos del "lote" se escribieron realmente. Puedes ignorar los insertedIds aquí ya que esta prueba en particular involucró realmente el suministro de _id valores. En el caso de que una propiedad diferente tuviera la restricción "única" que causó el error, los únicos valores aquí serían los de las escrituras exitosas reales. Un poco engañoso, pero fácil de probar y ver por ti mismo.

Como se indicó, el problema es la "incoherencia" que se puede demostrar con otro ejemplo ( async/await solo por brevedad de la lista):

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


})()

Todo es lo mismo, pero presta atención a cómo se registra el error aquí:

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

Tenga en cuenta que no hay información de "éxito", a pesar de que obtenemos la misma continuación de la lista haciendo el subsiguiente .find() y obtener la salida. Esto se debe a que la implementación solo actúa sobre el "error lanzado" en el rechazo y nunca pasa por el result real. parte. Entonces, aunque pedimos ordered: false , no obtenemos la información sobre lo que se completó a menos que ajustemos la devolución de llamada e implementemos la lógica nosotros mismos, como se muestra en las listas iniciales.

La otra "inconsistencia" importante ocurre cuando hay "más de un error". Entonces, descomente el valor adicional para _id: 4 nos da:

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

Aquí puede ver el código "ramificado" en presencia de e.writeErrors , que no existe cuando hay uno error. Por el contrario, la response anterior el objeto tiene tanto hasWriteErrors() y getWriteErrors() métodos, independientemente de que haya algún error presente. Esa es la interfaz más consistente y la razón por la que debería usarla en lugar de inspeccionar el err respuesta sola.

Correcciones del controlador MongoDB 3.x

Este comportamiento en realidad se solucionó en la próxima versión 3.x del controlador, que debe coincidir con la versión del servidor MongoDB 3.6. El comportamiento cambia en que el err la respuesta es más parecida al result estándar , pero por supuesto clasificado como BulkWriteError respuesta en lugar de MongoError que es actualmente.

Hasta que se publique (y, por supuesto, hasta que la dependencia y los cambios se propaguen a la implementación de "mangoose"), el curso de acción recomendado es tener en cuenta que la información útil está en el result y no el err . De hecho, su código probablemente debería buscar hasErrors() en el result y luego volver a comprobar err también, para atender el cambio que se implementará en el controlador.

Nota del autor: Gran parte de este contenido y la lectura relacionada ya se respondió aquí en Función insertMany() desordenada:¿forma correcta de obtener tanto los errores como el resultado? y el controlador nativo de MongoDB Node.js traga silenciosamente bulkWrite excepción. Pero repitiendo y elaborando aquí hasta que finalmente las personas se den cuenta de que esta es la forma en que maneja las excepciones en la implementación actual del controlador. Y realmente funciona, cuando miras en el lugar correcto y escribes tu código para manejarlo en consecuencia.