用JavaScript承诺思考(本例中为蓝鸟)
我试图了解一些不那么琐碎的承诺/异步用例。在我目前正在讨论的一个示例中,我有一个从knex查询返回的书籍数组(表数组),我希望将其插入数据库:用JavaScript承诺思考(本例中为蓝鸟),javascript,node.js,asynchronous,promise,bluebird,Javascript,Node.js,Asynchronous,Promise,Bluebird,我试图了解一些不那么琐碎的承诺/异步用例。在我目前正在讨论的一个示例中,我有一个从knex查询返回的书籍数组(表数组),我希望将其插入数据库: books.map(function(book) { // Insert into DB }); 每本书的内容如下所示: var book = { title: 'Book title', author: 'Author name' }; 然而,在插入每本书之前,我需要从一个单独的表中检索作者的ID,因为这些数据是标准化的。作
books.map(function(book) {
// Insert into DB
});
每本书的内容如下所示:
var book = {
title: 'Book title',
author: 'Author name'
};
然而,在插入每本书之前,我需要从一个单独的表中检索作者的ID,因为这些数据是标准化的。作者可能存在,也可能不存在,因此我需要:
- 检查数据库中是否有作者
- 如果是,请使用此ID
- 否则,请插入作者并使用新ID
有什么提示吗?假设您可以并行处理每本书。然后一切都非常简单(仅使用ES6 API): 问题是,在获得作者和创建新作者之间存在竞争条件。考虑以下事件顺序:
- 我们试图为B书找到作者A李>
- 获得作者A失败李>
- 我们请求创建一个作者,但尚未创建李>
- 我们试图为C书找到一位作者李>
- 获得作者A失败李>
- 我们要求创建一个作者(再次!)李>
- 第一个请求完成李>
- 第二个请求完成李>
const authorPromises = {};
function getAuthor(authorName) {
if (authorPromises[authorName]) {
return authorPromises[authorName];
}
const promise = getAuthorFromDatabase(authorName)
.catch(createAuthor.bind(null, authorName))
.then(author => {
delete authorPromises[authorName];
return author;
});
authorPromises[author] = promise;
return promise;
}
Promise
.all(books.map(book => {
return getAuthor(book.author)
.then(author => Object.assign(book, { author: author.id }))
.then(saveBook);
}))
.then(() => console.log('All done'))
就这样!现在,如果对author的请求正在进行中,那么将返回相同的承诺。以下是我将如何实现它。我认为一些重要的要求是:
- 不会创建重复的作者(这也应该是数据库本身的一个约束)
- 如果服务器在中间没有应答,则不插入不一致的数据。<李>
- 输入多个作者的可能性
- 不要对数据库进行
查询以查找n
内容-避免经典的“n+1”问题n
我会使用一个事务来确保更新是原子的,也就是说,如果运行了,客户端就在中间死亡了——没有任何一个作者是没有书的。同样重要的是,暂时性的失败不会导致内存泄漏(就像作者的回答中所说的遵守失败承诺的映射)
这样做的优点是只进行固定数量的查询,而且可能更安全、性能更好。此外,您的数据库未处于一致状态(尽管您可能需要对多个实例重试该操作)。是否需要“Upsert”类型的方法。i、 一个总是会返回给你一个作者id,如果它不存在,那么它会创建它,如果它确实存在,那么它只会返回现有的作者id?这样就减少了支票和插入之间出现跳转的可能性。你可能想在这里使用某种锁定机制。你要插入多少本书,你的数组有多大?最好先返回一组所有作者ID,然后在客户端合并该数据。此外,许多这种逻辑可能在数据库中执行得更好,而不是在JavaScript中执行。因为正确的方法值得一试,让数据库为您处理这些。问题是UPSERT是非常新的(5天前在Pg中实现),所以knex还没有赶上。另外,如果可能的话,您确实希望发送一个查询,而不是批处理
n
insert。在客户端实现这种逻辑和并发管理对我来说是非常愚蠢的。使用事务处理可能会更简单。真想不到在这里见到你!我可以用bluebird sugar来修饰你的代码吗?故意使用ES6专用API。请随意添加蓝鸟口味的例子进行比较!为了记录在案,这个答案虽然不错,但并没有真正解决OP的问题。防止多个作者应该在数据库级别而不是代码级别执行-如果节点的多个实例运行,或者服务器曾经关闭。。。更不用说如果createAuthor
因为网络故障而抛出,会发生什么。这是真的!更多的是一个脚本/数据引导解决方案。我也有兴趣看到蓝鸟漂亮的版本!我想我的很多观点是,你应该先考虑“数据库”,然后再考虑“承诺”。
const authorPromises = {};
function getAuthor(authorName) {
if (authorPromises[authorName]) {
return authorPromises[authorName];
}
const promise = getAuthorFromDatabase(authorName)
.catch(createAuthor.bind(null, authorName))
.then(author => {
delete authorPromises[authorName];
return author;
});
authorPromises[author] = promise;
return promise;
}
Promise
.all(books.map(book => {
return getAuthor(book.author)
.then(author => Object.assign(book, { author: author.id }))
.then(saveBook);
}))
.then(() => console.log('All done'))
knex.transaction(Promise.coroutine(function*(t) {
//get books inside the transaction
var authors = yield books.map(x => x.author);
// name should be indexed, this is a single query
var inDb = yield t.select("authors").whereIn("name", authors);
var notIn = authors.filter(author => !inDb.includes("author"));
// now, perform a single multi row insert on the transaction
// I'm assuming PostgreSQL here (return IDs), this is a bit different for SQLite
var ids = yield t("authors").insert(notIn.map(name => {authorName: name });
// update books _inside the transaction_ now with the IDs array
})).then(() => console.log("All done!"));