【问题标题】:Synchronous database queries with Node.js使用 Node.js 进行同步数据库查询
【发布时间】:2011-09-29 15:53:42
【问题描述】:

我有一个 Node.js/Express 应用程序,它在路由中查询 MySQL 数据库并将结果显示给用户。我的问题是如何在将用户重定向到他们请求的页面之前运行查询并阻止直到两个查询都完成?

在我的示例中,我有 2 个查询需要在呈现页面之前完成。如果我将查询 2 嵌套在查询 1 的“结果”回调中,我可以让查询同步运行。但是,当查询数量增加时,这将变得非常复杂。

如何在不将后续查询嵌套在先前查询的“结果”回调中的情况下同步运行多个(在本例中为 2)数据库查询?

我查看了 Node 模块中的“流控制/异步好东西”并尝试了 flow-js,但我无法让它与异步查询一起使用。

下面列出的是我尝试从“/home”路由执行的 2 个查询。 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 }});
});

【问题讨论】:

  • 我 .. 呃 ...我认为您这样做完全错误...重点是异步性。它实际上是一个更好的编程模型。您对 node.js 的用途有任何了解吗?它允许您使用事件回调来减少单个线程上的自旋时间,从而更多地利用系统。我真的建议您采用“在回调中做事”模型,因为这就是意图。或者切换到 Ruby。
  • 那么您将如何进行多个异步查询并将数据返回到 EJS 视图?
  • 因此,如果我必须执行 10 个查询,我会将每个查询堆叠/嵌套在前一个回调中吗?这不会很快变得令人费解吗?
  • 是的,有这种方法。相反,您想知道如何让数据库将 10 个结果集作为一个查询返回?
  • 是的,您要查询哪个平台?不应该有所作为,但可以。这是原始 sql select * from customers; select * from products;

标签: database node.js synchronous


【解决方案1】:

节点的目标不是关心事情发生的顺序。这会使一些场景复杂化。嵌套回调并不可耻。一旦你习惯了它的外观,你可能会发现你实际上更喜欢这种风格。我愿意;很清楚将触发什么订单回调。如果需要,您可以放弃匿名函数以减少冗长。

如果你愿意稍微重构你的代码,你可以使用“典型的”嵌套回调方法。如果你想避免回调,有许多异步框架会尝试帮助你做到这一点。您可能想要查看的一个是 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();
            }
        });
});

一个更好的方法是在每个“结果”回调中简单地调用 finishRequest() 来检查非空数组,然后再呈现响应。这是否适用于您的情况取决于您的要求。

【讨论】:

  • 我一直认为像这样的引用计数是一种“黑客行为”,但在涉及回调时它似乎是有效的。谢谢你的例子。
  • 这有点 hacky,如果我要在我的代码中编写它,我肯定会投资一种更强大的方法。如果您愿意进行一些重组,则可以避免这种情况。我用更多示例更新了我的答案。
  • 感谢 async.js 的第二个示例。这绝对是一个更简洁的模式。谢谢!
  • 非常感谢您的帮助解释!我最终使用了 caolan 的异步库,但你的解释让我开始朝着正确的方向前进。谢谢!
【解决方案2】:

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

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);
    }
  };
};

Example

用法:

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 链接!它指向错误的地方。
【解决方案3】:

我发现异步库最适合这种情况。 https://github.com/caolan/async#parallel

我无法测试这个或任何东西,如果有一些错别字,请原谅我。我将您的查询功能重构为可重用。因此,调用 queryRows 将返回一个与异步模块的并行回调函数格式匹配的函数。两个查询完成后,它将调用最后一个函数并将两个查询的结果作为参数传递,您可以读取该参数以传递给您的模板。

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} 
    });
  });
});

【讨论】:

  • 我喜欢这个。这个答案的好处是它不需要任何引用计数。我肯定会试一试,但可能会将所有查询移到存储过程中,这样就不需要构建复杂的动态 sql 语句。
  • 让一个帮助库实现所有像这样花哨的异步东西要容易得多。该库可能会进行引用计数或花哨的回调管理,但您永远不必担心。
  • 我来SO的答案。这应该在 IMO 中排名更高,这是对许多人的常见 JS 问题的解决方案。寻找并接收关于回调哲学的文字墙。
  • 现在我可能会使用 Promises,因为它已经被整合到 ES6 中。
  • 我在一个爱好项目中使用了 sync-mysql。优点是编程模型更简单、更干净。但它要慢得多。没关系;我不担心性能,所以不考虑池化或类似的东西。我还使用 mysql 包和 Promises 实现了它。这工作和运行得更快,但代码要重得多。我最终依靠 Promise.all()。
【解决方案4】:

这里有一些解决方案,但我认为最好的解决方案是以非常简单的方式使代码同步。

你可以使用“synchonize”包。

只是

npm 安装同步

然后var sync = require(synchronize);

使用

将应该同步的逻辑放入纤程中

sync.fiber(function() { //put your logic here }

两个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。之后将执行下面的代码。不需要嵌套承诺或类似的东西。

【讨论】:

  • 示例不能开箱即用,它会让人们变得更聪明。我喜欢这样的例子,谢谢。
【解决方案5】:

选项一:如果您的所有查询都相互关联,则创建存储过程,将所有数据逻辑放入其中并拥有一个 db.execute

选项二:如果您的数据库使用一个连接,则命令 a 保证串行执行,您可以将其用作异步助手

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
});

【讨论】:

  • 这是否意味着如果我将数据库连接池与异步助手一起使用,查询仍然可以并行运行?
  • 是的。如果两个查询转到两个不同的连接,您无法预测哪个连接首先完成。在 MySql 端,每个连接都在单独的线程中处理。简单示例:pool.query('select sleep (2)'); pool.query('select sleep(1)') - 第二次查询很可能会转到单独的链接,并且很可能在第一次查询前一秒完成。如果您使用一个链接发送查询,它们将一个接一个地串行处理(并且可能在 mysql 端的同一线程中)。
【解决方案6】:

您可以使用 Fiber 编写带有 Node.JS 的伪同步代码,看看 DB https://github.com/alexeypetrushin/mongo-lite/blob/master/test/collection.coffee 的这些测试 它们是异步的,但看起来是同步的,更多细节http://alexeypetrushin.github.com/synchronize

【讨论】:

    猜你喜欢
    • 2017-11-02
    • 1970-01-01
    • 1970-01-01
    • 2011-05-11
    • 1970-01-01
    • 2021-10-20
    • 2017-06-06
    • 2016-06-17
    • 1970-01-01
    相关资源
    最近更新 更多