简体   繁体   中英

Code inside .then executing before the Promise

I have a JSON file from Spotify with objects that represent artists. Inside of those objects, one of the properties is 'genres' that is an array of strings (genres).

What I'm trying to do is find or create those genres in MongoDB. Then add those object IDs into an array and pass that array when I create my Artist objects.

But, my Artists get created in the DB way before that find or create process for their genres is done.

Thus the line fillRest runs before fillGenres.

I've tried changing from Object.values.forEach to for..of, for..in, different asyncs, promises, modulating code...

Basically added await everywhere I can (most in the wrong places probably)

import * as data from '../spotify_data/artist_id.json';

async function fillGenres(array) {
  const genreIDs = []; // array of genre object IDs I'm trying to put inside every appropriate artist

  if (array !== 'undefined') {
    for (const element of array) {
      await Genre.findOrCreate({ name: element }, (err, result) => {
        genreIDs.push(result._id);
      });
    }
  }

  return genreIDs;
}

async function fillRest(entry, genreIDs) {
  const artist = {
    name: entry.ArtistName,
    genres: genreIDs,
    spotifyID: entry.ArtistID,
    popularity: entry.Popularity,
    spotifyFollowers: entry.Followers,
  };

  Artist.create([artist])
    .then((result) => {
      console.log(result);
    })
    .catch((error) => {
      console.log(error);
    });
}

async function spotifySeed() {
  const entries = Object.values(data);

  for (const entry of entries) {
     fillGenres(entry.Genres)
        .then((genreIDs) => {
          fillRest(entry, genreIDs); // this line gets executed before fillGenres above^ which is super weird
        });
  }
}

spotifySeed();

Artists get added to MongoDB with genres set as []. After that I get console output with good genreID arrays (that were supposed to be inside there^ instead of genres).

Resolved - EDIT

Thanks to everyone who helped. The problem was in findOrCreate as it did not return a promise. I used this package for mongoose instead that has Promises ( https://www.npmjs.com/package/mongoose-findorcreate ).

And the code now is

if (Array.isArray(array)) {
  // eslint-disable-next-line no-restricted-syntax
    for (const element of array) {
      await Genre.findOrCreate({ name: element })
        .then((result) => {
          genreIDs.push(result.doc._id);
        });
    }
  }

and in SpotifySeed

const genreIDs = await fillGenres(entry.Genres);
      await fillRest(entry, genreIDs);

I haven't used the Spotify APIs before, so I can't say much about that but there are a couple of issues I see at first glance. First, you're checking if (array !== 'undefined') { , which is checking if the array variable is a string that is literally 'undefined' (not the value undefined ). I'm fairly certain that is not what you intended. You would be better off using Array.isArray(array) here, if you're wanting to make sure array is actually an Array.

Second, you're using async functions and Promises mixed together, which (imo), you generally shouldn't do. You should use one or the other, so it's consistent and easier to follow. If you use await instead of .then , you will be able to write it in a more "synchronous" fashion and it should be easier to understand.

import * as data from '../spotify_data/artist_id.json';

async function fillGenres(array) {
  const genreIDs = []; // array of genre object IDs I'm trying to put inside every appropriate artist

  if (Array.isArray(array)) {
    for (const element of array) {
      const result = await Genre.findOrCreate({ name: element });
      genreIDs.push(result._id);
    }
  }

  return genreIDs;
}

async function fillRest(entry, genreIDs) {
  const artist = {
    name: entry.ArtistName,
    genres: genreIDs,
    spotifyID: entry.ArtistID,
    popularity: entry.Popularity,
    spotifyFollowers: entry.Followers,
  };

  try {
    const result = await Artist.create([artist]);
    console.log(result);
  } catch (error) {
    console.log(error);
  }
}

async function spotifySeed() {
  const entries = Object.values(data);

  for (const entry of entries) {
     const genreIDs = await fillGenres(entry.Genres);
     await fillRest(entry, genreIDs);
  }
}

await spotifySeed();

I don't know anything about the Spotify API, so this is just a guess. In fillGenres , you have:

await Genre.findOrCreate({ name: element }, (err, result) => {
  genreIDs.push(result._id);
});

You're passing a callback function. Sometimes libraries will allow you to use promises or callbacks. If you pass a callback, it won't return a promise. So I'm guessing that the loop starts off all the calls to Genre.findOrCreate , which doesn't return a promise since you're using a callback. and then immediately returns. Then fillRest is called and may finish before all the Genre.findOrCreate calls.

You want something like:

const result = await Genre.findOrCreate({ name: element });
genreIDs.push(result._id)

Though even better would be this:

function fillGenres(genreNames) {
  if(!genreNames || !genreNames.length) return Promise.resolve([])

  return Promise.all(genreNames.map(name => {
    return Genre.findOrCreate({ name })
      .then(result => result._id)
  })
}

This will run all the genre calls at the same time and return them all when they're done, rather than waiting to add one after another (as in your for loop).

If Genre.findOrCreate does not return a Promise, you could create a version that does:

function genreFindOrCreate(data) {
  return new Promise((resolve, reject) => {
    Genre.findOrCreate(data, (err, result) => {
      if(err) reject(err)
      else resolve(result)
    })
  })
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM