better-sqlite3-typescript для TypeScript: полное руководство с примерами
В мире разработки на TypeScript часто возникает потребность в локальном хранении данных без развертывания полноценной базы данных. SQLite — идеальное решение для таких задач: это встраиваемая реляционная база данных, работающая без отдельного сервера. Однако стандартная библиотека better-sqlite3 написана на JavaScript и не предоставляет встроенной типизации. Здесь на помощь приходит better-sqlite3-typescript — обертка, которая добавляет строгую типизацию TypeScript поверх синхронного API better-sqlite3.
Эта библиотека позволяет вам писать типобезопасный код для работы с SQLite, получая автодополнение в IDE, проверку типов на этапе компиляции и снижая количество ошибок времени выполнения. Вы получаете все преимущества синхронного доступа к базе данных (простота, предсказуемость, высокая производительность) с мощью системы типов TypeScript.
В этой статье мы подробно разберем установку, основные методы, приведем реальные примеры и дадим советы по производительности. Материал будет полезен как новичкам, так и опытным разработчикам, которые хотят эффективно использовать SQLite в своих TypeScript-проектах.
Установка
Для установки библиотеки выполните следующую команду в терминале вашего проекта:
npm install better-sqlite3-typescript better-sqlite3
Также потребуется установить типы для TypeScript:
npm install -D @types/better-sqlite3
После установки вы можете импортировать библиотеку в свой проект:
import Database from 'better-sqlite3-typescript';
Быстрый старт
Минимальный рабочий пример создания базы данных, создания таблицы и вставки данных:
import Database from 'better-sqlite3-typescript';
// Создаем или открываем базу данных
const db = new Database('my_database.db');
// Создаем таблицу
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER
)
`);
// Вставляем данные
const insert = db.prepare('INSERT INTO users (name, email, age) VALUES (?, ?, ?)');
insert.run('Alice', 'alice@example.com', 30);
insert.run('Bob', 'bob@example.com', 25);
// Читаем данные
const select = db.prepare('SELECT * FROM users');
const users = select.all();
console.log(users);
// Закрываем соединение
db.close();
Этот код создаст файл my_database.db в текущей директории, создаст таблицу users, добавит двух пользователей и выведет их в консоль.
Основные методы и функции
Рассмотрим ключевые методы библиотеки. Все примеры предполагают, что у вас уже есть экземпляр базы данных db.
1. new Database(filename, options?)
Сигнатура: new Database(filename: string, options?: DatabaseOptions): Database
Описание: Создает или открывает базу данных SQLite. Если файл не существует, он будет создан. Опции позволяют настроить режим открытия (readonly, memory и т.д.).
Пример:
// Открыть базу в памяти (не сохраняется на диск)
const memDb = new Database(':memory:');
// Открыть базу в режиме только для чтения
const readOnlyDb = new Database('data.db', { readonly: true });
2. db.prepare(sql)
Сигнатура: prepare(sql: string): Statement
Описание: Компилирует SQL-запрос в подготовленный statement. Возвращает объект Statement, который можно выполнять многократно с разными параметрами.
Пример:
const stmt = db.prepare('SELECT * FROM users WHERE age > ?');
const olderUsers = stmt.all(25); // все пользователи старше 25 лет
3. statement.run(...params)
Сигнатура: run(...params: BindParams[]): RunResult
Описание: Выполняет SQL-запрос (INSERT, UPDATE, DELETE) с переданными параметрами. Возвращает объект с информацией о результате: changes (количество измененных строк) и lastInsertRowid (ID последней вставленной записи).
Пример:
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
const result = insert.run('Charlie', 'charlie@example.com');
console.log(`Добавлено строк: ${result.changes}, ID: ${result.lastInsertRowid}`);
4. statement.get(...params)
Сигнатура: get(...params: BindParams[]): T | undefined
Описание: Выполняет SELECT-запрос и возвращает первую строку результата в виде объекта. Если строк нет, возвращает undefined. Тип T автоматически выводится из контекста.
Пример:
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
const user = stmt.get(1);
if (user) {
console.log(user.name); // TypeScript знает, что у user есть поле name
}
5. statement.all(...params)
Сигнатура: all(...params: BindParams[]): T[]
Описание: Выполняет SELECT-запрос и возвращает массив всех строк результата. Каждая строка представлена объектом.
Пример:
const stmt = db.prepare('SELECT name, age FROM users ORDER BY age DESC');
const users = stmt.all();
users.forEach(u => console.log(`${u.name}: ${u.age}`));
6. statement.iterate(...params)
Сигнатура: iterate(...params: BindParams[]): IterableIterator<T>
Описание: Возвращает итератор для последовательного чтения строк результата. Полезно для обработки больших наборов данных без загрузки всего результата в память.
Пример:
const stmt = db.prepare('SELECT * FROM users');
for (const user of stmt.iterate()) {
console.log(user.name);
// Можно прервать цикл досрочно
if (user.id === 5) break;
}
7. db.transaction(fn)
Сигнатура: transaction(fn: (...args: any[]) => T): (...args: any[]) => T
Описание: Оборачивает функцию в транзакцию. Если внутри функции возникает ошибка, все изменения откатываются. Транзакции значительно ускоряют массовые операции.
Пример:
const insertMany = db.transaction((users: {name: string, email: string}[]) => {
const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
for (const user of users) {
stmt.run(user.name, user.email);
}
});
insertMany([
{ name: 'David', email: 'david@example.com' },
{ name: 'Eve', email: 'eve@example.com' }
]);
8. db.exec(sql)
Сигнатура: exec(sql: string): void
Описание: Выполняет один или несколько SQL-запросов, разделенных точкой с запятой. Не возвращает данные, используется для DDL-операций (CREATE, ALTER, DROP) и массовых вставок.
Пример:
db.exec(`
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY,
message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO logs (message) VALUES ('Application started');
INSERT INTO logs (message) VALUES ('First log entry');
`);
9. db.pragma(pragma, value?)
Сигнатура: pragma(pragma: string, value?: any): any
Описание: Устанавливает или получает значение PRAGMA (директивы SQLite). Позволяет настраивать поведение базы данных: режим синхронизации, размер кэша, внешние ключи и т.д.
Пример:
// Включить поддержку внешних ключей
db.pragma('foreign_keys = ON');
// Получить текущий режим синхронизации
const syncMode = db.pragma('synchronous');
console.log(syncMode); // 'FULL' или 'NORMAL'
10. db.close()
Сигнатура: close(): void
Описание: Закрывает соединение с базой данных. После закрытия все попытки выполнить запросы вызовут ошибку. Всегда закрывайте базу данных после завершения работы.
Пример:
db.close();
// db.prepare('SELECT 1'); // Ошибка: Database is closed
Реальный практический пример
Создадим систему управления задачами (Todo List) с использованием better-sqlite3-typescript. Пример включает создание таблиц, CRUD-операции и работу с транзакциями.
import Database from 'better-sqlite3-typescript';
interface Task {
id?: number;
title: string;
description?: string;
completed: boolean;
created_at?: string;
}
class TodoDatabase {
private db: Database.Database;
constructor(dbPath: string = 'todos.db') {
this.db = new Database(dbPath);
this.initialize();
}
private initialize(): void {
this.db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
completed INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
// Включаем WAL-режим для лучшей производительности
this.db.pragma('journal_mode = WAL');
}
// Создать задачу
createTask(title: string, description?: string): Task {
const stmt = this.db.prepare(
'INSERT INTO tasks (title, description) VALUES (?, ?)'
);
const result = stmt.run(title, description || null);
return this.getTaskById(result.lastInsertRowid as number)!;
}
// Получить задачу по ID
getTaskById(id: number): Task | undefined {
const stmt = this.db.prepare('SELECT * FROM tasks WHERE id = ?');
const task = stmt.get(id) as Task | undefined;
if (task) {
task.completed = Boolean(task.completed);
}
return task;
}
// Получить все задачи
getAllTasks(includeCompleted: boolean = true): Task[] {
let sql = 'SELECT * FROM tasks';
if (!includeCompleted) {
sql += ' WHERE completed = 0';
}
sql += ' ORDER BY created_at DESC';
const stmt = this.db.prepare(sql);
const tasks = stmt.all() as Task[];
return tasks.map(task => ({ ...task, completed: Boolean(task.completed) }));
}
// Обновить задачу
updateTask(id: number, updates: Partial>): Task | undefined {
const fields: string[] = [];
const values: any[] = [];
if (updates.title !== undefined) {
fields.push('title = ?');
values.push(updates.title);
}
if (updates.description !== undefined) {
fields.push('description = ?');
values.push(updates.description);
}
if (updates.completed !== undefined) {
fields.push('completed = ?');
values.push(updates.completed ? 1 : 0);
}
if (fields.length === 0) return this.getTaskById(id);
values.push(id);
const sql = `UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`;
this.db.prepare(sql).run(...values);
return this.getTaskById(id);
}
// Удалить задачу
deleteTask(id: number): boolean {
const stmt = this.db.prepare('DELETE FROM tasks WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
}
// Массовое создание задач в транзакции
createMultipleTasks(tasks: { title: string; description?: string }[]): Task[] {
const insertMany = this.db.transaction((items: { title: string; description?: string }[]) => {
const stmt = this.db.prepare('INSERT INTO tasks (title, description) VALUES (?, ?)');
const ids: number[] = [];
for (const item of items) {
const result = stmt.run(item.title, item.description || null);
ids.push(result.lastInsertRowid as number);
}
return ids;
});
const ids = insertMany(tasks);
return ids.map(id => this.getTaskById(id)!);
}
// Получить статистику
getStats() {
const stmt = this.db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN completed = 1 THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN completed = 0 THEN 1 ELSE 0 END) as pending
FROM tasks
`);
return stmt.get() as { total: number; completed: number; pending: number };
}
close(): void {
this.db.close();
}
}
// Использование
const todoDb = new TodoDatabase();
// Создаем задачи
todoDb.createTask('Купить продукты', 'Молоко, хлеб, яйца');
todoDb.createTask('Написать статью', 'Закончить документацию по better-sqlite3-typescript');
// Массовое создание
todoDb.createMultipleTasks([
{ title: 'Позвонить врачу' },
{ title: 'Заплатить налоги', description: 'До 25 числа' },
{ title: 'Сходить в спортзал' }
]);
// Об