sql >> Base de Datos >  >> NoSQL >> MongoDB

Resultados en bucle con una llamada API externa y findOneAndUpdate

Lo principal que realmente se está perdiendo es que los métodos de la API de Mongoose también usan "Promesas" , pero parece que solo está copiando de la documentación o ejemplos antiguos usando devoluciones de llamada. La solución a esto es pasar a usar Promises solamente.

Trabajar 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 las devoluciones de llamadas, el cambio principal es usar Promise.all() para resolver la salida del Array.map() siendo procesado en los resultados de .find() en lugar del for círculo. Ese es en realidad uno de los mayores problemas en su intento, ya que el for en realidad no puede controlar cuándo se resuelven las funciones asíncronas. El otro problema es "mezclar devoluciones de llamadas", pero eso es lo que generalmente abordamos aquí usando solo Promesas.

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

Promise.all() permite que una "matriz de Promise" resuelva y devuelva una matriz de resultados. Estos se ven como updatedDocs . Otra ventaja aquí es que los métodos internos se dispararán en "paralelo" y no en serie. Esto generalmente significa una resolución más rápida, aunque requiere 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 Model.find() porque esos son los únicos que se usan en las llamadas restantes. Esto ahorra tener que devolver el documento completo para cada resultado allí cuando no usa los otros valores.

Simplemente podría devolver el Promise de findOneAndUpdate() , pero solo estoy agregando console.log() para que pueda ver que la salida se dispara en ese punto.

El uso normal de producción debería prescindir de é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 "ajuste" podría ser usar la implementación "bluebird" de Promise.map() , que combina el común Array.map() a Promise (s) implementación con la capacidad de controlar la "concurrencia" de ejecución de 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" sería ejecutar en secuencia. Esto podría considerarse si demasiados resultados provocan demasiadas llamadas a la API y llamadas para volver 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, permitiéndoles resolverse secuencialmente. Tenga en cuenta que la matriz de resultados se mantiene dentro del alcance y se intercambia con el .then() final agregado al final de la cadena unida ya que necesita dicha técnica para "recopilar" los resultados de las Promesas que se resuelven en diferentes puntos de esa "cadena".

Asíncrono/Espera

En entornos modernos a partir de NodeJS V8.x, que en realidad es la versión LTS actual y lo ha sido durante un tiempo, en realidad tiene soporte para async/await . Esto le permite escribir su flujo de forma más 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);
}

O incluso posiblemente procesar secuencialmente, 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)
}

Observando también que findByIdAndUpdate() también se puede usar para hacer coincidir el _id ya está implícito, por lo que no necesita un documento de consulta completo como primer argumento.

Escritura masiva

Como nota final, si en realidad no necesita los documentos actualizados como 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 async/await sintaxis:

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

Prácticamente todas las combinaciones que se muestran arriba se pueden variar en esto como bulkWrite() El método toma una "matriz" de instrucciones, por lo que puede construir esa matriz a partir de las llamadas API procesadas de cada método anterior.