使用Node.js进行同步数据库查询

57
我有一个Node.js/Express应用程序,它在路由中查询MySQL数据库并将结果显示给用户。 我的问题是如何运行查询并在重定向用户到他们请求的页面之前阻塞,直到两个查询都完成?
在我的示例中,我有两个需要完成的查询才能呈现页面。 如果我将查询2嵌套在查询1的“结果”回调中,我可以使查询同步运行。 但是,当查询数量增加时,这种方式会变得非常复杂。
如何同时运行多个(在本例中为2个)数据库查询,而不在前一个查询的“结果”回调中嵌套后续查询?
我查看了Node模块中的“Flow control / Async goodies”,并尝试过flow-js,但我无法使用异步查询使其正常工作。
下面列出了我正在尝试从“/home”路由执行的两个查询。 Node专家们能否解释一下正确的方法?
app.get('/home', function (req,res) {
    var user_array = [];
    var title_array = [];

    // first query
    var sql = 'select user_name from users';
    db.execute(sql)
        .addListener('row', function(r) {
            user_array.push( { user_name: r.user_name } );
        })
        .addListener('result', function(r) {
            req.session.user_array = user_array;
        });

    // second query
    var sql = 'select title from code_samples';
    db.execute(sql)
        .addListener('row', function(r) {
            title_array.push( { title: r.title } );
        })
        .addListener('result', function(r) {
            req.session.title_array = title_array;
        });

        // because the queries are async no data is returned to the user
        res.render('home.ejs', {layout: false, locals: { user_name: user_array, title: title_array }});
});

3
如何进行多个异步查询并将数据返回到EJS视图? - Rick
4
那么如果我需要做10个查询,我需要将每个查询都嵌套在前一个回调里吗?这样会很快变得混乱吧? - Rick
1
是的,有这种方法。不过,你考虑一下如何让数据库在一个查询中返回10个结果集,而不是采用那种方法呢? - jcolebrand
1
是的,您要查询哪个平台?理论上没有影响,但实际上可能会有。这是原始 SQL:select * from customers; select * from products; - jcolebrand
Rick,如果你想要执行多个可能包含连接的查询,并且还想使用异步方法,那么你可以使用Node.js的async模块。 - user2724057
显示剩余5条评论
6个回答

56

在Node中的目标不是关心事件发生的顺序,这可能会使某些情况变得复杂。在嵌套回调中没有什么可耻的。一旦你习惯了它的外观,你可能会发现你实际上更喜欢那种样式。我就是这样;非常清楚回调将按什么顺序触发。如果必须,您可以放弃匿名函数以使其更简洁。

