PostgreSQL: основы и pg
pg (node-postgres) — официальный клиент для работы с PostgreSQL из Node.js, предоставляющий как callback, так и Promise API, пул соединений и поддержку транзакций.
Зачем нужно
PostgreSQL — мощная реляционная СУБД с поддержкой ACID, JSON/JSONB, полнотекстового поиска и многого другого. pg — минималистичный нативный клиент без магии ORM: вы пишете SQL напрямую, получаете типизированные результаты. Это даёт полный контроль над запросами и производительностью.
Где используется
- Прямые SQL-запросы без ORM (максимальная гибкость)
- Под капотом Prisma, Sequelize, Knex (все используют
pg) - Сложные отчёты с JOIN, CTE, window-функциями
- Транзакции с ручным контролем изоляции
Основной контент
Установка и подключение
npm install pg
// db/pool.js
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// или по частям:
// host: 'localhost',
// port: 5432,
// database: 'myapp',
// user: 'postgres',
// password: 'password',
max: 10, // максимум соединений в пуле
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
process.exit(-1);
});
module.exports = pool;
Основные запросы
const pool = require('./db/pool');
// SELECT
const { rows } = await pool.query('SELECT * FROM users WHERE active = $1', [true]);
console.log(rows); // [{ id: 1, name: 'Alice', active: true }, ...]
// INSERT с RETURNING
const { rows: [user] } = await pool.query(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
['Alice', 'alice@example.com']
);
console.log(user.id); // автогенерированный id
// UPDATE
const result = await pool.query(
'UPDATE users SET name = $1 WHERE id = $2',
['Bob', 1]
);
console.log(result.rowCount); // количество обновлённых строк
// DELETE
await pool.query('DELETE FROM users WHERE id = $1', [1]);
Параметризованные запросы (защита от SQL Injection)
// ПРАВИЛЬНО — параметры через $1, $2
const { rows } = await pool.query(
'SELECT * FROM products WHERE category = $1 AND price < $2',
[req.query.category, req.query.maxPrice]
);
// НЕПРАВИЛЬНО — конкатенация строк (SQL Injection!)
// const query = `SELECT * FROM users WHERE name = '${req.body.name}'`;
Транзакции
const client = await pool.connect;
try {
await client.query('BEGIN');
const { rows: [order] } = await client.query(
'INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id',
[userId, total]
);
await client.query(
'UPDATE products SET stock = stock - $1 WHERE id = $2',
[quantity, productId]
);
await client.query('COMMIT');
return order;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release(); // вернуть соединение в пул
}
Вспомогательная функция для транзакций
// db/transaction.js
async function withTransaction(callback) {
const client = await pool.connect;
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
// Использование
await withTransaction(async (client) => {
await client.query('INSERT INTO logs (action) VALUES ($1)', ['order_created']);
await client.query('UPDATE inventory SET count = count - 1 WHERE product_id = $1', [42]);
});
Частые ошибки
- Не использовать пул —
new Clientна каждый запрос: соединение создаётся заново; использоватьPool - Не вернуть клиента в пул —
client.release()обязательно, лучше вfinally - Конкатенировать пользовательский ввод — SQL injection; всегда
$1, $2 - Не обработать ошибку соединения —
pool.on('error', ...)критичен для production