繁体   English   中英

思考JavaScript承诺(在这种情况下蓝鸟)

[英]Thinking in JavaScript promises (Bluebird in this case)

我试图了解一些不那么琐碎的承诺/异步用例。 在一个我正在努力解决的例子中,我有一个从knex查询(可编组数组)返回的书籍数组我希望插入到数据库中:

books.map(function(book) {

  // Insert into DB

});

每个书籍项目如下:

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

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

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

但是,上述操作也都是异步的。

我可以在原始映射中使用promise(获取和/或插入ID)作为插入操作的先决条件。 但问题在于,因为所有内容都是异步运行的,所以代码可能会插入重复的作者,因为初始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失败;
  • 我们要求创建作者A,但它尚未创建;
  • 我们试图为作者C获得作者A;
  • 让作者A失败;
  • 我们要求创建作者A(再次!);
  • 第一次请求完成;
  • 第二个请求完成;

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

这似乎很难,但在我们的案例中可以简化很多,因为我们可以使用我们的请求promises而不是lock:

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

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

以下是我将如何实现它。 我认为一些重要的要求是:

  • 没有创建重复的作者(这也应该是数据库本身的约束)。
  • 如果服务器没有在中间回复 - 则不会插入不一致的数据。
  • 可以进入多位作者。
  • 不要让n查询到数据库n东西-避免了经典的“N + 1”的问题。

我使用一个事务,以确保更新是原子的 - 即如果操作运行并且客户端在中间死亡 - 没有作者是在没有书籍的情况下创建的。 同样重要的是,临时故障不会导致内存泄漏(例如在作者映射的答案中保持失败的承诺)。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM