Синхронные запросы к базе данных с Node.js - PullRequest
52 голосов
/ 06 июля 2011

У меня есть приложение Node.js / Express, которое запрашивает базу данных MySQL по маршруту и ​​отображает результат пользователю. Моя проблема заключается в том, как выполнить запросы и заблокировать их до тех пор, пока оба запроса не будут выполнены, прежде чем перенаправить пользователя на запрошенную страницу?

В моем примере у меня есть 2 запроса, которые нужно завершить, прежде чем я отобразлю страницу. Я могу заставить запросы выполняться синхронно, если я вложу запрос 2 в обратный вызов 'result' запроса 1. Это, однако, станет очень запутанным, когда число запросов увеличится.

Как выполнить синхронные множественные (в данном случае 2) запросы к базе данных, не вкладывая последующий запрос в обратный вызов 'result' предыдущего запроса?

Я посмотрел на «Управление потоком данных / Асинхронность» в модулях Node и попробовал flow-js, но не могу заставить его работать с асинхронными запросами.

Ниже перечислены 2 запроса, которые я пытаюсь выполнить по маршруту '/ home'. Могут ли эксперты Узла объяснить «правильный» способ сделать это.

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

Ответы [ 6 ]

54 голосов
/ 07 июля 2011

Цель работы с узлом - не заботиться о том, в каком порядке происходят события. Это может усложнить некоторые сценарии. Нет никакого позора во вложенных обратных вызовах. Как только вы привыкнете к тому, как это выглядит, вы можете обнаружить, что действительно предпочитаете этот стиль. Я делаю; очень ясно, какие обратные вызовы заказа сработают. Вы можете отказаться от анонимных функций, чтобы сделать их менее многословными, если это необходимо.

Если вы хотите немного реструктурировать свой код, вы можете использовать «типичный» вложенный метод обратного вызова. Если вы хотите избежать обратных вызовов, существует множество асинхронных сред, которые попытаются помочь вам в этом. Один из них, который вы можете проверить, - 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 () в каждом обратном вызове «result» проверку непустых массивов до того, как вы обработаете ответ. Будет ли это работать в вашем случае, зависит от ваших требований.

17 голосов
/ 08 июля 2011

Вот очень простой трюк для обработки нескольких обратных вызовов.

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

14 голосов
/ 11 июля 2011

Я считаю, что асинхронная библиотека лучше всего подходит для подобных вещей.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} 
    });
  });
});
3 голосов
/ 30 октября 2016

Здесь есть несколько решений, но, на мой взгляд, лучшее решение - сделать код синхронным, очень простым способом.

Вы можете использовать пакет "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 (), он вставляет строку в основную таблицу и получает последний вставленный идентификатор. После этого код ниже будет выполнен. Нет необходимости вкладывать обещания или тому подобное.

1 голос
/ 11 мая 2012

Вы можете использовать волокна для написания псевдосинхронного кода с Node.JS взгляните на эти тесты для БД https://github.com/alexeypetrushin/mongo-lite/blob/master/test/collection.coffee они асинхронные, но выглядят как синхронные, подробнее http://alexeypetrushin.github.com/synchronize

1 голос
/ 07 июля 2011

вариант один: если все ваши запросы связаны друг с другом, создайте хранимую процедуру, поместите в нее всю логику данных и используйте один db.execute

вариант два: если ваш БД использует одно соединение, то командыгарантированно выполняется последовательно, и вы можете использовать это как async helper

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