简体   繁体   中英

Chained promises in a loop: Firebase read works fine, Firebase write does not

I need to loop over the data returned from an API call and insert it into Firebase. In order to prevent the Firebase API from overloading and timing out, I am using this "chain" technique to ensure the previous loop and all its promises are done before going onto the next one. The read/get from Firebase triggers and returns the expected results.

The write/set to Firebase triggers, I know this because "console.log(parents)" executes and displays the proper data, and the "then" that follows the "set" prints "Added student with ID". However, no data is ever written to the datastore in the cloud and no errors are displayed. If I run the write/set on its own, outside of a loop and the chain, it works fine.

function writeDataToFirestoreStudents(data) {
        let chain = Promise.resolve();
        for (let i = 0; i < data.length; ++i) {
            var parents = [];
            chain = chain.then(()=> {
                db.collection("parents").where("student_id", "==", data[i].kp_ID)
                .get()
                .then(function(querySnapshot) {
                    parents = [];
                    querySnapshot.forEach(function(doc) {
                        //console.log(doc.id, " => ", doc.data());
                        parents.push(doc.data().bb_first_name + " " + doc.data().bb_last_name);
                    });
                    return parents;
                }).then(function(parents) {
                    console.log(parents);
                    var docRef = db.collection('students').doc(data[i].kp_ID);
                    var setAda = docRef.set({
                        bb_first_name: data[i].bb_first_name,
                        bb_last_name: data[i].bb_last_name,
                        bb_current_grade: data[i].bb_current_grade,
                        bb_class_of: data[i].bb_class_of,
                        parents: parents,
                    }).then(ref => {
                        console.log('Added student with ID: ', data[i].kp_ID);
                    });
                }).catch(function(error) {
                    console.error("Error writing document: ", error);
                });
            })
            .then(Wait)
        }
    }

function Wait() {
    return new Promise(r => setTimeout(r, 25))
}

.batch . This works fine for me.

    pub.addToFirestore = function(){

        var countries = [
{"name": "Afghanistan", "code": "AF"},
{"name": "Åland Islands", "code": "AX"},
{"name": "Albania", "code": "AL"},
{"name": "Algeria", "code": "DZ"},
{"name": "American Samoa", "code": "AS"},
{"name": "AndorrA", "code": "AD"},
{"name": "Angola", "code": "AO"},
{"name": "Anguilla", "code": "AI"},
{"name": "Antarctica", "code": "AQ"},
{"name": "Antigua and Barbuda", "code": "AG"},
{"name": "Zimbabwe", "code": "ZW"}
];
        var db = firebase.firestore();
        var batch = db.batch();

        for (var i = 0; i < countries.length; i++) {
            var newRef = db.collection("countries").doc(countries[i].code);
            batch.set(newRef, {name: countries[i].name});
        }

        //Commit the batch
        batch.commit().then(function () {
            console.log("done");
        });   

    };

The main problem you're having is you're trying to mix synchronous and asynchronous elements together. You should really isolate all the required variables for the async sections.

I apologise for the 'long answer'

An example of this would be: (Source: https://codeburst.io/asynchronous-code-inside-an-array-loop-c5d704006c99 )

var output = '';
var array = ['h','e','l','l','o'];
// This is an example of an async function, like an AJAX call
var fetchData = function () {
 return new Promise(function (resolve, reject) {
    resolve();
  });
};
/* 
  Inside the loop below, we execute an async function and create
  a string from each letter in the callback.
   - Expected output: hello
   - Actual output: undefinedundefinedundefinedundefined
*/
for (var i = 0; i < array.length; i++) {
  fetchData(array[i]).then(function () {
    output += array[i];
  });
}

There are a few ways to solve this problem, you could wrap the inside of the for loop with a function, for example:

for (var i = 0; i < array.length; i++) {
    (function (letter) {
        fetchData(letter).then(function () {
            output += letter;
        });
    })(array[i])
}

However, the draw back with this is that there is no way to know when the calls are done (without a callback monster), and the error handling becomes... hard... Interestingly, JavaScript has now got a built in tool for this. Meaning you can do something like this:

array.forEach(letter => {
    fetchData(letter)
    .then(function () {
        output += letter;
    })
})

But, there is still the issue of not knowing when everything is complete. This is the point where promises come in incredibly useful. Let us say that you have 2 functions that take different length of time to complete. You can use Promise.all to wait for them both to resolve before you continue.

const foo500 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'foo500');
});
const bar800 = new Promise((resolve, reject) => {
    setTimeout(resolve, 800, 'bar800');
});

const both = Promise.all([foo500, bar800])
.then(function(values) {
    // Logs: Array ['foo500', 'bar800']
    console.log(values);
});

In this example after 800 ms the 'all' will resolve providing an array of values from the two promises.

There is one more piece to this, that being ' forEach ' does not output the value from it's function, This is where ' map ' steps in. This means we can easily output or promises into an array for Promise.all to 'wait' for.

This finally leads me to hopefully helping.

function writeDataToFirestoreStudents(data) {
    // Allow the output to know when we're done.
    return Promise.all(
        // Here we build our promises
        data.map(student => 
            // Do the first database query
            db.collection('parents')
            .where('student_id', '==', student.kp_ID)
            .get()

            // Convert the results into an array of parents.
            .then(snapshot => snapshot.map(doc =>
                doc.data().bb_first_name + " " +
                doc.data().bb_last_name
            ))

            // Use the 'parent' array.
            .then(parents =>
                // Perform the next database query.
                db.collection('students')
                .doc(student.kp_ID)
                .set({
                    // Note here 'student' has not changed and is in scope.
                    bb_first_name: student.bb_first_name,
                    bb_last_name: student.bb_last_name,
                    bb_current_grade: student.bb_current_grade,
                    bb_class_of: student.bb_class_of,
                    // ES6 short hand.
                    parents
                })
            )

            // Spread your error net
            .catch(err => console.error(
                "Error writing document: ", error
            ))
        )
    )
}

// You could use the function like so
writeDataToFirestoreStudents(data)

// You can now use 'then' to tell when we're done.
.then(students => {
    // This is the array of students
    console.log(students);
})

It is important to note that I have not tested this code, so I cannot guarantee it will work first time.

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