insertMany 处理重复错误

Jen*_*Mok 4 mongoose mongodb node.js

我想将(对象数组)批量插入到我的文档中,但我想防止重复记录,找不到使用 insertMany 的方法。

const Song = require('../models/song');
Song.insertMany([{id:1, name:"something"},{id:2, name:"something else"])
    .then((result) => {
      res.json({
        result
      })
    })
Run Code Online (Sandbox Code Playgroud)

上面的代码有效,但如果记录相同,它仍然会被插入。

Nei*_*unn 11

实际上,MongoDB 在“默认”情况下不会创建包含“唯一键”的重复数据,其中_id(由 mongoose as 别名id,但被忽略,insertMany()因此您需要小心),但是有一个更大的这个故事你真的需要知道。

这里的基本问题是,insertMany()温和地说,目前的“猫鼬”实现和底层驱动程序都有些“无聊”。驱动程序在“批量”操作中传递错误响应的方式存在一些不一致,这实际上是由“猫鼬”并没有真正“在正确的位置”获取实际错误信息而加剧的。

您缺少的“快速”部分是对{ ordered: false }“批量”操作的添加,其中.insertMany()简单地包装了对的调用。设置此项可确保“批”请求实际上“完全”提交,并且在发生错误时不会停止执行。

但是由于“猫鼬”不能很好地处理这个问题(驱动程序也不能“一致地”),我们实际上需要在“响应”中寻找可能的“错误”,而不是底层回调的“错误”结果。

作为演示:

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(); } );
Run Code Online (Sandbox Code Playgroud)

或者也许更好一些,因为当前的 LTS node.js 有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();
  }


})()
Run Code Online (Sandbox Code Playgroud)

无论如何,您会得到相同的结果,表明写入仍在继续,并且我们恭敬地“忽略”与“重复键”或其他称为错误代码相关的错误11000。“安全处理”是我们期望此类错误并丢弃它们,同时寻找我们可能只想注意的“其他错误”的存在。我们还看到其余代码继续并通过执行后续.find()调用列出实际插入的所有文档:

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"
  }
]
Run Code Online (Sandbox Code Playgroud)

那么为什么会有这个过程呢?原因是底层调用实际上返回err和result,如回调实现中所示,但返回的内容不一致。这样做的主要原因是让您实际看到“结果”,其中不仅有成功操作的结果,还有错误信息。

与错误信息一起的是nInserted: 3指示实际写入的“批次”中有多少。您几乎可以忽略insertedIds此处,因为此特定测试涉及实际提供_id值。如果不同属性具有导致错误的“唯一”约束,则此处唯一的值将是来自实际成功写入的值。有点误导,但很容易自己测试和查看。

如前所述,问题是“不一致”,可以用另一个例子来证明(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" },
  { _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();
  }


})()
Run Code Online (Sandbox Code Playgroud)

一切都差不多,但请注意此处的错误记录方式:

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"
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,没有“成功”信息,即使我们通过执行后续操作.find()并获得输出来获得列表的相同延续。这是因为实现只对拒绝中的“抛出错误”起作用,而从不通过实际result部分。因此,即使我们要求ordered: false,我们也无法获得有关已完成内容的信息,除非我们包装回调并自己实现逻辑,如初始清单中所示。

另一个重要的“不一致”发生在“多个错误”时。所以取消注释附加值_id: 4给我们:

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"
    }
  }
]
Run Code Online (Sandbox Code Playgroud)

在这里,您可以看到存在 的代码“分支” e.writeErrors,出现一个错误时不存在。相比之下,较早的response对象同时具有hasWriteErrors()和getWriteErrors()方法,无论是否存在任何错误。所以这是更一致的接口,也是为什么你应该使用它而不是err单独检查响应的原因。


MongoDB 3.x 驱动程序修复

此行为实际上已在即将发布的驱动程序 3.x 版本中修复,该版本旨在与 MongoDB 3.6 服务器版本一致。行为的变化在于err响应更类似于标准result,但当然归类为BulkWriteError响应而不是MongoError现在。

直到被释放(当然,直到依赖,并更改传播到了“猫鼬”实现),然后采取行动的建议当然是要认识到,有用的信息是在result和没有的err。其实你的代码也许应该寻找hasErrors()的result,然后回退检查err为好,以满足在驱动程序中实现的改变。

作者注:这些内容和相关阅读的大部分内容实际上已经在Function insertMany() unordered: right way to get the errors 和 result 上得到了回答?和MongoDB Node.js 本机驱动程序默默地吞下bulkWrite异常。但是在这里重复和详细说明,直到它最终让人们意识到这是您在当前驱动程序实现中处理异常的方式。当您查看正确的位置并编写相应的代码来处理它时,它确实有效。

  • @JennyMok 如果您“尝试过”,实际运行示例并理解它们将花费 5 分钟以上的时间。不要“摘樱桃”!这是一个你不理解的概念,一些好心人花时间为你写了必要的讲义和练习。坐下来解决它。我给你的代码演示了该怎么做。运行!学习它!“然后”尝试应用于您的唯一用法。但只有当你理解它时。“喘口气”意味着我真的不需要每两分钟收到你的评论通知。 (2认同)