node.js, express – ejecutando consultas mysql una tras otra dentro de bucles de forma sincrónica

En mi node.js, aplicación Express, estoy haciendo una llamada ajax con el middleware superagente . La llamada obtiene datos de la base de datos en una matriz compleja utilizando el middleware node-mysql a través de bastantes consultas de base de datos.

Antes de pegar el código, estoy tratando de explicar con palabras lo que estoy tratando de hacer, aunque el código sería suficiente para decir lo que quiere hacer con la adición de que todas las cosas asíncronas dentro de la primera callback deben hacerse de manera sincrónica.

Explicación:

Dentro de la callback de la primera consulta, se ejecuta un bucle for para ejecutar la segunda consulta varias veces y después de cada bucle, se debe llamar al siguiente bucle solo después de que se complete la callback de la segunda consulta. Las cosas son iguales para las siguientes líneas de código también.

Código:

Sin embargo, puede omitir las entrañas (marcadas en los comentarios) de los bucles for para hacer las cosas breves y fáciles si lo desea.

conn.query("SELECT * FROM `super_cats`",function(error, results, fields) { if(error){console.log("erro while fetching products for homepage "+ error);} for(var i in results) { // FIRST FOR LOOP INSIDE THE FIRST QUERY CALLBACK /*Innards of for loop starts*/ var elem = new Object(); var supcat_id=results[i].id; elem.super_id =supcat_id; elem.cats=new Array(); var cat= ''; /*Innards of for loop ends*/ conn.query("SELECT * FROM `categories` WHERE `supcat_id`="+supcat_id,function(error_cats, results_cats, fields_cats) { if (error_cats) {console.log("erro while fetching cats for menu " + error_cats);} for(var j in results_cats) { /*Innards of for loop starts*/ cat= new Object(); var cat_id=results_cats[j].id; cat.cat_id=cat_id; cat.cat_name=results_cats[j].cat_name; cat.subcats=new Array(); /*Innards of for loop starts*/ conn.query("SELECT * FROM `subcategories` WHERE `category`="+cat_id,function(error_subcats, results_subcats, fields_subcats) { if (error_subcats) {console.log("erro while fetching subcats for menu " + error_subcats);} for(var k in results_subcats ){ /*Innards of for loop starts*/ var subcat=new Object(); var subcat_id=results_subcats[k].id; subcat.subcat_id=subcat_id; subcat.subcat_name=results_subcats[k].subcategory; cat.subcats.push(subcat); elem.cats.push(cat); /*Innards of for loop starts*/ }// end of for loop for results_subcats }); }// end of for loop for result_cats }); super_cats.push(elem); }// end of for supercat results res.send(super_cats) }); 

Lo intenté con el middleware async, pero fue en vano, ya que simplemente no pude averiguar qué función utilizar en este caso.

Para ser breve, los requisitos son:

1) Todas las cosas asíncronas dentro de la primera callback deben hacerse de forma síncrona.

2) la respuesta debe enviarse a la llamada ajax solo después de que se hayan realizado todos los cálculos y no antes de eso (como probablemente sucedería si las cosas fueran asíncronas como están en el código existente, ¿no? )

Puede ser solo semántica, pero es importante comprender que no puede ejecutarse de forma sincrónica. Debe ejecutarlo de forma asíncrona y administrar el orden del procesamiento para obtener el efecto deseado. Me parece útil pensar en este tipo de problemas más en términos de cómo quiero transformar los datos (a la progtwigción funcional) en lugar del código imperativo que escribiría en un entorno más sincrónico.

Por lo que puedo decir por el código, quieres terminar con una estructura de datos en super_cats que se parece a esto:

 [ { super_id: 1, cats: [ { cat_id: 2, cat_name: "Category", subcats: [ { subcat_id: 3, subcat_name: "Subcategory" }, ... ] }, ... ] }, ... ] 

Comencemos extrayendo esto en una sola llamada de función con una sola callback.

 function getCategoryTree(callback) { } 

Ahora, entonces, vamos a tomarlo desde la parte superior. Desea ejecutar una sola función asíncrona (una consulta SQL) y desea producir una matriz con una entrada por resultado. Eso suena como una operación de map para mí. Sin embargo, como queremos que uno de los valores ( cats ) se determine de forma asíncrona, necesitamos usar un mapa asíncrono, que proporciona la biblioteca async .

Solo async.map firma async.map por ahora; queremos mapear nuestros results (este es el equivalente funcional de nuestro bucle for ), y para cada uno queremos convertir el resultado en algo: la función asíncrona que hace algo se llama iterador. Finalmente, una vez que tenemos todos nuestros elementos de matriz transformados, queremos llamar a la callback dada a nuestra función.

 function getCategoryTree(callback) { conn.query("SELECT * FROM `super_cats`", function(error, results, fields) { async.map(results, iterator, callback); }); } 

Creemos una nueva función para obtener la información de categoría de nivel superior y utilicemos su nombre en lugar de nuestro marcador de posición de iterator .

 function getCategoryTree(callback) { conn.query("SELECT * FROM `super_cats`", function(error, results, fields) { async.map(results, getSuperCategory, callback); }); } function getSuperCategory(resultRow, callback) { } 

Ahora tenemos que decidir qué queremos devolver para cada resultRow . Basándonos en nuestro diagtwig anterior, queremos un objeto con super_id igual a la ID de la fila y cats igual a todas las categorías en la categoría de nivel superior. Sin embargo, dado que los cats también se determinan de forma asíncrona, debemos ejecutar la siguiente consulta y transformar esos resultados antes de poder continuar.

Al igual que la última vez, queremos que cada elemento de nuestra matriz de cats sea ​​un objeto con cierta información del resultado de la consulta, pero también queremos una matriz de subcats , que de nuevo se determina de forma asíncrona, así que usaremos async.map nuevo. Esta vez, sin embargo, usaremos una función anónima para la callback, ya que queremos hacer algo con los resultados antes de entregarlos a la callback de nivel superior.

 function getSuperCategory(resultItem, callback) { var supcat_id = resultItem.id; conn.query("SELECT * FROM `categories` WHERE supcat_id` = " + supcat_id, function(error, results, fields) { async.map(results, getCategory, function(err, categories) { callback(err, { super_id: supcat_id, cats: categories }); }); }); } 

Como puede ver, una vez que se hace este async.map , significa que tenemos todas las categorías bajo esta async.map ; por lo tanto, podemos llamar a nuestra callback con el objeto que queremos que esté en la matriz.

Ahora que ya está hecho, solo necesitamos implementar getCategory . Se verá muy similar a getSuperCategory , porque queremos hacer básicamente lo mismo: para cada resultado, devolver un objeto que tenga algunos datos de la consulta, pero también un componente asíncrono.

 function getCategory(resultItem, callback) { var cat_id = resultItem.id; var cat_name = resultItem.cat_name; conn.query("SELECT * FROM `subcategories` WHERE `category` = " + cat_id, function(error, results, fields) { async.map(results, getSubCategory, function(err, subcategories) { callback(err, { cat_id: cat_id, cat_name: cat_name, subcats: subcategories }); }); }); } 

Ahora, solo necesitamos implementar getSubCategory .

 function getSubCategory(resultItem, callback) { callback(null, { subcat_id: resultItem.id, subcat_name: resultItem.subcategory }); } 

Ups! ¡Los datos que necesitamos de getSubCategory no tienen un componente asíncrono! Resulta que no necesitamos ese último async.map en absoluto; podríamos haber utilizado un mapa de matriz regular; vamos a cambiar getCategory y getSubCategory para que funcionen de esa manera.

 function getCategory(resultItem, callback) { var cat_id = resultItem.id; var cat_name = resultItem.cat_name; conn.query("SELECT * FROM `subcategories` WHERE `category` = " + cat_id, function(error, results, fields) { var subcategories = results.map(getSubCategory); callback(error, { cat_id: cat_id, cat_name: cat_name, subcats: subcategories }); }); } function getSubCategory(resultItem) { return { subcat_id: resultItem.id, subcat_name: resultItem.subcategory }; } 

Vale la pena señalar que nuestro método original funcionó bien; Si existe la posibilidad de que getSubCategory tenga un componente asíncrono, podría dejarlo como estaba.

¡Y eso es! Aquí está el código que escribí mientras escribía esta respuesta; Tenga en cuenta que tuve que falsificar un poco el SQL, pero creo que la idea está ahí:

 var async = require("async"); // fake out sql queries queryNum = 0; var conn = { query: function(query, callback) { queryNum++; var results = [1, 2, 3, 4, 5].map(function(elem) { return { id: queryNum + "-" + elem, cat_name: "catname-" + queryNum + "-" + elem, subcategory: "subcategory-" + queryNum + "-" + elem }; }); callback(null, results, null); } }; function getCategoryTree(callback) { conn.query("SELECT * FROM `super_cats`", function(error, results, fields) { async.map(results, getSuperCategory, callback); }); } function getSuperCategory(resultItem, callback) { var supcat_id = resultItem.id; conn.query("SELECT * FROM `categories` WHERE supcat_id` = " + supcat_id, function(error, results, fields) { async.map(results, getCategory, function(err, categories) { callback(err, { super_id: supcat_id, cats: categories }); }); }); } function getCategory(resultItem, callback) { var cat_id = resultItem.id; var cat_name = resultItem.cat_name; conn.query("SELECT * FROM `subcategories` WHERE `category` = " + cat_id, function(error, results, fields) { var subcategories = results.map(getSubCategory); callback(error, { cat_id: cat_id, cat_name: cat_name, subcats: subcategories }); }); } function getSubCategory(resultItem) { return { subcat_id: resultItem.id, subcat_name: resultItem.subcategory }; } getCategoryTree(function(err, result) { console.log(JSON.stringify(result, null, " ")); }); 

Hay algunas ineficiencias aquí, pero por simplicidad las he pasado por alto. Por ejemplo, en lugar de ejecutar la segunda subconsulta una y otra vez, puede consultar de una sola vez todos los identificadores de categoría, luego consultar todas las categorías de una vez, etc. Luego, una vez que tenga todos los datos, puede recorrer cada uno de ellos. Arregle sincrónicamente para sacar las piezas que necesita.

Además, hay mejores maneras de almacenar estructuras de árbol en bases de datos relacionales; En particular, eche un vistazo a Modorder Preorder Tree Traversal.