MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Looping de resultados com uma chamada de API externa e findOneAndUpdate


A coisa principal que você realmente está perdendo é que os métodos da API do Mongoose também usam "Promessas" , mas parece que você está apenas copiando da documentação ou de exemplos antigos usando retornos de chamada. A solução para isso é converter para usar apenas Promises.

Trabalhando com promessas

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

Além da conversão geral de retornos de chamada, a principal mudança é usar Promise.all() para resolver a saída do Array.map() sendo processado nos resultados de .find() em vez do for ciclo. Esse é realmente um dos maiores problemas em sua tentativa, já que o for não pode realmente controlar quando as funções assíncronas são resolvidas. O outro problema é "misturar retornos de chamada", mas é isso que geralmente estamos abordando aqui usando apenas Promises.

Dentro do Array.map( ) retornamos a Promise da chamada da API, encadeada ao findOneAndUpdate() que está realmente atualizando o documento. Também usamos new:true para realmente retornar o documento modificado.

Promise.all() permite que um "array of Promise" resolva e retorne um array de resultados. Estes você vê como updatedDocs . Outra vantagem aqui é que os métodos internos serão acionados em "paralelo" e não em série. Isso geralmente significa uma resolução mais rápida, embora exija mais alguns recursos.

Observe também que usamos a "projeção" de { _id:1, tweet:1 } para retornar apenas esses dois campos do Model.find() resultado porque esses são os únicos usados ​​nas chamadas restantes. Isso economiza o retorno de todo o documento para cada resultado quando você não usa os outros valores.

Você pode simplesmente retornar a Promise do findOneAndUpdate() , mas estou apenas adicionando no console.log() para que você possa ver que a saída está disparando nesse ponto.

O uso normal de produção deve prescindir dele:
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))

Outro "ajuste" poderia ser usar a implementação "bluebird" de Promise. mapa() , que combina o comum Array.map() para Promise (s) implementação com a capacidade de controlar a "simultaneidade" da execução de chamadas paralelas:
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))

Uma alternativa para "paralelo" seria executada em sequência. Isso pode ser considerado se muitos resultados causarem muitas chamadas de API e chamadas para gravar de volta no banco de dados:
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))

Lá podemos usar Array. reduzir() para "encadear" as promessas, permitindo que elas sejam resolvidas sequencialmente. Observe que a matriz de resultados é mantida no escopo e trocada pelo .then() final anexado ao final da cadeia unida, pois você precisa dessa técnica para "coletar" resultados de Promessas que são resolvidas em diferentes pontos dessa "cadeia".

Assíncrono/Aguardar


Em ambientes modernos a partir do NodeJS V8.x, que na verdade é a versão LTS atual e já faz um tempo, você realmente tem suporte para async/await . Isso permite que você escreva seu fluxo de forma mais natural
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);
}

Ou até mesmo processar sequencialmente, se os recursos forem um problema:
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)
}

Observando também que findByIdAndUpdate() também pode ser usado para corresponder ao _id já está implícito, então você não precisa de um documento de consulta inteiro como primeiro argumento.

Gravação em massa


Como observação final, se você realmente não precisar dos documentos atualizados em resposta, bulkWrite() é a melhor opção e permite que as gravações geralmente sejam processadas no servidor em uma única solicitação:
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))

Ou via async/await sintaxe:
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);
}

Praticamente todas as combinações mostradas acima podem ser alteradas para isso como o bulkWrite() O método recebe uma "matriz" de instruções, para que você possa construir essa matriz a partir das chamadas de API processadas de todos os métodos acima.