Kodokon kodokon.com

Хранение данных в SQLite: слой доступа к данным

Используй better-sqlite3 с подготовленными запросами, транзакциями и отдельным репозиторием ради быстрого и безопасного хранения.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

SQLite работает внутри твоего процесса: ни сервера, ни сети, задержка на запрос порядка микросекунды. Именно поэтому синхронный API у better-sqlite3 не просто приемлем, а часто быстрее асинхронного драйвера: нет смысла платить промисом и оборотом цикла событий за чтение короче одного тика. Компромисс реален на записи: медленный запрос блокирует цикл. Держи запросы индексированными и короткими - и SQLite вытянет десятки тысяч чтений в секунду.

BASH
npm install better-sqlite3
Нативный модуль: при необходимости он компилируется во время установки.

При открытии базы разницу делают две настройки. Режим WAL (write-ahead logging) разрешает чтение во время записи вместо блокировки всего файла. А схема применяется идемпотентно через CREATE TABLE IF NOT EXISTS - этого более чем достаточно, пока ты не завёл настоящий инструмент миграций.

JAVASCRIPT
import Database from 'better-sqlite3';

export function createDb(path = 'app.db') {
  const db = new Database(path);
  db.pragma('journal_mode = WAL');
  db.exec(`CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL
  )`);
  return db;
}
connection.js: открытие, настройки, идемпотентная схема.

Слой доступа к данным - репозиторий - единственный модуль, который говорит на SQL. Запросы подготавливаются один раз при создании и потом переиспользуются на каждом вызове: движок не разбирает текст SQL заново, а значения проходят через плейсхолдеры ?, но никогда через конкатенацию. Это ровно тот интерфейс findAll, findByEmail, insert, которого ждёт сервис из предыдущего урока.

JAVASCRIPT
export function createUserRepository(db) {
  const insertStmt = db.prepare(
    'INSERT INTO users (email, name) VALUES (?, ?)',
  );
  const byEmailStmt = db.prepare(
    'SELECT * FROM users WHERE email = ?',
  );
  const allStmt = db.prepare(
    'SELECT * FROM users ORDER BY id',
  );
  return {
    insert(user) {
      const info = insertStmt.run(user.email, user.name);
      return { id: info.lastInsertRowid, ...user };
    },
    findByEmail(email) {
      return byEmailStmt.get(email);
    },
    findAll() {
      return allStmt.all();
    },
  };
}
Подготовлено один раз, выполнено тысячу раз: run, get, all.
JAVASCRIPT
import { createDb } from './connection.js';

const db = createDb();
const insertStmt = db.prepare(
  'INSERT INTO users (email, name) VALUES (?, ?)',
);
const insertMany = db.transaction((users) => {
  for (const user of users) {
    insertStmt.run(user.email, user.name);
  }
});
insertMany([
  { email: 'ada@example.com', name: 'Ada' },
  { email: 'linus@example.com', name: 'Linus' },
]);
db.transaction: либо всё фиксируется, либо всё откатывается.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Почему синхронный API better-sqlite3 не противоречит духу Node?
    • Потому что Node запускает нативные модули в отдельном потоке
    • Потому что SQLite работает внутри процесса: запрос стоит дешевле, чем асинхронная оркестрация, от которой он избавляет
    • Потому что записи автоматически ставятся в очередь
    • Потому что режим WAL делает любой запрос неблокирующим
  2. Что гарантируют плейсхолдеры ? в подготовленном запросе?
    • Значения передаются отдельно от SQL и никогда не могут быть выполнены как код
    • Значения шифруются перед сохранением
    • Запрос автоматически кэшируется на диск
  3. Какую двойную выгоду даёт db.transaction для тысячи вставок?
    • Сжатие данных и автоматическую дедупликацию
    • Атомарность (всё или ничего) и одну синхронизацию с диском вместо тысячи
    • Проверку схемы и создание недостающих индексов