取消嵌套节点数据库调用

Posted

技术标签:

【中文标题】取消嵌套节点数据库调用【英文标题】:Unnesting Node database calls 【发布时间】:2021-02-04 10:13:58 【问题描述】:

我有一个普通的

var express = require('express')

Node 表示 www 页面,像往常一样使用 session、pug 等。我的数据库调用

var db = require('./scripts/myHappymysqlScript')

我很自然地使用mysql,所以在db脚本中

var mysql = require('mysql')

例如

app.get('/catPhotos', (req, response) => 
    response.render('catPhotos.pug');
)

假设一个页面有一个表格,其中显示了 petNames 数据库中的某些内容,

app.get('/pets', function(req, res, next) 
    db.allPetNames(function(err, petsList) 
        res.render('pets.pug',
            
                'petsList': petsList,
                'pretty' : true
            )
    )

到目前为止一切顺利。

但这里的例子是 pug 页面上有 三个表,以及三个不同的数据库调用:

db.cats(function(err, c) 
    db.dogs(function(err, d) 
        db.budgies(function(err, b) 
            res.render('bigScreen.pug',
                
                    'cats' : c,
                    'k9s': d,
                    'budgies': b,
                    'pretty' : true
                )
        )
    )
)

我只是这样嵌套它们。

这似乎工作得很好。

它正确地按顺序等待。错误通过并得到正确处理,依此类推。

但是有更好的语法,更好的方法吗? 真正的™ Node(而非 Swift)程序员的 Node 方式是什么?!

也许考虑到我正在使用mysql 库,如果相关的话。


注意,总体而言,一种更好的方法是使用 Ajax 之类的东西在网页的每个“部分”中进行流式传输。确实,我一直都这样做。我在这里问什么,假设在 res.render 我确实想一次返回所有这些信息,有没有比这样的嵌套更好的东西?干杯

【问题讨论】:

【参考方案1】:

您可以使用promises 摆脱嵌套的数据库调用。

由于您提到您正在使用mysql 库与数据库进行交互,不幸的是,该库不提供基于 Promise 的 API。因此,要摆脱代码中的嵌套数据库调用,您需要围绕数据库调用的回调版本创建一个基于 Promise 的包装器。

有关什么是 Promise 及其工作原理的总体概述,请参阅以下链接:

MDN - PromiseMDN - Using Promises

以下是一个示例,说明如何创建基于 Promise 的包装器,然后使用该包装器摆脱嵌套的数据库调用。

这个基于 Promise 的包装器只是一个返回 Promise 的函数。它创建一个 Promise 实例,包装底层数据库调用,最终当数据库调用返回数据时,它会通知您的代码。

function getCats() 
   return new Promise((resolve, reject) => 
       // make the database call
       db.cats((error, cats) => 
           // in case of an error, reject the promise by
           // calling "reject" function
           // Also pass the "error" object to the "reject" function
           // as an argument to get access to the error message 
           // in the code that calls this "getCats" function
           if (error) 
              reject(error);
              return;
           
           
           // if there was no error, call "resolve" function
           // to resolve the promise. Promise will be resolved 
           // in case of successful database call
           // Also pass the data to "resolve" function
           // to access this data in the code that calls this
           // "getCats" function
           resolve(cats);
       );
   );

现在在您的路由处理函数中,调用 getCats 包装函数,而不是调用 db.cats(...)

有两种方法可以调用返回承诺的函数:

Promise-chaining(详情请访问上述链接) async-await 语法(推荐)

以下代码示例使用async-await 语法。为此,首先在function 关键字之前使用async 关键字将路由处理函数标记为async。这样做,我们可以在这个路由处理函数中使用await关键字。

app.get('/pets', async function(req, res, next) 
    try 
       const cats = await getCats();
       // similar wrappers for other database calls
       const dogs = await getDogs();
       const budgies = await getBudgies();
       
       // render the pub template, passing in the data
       // fetched from the database 
       ...

     catch (error) 
       // catch block will be invoked if the promise returned by
       // the promise-based wrapper function is rejected
       // handle the error appropriately
     
);

以上代码示例仅展示了如何将db.cats(...) 数据库调用包装在基于承诺的包装器中,并使用该包装器从数据库中获取数据。同样,您可以为 db.dogs(...)db.budgies(...) 调用创建包装器。

最好不要为每个数据库调用创建一个单独的基于 Promise 的包装器,理想情况下,您应该创建一个可重用的基于 Promise 的包装器函数,它接收要调用的函数并包装该函数调用在一个promise中,就像上面的代码示例所示,即getCats函数。

并行数据库调用

在路由处理函数的上述代码中需要注意的一点

const cats = await getCats();
const dogs = await getDogs();
const budgies = await getBudgies();

这会导致连续的数据库调用,这可能是你想要的,也可能不是你想要的。

如果这些数据库调用不相互依赖,那么您可以使用Promise.all() 方法并行调用基于promise 的包装器。

以下代码示例展示了如何使用 Promise.all() 并行调用基于 Promise 的包装函数。

app.get('/pets', async function(req, res, next) 
    try 
       // "petsData" will be an array that will contain all the data from 
       // three database calls.
       const petsData = await Promise.all([getCats(), getDogs(), getBudgies()]);
       
       // render the pub template, passing in the data
       // fetched from the database 
       ...
 
     catch (error) 
       ...
     
 );

我希望这足以帮助您摆脱当前代码中的嵌套数据库调用,并开始在您的代码中使用 Promise。

【讨论】:

这仍然感觉像是在链接。我认为第 1 步是获取数据,第 2 步是渲染。但即使是基于 Promise 的代码似乎也将渲染代码 inside 隐藏在 get-data 代码中,而不是 after 它。 @RickJames “即使是基于 Promise 的代码似乎也将渲染代码隐藏在 get-data 代码中,而不是在它之后” - 如果您查看try 块在路由处理程序的回调函数中,您将看到您在评论开头提到的两个步骤。基于 Promise 的代码只关心从数据库中获取数据,渲染代码在数据获取代码之后try 块中有一条注释应该写渲染代码。 我就是这么想的。有时我想做很多处理,包括一些 SQL 调用。我想要一个function 可以访问数据库并返回数据。由于我需要之前的结果,我什至不需要“异步”。 Parallel Database calls 就是这样。非常干净,易于维护且灵活。使用 Promise.all,如果任何调用失败,它都会快速失败。您可以查看其他 Promise 选项,并在部分失败但仍进行部分渲染时可能支持案例。【参考方案2】:

如果您尝试将 MySQL 与 Nodejs 一起使用,您应该寻找的模块是 mysql2 而不是 mysql

mysql2 提供了一种基于 promise 的方法,并且是用于 nodejs 的 mysql 模块的改进版本。

例如,对于执行查询,

    mysql
con.query(sql_query, (err, rows, field)=> //some code here 
    mysql2 中,您可以使用异步方法和承诺方法。此外,mysql2 中的prepared statements 比mysql 更容易。
//async approach
class A 
   static async fn(sql, params)
      const [data] = await con.execute(sql, [params]);
    return data;
    


//promise approach remains same as **mysql** itself.

这里是文档 mysql2 & more docs

【讨论】:

【参考方案3】:

如果您的数据库调用返回 Promise 而不是使用回调,您可以:

const cats = await db.cats();
const dogs = await db.dogs();
const budgies = await db.budgies();

res.render('bigScreen.pug', 
  cats : cats,
  k9s: dogs,
  budgies: budgies,
  pretty : true
);


// Or request them all in parallel instead of waiting for each to finish
const [
  cats,
  dogs,
  budgies
] = Promise.all([
  dg.cats(),
  dg.dogs(),
  db.budgies()
]);

【讨论】:

Dylan,谢谢,我正在使用 'mysql' 库,所以... ?!?!?【参考方案4】:

使用nodejs标准库util.promisify简单地将mysql函数转换成promise

示例:

const  promisify  = require('util');

const catsPromise = promisify(db.cats);
const dogsPromise = promisify(db.dogs);
const budgiesPromise = promisify(db.budgies);

async function routeHandler() 
  let err = null;

  try 
    const cats = await catsPromise();
    const dogs = await dogsPromise();
    const budgies = await budgiesPromise();
   catch(error) 
    err = error;
  

  if (err) 
    console.log(err);
    // you should res.end() or res.render(someErrorPage) here
    // failure to do so will leave the request open
   else 
    res.render('bigScreen.pug', 
      'cats' : cats,
      'k9s': dogs,
      'budgies': budgies,
      'pretty' : true
    );
  

【讨论】:

【参考方案5】:

Promise.all() 方法似乎是一种更著名、更简洁的方法,可以像您的用例一样并行进行多个调用。

但还有另一种方法。 :Multiple statement queries

要使用此功能,您必须为您的连接启用它:

var connection = mysql.createConnection(multipleStatements: true);

启用后,您可以像执行任何其他查询一样执行多个语句查询:

db.query('SELECT cats; SELECT dogs', function (error, results, fields) 
  if (error) throw error;
  // `results` is an array with one element for every statement in the query:
  console.log(results[0]); // [cat1,cat2]
  console.log(results[1]); // [dog1,dog2]
);

它在技术上更高效,因为与 MySQL 连接的来回次数更少。

(但是,默认情况下禁用此功能,因为如果值未正确转义,它允许 SQL 注入攻击)。要使用此功能,您必须为您的连接启用它。)

【讨论】:

以上是关于取消嵌套节点数据库调用的主要内容,如果未能解决你的问题,请参考以下文章

取消嵌套嵌套 json 数据以显示在 Quicksight 中

取消嵌套列并填充数据 - 找不到未嵌套的列

如何使用python取消嵌套json格式的数据

是否可以在 BigQuery 中取消嵌套数组,以便将嵌套数据按键值拆分为列?

如何取消嵌套不规则的 JSON 数据

尝试进行嵌套查询时已调用节点异步回调