使用Promise.all避免唯一错误E11000

时间:2017-11-07 17:51:13

标签: javascript node.js mongodb mongoose promise

我一直在使用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结果的插件,但是,这里可能有一个更好的方法 - 可能是我不知道的本地猫鼬。

1 个答案:

答案 0 :(得分:2)

这当然取决于你对此的预期用法,但我总体上说,“插件”并不是必需的。您正在寻找的基本功能已经使用"upserts"“内置”到MongoDB中。

根据定义,只要使用集合的“唯一密钥”发出“选择”文档的查询条件,“upsert”就不会产生“重复密钥错误”。在这种情况下"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 })
]);

只会在第一次调用时“创建”该项目,而该项目尚不存在,或“返回”现有项目。这就是“upserts”的工作原理。

[
  {
    "_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
  }
]

所以通常它是你想要的“upserts”,根据你的意图,你要么使用单独的调用来返回每个修改/创建,要么批量发出“写入”。

作为展示以上所有内容的完整列表:

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

N.B 为了应用__v密钥,$setOnInsert在所有“mongoose”操作中也是“暗示”的。因此,除非你关闭它,否则该语句总是与发出的任何内容“合并”,因此允许第一个示例中的{}“更新”块,由于没有更新修饰符,因此核心驱动程序中的错误应用,但是mongoose为你添加了这个。

     

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