用JavaScript承诺思考(本例中为蓝鸟)

用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,因为这些数据是标准化的。作

我试图了解一些不那么琐碎的承诺/异步用例。在我目前正在讨论的一个示例中,我有一个从knex查询返回的书籍数组(表数组),我希望将其插入数据库:

books.map(function(book) {

  // Insert into DB

});
每本书的内容如下所示:

var book = {
    title: 'Book title',
    author: 'Author name'
};
然而,在插入每本书之前,我需要从一个单独的表中检索作者的ID,因为这些数据是标准化的。作者可能存在,也可能不存在,因此我需要:

  • 检查数据库中是否有作者
  • 如果是,请使用此ID
  • 否则,请插入作者并使用新ID
但是,上述操作也都是异步的

我可以使用原始映射中的承诺(获取和/或插入ID)作为插入操作的先决条件。但这里的问题是,由于所有操作都是异步运行的,代码很可能会插入重复的作者,因为初始检查是否存在作者与insert-a-new-AUTHER块是解耦的

我可以想出一些方法来实现上述目标,但它们都涉及到拆分承诺链,通常看起来有点混乱。这似乎是一种很常见的问题。我肯定我错过了一些基本的东西


有什么提示吗?

假设您可以并行处理每本书。然后一切都非常简单(仅使用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
    内容-避免经典的“n+1”问题

我会使用一个事务来确保更新是原子的,也就是说,如果运行了,客户端就在中间死亡了——没有任何一个作者是没有书的。同样重要的是,暂时性的失败不会导致内存泄漏(就像作者的回答中所说的遵守失败承诺的映射)


这样做的优点是只进行固定数量的查询,而且可能更安全、性能更好。此外,您的数据库未处于一致状态(尽管您可能需要对多个实例重试该操作)。

是否需要“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!"));