better-sqlite3-typescript для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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: 'Сходить в спортзал' }
]);

// Об                    

Рекомендаций