MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Risultati in loop con una chiamata API esterna e findOneAndUpdate

La cosa fondamentale che ti manca davvero è che i metodi API Mongoose usano anche "Promesse" , ma sembra che tu stia semplicemente copiando dalla documentazione o da vecchi esempi usando i callback. La soluzione a questo è convertire all'utilizzo solo delle promesse.

Lavorare con le promesse

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

A parte la conversione generale dai callback, la modifica principale è l'utilizzo di Promise.all() per risolvere l'output da Array.map() in corso di elaborazione sui risultati di .find() invece di for ciclo continuo. Questo è in realtà uno dei maggiori problemi nel tuo tentativo, dal momento che for non può effettivamente controllare quando le funzioni asincrone si risolvono. L'altro problema è "combinare i callback", ma questo è ciò che generalmente affrontiamo qui utilizzando solo Promises.

All'interno di Array.map() restituiamo la Promise dalla chiamata API, concatenata a findOneAndUpdate() che sta effettivamente aggiornando il documento. Usiamo anche new: true per restituire effettivamente il documento modificato.

Promise.all() consente a una "matrice di promesse" di risolvere e restituire una matrice di risultati. Questi li vedi come updatedDocs . Un altro vantaggio qui è che i metodi interni si attiveranno in "parallelo" e non in serie. Questo di solito significa una risoluzione più rapida, anche se richiede alcune risorse in più.

Nota anche che utilizziamo la "proiezione" di { _id: 1, tweet: 1 } per restituire solo quei due campi da Model.find() risultato perché quelli sono gli unici utilizzati nelle restanti chiamate. Ciò consente di risparmiare sulla restituzione dell'intero documento per ogni risultato quando non si utilizzano gli altri valori.

Potresti semplicemente restituire la Promise dal findOneAndUpdate() , ma sto solo aggiungendo in console.log() quindi puoi vedere che l'output si sta attivando a quel punto.

Il normale uso di produzione dovrebbe farne a meno:

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

Un'altra "modifica" potrebbe consistere nell'utilizzare l'implementazione "bluebird" di Promise.map() , che combina entrambi il comune Array.map() a Promise (s) implementazione con la capacità di controllare la "concorrenza" dell'esecuzione di chiamate parallele:

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

Un'alternativa a "parallelo" verrebbe eseguita in sequenza. Questo potrebbe essere preso in considerazione se troppi risultati causano la riscrittura di troppe chiamate API e chiamate nel database:

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ì possiamo usare Array.reduce() per "incatenare" le promesse insieme permettendo loro di risolversi in sequenza. Nota che l'array di risultati viene mantenuto nell'ambito e sostituito con il .then() finale aggiunto all'estremità della catena unita poiché è necessaria una tale tecnica per "raccogliere" i risultati delle promesse che si risolvono in diversi punti di quella "catena".

Asincrono/In attesa

Negli ambienti moderni come da NodeJS V8.x che è in realtà l'attuale versione LTS ed è stata per un po' di tempo, in realtà hai il supporto per async/await . Ciò ti consente di scrivere il tuo flusso in modo più naturale

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 anche eventualmente elaborare in sequenza, se le risorse sono 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)
}

Notando inoltre che findByIdAndUpdate() può anche essere utilizzato come corrispondenza di _id è già implicito, quindi non è necessario un intero documento di query come primo argomento.

Scrittura in blocco

Come nota finale, se in realtà non hai bisogno dei documenti aggiornati in risposta, allora bulkWrite() è l'opzione migliore e consente alle scritture di elaborare generalmente sul server in una singola richiesta:

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

Oppure tramite async/await sintassi:

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 tutte le combinazioni mostrate sopra possono essere variate in questo come bulkWrite() richiede un "array" di istruzioni, quindi puoi costruire quell'array dalle chiamate API elaborate da ogni metodo sopra.