【问题标题】:Avoiding Unique error E11000 with Promise.all使用 Promise.all 避免唯一错误 E11000
【发布时间】:2018-04-20 05:40:48
【问题描述】:

我一直在使用this mongoose plugin 来执行代码库中经常使用的findOrCreate

我最近意识到,在创建唯一索引时执行多个异步 findOrCreate 操作很容易导致 E11000 重复键错误。

下面的例子可以使用Promise.all来描述。假设 name 是唯一的:

const promises = await Promise.all([
  Pokemon.findOrCreate({ name: 'Pikachu' }),
  Pokemon.findOrCreate({ name: 'Pikachu' }),
  Pokemon.findOrCreate({ name: 'Pikachu' })
]);

上面肯定会失败,因为findOrCreate 不是原子的。在考虑了它为什么会失败之后,这是有道理的,但是,我想要的是一种解决这个问题的简化方法。

我的许多模型都使用findOrCreate,它们都受到这个问题的影响。想到的一种解决方案是创建一个插件来捕获错误,然后返回 find 的结果,但是,这里可能有更好的方法 - 可能是我不知道的原生猫鼬。

【问题讨论】:

    标签: javascript node.js mongodb mongoose promise


    【解决方案1】:

    这当然取决于您对它的预期用途,但我会说总体上不需要“插件”。您正在寻找的基本功能已经通过"upserts"“内置”到 MongoDB 中。

    根据定义,只要使用集合的“唯一键”发出“选择”文档的查询条件,“更新插入”就不会产生“重复键错误”。在这种情况下"name"

    简而言之,您只需执行以下操作即可模仿与上述相同的行为:

    let results = await Promise.all([
      Pokemon.findOneAndUpdate({ "name": "Pikachu" },{},{ "upsert": true, "new": true }),
      Pokemon.findOneAndUpdate({ "name": "Pikachu" },{},{ "upsert": true, "new": true }),
      Pokemon.findOneAndUpdate({ "name": "Pikachu" },{},{ "upsert": true, "new": true })
    ]);
    

    这将在第一次调用时简单地“创建”它不存在的项目,或者“返回”现有项目。这就是“upsert”的工作原理。

    [
      {
        "_id": "5a022f48edca148094f30e8c",
        "name": "Pikachu",
        "__v": 0
      },
      {
        "_id": "5a022f48edca148094f30e8c",
        "name": "Pikachu",
        "__v": 0
      },
      {
        "_id": "5a022f48edca148094f30e8c",
        "name": "Pikachu",
        "__v": 0
      }
    ]
    

    如果您真的不关心“返回”每个调用,而只是想“更新或创建”,那么使用bulkWrite() 发送一个请求实际上要高效得多:

    // Issue a "batch" in Bulk
    let result = await Pokemon.bulkWrite(
      Array(3).fill(1).map( (e,i) => ({
        "updateOne": {
          "filter": { "name": "Pikachu" },
          "update": {
            "$set": { "skill": i }
          },
          "upsert": true
        }
      }))
    );
    

    因此,您不必等待服务器解决三个异步调用,而只需创建 一个,它可以“创建”项目或“更新”您在找到时在 $set 修饰符中使用的任何内容。这些适用于每场比赛,包括第一场比赛,如果您想“仅在创建时”,$setOnInsert 可以做到这一点。

    当然这只是一个“写”,所以这真的取决于你是否返回修改后的文档是否重要。因此,“批量”操作只是“写入”并且它们不会返回,而是返回有关“批次”的信息,指示“更新”和“修改”的内容,如下所示:

    {
      "ok": 1,
      "writeErrors": [],
      "writeConcernErrors": [],
      "insertedIds": [],
      "nInserted": 0,
      "nUpserted": 1,           // <-- created 1 time
      "nMatched": 2,            // <-- matched and modified the two other times
      "nModified": 2,
      "nRemoved": 0,
      "upserted": [
        {
          "index": 0,
          "_id": "5a02328eedca148094f30f33"  // <-- this is the _id created in upsert
        }
      ],
      "lastOp": {
        "ts": "6485801998833680390",
        "t": 23
      }
    }
    

    因此,如果您确实想要“返回”,那么更典型的情况是在“创建”时将您想要的数据与“更新”时需要哪些数据分开。请注意,$setOnInsert 本质上是“暗示”选择文档的“查询”条件中的任何值:

    // Issue 3 pokemon as separate calls
    let sequence = await Promise.all(
      Array(3).fill(1).map( (e,i) =>
        Pokemon.findOneAndUpdate(
          { name: "Pikachu" },
          { "$set": { "skill": i } },
          { "upsert": true, "new": true }
        )
      )
    );
    

    这将显示在每个原子事务的“序列”中应用的修改:

    [
      {
        "_id": "5a02328fedca148094f30f38",
        "name": "Pikachu",
        "__v": 0,
        "skill": 0
      },
      {
        "_id": "5a02328fedca148094f30f39",
        "name": "Pikachu",
        "__v": 0,
        "skill": 1
      },
      {
        "_id": "5a02328fedca148094f30f38",
        "name": "Pikachu",
        "__v": 0,
        "skill": 2
      }
    ]
    

    因此,通常这里是您想要的“更新插入”,根据您的意图,您可以使用单独的调用来返回每个修改/创建,或者批量发出“写入”。

    作为演示以上所有内容的完整清单:

    const mongoose = require('mongoose'),
          Schema = mongoose.Schema;
    
    mongoose.Promise = global.Promise;
    mongoose.set('debug', true);
    
    const uri = 'mongodb://localhost/test',
          options = { useMongoClient: true };
    
    const pokemonSchema = new Schema({
      name: String,
      skill: Number
    },{ autoIndex: false });
    
    pokemonSchema.index({ name: 1 },{ unique: true, background: false });
    
    const Pokemon = mongoose.model('Pokemon', pokemonSchema);
    
    function log(data) {
      console.log(JSON.stringify(data, undefined, 2))
    }
    
    (async function() {
    
      try {
    
        const conn = await mongoose.connect(uri,options);
    
        // Await index creation, otherwise we error
        await Pokemon.ensureIndexes();
    
        // Clean data for test
        await Pokemon.remove();
    
        // Issue 3 pokemon as separate calls
        let pokemon = await Promise.all(
          Array(3).fill(1).map( e =>
            Pokemon.findOneAndUpdate({ name: "Pikachu" },{},{ "upsert": true, "new": true })
          )
        );
    
        log(pokemon);
    
        // Clean data again
        await Pokemon.remove();
    
    
        // Issue a "batch" in Bulk
        let result = await Pokemon.bulkWrite(
          Array(3).fill(1).map( (e,i) => ({
            "updateOne": {
              "filter": { "name": "Pikachu" },
              "update": {
                "$set": { "skill": i }
              },
              "upsert": true
            }
          }))
        );
    
        log(result);
    
        let allPokemon = await Pokemon.find();
        log(allPokemon);
    
        // Clean data again
        await Pokemon.remove();
    
        // Issue 3 pokemon as separate calls
        let sequence = await Promise.all(
          Array(3).fill(1).map( (e,i) =>
            Pokemon.findOneAndUpdate(
              { name: "Pikachu" },
              { "$set": { "skill": i } },
              { "upsert": true, "new": true }
            )
          )
        );
    
        log(sequence);
    
    
      } catch(e) {
        console.error(e);
      } finally {
        mongoose.disconnect();
      }
    
    
    })()
    

    这会产生输出(对于那些懒得自己运行的人):

    Mongoose: pokemons.ensureIndex({ name: 1 }, { unique: true, background: false })
    Mongoose: pokemons.remove({}, {})
    Mongoose: pokemons.findAndModify({ name: 'Pikachu' }, [], { '$setOnInsert': { __v: 0 } }, { upsert: true, new: true, remove: false, fields: {} })
    Mongoose: pokemons.findAndModify({ name: 'Pikachu' }, [], { '$setOnInsert': { __v: 0 } }, { upsert: true, new: true, remove: false, fields: {} })
    Mongoose: pokemons.findAndModify({ name: 'Pikachu' }, [], { '$setOnInsert': { __v: 0 } }, { upsert: true, new: true, remove: false, fields: {} })
    [
      {
        "_id": "5a023461edca148094f30f82",
        "name": "Pikachu",
        "__v": 0
      },
      {
        "_id": "5a023461edca148094f30f82",
        "name": "Pikachu",
        "__v": 0
      },
      {
        "_id": "5a023461edca148094f30f82",
        "name": "Pikachu",
        "__v": 0
      }
    ]
    Mongoose: pokemons.remove({}, {})
    Mongoose: pokemons.bulkWrite([ { updateOne: { filter: { name: 'Pikachu' }, update: { '$set': { skill: 0 } }, upsert: true } }, { updateOne: { filter: { name: 'Pikachu' }, update: { '$set': { skill: 1 } }, upsert: true } }, { updateOne: { filter: { name: 'Pikachu' }, update: { '$set': { skill: 2 } }, upsert: true } } ], {})
    {
      "ok": 1,
      "writeErrors": [],
      "writeConcernErrors": [],
      "insertedIds": [],
      "nInserted": 0,
      "nUpserted": 1,
      "nMatched": 2,
      "nModified": 2,
      "nRemoved": 0,
      "upserted": [
        {
          "index": 0,
          "_id": "5a023461edca148094f30f87"
        }
      ],
      "lastOp": {
        "ts": "6485804004583407623",
        "t": 23
      }
    }
    Mongoose: pokemons.find({}, { fields: {} })
    [
      {
        "_id": "5a023461edca148094f30f87",
        "name": "Pikachu",
        "skill": 2
      }
    ]
    Mongoose: pokemons.remove({}, {})
    Mongoose: pokemons.findAndModify({ name: 'Pikachu' }, [], { '$setOnInsert': { __v: 0 }, '$set': { skill: 0 } }, { upsert: true, new: true, remove: false, fields: {} })
    Mongoose: pokemons.findAndModify({ name: 'Pikachu' }, [], { '$setOnInsert': { __v: 0 }, '$set': { skill: 1 } }, { upsert: true, new: true, remove: false, fields: {} })
    Mongoose: pokemons.findAndModify({ name: 'Pikachu' }, [], { '$setOnInsert': { __v: 0 }, '$set': { skill: 2 } }, { upsert: true, new: true, remove: false, fields: {} })
    [
      {
        "_id": "5a023461edca148094f30f8b",
        "name": "Pikachu",
        "__v": 0,
        "skill": 0
      },
      {
        "_id": "5a023461edca148094f30f8b",
        "name": "Pikachu",
        "__v": 0,
        "skill": 1
      },
      {
        "_id": "5a023461edca148094f30f8b",
        "name": "Pikachu",
        "__v": 0,
        "skill": 2
      }
    ]
    

    注意为了应用__v 键,$setOnInsert 也在所有“猫鼬”操作中“隐含”。因此,除非您将其关闭,否则该语句始终与发出的任何内容“合并”,因此允许在第一个示例“更新”块中使用 {},由于未应用更新修饰符,这将是核心驱动程序中的错误,然而 mongoose 为你添加了这个。

    还要注意bulkWrite() 实际上并没有引用模型的“模式”并绕过它。这就是为什么在那些已发布的更新中没有__v 的原因,它确实也绕过了所有验证。这通常不是问题,但您应该注意这一点。

    【讨论】:

    • 嗨,尼尔,非常感谢您提供的非常详细的答案。真的感谢您抽出宝贵的时间!我从你的例子中学到了很多。然而,我确实遇到了一个有趣的问题。您的第一个异步 findOneAndUpdate 示例运行良好,但是当我将 unique: true 添加到字段 name 时,猫鼬会引发 E11000 错误。更具体地说,我使用mongoose.connection.collections['pokemons'].createIndex({ 'name': 1 }, { 'unique': true }, function(){}); 在我的测试套件中添加了索引。我想知道你是否可以对此有所了解。谢谢!
    • @dipole_moment 你真的需要再读一遍并了解更多。在提供的列表中创建了一个“唯一索引”,但我没有像您认为的那样创建索引。还有另一种创建索引的方法,它实际上确保在创建过程中对第一次写入没有“竞争条件”。仔细看,我在做任何其他事情之前故意“等待”索引创建。代码与唯一索引一起工作正常,这是一个“时间”问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-06
    • 1970-01-01
    • 2015-03-22
    • 2011-10-01
    • 2014-07-19
    • 2017-08-30
    • 1970-01-01
    相关资源
    最近更新 更多