MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

insertMany Manipula Erros Duplicados


Bem, na verdade, o MongoDB por "padrão" não criará dados duplicados onde houver uma "chave única" envolvida, da qual _id (alias por mangusto como id , mas ignorado por insertMany() então você precisa ter cuidado ), mas há uma história muito maior para isso que você realmente precisa estar ciente .

O problema básico aqui é que tanto a implementação "mangusto" de insertMany() bem como o driver subjacente estão atualmente um pouco "borked" para dizer o mínimo. Sendo que existe um pouco de inconsistência em como o driver passa a resposta de erro em operações "em massa" e isso é realmente agravado por "mangusto" não realmente "procurando no lugar certo" para as informações de erro reais.

A parte "rápida" que está faltando é a adição de { ordered: false } para a operação "Bulk" da qual .insertMany() simplesmente envolve uma chamada para. Definir isso garante que o "lote" de solicitações seja realmente enviado "completamente" e não interrompa a execução quando ocorrer um erro.

Mas como "mangusto" não lida muito bem com isso (nem o driver "consistentemente"), na verdade precisamos procurar possíveis "erros" na "resposta" em vez do resultado "erro" do retorno de chamada subjacente.

Como demonstração:
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 talvez um pouco melhor, já que o node.js LTS atual tem 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();
  }


})()

De qualquer forma, você obtém o mesmo resultado, mostrando que as gravações são continuadas e que respeitosamente "ignoramos" erros relacionados a uma "chave duplicada" ou também conhecido como código de erro 11000 . O "manuseio seguro" é que esperamos esses erros e os descartamos enquanto procuramos a presença de "outros erros" aos quais podemos apenas prestar atenção. Também vemos que o resto do código continua e lista todos os documentos realmente inseridos executando um .find() subsequente ligar:
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"
  }
]

Então por que esse processo? A razão é que a chamada subjacente na verdade retorna tanto o err e result conforme mostrado na implementação de retorno de chamada, mas há uma inconsistência no que é retornado. A principal razão para fazer isso é para que você realmente veja o "resultado", que não só tem o resultado da operação bem-sucedida, mas também a mensagem de erro.

Junto com as informações de erro está o nInserted: 3 indicando quantos do "lote" realmente foram escritos. Você pode praticamente ignorar os insertedIds aqui já que este teste em particular envolveu realmente fornecer _id valores. No caso de uma propriedade diferente ter a restrição "única" que causou o erro, os únicos valores aqui seriam aqueles de gravações bem-sucedidas reais. Um pouco enganador, mas fácil de testar e ver por si mesmo.

Como dito, o problema é a "incosistência" que pode ser demonstrada com outro exemplo ( async/await apenas para brevidade da 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();
  }


})()

Tudo quase a mesma coisa, mas preste atenção em como o erro é registrado aqui:
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"
  }
}

Observe que não há informações de "sucesso", mesmo que obtenhamos a mesma continuação da listagem fazendo o .find() subsequente e obtendo a saída. Isso ocorre porque a implementação atua apenas no "erro lançado" na rejeição e nunca passa pelo result real papel. Portanto, mesmo solicitando ordered: false , não obteremos as informações sobre o que foi concluído, a menos que envolvamos o retorno de chamada e implementemos a lógica nós mesmos, conforme mostrado nas listagens iniciais.

A outra "inconsistência" importante acontece quando há "mais de um erro". Portanto, descomentar o valor adicional para _id: 4 nos 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"
    }
  }
]

Aqui você pode ver o código "ramificado" na presença de e.writeErrors , que não existe quando há um erro. Por outro lado, a response anterior objeto tem tanto o hasWriteErrors() e getWriteErrors() métodos, independentemente de qualquer erro estar presente. Portanto, essa é a interface mais consistente e a razão pela qual você deve usá-la em vez de inspecionar o err resposta sozinho.

Correções de driver MongoDB 3.x


Esse comportamento foi corrigido na próxima versão 3.x do driver, que deve coincidir com a versão do servidor MongoDB 3.6. O comportamento muda na medida em que o err resposta é mais parecida com o padrão result , mas é claro classificado como um BulkWriteError resposta em vez de MongoError que atualmente é.

Até que isso seja liberado (e, claro, até que a dependência e as alterações sejam propagadas para a implementação do "mangusto"), o curso de ação recomendado é estar ciente de que as informações úteis estão no result e não o err . Na verdade, seu código provavelmente deve procurar por hasErrors() no result e, em seguida, volte para verificar err também, a fim de atender a mudança a ser implementada no driver.

Observação do autor: Grande parte desse conteúdo e leitura relacionada na verdade já está respondida aqui na Função insertMany() não ordenada:maneira correta de obter os erros e o resultado? e o driver nativo MongoDB Node.js engole silenciosamente bulkWrite exceção. Mas repetindo e elaborando aqui até que finalmente as pessoas entendam que essa é a maneira como você lida com exceções na implementação do driver atual. E realmente funciona, quando você olha no lugar correto e escreve seu código para lidar com isso de acordo.