MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Résultats en boucle avec un appel d'API externe et findOneAndUpdate

La chose essentielle qui vous manque vraiment est que les méthodes de l'API Mongoose utilisent également "Promesses" , mais vous semblez simplement copier de la documentation ou d'anciens exemples en utilisant des rappels. La solution à cela est de convertir à l'aide de promesses uniquement.

Travailler avec des promesses

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

Outre la conversion générale des rappels, le principal changement consiste à utiliser Promise.all() pour résoudre la sortie du Array.map() en cours de traitement sur les résultats de .find() au lieu du for boucle. C'est en fait l'un des plus gros problèmes de votre tentative, car le for ne peut pas réellement contrôler le moment où les fonctions asynchrones se résolvent. L'autre problème est "mélanger les rappels", mais c'est ce que nous traitons généralement ici en utilisant uniquement Promises.

Dans le Array.map() nous retournons la Promise à partir de l'appel d'API, chaîné à findOneAndUpdate() qui est en train de mettre à jour le document. Nous utilisons également new: true pour renvoyer réellement le document modifié.

Promise.all() permet à un "tableau de promesses" de résoudre et de renvoyer un tableau de résultats. Ceux-ci vous voyez comme updatedDocs . Un autre avantage ici est que les méthodes internes se déclencheront en "parallèle" et non en série. Cela signifie généralement une résolution plus rapide, bien que cela nécessite un peu plus de ressources.

Notez également que nous utilisons la "projection" de { _id: 1, tweet: 1 } pour ne renvoyer que ces deux champs du Model.find() résultat parce que ce sont les seuls utilisés dans les appels restants. Cela évite de renvoyer le document entier pour chaque résultat lorsque vous n'utilisez pas les autres valeurs.

Vous pouvez simplement renvoyer la Promise de findOneAndUpdate() , mais j'ajoute juste dans le console.log() afin que vous puissiez voir que la sortie se déclenche à ce stade.

Une utilisation normale en production devrait s'en passer :

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 autre "tweak" pourrait être d'utiliser l'implémentation "bluebird" de Promise.map() , qui combinent le Array.map() à Promise (s) implémentation avec la possibilité de contrôler la "concurrence" de l'exécution d'appels parallèles :

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

Une alternative à "parallèle" s'exécuterait en séquence. Cela peut être envisagé si trop de résultats entraînent trop d'appels d'API et d'appels à réécrire dans la base de données :

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à, nous pouvons utiliser Array.reduce() pour "enchaîner" les promesses ensemble leur permettant de se résoudre séquentiellement. Notez que le tableau de résultats est conservé dans la portée et remplacé par le dernier .then() ajouté à la fin de la chaîne jointe puisque vous avez besoin d'une telle technique pour "collecter" les résultats des promesses se résolvant à différents points de cette "chaîne".

Asynchrone/Attente

Dans les environnements modernes à partir de NodeJS V8.x qui est en fait la version actuelle de LTS et ce depuis un moment maintenant, vous avez en fait un support pour async/await . Cela vous permet d'écrire plus naturellement votre flux

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 même éventuellement traiter de manière séquentielle, si les ressources posent problème :

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

Notant également que findByIdAndUpdate() peut également être utilisé comme correspondant au _id est déjà implicite, vous n'avez donc pas besoin d'un document de requête entier comme premier argument.

Écriture en masse

Enfin, si vous n'avez pas du tout besoin des documents mis à jour en réponse, alors bulkWrite() est la meilleure option et permet de traiter généralement les écritures sur le serveur en une seule requête :

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 syntaxe :

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

Presque toutes les combinaisons présentées ci-dessus peuvent être modifiées en cela comme le bulkWrite() prend un "tableau" d'instructions, vous pouvez donc construire ce tableau à partir des appels d'API traités à partir de chaque méthode ci-dessus.