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

Связанные темы

Ресурсы