Синхронные запросы к базе данных с Node.js
У меня есть приложение Node.js/Express, которое запрашивает базу данных MySQL по маршруту и отображает результат пользователю. Моя проблема заключается в том, как выполнить запросы и заблокировать их до тех пор, пока оба запроса не будут выполнены, прежде чем перенаправить пользователя на запрошенную страницу?
В моем примере у меня есть 2 запроса, которые необходимо завершить, прежде чем я отрисовываю страницу. Я могу заставить запросы выполняться синхронно, если я вложу запрос 2 в обратный вызов 'result' запроса 1. Это, однако, станет очень запутанным, когда число запросов увеличится.
Как мне выполнить несколько (в данном случае 2) запросов к базе данных синхронно, не вкладывая последующий запрос в обратный вызов 'result' предыдущего запроса?
Я посмотрел на "Flow control / Async goodies" в модулях Node и попробовал flow-js, но не могу заставить его работать с асинхронными запросами.
Ниже перечислены 2 запроса, которые я пытаюсь выполнить по маршруту '/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 }});
});
6 ответов
Цель узла - не заботиться о порядке вещей. Это может усложнить некоторые сценарии. Нет никакого позора во вложенных обратных вызовах. Как только вы привыкнете к тому, как это выглядит, вы можете обнаружить, что на самом деле предпочитаете этот стиль. Я делаю; очень ясно, какие обратные вызовы заказа сработают. Вы можете отказаться от анонимных функций, чтобы сделать их менее многословными, если это необходимо.
Если вы хотите немного реструктурировать свой код, вы можете использовать "типичный" вложенный метод обратного вызова. Если вы хотите избежать обратных вызовов, существует множество асинхронных сред, которые попытаются помочь вам в этом. Например, вы можете попробовать 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" проверку непустых массивов перед тем, как вы отрендерите ответ. Будет ли это работать в вашем случае, зависит от ваших требований.
Вот действительно простой трюк для обработки нескольких обратных вызовов.
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
Я считаю, что асинхронная библиотека лучше всего подходит для подобных вещей. https://github.com/caolan/async
Я не могу проверить это или что-то еще, так что прости меня, если есть какие-то опечатки. Я реорганизовал вашу функцию запроса для повторного использования. Таким образом, вызов 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}
});
});
});
Здесь есть несколько решений, но, на мой взгляд, лучшее решение - сделать код синхронным, очень простым способом.
Вы можете использовать пакет "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(), он вставляет строку в основную таблицу и получает последний вставленный идентификатор. После этого код ниже будет выполнен. Не нужно вкладывать обещания или что-то в этом роде.
Вариант первый: если все ваши запросы связаны друг с другом, создайте хранимую процедуру, поместите в нее всю логику данных и получите одну 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
});
Вы можете использовать волокна для написания псевдосинхронного кода с Node.JS. Посмотрите на эти тесты для БД. https://github.com/alexeypetrushin/mongo-lite/blob/master/test/collection.coffee они асинхронные, но выглядят как синхронно, более подробная информация http://alexeypetrushin.github.com/synchronize