如果您愿意稍微重构代码,可以使用"典型"的嵌套回调方法。如果您想避免回调,则有许多异步框架可以帮助您实现此目的。其中一个您可能想查看的是async.js(https://github.com/fjakobs/async.js)。每个示例:

app.get('/home', function (req,res) {
    var lock = 2;
    var result = {};
    result.user_array = [];
    result.title_array = [];

    var finishRequest = function(result) {
        req.session.title_array = result.title_array;
        req.session.user_array = result.user_array;
        res.render('home.ejs', {layout: false, locals: { user_name: result.user_array, title: result.title_array }});
    };

    // first query
    var q1 = function(fn) {
      var sql = 'select user_name from users';
      db.execute(sql)
          .addListener('row', function(r) {
              result.user_array.push( { user_name: r.user_name } );
          })
          .addListener('result', function(r) {
              return fn && fn(null, result);
        });
    };

    // second query
    var q2 = function(fn) {
      var sql = 'select title from code_samples';
      db.execute(sql)
          .addListener('row', function(r) {
              result.title_array.push( { title: r.title } );
          })
          .addListener('result', function(r) {
              return fn && fn(null, result);
          });
    }

    //Standard nested callbacks
    q1(function (err, result) {
      if (err) { return; //do something}

      q2(function (err, result) {
        if (err) { return; //do something}

        finishRequest(result);
      });
    });

    //Using async.js
    async.list([
        q1,
        q2,
    ]).call().end(function(err, result) {
      finishRequest(result);
    });

});

对于一次性的操作,我可能会使用引用计数类型的方法。只需跟踪要执行的查询数量,并在所有查询完成时呈现响应。

app.get('/home', function (req,res) {
    var lock = 2;
    var user_array = [];
    var title_array = [];

    var finishRequest = function() {
        res.render('home.ejs', {layout: false, locals: { user_name: user_array, title: title_array }});
    }

    // first query
    var sql = 'select user_name from users';
    db.execute(sql)
        .addListener('row', function(r) {
            user_array.push( { user_name: r.user_name } );
        })
        .addListener('result', function(r) {
            req.session.user_array = user_array;
            lock -= 1;

            if (lock === 0) {
              finishRequest();
            }
        });

    // second query
    var sql = 'select title from code_samples';
    db.execute(sql)
        .addListener('row', function(r) {
            title_array.push( { title: r.title } );
        })
        .addListener('result', function(r) {
            req.session.title_array = title_array;
            lock -= 1;

            if (lock === 0) {
              finishRequest();
            }
        });
});

更好的方法是在每个“result”回调中简单地调用finishRequest(),并在渲染响应之前检查非空数组。这是否适用于您的情况取决于您的要求。


我一直认为像这样的引用计数是一种“hack”,但当涉及到回调时,它似乎是有效的。感谢你的例子。 - Rick
这有点“hacky”,如果我要在我的代码中一遍又一遍地编写它,我肯定会投资于更强大的方法。如果你愿意重新结构一下,就可以避免这种情况。我已经用更多的例子更新了我的答案。 - jslatts
谢谢您提供关于async.js的第二个例子,这绝对是一个更简洁的模式。谢谢! - Rick
非常感谢您的有益解释!我最终使用了Caolan的异步库,但是您的解释让我朝着正确的方向开始了。谢谢! - andyengle

18

这里有一个非常简单的技巧来处理多个回调。

var after = function _after(count, f) {
  var c = 0, results = [];
  return function _callback() {
    switch (arguments.length) {
      case 0: results.push(null); break;
      case 1: results.push(arguments[0]); break;
      default: results.push(Array.prototype.slice.call(arguments)); break;
    }
    if (++c === count) {
      f.apply(this, results);
    }
  };
};

示例

用法:

var handleDatabase = after(2, function (res1, res2) {
  res.render('home.ejs', { locals: { r1: res1, r2: res2 }):
})

db.execute(sql1).on('result', handleDatabase);
db.execute(sql2).on('result', handleDatabase);

所以基本上您需要引用计数。这是这些情况的标准方法。我实际上使用此小型实用程序函数而不是流程控制。

如果您想要完整的流程控制解决方案,我建议使用futuresJS


请移除 FutureJs 的链接!它指向了错误的位置。 - DigviJay Patil

14

我发现 Async 库在处理此类问题时是最好的选择。 https://github.com/caolan/async#parallel

我无法对其进行测试或任何操作,如果有一些笔误请谅解。我将你的查询函数重构为可重复使用的形式。因此,调用 queryRows 将返回一个符合 Async 模块并行回调函数格式的函数。在两个查询都完成后,它会调用最后一个函数,并将两个查询的结果作为参数传递,您可以读取并传递给模板。

function queryRows(col, table) {
  return function(cb) {
    var rows = [];
    db.execute('SELECT ' + col + ' FROM ' + table)
      .on('row', function(r) {
        rows.push(r)        
      })
      .on('result', function() {
        cb(rows);
      });
  }
}

app.get('/home', function(req, res) {
  async.parallel({
    users: queryRow('user_name', 'users'),
    titles: queryRow('title', 'code_samples')
  },
  function(result) {
    res.render('home.ejs', { 
      layout: false,
      locals: {user_name: result.users, title: result.titles} 
    });
  });
});

1
我喜欢这个。这个答案的好处是它不需要任何引用计数。我一定会尝试一下,但可能会将所有查询移动到存储过程中,以便无需构建复杂的动态SQL语句。 - Rick
1
让一个辅助库来实现所有这些花哨的异步操作要容易得多。该库可能会使用引用计数或者复杂的回调管理,但你永远不需要担心这些。 - loganfsmyth
1
我来到SO寻找的答案。在我看来,这应该排名更高,因为它提供了一个常见JS问题的解决方案,而许多人却只能收到有关回调哲学的大量文字。 - aliopi
现在我可能会使用 Promises,因为它们已经被纳入 ES6 中了。 - loganfsmyth
我正在使用sync-mysql进行一个业余项目。优点是编程模型更简单、更清晰。但速度要慢得多。这没关系;我不担心性能,所以不会考虑连接池或其他类似的东西。我还使用mysql包和Promises实现了它。那样可以运行得更快,但代码更重。最终我倾向于使用Promise.all()。 - Conrad Damon

6
这里有一些解决方案,但在我看来,最好的解决方案是用一种非常简单的方式同步代码。您可以使用“synchronize”包。只需运行以下命令:npm install synchronize。然后,使用var sync = require(synchronize)引入该包。通过使用sync.fiber将应该是同步的逻辑放入一个纤程中。下面是两个mysql查询的示例:
var express = require('express');
var bodyParser = require('body-parser');
var mysql = require('mysql');
var sync = require('synchronize');

var db = mysql.createConnection({
    host     : 'localhost',
    user     : 'user',
    password : 'password',
    database : 'database'
});

db.connect(function(err) {
    if (err) {
        console.error('error connecting: ' + err.stack);
        return;
    }
});

function saveSomething() {
    var post  = {id: newId};
    //no callback here; the result is in "query"
    var query = sync.await(db.query('INSERT INTO mainTable SET ?', post, sync.defer()));
    var newId = query.insertId;
    post  = {foreignKey: newId};
    //this query can be async, because it doesn't matter in this case
    db.query('INSERT INTO subTable SET ?', post, function(err, result) {
        if (err) throw err;
    });
}

当调用“saveSomething()”时,它会在主表中插入一行并接收最后插入的ID。之后将执行以下代码。无需嵌套承诺或其他类似的东西。

例子不能直接使用,但它能让人更聪明。我喜欢这样的例子,谢谢。 - user1742529

1

选项一:如果您的所有查询彼此相关,请创建存储过程,将所有数据逻辑放入其中,并使用单个db.execute执行。

选项二:如果您的数据库使用一个连接,则命令保证按顺序执行,您可以将其用作异步帮助程序。

db.execute(sql1).on('row', function(r) {
   req.session.user_array.push(r.user);
});
db.execute(sql2)
.on('row', function(r) {
   req.session.title_array.push(r.title);
})
.on('end'), function() {
   // render data from req.session
});

那么这是否意味着,如果我在异步帮助程序中使用数据库连接池,查询仍然可以并行运行? - Rick
1
如果两个查询分别发送到两个不同的连接中,你无法预测哪一个会先完成。在MySQL端,每个连接都在单独的线程中处理。简单的例子:pool.query('select sleep (2)'); pool.query('select sleep(1)') - 第二个查询很可能会发送到另一个链接,并且很可能在第一个查询之前一秒钟就完成了。如果你使用一个链接发送查询,它们将按顺序(并且很可能在mysql端的同一个线程中)一个接一个地进行处理。 - Andrey Sidorov

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接