在JavaScript中的思考诺言(蓝鸟在这种情况下)

我试图让我的头一些不那么微不足道的承诺/asynchronous使用情况。 在一个例子中,我现在正在摔跤,我有一个从一个knex查询(可数组)返回的书籍数组,我想插入到数据库中:

books.map(function(book) { // Insert into DB }); 

每本书的内容如下所示:

 var book = { title: 'Book title', author: 'Author name' }; 

但是,在插入每本书之前,我需要从一个单独的表中检索作者的ID,因为这些数据是正常的。 作者可能也可能不存在,所以我需要:

  • 检查作者是否存在于数据库中
  • 如果是,请使用此ID
  • 否则,插入作者并使用新的ID

但是,上述操作也都是asynchronous的。

作为插入操作的先决条件,我可以只使用原始映射中的承诺(获取和/或插入标识)。 但是这里的问题在于,因为所有东西都是asynchronous运行的,代码可能会插入重复的作者,因为初始的check-if-author-exists与insert-a-new-author块是分离的。

我可以想出几个办法来达到上述目的,但都涉及分拆承诺链,一般显得有些杂乱。 这似乎是一个相当普遍的问题。 我确定我在这里错过了一些基本的东西!

有小费吗?

假设您可以并行处理每本书。 那么一切都很简单(只使用ES6 API):

 Promise .all(books.map(book => { return getAuthor(book.author) .catch(createAuthor.bind(null, book.author)); .then(author => Object.assign(book, { author: author.id })) .then(saveBook); })) .then(() => console.log('All done')) 

问题在于作者和创作者之间存在竞争条件。 考虑以下事件顺序:

  • 我们试图获得书B的作者A;
  • 得到作者A失败;
  • 我们要求创build作者A,但是还没有创build;
  • 我们试图获得书C的作者A;
  • 得到作者A失败;
  • 我们请求创build作者A(再次!);
  • 第一个请求完成;
  • 第二个请求完成;

现在我们在作者表中有两个A的实例。 这不好! 为了解决这个问题,我们可以使用传统的方法:locking。 我们需要保留每个作者的锁表。 当我们发送创build请求时,我们locking相应的锁。 请求完成后,我们解锁它。 涉及同一作者的所有其他操作在做任何事之前都需要先获取锁。

这似乎很难,但在我们的情况下可以简化很多,因为我们可以使用我们的请求promise而不是锁:

 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')) 

而已! 现在,如果作者的请求在飞行中,相同的承诺将被返回。

这是我将如何实施它。 我认为一些重要的要求是:

  • 永远不会创build重复的作者(这也应该是数据库本身的一个约束)。
  • 如果服务器没有在中间回复 – 没有插入不一致的数据。
  • 可以input多个作者。
  • 不要对n数据库进行n查询 – 避免经典的“n + 1”问题。

我会使用一个事务来确保更新是primefaces化的 – 也就是说,如果操作运行并且客户端在中间死亡 – 没有作者是没有书籍创build的。 同样重要的是,一个特定的失败不会导致内存泄漏(就像在作者地图的答案中保持失败的承诺一样)。

 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!")); 

这样做的好处是只能进行固定数量的查询,并且可能会更安全,性能更好。 此外,您的数据库不是一致的状态(虽然您可能不得不重试多个实例的操作)。