Resultados de bucle con una llamada de API externa y findOneAndUpdate

Estoy tratando de escribir un progtwig que obtenga los documentos de una base de datos de Mongo con mongoose y procesarlos utilizando una API y luego edite cada documento en la base de datos con los resultados del procesamiento. Mi problema es que tengo problemas porque no entiendo completamente los nodejs y los asíncronos. Este es mi código:

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

El problema es que en el FindOneAndUpdate, los tweets no están definidos, por lo que no puede encontrar esa ID. ¿Alguna solución? Gracias

Lo que realmente te falta es que los métodos de la API de Mongoose también usan “Promesas” , pero parece que simplemente estás copiando de la documentación o ejemplos antiguos que usan devoluciones de llamada. La solución a esto es convertir usar promesas solamente.

Trabajando con Promesas

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

Aparte de la conversión general de devoluciones de llamada, el cambio principal está utilizando Promise.all() para resolver la Array.map() del Array.map() se procesa en los resultados de .find() lugar del bucle for . Ese es realmente uno de los mayores problemas en su bash, ya que el for no puede controlar realmente cuando se resuelven las funciones asíncronas. El otro problema es “mezclar devoluciones de llamada”, pero eso es a lo que generalmente nos dirigimos aquí utilizando solo Promesas.

Dentro de Array.map() , devolvemos la Promise de la llamada a la API, encadenada a findOneAndUpdate() que en realidad está actualizando el documento. También utilizamos new: true para devolver realmente el documento modificado.

Promise.all() permite que una “matriz de Promesa” resuelva y devuelva una matriz de resultados. Estos los ves como updatedDocs . Otra ventaja aquí es que los métodos internos se activarán en “paralelo” y no en serie. Esto generalmente significa una resolución más rápida, aunque toma algunos recursos más.

Tenga en cuenta también que usamos la “proyección” de { _id: 1, tweet: 1 } para devolver solo esos dos campos del resultado de Model.find() porque esos son los únicos que se usan en las llamadas restantes. Esto ahorra en devolver todo el documento para cada resultado allí cuando no usa los otros valores.

Simplemente puede devolver la Promise desde findOneAndUpdate() , pero solo estoy agregando la console.log() para que pueda ver que la salida se está disparando en ese momento.

El uso normal de la producción debe hacer sin él:

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

Otro “tweak” podría ser usar la implementación ” Promise.map() ” bluebird “, que combina la Array.map() común de Promise (s) Array.map() con la capacidad de controlar la” concurrencia “de ejecutar llamadas 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)) 

Una alternativa a “paralelo” se ejecutaría en secuencia. Esto podría considerarse si demasiados resultados hacen que muchas llamadas a la API y llamadas a escribir en la base de datos:

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

Allí podemos usar Array.reduce() para “encadenar” las promesas y permitir que se resuelvan secuencialmente. Tenga en cuenta que el conjunto de resultados se mantiene dentro del scope y se intercambia con el final .then() añadido al final de la cadena unida, ya que necesita esa técnica para “recostackr” los resultados de las Promesas que se resuelven en diferentes puntos de esa “cadena”.


Async / Await

En entornos modernos a partir de NodeJS V8.x, que en realidad es la versión actual de LTS y ha sido por un tiempo, en realidad tiene soporte para async/await . Esto le permite escribir más naturalmente su flujo

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

O incluso, posiblemente, procesar de forma secuencial, si los recursos son un 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) } 

Teniendo en cuenta que findByIdAndUpdate() también se puede usar como coincidencia con _id ya está implícito, por lo que no necesita un documento de consulta completo como primer argumento.


BulkWrite

Como nota final, si realmente no necesita los documentos actualizados en respuesta, entonces bulkWrite() es la mejor opción y permite que las escrituras se procesen generalmente en el servidor en una sola solicitud:

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

O a través de la syntax 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); } 

Casi todas las combinaciones que se muestran arriba pueden bulkWrite() ya que el método bulkWrite() toma una “matriz” de instrucciones, por lo que puede construir esa matriz a partir de las llamadas a la API procesadas a partir de todos los métodos anteriores.