Пошук уроків, статей та іншого контенту
Навчитеся створювати, перевіряти та коректно закривати з’єднання з базою даних у застосунку.
Застосунок Node.js не спілкується з базою даних безпосередньо. Спочатку він створює з’єднання, а потім використовує його для виконання SQL-запитів.
Типовий життєвий цикл з’єднання:
Створити з’єднання або пул з’єднань.
Перевірити, що база даних доступна.
Виконати запит.
Повернути з’єднання до пулу або закрити його.
Коректно завершити роботу під час зупинки застосунку.
Якщо не закривати з’єднання, застосунок може:
вичерпати ліміт з’єднань бази даних;
не завершити процес Node.js;
залишити незавершені операції;
отримати помилки під час наступних запитів.
У прикладах використаємо PostgreSQL і пакет pg.
Замість створення нового з’єднання для кожного запиту зазвичай використовують пул з’єднань.
Пул — це набір готових з’єднань, якими застосунок може повторно користуватися:
коли потрібен запит, пул надає вільне з’єднання;
після завершення запиту з’єднання повертається до пулу;
коли застосунок завершується, пул закриває всі свої з’єднання.
Встановіть пакет:
npm install pgСтворити пул можна так:
const { Pool } = require('pg');
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'app_db',
user: 'app_user',
password: 'secret',
max: 10
});Параметр max визначає максимальну кількість одночасних з’єднань у пулі.
На практиці дані для підключення краще зберігати в змінних середовища, а не безпосередньо в коді:
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10
});Наприклад, змінна може мати такий вигляд:
postgresql://app_user:secret@localhost:5432/app_dbДля перевірки доступності бази даних достатньо виконати простий запит SELECT 1.
Метод pool.query() самостійно бере з’єднання з пулу, виконує запит і повертає з’єднання назад:
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
async function checkDatabaseConnection() {
try {
const result = await pool.query('SELECT 1 AS connected');
console.log('База даних доступна:', result.rows[0]);
} catch (error) {
console.error('Не вдалося підключитися до бази даних:', error.message);
}
}
checkDatabaseConnection();Якщо база даних доступна, результат буде приблизно таким:
База даних доступна: { connected: 1 }Якщо база недоступна, метод pool.query() відхилить Promise, а помилка буде оброблена в блоці catch.
Для простих одиночних запитів використовуйте pool.query():
const result = await pool.query(
'SELECT id, name FROM users WHERE email = $1',
[email]
);Значення передається окремим масивом. Позначка $1 є параметром запиту.
Такий підхід важливий, тому що не потрібно вставляти значення безпосередньо в SQL-рядок:
// Небезпечно: значення вставляється безпосередньо в SQL
const query = `SELECT * FROM users WHERE email = '${email}'`;Параметризовані запити допомагають уникати SQL-ін’єкцій:
// Безпечно: значення передається окремо від SQL-запиту
const result = await pool.query(
'SELECT * FROM users WHERE email = $1',
[email]
);Іноді потрібно отримати конкретне з’єднання з пулу. Наприклад, це необхідно для транзакції, де кілька запитів мають виконуватися через одне з’єднання.
Для цього використовують pool.connect():
const client = await pool.connect();
try {
const result = await client.query('SELECT NOW() AS current_time');
console.log(result.rows[0]);
} catch (error) {
console.error('Помилка запиту:', error.message);
} finally {
client.release();
}Метод client.release() не закриває з’єднання назавжди. Він повертає його до пулу, щоб його можна було використати повторно.
Виклик release() потрібно розміщувати у finally. Цей блок виконається і після успішного запиту, і після помилки.
release()Неправильний код:
const client = await pool.connect();
const result = await client.query('SELECT NOW()');
console.log(result.rows[0]);
// З'єднання не повернуто до пулуЯкщо так робити багато разів, усі з’єднання пулу можуть залишитися зайнятими. Нові запити більше не зможуть отримати вільне з’єднання.
Правильний варіант:
const client = await pool.connect();
try {
const result = await client.query('SELECT NOW()');
console.log(result.rows[0]);
} finally {
client.release();
}Якщо помилка може виникнути ще під час отримання з’єднання, використовуйте змінну:
let client;
try {
client = await pool.connect();
await client.query('SELECT NOW()');
} catch (error) {
console.error('Помилка роботи з базою:', error.message);
} finally {
if (client) {
client.release();
}
}Коли застосунок завершує роботу, пул потрібно закрити:
await pool.end();Метод pool.end() очікує завершення поточних операцій і закриває всі з’єднання пулу.
Для серверного застосунку зручно обробити сигнали операційної системи:
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);SIGINT зазвичай надходить після натискання Ctrl+C, а SIGTERM часто використовується середовищами запуску для коректної зупинки процесу.
У цьому прикладі застосунок:
створює пул;
перевіряє з’єднання;
виконує запит;
коректно закриває пул після сигналу завершення.
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://app_user:secret@localhost:5432/app_db',
max: 10
});
async function checkDatabase() {
const result = await pool.query('SELECT 1 AS connected');
console.log('З’єднання з базою перевірено:', result.rows[0]);
}
async function getUsers() {
const result = await pool.query(
'SELECT id, name FROM users ORDER BY id'
);
return result.rows;
}
async function shutdown(signal) {
console.log(`Отримано сигнал ${signal}. Завершення роботи...`);
try {
await pool.end();
console.log('Пул з’єднань закрито.');
process.exit(0);
} catch (error) {
console.error('Помилка під час закриття пулу:', error.message);
process.exit(1);
}
}
async function main() {
try {
await checkDatabase();
const users = await getUsers();
console.log('Користувачі:', users);
} catch (error) {
console.error('Помилка роботи із базою даних:', error.message);
// Закриваємо пул, якщо застосунок не може продовжити роботу
await pool.end();
process.exit(1);
}
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
main();Для запуску потрібно, щоб:
PostgreSQL був запущений;
база даних app_db існувала;
користувач app_user мав доступ до цієї бази;
таблиця users існувала, якщо виконується запит getUsers().
Якщо викликати pool.end() лише в разі успіху, під час помилки з’єднання можуть залишитися відкритими.
Для довготривалого серверного застосунку пул зазвичай працює протягом усього життя процесу. Але під час завершення процесу його потрібно закрити.
Після pool.connect() завжди використовуйте client.release() у finally.
const client = await pool.connect();
try {
await client.query('SELECT 1');
} finally {
client.release();
}Неправильно створювати новий Pool всередині кожної функції:
async function getUsers() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
return pool.query('SELECT * FROM users');
}Так код створює нові з’єднання замість повторного використання вже наявних.
Створіть один пул і використовуйте його в різних функціях:
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
async function getUsers() {
return pool.query('SELECT * FROM users');
}Операції з базою даних є асинхронними й можуть завершитися помилкою. Використовуйте try...catch для обробки async-функцій:
try {
await pool.query('SELECT 1');
} catch (error) {
console.error('Запит не виконано:', error.message);
}release()Пул може мати обмежену кількість з’єднань. Якщо клієнти не повертаються до пулу, наступні запити чекатимуть або завершаться з помилкою.
Для роботи з базою даних у Node.js зручно використовувати пул з’єднань.
Для простого запиту використовуйте pool.query().
Для кількох операцій через одне з’єднання використовуйте pool.connect().
З’єднання, отримане через pool.connect(), потрібно повертати викликом client.release().
Перевірити доступність бази можна запитом SELECT 1.
Під час завершення застосунку закривайте пул методом pool.end().
Обробляйте помилки в усіх асинхронних операціях із базою даних.