MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

insertMany Doppelte Fehler behandeln

Tatsächlich erstellt MongoDB standardmäßig keine doppelten Daten, wenn ein "eindeutiger Schlüssel" beteiligt ist, von dem _id ( Alias ​​von Mungo als id , aber von insertMany() ignoriert Sie müssen also vorsichtig sein ), aber es gibt eine viel größere Geschichte, derer Sie sich wirklich bewusst sein müssen .

Das grundlegende Problem hier ist, dass sowohl die "Mungo"-Implementierung von insertMany() sowie der zugrunde liegende Treiber sind derzeit etwas "gebohrt", um es milde auszudrücken. Dass es eine kleine Inkonsistenz gibt, wie der Treiber die Fehlerantwort in "Bulk"-Operationen übergibt, und dies wird tatsächlich dadurch verstärkt, dass "Mungo" nicht wirklich "an der richtigen Stelle" nach den tatsächlichen Fehlerinformationen sucht.

Der "schnelle" Teil, den Sie vermissen, ist die Hinzufügung von { ordered: false } zu der "Bulk"-Operation, von der .insertMany() wickelt einfach einen Anruf um. Durch diese Einstellung wird sichergestellt, dass der "Batch" von Anfragen tatsächlich "vollständig" übermittelt wird und die Ausführung nicht gestoppt wird, wenn ein Fehler auftritt.

Aber da "mongoose" dies nicht sehr gut handhabt ( und der Treiber "konsistent" auch nicht), müssen wir tatsächlich nach möglichen "Fehlern" in der "Antwort" suchen und nicht im "Fehler" -Ergebnis des zugrunde liegenden Callbacks.

Zur Demonstration:

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

Oder vielleicht ein bisschen schöner, da das aktuelle LTS node.js async/await hat :

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


})()

Auf jeden Fall erhalten Sie das gleiche Ergebnis, das zeigt, dass beide Schreibvorgänge fortgesetzt werden und dass wir Fehler, die sich auf einen „duplizierten Schlüssel“ beziehen oder anderweitig als Fehlercode 11000 bekannt sind, respektvoll „ignorieren“. . Die "sichere Handhabung" besteht darin, dass wir solche Fehler erwarten und sie verwerfen, während wir nach "anderen Fehlern" suchen, auf die wir vielleicht nur achten möchten. Auch den Rest des Codes sehen wir weiter und listen alle tatsächlich eingefügten Dokumente auf, indem wir ein nachfolgendes .find() ausführen Aufruf:

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

Warum also dieser Prozess? Der Grund dafür ist, dass der zugrunde liegende Aufruf tatsächlich sowohl den err zurückgibt und result wie in der Callback-Implementierung gezeigt, aber es gibt eine Inkonsistenz in dem, was zurückgegeben wird. Der Hauptgrund dafür ist, dass Sie tatsächlich das "Ergebnis" sehen, das nicht nur das Ergebnis der erfolgreichen Operation enthält, sondern auch die Fehlermeldung.

Zusammen mit den Fehlerinformationen steht nInserted: 3 gibt an, wie viele aus dem "Batch" tatsächlich geschrieben wurden. Sie können die insertedIds ziemlich ignorieren hier, da dieser spezielle Test die tatsächliche Bereitstellung von _id beinhaltete Werte. Für den Fall, dass eine andere Eigenschaft die "eindeutige" Einschränkung hatte, die den Fehler verursachte, wären die einzigen Werte hier die von tatsächlich erfolgreichen Schreibvorgängen. Etwas irreführend, aber einfach zu testen und selbst zu sehen.

Wie gesagt, der Haken ist die "Inkonsistenz", die an einem anderen Beispiel demonstriert werden kann ( async/await nur zur Kürze der Auflistung):

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


})()

Alles ziemlich gleich, aber achten Sie darauf, wie der Fehler hier protokolliert wird:

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

Beachten Sie, dass es keine „Erfolgs“-Informationen gibt, obwohl wir die gleiche Fortsetzung der Auflistung erhalten, indem wir das nachfolgende .find() ausführen und die Ausgabe erhalten. Dies liegt daran, dass die Implementierung nur auf den "ausgeworfenen Fehler" bei der Ablehnung reagiert und niemals das eigentliche result durchläuft Teil. Obwohl wir nach ordered: false gefragt haben , erhalten wir keine Informationen darüber, was abgeschlossen wurde, es sei denn, wir verpacken den Rückruf und implementieren die Logik selbst, wie in den anfänglichen Auflistungen gezeigt.

Die andere wichtige „Inkonsistenz“ tritt auf, wenn „mehr als ein Fehler“ vorliegt. Kommentieren Sie also den zusätzlichen Wert für _id: 4 aus gibt uns:

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

Hier sieht man den Code "verzweigt" auf das Vorhandensein von e.writeErrors , das nicht existiert, wenn eins vorhanden ist Error. Im Gegensatz dazu die frühere response Objekt hat sowohl die hasWriteErrors() und getWriteErrors() Methoden, unabhängig davon, ob überhaupt ein Fehler vorliegt. Das ist also die konsistentere Schnittstelle und der Grund, warum Sie sie verwenden sollten, anstatt den err zu untersuchen Antwort allein.

MongoDB 3.x-Treiberkorrekturen

Dieses Verhalten wird tatsächlich in der kommenden Version 3.x des Treibers behoben, die mit der Serverversion von MongoDB 3.6 zusammenfallen soll. Das Verhalten ändert sich dahingehend, dass der err Die Antwort ähnelt eher dem standardmäßigen result , aber natürlich als BulkWriteError klassifiziert Antwort anstelle von MongoError was es derzeit ist.

Bis dies freigegeben ist (und natürlich bis diese Abhängigkeit und Änderungen an die "Mungo"-Implementierung weitergegeben werden), besteht die empfohlene Vorgehensweise darin, sich bewusst zu sein, dass die nützlichen Informationen im result enthalten sind und nicht der err . Tatsächlich sollte Ihr Code wahrscheinlich nach hasErrors() suchen im result und dann zurückfallen, um err zu überprüfen auch, um die im Treiber zu implementierende Änderung zu berücksichtigen.

Anmerkung des Autors: Ein Großteil dieses Inhalts und der damit verbundenen Lektüre wird eigentlich bereits hier auf Function insertMany() unordered beantwortet:richtiger Weg, um sowohl die Fehler als auch das Ergebnis zu erhalten? und der native Treiber von MongoDB Node.j schluckt bulkWrite stillschweigend Ausnahme. Aber hier wiederholen und ausarbeiten, bis den Leuten endlich klar wird, dass Sie Ausnahmen in der aktuellen Treiberimplementierung auf diese Weise behandeln. Und es funktioniert tatsächlich, wenn Sie an der richtigen Stelle suchen und Ihren Code schreiben, um es entsprechend zu handhaben.