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

我一直在使用这个mongoose插件来执行代码库中经常使用的findOrCreate

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

一个例子可以用Promise.all来描述。 假设name是唯一的,那么:

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

以上将肯定失败,因为findOrCreate不是primefaces的。 这是有道理的,考虑之后为什么会失败,但我想要的是一个简化的方法来解决这个问题。

我的许多模型使用findOrCreate ,他们都受到这个问题。 想到的一个解决scheme是创build一个能够捕获错误的插件,然后返回find的结果,但是在这里可能会有更好的方法 – 可能是一个我不知道的本地mongoose。

这当然取决于你的这个预期的用法,但我会说,总体来说,“插件”是不需要的。 你正在寻找的基本function已经“内置”MongoDB与“upserts” 。

根据定义,只要使用“唯一密钥”为集合发出“select”文档的查询条件,“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 }) ]); 

哪个只是在第一次调用中“创build”它不存在的项目,或者“返回”现有项目。 这是“upserts”如何工作。

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

如果你真的不在意“返回”每个调用,只是想“更新或创build”,那么发送一个具有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 } })) ); 

因此,不是等待服务器parsing三个asynchronous调用,而只是使用$set修饰符中的任何内容创build项目或“更新”。 这些适用于包括第一个匹配的每个匹配,如果你想“只在创build”,有$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 } } 

所以,如果你想要“回报”,那么更典型的情况是分离你想要的“创build”和“更新”需要哪些数据。 注意$setOnInsert本质上是“隐含的”,无论在“查询”条件中select文档的值是什么:

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

这将显示在每个primefaces事务的“顺序”中应用的修改:

 [ { "_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”,根据你的意图,你可以使用不同的调用来返回每个修改/创build,或者你批量发出你的“写入”。

作为一个完整的列表来演示以上所有内容:

 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”操作中也是“隐含”的。 所以,除非你把它关掉,否则这个语句总是与发出的任何东西“合并”,从而允许在第一个例子“更新”块中的{} ,由于没有应用更新修饰符,这将是核心驱动程序中的错误,mongoose为你添加这一个。

另外请注意, bulkWrite()实际上并不引用模型的“模式”并绕过它。 这就是为什么在这些发布的更新中没有__v ,并且确实也绕过了所有的validation。 这通常不是一个问题,但它是你应该知道的。