ts-sql-query для TypeScript: полное руководство с примерами
1. Что это и зачем нужна
ts-sql-query — это мощная библиотека для TypeScript, которая позволяет писать SQL-запросы с полной поддержкой типов (type-safe). Она предоставляет удобный DSL (Domain-Specific Language) для построения SQL-запросов, при этом каждый запрос проверяется компилятором TypeScript на соответствие схеме базы данных. Это означает, что ошибки в названиях таблиц, полей или типах данных будут обнаружены на этапе компиляции, а не в рантайме.
Зачем это нужно? В традиционных подходах (сырые строки SQL или ORM) разработчики часто сталкиваются с проблемами: опечатки в именах колонок, несоответствие типов, сложность рефакторинга. ts-sql-query решает эти проблемы, предоставляя строгую типизацию для всех операций: SELECT, INSERT, UPDATE, DELETE, JOIN, подзапросы, агрегатные функции и многое другое. Библиотека поддерживает множество СУБД (PostgreSQL, MySQL, SQLite, MariaDB, MSSQL) и может работать как с драйверами напрямую, так и через пулы соединений.
В отличие от ORM (например, TypeORM или Prisma), ts-sql-query не скрывает SQL за абстракциями, а делает его частью TypeScript-кода. Вы по-прежнему пишете SQL, но с автодополнением, проверкой типов и без риска SQL-инъекций. Это идеальный выбор для проектов, где важна производительность и полный контроль над запросами, но при этом хочется использовать преимущества статической типизации.
2. Установка
Установка производится через npm. Вам потребуется установить саму библиотеку и драйвер для вашей базы данных.
# Установка основной библиотеки
npm install ts-sql-query
# Установка драйвера для PostgreSQL (пример)
npm install pg
# Установка драйвера для MySQL
npm install mysql2
# Установка драйвера для SQLite
npm install better-sqlite3
# Установка драйвера для SQLite (асинхронный)
npm install sql.js
# Установка типов для Node.js (если используете)
npm install --save-dev @types/node
После установки драйвера, вам нужно создать соединение с базой данных. ts-sql-query предоставляет адаптеры для каждого драйвера.
3. Быстрый старт — минимальный рабочий пример
Рассмотрим пример для PostgreSQL. Создадим простую таблицу users и выполним SELECT-запрос.
import { createPool } from 'pg';
import { PostgreSqlConnection } from 'ts-sql-query/connections/PostgreSqlConnection';
// 1. Создаем пул соединений
const pool = createPool({
host: 'localhost',
port: 5432,
database: 'testdb',
user: 'user',
password: 'password',
});
// 2. Создаем класс соединения
class DBConnection extends PostgreSqlConnection {
// Здесь можно определить методы для работы с таблицами
}
// 3. Создаем экземпляр соединения
const connection = new DBConnection(pool);
// 4. Определяем тип для таблицы users
interface User {
id: number;
name: string;
email: string;
age: number;
}
// 5. Создаем объект для работы с таблицей
const users = connection.table('users').columns({
id: 'id',
name: 'name',
email: 'email',
age: 'age',
});
// 6. Выполняем запрос
async function getUsers() {
const query = connection
.selectFrom(users)
.select(users.id, users.name, users.email)
.where(users.age.greaterThan(18))
.orderBy(users.name, 'asc');
const result = await query.executeSelectMany();
return result;
}
getUsers().then(users => {
console.log('Users:', users);
}).catch(err => {
console.error('Error:', err);
});
В этом примере мы создали соединение, определили таблицу и выполнили SELECT-запрос. Все типы проверяются компилятором.
4. Основные методы/функции/классы
Рассмотрим ключевые методы библиотеки. Каждый метод имеет строгую типизацию и возвращает объект, который можно использовать для дальнейшего построения запроса.
4.1. table()
Сигнатура: connection.table(tableName: string, alias?: string): Table
Что делает: Создает объект для работы с таблицей. Позволяет указать имя таблицы и опциональный псевдоним.
const users = connection.table('users');
const orders = connection.table('orders', 'o'); // с псевдонимом
4.2. columns()
Сигнатура: table.columns(columns: Record<string, string>): TableWithColumns
Что делает: Определяет колонки таблицы. Принимает объект, где ключи — это имена полей в TypeScript, а значения — имена колонок в БД.
const users = connection.table('users').columns({
id: 'id',
name: 'name',
email: 'email',
age: 'age',
createdAt: 'created_at',
});
4.3. selectFrom()
Сигнатура: connection.selectFrom(table: TableWithColumns): SelectQuery
Что делает: Начинает построение SELECT-запроса. Принимает таблицу, из которой нужно выбрать данные.
const query = connection.selectFrom(users);
4.4. select()
Сигнатура: selectQuery.select(...columns: Column[]): SelectQuery
Что делает: Указывает, какие колонки включить в результат. Можно передавать несколько колонок.
const query = connection
.selectFrom(users)
.select(users.id, users.name, users.email);
4.5. where()
Сигнатура: selectQuery.where(condition: Condition): SelectQuery
Что делает: Добавляет условие WHERE. Принимает объект условия, который можно строить с помощью методов колонок (например, equals(), greaterThan(), like()).
const query = connection
.selectFrom(users)
.select(users.id, users.name)
.where(users.age.greaterThan(18))
.where(users.name.like('%John%')); // можно комбинировать
4.6. orderBy()
Сигнатура: selectQuery.orderBy(column: Column, direction?: 'asc' | 'desc'): SelectQuery
Что делает: Добавляет сортировку. Можно указать направление (по умолчанию 'asc').
const query = connection
.selectFrom(users)
.select(users.id, users.name)
.orderBy(users.name, 'asc')
.orderBy(users.id, 'desc'); // множественная сортировка
4.7. limit() и offset()
Сигнатура: selectQuery.limit(limit: number): SelectQuery и selectQuery.offset(offset: number): SelectQuery
Что делает: Добавляет LIMIT и OFFSET для пагинации.
const query = connection
.selectFrom(users)
.select(users.id, users.name)
.limit(10)
.offset(20);
4.8. innerJoin(), leftJoin(), rightJoin()
Сигнатура: selectQuery.innerJoin(table: TableWithColumns, condition: Condition): SelectQuery
Что делает: Добавляет JOIN. Поддерживаются INNER, LEFT, RIGHT JOIN.
const orders = connection.table('orders').columns({
id: 'id',
userId: 'user_id',
total: 'total',
});
const query = connection
.selectFrom(users)
.innerJoin(orders, orders.userId.equals(users.id))
.select(users.name, orders.total);
4.9. insertInto()
Сигнатура: connection.insertInto(table: TableWithColumns): InsertQuery
Что делает: Начинает построение INSERT-запроса.
const query = connection
.insertInto(users)
.values({
name: 'John Doe',
email: 'john@example.com',
age: 30,
});
await query.executeInsert();
4.10. update()
Сигнатура: connection.update(table: TableWithColumns): UpdateQuery
Что делает: Начинает построение UPDATE-запроса.
const query = connection
.update(users)
.set({
name: 'Jane Doe',
age: 25,
})
.where(users.id.equals(1));
await query.executeUpdate();
4.11. deleteFrom()
Сигнатура: connection.deleteFrom(table: TableWithColumns): DeleteQuery
Что делает: Начинает построение DELETE-запроса.
const query = connection
.deleteFrom(users)
.where(users.id.equals(1));
await query.executeDelete();
4.12. executeSelectMany(), executeSelectOne(), executeSelectNone()
Сигнатура: query.executeSelectMany(): Promise<T[]>, executeSelectOne(): Promise<T | null>, executeSelectNone(): Promise<void>
Что делает: Выполняют SELECT-запрос и возвращают результат в разных форматах: массив, один объект или ничего.
const allUsers = await query.executeSelectMany();
const firstUser = await query.executeSelectOne();
await query.executeSelectNone(); // если не нужен результат
5. Реальный практический пример (100-200 строк кода)
Рассмотрим полноценный пример работы с интернет-магазином: таблицы users, products, orders, order_items. Реализуем CRUD-операции и сложный запрос с JOIN.
import { createPool } from 'pg';
import { PostgreSqlConnection } from 'ts-sql-query/connections/PostgreSqlConnection';
// Настройка подключения
const pool = createPool({
host: 'localhost',
port: 5432,
database: 'shop',
user: 'admin',
password: 'secret',
});
class DBConnection extends PostgreSqlConnection {}
const connection = new DBConnection(pool);
// Определение таблиц
const users = connection.table('users').columns({
id: 'id',
name: 'name',
email: 'email',
createdAt: 'created_at',
});
const products = connection.table('products').columns({
id: 'id',
name: 'name',
price: 'price',
stock: 'stock',
});
const orders = connection.table('orders').columns({
id: 'id',
userId: 'user_id',
total: 'total',
status: 'status',
createdAt: 'created_at',
});
const orderItems = connection.table('order_items').columns({
id: 'id',
orderId: 'order_id',
productId: 'product_id',
quantity: 'quantity',
price: 'price',
});
// Функция для создания пользователя
async function createUser(name: string, email: string) {
const query = connection
.insertInto(users)
.values({ name, email });
return await query.executeInsert();
}
// Функция для получения всех заказов пользователя с товарами
async function getUserOrdersWithItems(userId: number) {
const query = connection
.selectFrom(orders)
.innerJoin(orderItems, orderItems.orderId.equals(orders.id))
.innerJoin(products, products.id.equals(orderItems.productId))
.select(
orders.id,
orders.total,
orders.status,
orderItems.quantity,
products.name,
products.price
)
.where(orders.userId.equals(userId))
.orderBy(orders.createdAt, 'desc');
return await query.executeSelectMany();
}
// Функция для обновления статуса заказа
async function updateOrderStatus(orderId: number, status: string) {
const query = connection
.update(orders)
.set({ status })
.where(orders.id.equals(orderId));
return await query.executeUpdate();
}
// Функция для удаления товара (если нет в заказах)
async function deleteProductIfNotOrdered(productId: number) {
// Проверяем, есть ли товар в заказах
const checkQuery = connection
.selectFrom(orderItems)
.select(orderItems.id)
.where(orderItems.productId.equals(productId))
.limit(1);
const existing = await checkQuery.executeSelectOne();
if (existing) {
throw new Error('Cannot delete product: it is in orders');
}
const deleteQuery = connection
.deleteFrom(products)
.where(products.id.equals(productId));
return await deleteQuery.executeDelete();
}
// Также в библиотеке
Работа с Excel в TypeScript: библиотеки, примеры и инструменты
Лучшие библиотеки логирования для Go (Golang): обзор и сравнение
Отправка email на Rust: библиотеки lettre и mailparse для работы с почтой
Работа с ZIP-архивами в Rust: библиотека zip и полное руководство
Accompanist для Kotlin: полное руководство с примерами