使用外部 API 调用和 findOneAndUpdate 循环结果

Posted

技术标签:

【中文标题】使用外部 API 调用和 findOneAndUpdate 循环结果【英文标题】:Looping Results with an External API Call and findOneAndUpdate 【发布时间】:2018-11-12 22:02:26 【问题描述】:

我正在尝试编写一个程序,该程序使用 mongoose 从 mongo 数据库中获取文档并使用 API 处理它们,然后使用处理结果编辑数据库中的每个文档。我的问题是我有问题,因为我不完全了解 nodejs 和异步。这是我的代码:

Model.find(function (err, tweets) 
    if (err) return err;
    for (var i = 0; i < tweets.length; i++) 
        console.log(tweets[i].tweet);
        api.petition(tweets[i].tweet)
            .then(function(res) 
                TweetModel.findOneAndUpdate(_id: tweets[i]._id, result: res, function (err, tweetFound) 
                    if (err) throw err;
                    console.log(tweetFound);
                );
            )
            .catch(function(err) 
                console.log(err);
            )
    
)

问题是在 findOneAndUpdate 中,tweets 是未定义的,所以它找不到那个 id。有什么解决办法吗?谢谢

【问题讨论】:

【参考方案1】:

您真正缺少的核心是 Mongoose API 方法也使用 "Promises",但您似乎只是在使用回调从文档或旧示例中复制。解决方案是转换为仅使用 Promises。

使用 Promise

Model.find(, _id: 1, tweet: 1).then(tweets => 
  Promise.all(
    tweets.map(( _id, tweet ) => 
      api.petition(tweet).then(result =>   
       TweetModel.findOneAndUpdate( _id ,  result ,  new: true )
         .then( updated =>  console.log(updated); return updated )
      )
    )
  )
)
.then( updatedDocs => 
  // do something with array of updated documents
)
.catch(e => console.error(e))

除了回调的一般转换之外,主要变化是使用Promise.all() 来解析来自Array.map() 的输出,该输出正在处理来自.find() 的结果,而不是for 循环。这实际上是您尝试中最大的问题之一,因为 for 无法实际控制异步函数何时解析。另一个问题是“混合回调”,但这是我们在这里通常只使用 Promises 来解决的问题。

Array.map() 中,我们从API 调用返回Promise,链接到实际更新文档的findOneAndUpdate()。我们还使用new: true 来实际返回修改后的文档。

Promise.all() 允许“Promise 数组”解析并返回结果数组。这些您可以看到updatedDocs。这里的另一个优点是内部方法将以“并行”而不是串行方式触发。这通常意味着更快的分辨率,尽管它需要更多的资源。

还请注意,我们使用 _id: 1, tweet: 1 的“投影”仅从Model.find() 结果返回这两个字段,因为它们是其余调用中唯一使用的字段。当您不使用其他值时,这样可以节省为每个结果返回整个文档的时间。

您可以简单地从findOneAndUpdate() 返回Promise,但我只是添加了console.log(),这样您就可以看到输出正在触发。

正常的生产使用应该没有它:

Model.find(, _id: 1, tweet: 1).then(tweets => 
  Promise.all(
    tweets.map(( _id, tweet ) => 
      api.petition(tweet).then(result =>   
       TweetModel.findOneAndUpdate( _id ,  result ,  new: true )
      )
    )
  )
)
.then( updatedDocs => 
  // do something with array of updated documents
)
.catch(e => console.error(e))

另一个“调整”可能是使用Promise.map() 的“bluebird”实现,它结合了常见的Array.map()Promise(s) 实现以及控制运行并行调用的“并发性”的能力:

const Promise = require("bluebird");

Model.find(, _id: 1, tweet: 1).then(tweets => 
  Promise.map(tweets, ( _id, tweet ) => 
    api.petition(tweet).then(result =>   
      TweetModel.findOneAndUpdate( _id ,  result ,  new: true )
    ),
     concurrency: 5 
  )
)
.then( updatedDocs => 
  // do something with array of updated documents
)
.catch(e => console.error(e))

“并行”的替代方案将按顺序执行。如果太多的结果导致太多的 API 调用和调用写回数据库,则可以考虑这样做:

