Пошук уроків, статей та іншого контенту
Налаштуєте пул з’єднань, розберете його параметри та навчитеся безпечно використовувати його в обробниках запитів.
Пул з’єднань — це набір уже відкритих або готових до відкриття з’єднань із базою даних. Замість створення нового з’єднання для кожного запиту застосунок повторно використовує з’єднання з пулу.
Без пулу обробник запиту міг би працювати так:
відкрити з’єднання з базою;
виконати SQL-запит;
закрити з’єднання.
Це створює зайві витрати часу та ресурсів. Пул автоматизує цей процес:
обробник бере вільне з’єднання з пулу;
виконує запит;
повертає з’єднання назад у пул.
Якщо всі з’єднання зайняті, нові запити чекають, доки одне з’єднання звільниться.
Пул з’єднань створюють один раз для всього застосунку, а не для кожного HTTP-запиту.
Для роботи з PostgreSQL у Node.js часто використовують пакет pg.
Встановлення:
npm install pgПул створюють за допомогою класу Pool:
const { Pool } = require('pg');
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'app_db',
user: 'app_user',
password: 'secret',
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
});Основні параметри:
host — адреса сервера бази даних;
port — порт бази даних;
database — назва бази даних;
user і password — облікові дані;
max — максимальна кількість одночасних з’єднань у пулі;
idleTimeoutMillis — скільки мілісекунд невикористовуване з’єднання може залишатися відкритим;
connectionTimeoutMillis — скільки мілісекунд чекати на встановлення нового з’єднання.
maxmax обмежує кількість з’єднань, які пул може використовувати одночасно.
Наприклад:
const pool = new Pool({
max: 5,
});У цьому випадку не більше п’яти операцій зможуть одночасно використовувати з’єднання цього пулу. Якщо шостий запит надійде тоді, коли всі з’єднання зайняті, він чекатиме.
Занадто велике значення max може перевантажити базу даних. Занадто мале значення призводить до довгого очікування запитів. Початкове значення варто підбирати з урахуванням можливостей бази та навантаження застосунку.
Тайм-аути захищають застосунок від нескінченного очікування:
connectionTimeoutMillis обмежує час встановлення з’єднання;
idleTimeoutMillis звільняє з’єднання, які довго не використовуються.
Тайм-аут не скасовує вже запущений SQL-запит. Він стосується відповідного етапу роботи з пулом або з’єднанням.
Для одного SQL-запиту найпростіше використовувати метод pool.query().
Метод самостійно:
бере з’єднання з пулу;
виконує запит;
повертає з’єднання назад у пул.
Приклад застосунку з HTTP-сервером:
const http = require('node:http');
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT || 5432),
database: process.env.DB_NAME || 'app_db',
user: process.env.DB_USER || 'app_user',
password: process.env.DB_PASSWORD || 'secret',
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
});
pool.on('error', (error) => {
// Логуємо помилки неактивних з’єднань
console.error('Помилка пулу:', error);
});
const server = http.createServer(async (request, response) => {
if (request.method !== 'GET' || request.url !== '/time') {
response.statusCode = 404;
response.setHeader('Content-Type', 'application/json; charset=utf-8');
response.end(JSON.stringify({ error: 'Маршрут не знайдено' }));
return;
}
try {
const result = await pool.query(
'SELECT NOW() AS current_time'
);
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json; charset=utf-8');
response.end(JSON.stringify({
currentTime: result.rows[0].current_time,
}));
} catch (error) {
// Не передаємо внутрішні деталі помилки клієнту
console.error('Помилка запиту:', error);
response.statusCode = 500;
response.setHeader('Content-Type', 'application/json; charset=utf-8');
response.end(JSON.stringify({
error: 'Внутрішня помилка сервера',
}));
}
});
server.listen(3000, () => {
console.log('Сервер запущено на http://localhost:3000');
});
async function shutdown() {
// Спочатку припиняємо приймати нові HTTP-запити
server.close(async () => {
// Закриваємо всі з’єднання пулу
await pool.end();
process.exit(0);
});
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);Запуск:
node server.jsПісля цього HTTP-запит до http://localhost:3000/time виконає SQL-запит через пул.
Дані від користувача не потрібно вставляти безпосередньо в SQL-рядок. Для цього використовують параметри:
const userId = 42;
const result = await pool.query(
'SELECT id, name FROM users WHERE id = $1',
[userId]
);$1 — це місце для першого параметра, а значення передають у масиві [userId].
Параметризовані запити:
захищають від SQL-ін’єкцій;
відокремлюють SQL-код від даних;
роблять код зрозумілішим.
Небезпечний варіант:
const userId = request.query.id;
const result = await pool.query(
`SELECT id, name FROM users WHERE id = ${userId}`
);Безпечний варіант:
const userId = request.query.id;
const result = await pool.query(
'SELECT id, name FROM users WHERE id = $1',
[userId]
);Параметризація не замінює перевірку даних. Наприклад, ідентифікатор користувача все одно потрібно перетворити на число та перевірити, що воно коректне.
pool.query() підходить для незалежного SQL-запиту. Якщо кілька запитів мають виконуватися в межах однієї транзакції, потрібно отримати окреме з’єднання з пулу:
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[100, 1]
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[100, 2]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
// З’єднання потрібно повернути в пул у будь-якому випадку
client.release();
}Важлива послідовність:
pool.connect() бере з’єднання з пулу;
BEGIN починає транзакцію;
SQL-запити виконуються через client;
COMMIT підтверджує зміни або ROLLBACK скасовує їх;
client.release() повертає з’єднання в пул.
Після client.release() не можна виконувати через цей об’єкт нові запити.
Обробник запиту має повертати з’єднання в пул навіть тоді, коли SQL-запит завершився помилкою. Саме для цього використовують try...finally:
const client = await pool.connect();
try {
const result = await client.query('SELECT 1');
return result.rows;
} finally {
client.release();
}Якщо викликати release() лише після успішного запиту, помилка може залишити з’єднання зайнятим:
// Небезпечний варіант
const client = await pool.connect();
const result = await client.query('SELECT 1');
client.release();Якщо client.query() завершиться помилкою, client.release() не виконається. Після кількох таких помилок пул може залишитися без доступних з’єднань.
Для одного запиту без транзакції краще використовувати pool.query(), оскільки він автоматично повертає з’єднання:
const result = await pool.query(
'SELECT id, name FROM users WHERE id = $1',
[userId]
);Помилки під час роботи з пулом можуть виникати через:
недоступність бази даних;
неправильні параметри підключення;
перевищення часу очікування;
помилку в SQL;
тимчасове перевантаження бази даних.
Обробник HTTP-запиту не повинен повертати клієнту пароль, SQL-запит або повний текст внутрішньої помилки. Внутрішню інформацію потрібно записати в журнал, а клієнту повернути загальне повідомлення.
try {
const result = await pool.query('SELECT * FROM users');
response.end(JSON.stringify(result.rows));
} catch (error) {
console.error(error);
response.statusCode = 500;
response.end(JSON.stringify({
error: 'Не вдалося отримати дані',
}));
}Параметри підключення краще зберігати в змінних середовища, а не безпосередньо в коді:
const pool = new Pool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});Під час завершення Node.js-процесу пул потрібно закрити:
await pool.end();Це дає змогу завершити активні операції та закрити з’єднання з базою. У серверних застосунках pool.end() зазвичай викликають після сигналу SIGINT або SIGTERM, як у прикладі вище.
Не потрібно викликати pool.end() після кожного HTTP-запиту. Після цього пул більше не зможе обслуговувати наступні запити.
Неправильно:
function handleRequest(request, response) {
const pool = new Pool({
database: 'app_db',
});
// ...
}Так застосунок створює новий пул для кожного запиту. Це може швидко вичерпати доступні з’єднання.
Правильно — створити один пул під час запуску застосунку та використовувати його в усіх обробниках.
release()Якщо з’єднання отримано через pool.connect(), його потрібно повернути в пул:
const client = await pool.connect();
try {
await client.query('SELECT 1');
} finally {
client.release();
}pool.end() в обробникуpool.end() призначений для завершення всього застосунку, а не окремого запиту. Якщо викликати його в обробнику, наступні запити не зможуть використовувати пул.
Не вставляйте значення користувача в SQL-рядок через конкатенацію або шаблонний рядок. Використовуйте $1, $2 та масив параметрів.
maxВеликий пул не завжди підвищує продуктивність. Кожне з’єднання споживає ресурси бази даних. Кількість з’єднань потрібно узгоджувати з можливостями бази та кількістю екземплярів Node.js-застосунку.
Пул з’єднань повторно використовує з’єднання з базою даних.
Пул створюють один раз на весь Node.js-застосунок.
Параметр max обмежує кількість одночасних з’єднань.
idleTimeoutMillis і connectionTimeoutMillis допомагають контролювати очікування та невикористовувані з’єднання.
Для одного незалежного запиту зручно використовувати pool.query().
Для транзакцій потрібно отримати з’єднання через pool.connect().
З’єднання, отримане через pool.connect(), потрібно повертати через client.release() у блоці finally.
Дані користувача потрібно передавати параметрами SQL-запиту.
Під час завершення застосунку пул закривають за допомогою pool.end().