Model.find(, _id: 1, tweet: 1).then(tweets => 
  let updatedDocs = [];
  return tweets.reduce((o, _id, tweet ) => 
    o.then(() => api.petition(tweet))
      .then(result => TweetModel.findByIdAndUpdate(_id,  result ,  new: true )
      .then(updated => updatedDocs.push(updated))
    ,Promise.resolve()
  ).then(() => updatedDocs);
)
.then( updatedDocs => 
  // do something with array of updated documents
)
.catch(e => console.error(e))

在那里,我们可以使用Array.reduce() 将promise“链接”在一起,使它们能够按顺序解决。请注意,结果数组保持在范围内,并用附加到连接链末尾的最终 .then() 进行交换,因为您需要这种技术来“收集”来自 Promises 在该“链”中不同点解析的结果。


异步/等待

在从 NodeJS V8.x 开始的现代环境中,这实际上是当前的 LTS 版本并且已经有一段时间了,您实际上已经支持 async/await。这使您可以更自然地编写流程

try 
  let tweets = await Model.find(, _id: 1, tweet: 1);

  let updatedDocs = await Promise.all(
    tweets.map(( _id, tweet ) => 
      api.petition(tweet).then(result =>   
        TweetModel.findByIdAndUpdate(_id,  result ,  new: true )
      )
    )
  );

  // Do something with results
 catch(e) 
  console.error(e);

如果资源有问题,甚至可能按顺序处理:

try 
  let cursor = Model.collection.find().project( _id: 1, tweet: 1 );

  while ( await cursor.hasNext() ) 
    let  _id, tweet  = await cursor.next();
    let result = await api.petition(tweet);
    let updated = await TweetModel.findByIdAndUpdate(_id,  result , new: true );
    // do something with updated document
  

 catch(e) 
  console.error(e)

还要注意 findByIdAndUpdate() 也可以用作匹配 _id 已经隐含,因此您不需要将整个查询文档作为第一个参数。


批量写入

最后一点,如果您实际上根本不需要更新的文档来响应,那么bulkWrite() 是更好的选择,它允许写入通常在单个请求中在服务器上处理:

Model.find(, _id: 1, tweet: 1).then(tweets => 
  Promise.all(
    tweets.map(( _id, tweet ) => api.petition(tweet).then(result => ( _id, result ))
  )
).then( results =>
  Tweetmodel.bulkWrite(
    results.map(( _id, result ) => 
      ( updateOne:  filter:  _id , update:  $set:  result    )
    )
  )
)
.catch(e => console.error(e))

或者通过async/await语法:

try 
  let tweets = await Model.find(, _id: 1, tweet: 1);

  let writeResult = await Tweetmodel.bulkWrite(
    (await Promise.all(
      tweets.map(( _id, tweet ) => api.petition(tweet).then(result => ( _id, result ))
    )).map(( _id, result ) =>
      ( updateOne:  filter:  _id , update:  $set:  result    )
    )
  );
 catch(e) 
  console.error(e);

上面显示的几乎所有组合都可以变成这样,因为bulkWrite() 方法采用指令“数组”,因此您可以从上述每个方法中处理的 API 调用构造该数组。

【讨论】:

非常感谢您的回答,我现在明白了,它可以工作了! :) 我更改了答案,因为我有另一种类型的请愿书,但我不知道如何更改它以使用承诺。知道我应该改变什么吗? @isanma 如果您还有其他问题,那么Ask a new Question。您已获得“您提出的问题”的答案。在给出答案后,任何时候都不能将现有问题更改为新问题。请Accept what answers the orignal question,然后在您需要进一步帮助的地方发布一个新的、明确的问题。 @isanma 当然我可以简单地指出request-promise 提供了与原始问题完全相同的 Promise API。如果您仍然不明白,请提出一个新问题。

以上是关于使用外部 API 调用和 findOneAndUpdate 循环结果的主要内容,如果未能解决你的问题,请参考以下文章

NodeJS Rest API - 调用外部 API 的正确位置在哪里

使用 Tibco BW Vers6 调用外部 API

减少对外部 API 的身份验证调用(Laravel 5.6)

从 Web API 同步调用外部 api

通过 Web API 操作使用 HttpClient 调用外部 HTTP 服务

如何使用 Dart 的方法和数据向外部 API 发布信息