Представьте себе мусорный бак для вторсырья, на который вы четко наклеили этикетку с надписью «Только пластиковые бутылки», но когда кто-то бросает туда кусок бумаги, он молча принимает его без единого слова протеста?
Это тот самый леденящий душу опыт, с которым сталкиваются разработчики, когда впервые знакомятся с системой типов SQLite.
Если вы привыкли к строгому стилю таможенника PostgreSQL (где некорректные типы данных немедленно депортируются обратно), непринужденность SQLite может заставить вас усомниться в своей жизни.
Что еще более пугающе, когда вы захотите изменить структуру таблицы, база данных скажет вам:
«Таблицу нельзя изменить напрямую. Пожалуйста, постройте новый дом, перевезите туда мебель, а затем взорвите старый дом».
Под капотом у SQLite всего 5 классов хранения
Независимо от того, какие изысканные названия типов вы объявляете в CREATE TABLE (VARCHAR(255), BIGINT, DECIMAL), SQLite под капотом распознает только эти 5 классов хранения:
| Класс хранения | Описание |
|---|---|
| NULL | Пустое значение |
| INTEGER | Целое число (автоматически занимает от 1 до 8 байт в зависимости от величины значения) |
| REAL | Число с плавающей запятой (фиксированные 8 байт) |
| TEXT | Текстовая строка (по умолчанию кодировка UTF-8) |
| BLOB | Большой двоичный объект (хранится точно в том виде, в каком был введен) |
Типы, которые вы устанавливаете для столбцов, являются лишь «рекомендациями» для SQLite, а не «обязательными правилами».
Это называется «Type Affinity» (Родство типов).
SQLiteпопытается преобразовать ваши данные в рекомендуемый тип, но если не сможет, то просто вставит исходные данные в любом случае без вызова ошибки.
Вы можете объявить столбец age INTEGER в SQLite и затем вставить строку 'вечно восемнадцать'; база данных с радостью примет ее.
Три ловушки типов, в которые легче всего угодить
Ловушка 1: Отсутствие встроенного Boolean
В SQLite нет типа данных boolean. Значения True и False могут быть представлены только целыми числами 1 и 0.
Когда вы извлекаете данные из SQLite с помощью Node.js, вы получаете число 1 или 0, а не true или false.
Если вы напрямую выполните проверку вида if (user.is_admin === true), она никогда не будет истинной.
Ловушка 2: Отсутствие типа Date/Time
В SQLite нет типа данных даты/времени. Вы можете хранить время только следующими способами:
| Способ хранения | Пример | Плюсы и минусы |
|---|---|---|
| TEXT (строка ISO-8601) | '2026-05-19T18:00:00Z' |
Наиболее рекомендуемый, высокая читаемость, бесшовное преобразование при переходе на PostgreSQL в будущем |
| INTEGER (Unix Timestamp) | 1747656000 |
Малый размер, но не читается человеком |
Никогда не храните даты в произвольных пользовательских форматах, таких как 19.05.2026 18:00, иначе будущая миграция данных обернется катастрофой.
Ловушка 3: Вставка строки в столбец Integer не вызовет ошибку
В PostgreSQL вставка строки в столбец INTEGER сразу же вызывает ошибку. Но SQLite лишь попытается тихо преобразовать ее, а в случае неудачи примет как есть.
Это означает, что грязные данные могут незаметно просочиться в вашу базу данных, пока в один прекрасный день ваша программа не упадет из-за получения неожиданного типа. Только тогда вы обнаружите проблему.
Оборонительное программирование: Отношение к непринужденной базе данных со строгой позиции
Столкнувшись с непринужденностью SQLite, вы должны выстроить строгие механизмы защиты при разработке на Node.js:
| Уровень защиты | Инструмент | Роль |
|---|---|---|
| Страж времени компиляции | TypeScript | Перехват некорректных типов на этапе написания кода |
| Валидация на входе API | Zod | Строгая проверка входящих данных (гарантия того, что age всегда является числом) |
| Неявное преобразование типов | Prisma / Drizzle ORM | Автоматическая обработка различий типов между SQLite и PostgreSQL |
Перенос «вышибалы валидации данных» с уровня базы данных на уровень приложения
Node.js— ключевая стратегия для использования преимуществ высокой скорости разработки SQLite при обеспечении будущей масштабируемости.
При использовании ORM, если вы объявите type: 'boolean' в своем коде, ORM автоматически преобразует его в 1/0 при сохранении в SQLite и обратно в true/false при чтении, идеально скрывая различия базовых типов.
ALTER TABLE неполноценен: Что можно и что нельзя изменять
Поддержка изменения структуры таблиц в SQLite крайне ограничена:
| Операция | Поддерживается |
|---|---|
Добавление столбца (ADD COLUMN) |
Да |
Переименование столбца (RENAME COLUMN) |
Да |
Удаление столбца (DROP COLUMN) |
Да (в более новых версиях) |
Переименование таблицы (RENAME TO) |
Да |
| Изменение типа столбца | Нет |
Добавление/удаление ограничений UNIQUE, NOT NULL |
Нет |
| Изменение первичного ключа (Primary Key) | Нет |
| Изменение внешнего ключа (Foreign Key) | Нет |
Как только вам понадобится выполнить любое из «неподдерживаемых» изменений,
SQLiteпотребует от вас выполнения стратегии «пересоздания и переноса».
Четыре шага пересоздания и переноса: Метод обновления таблиц SQLite
Поскольку прямое изменение невозможно, стандартная процедура, рекомендуемая официальной документацией, выглядит так: построить новый дом, перевезти мебель, взорвать старый дом и повесить новую табличку на дверь:
| Шаг | Действие | Описание |
|---|---|---|
| 1 | Создание новой таблицы | Создать CREATE TABLE users_new (...) с правильной структурой |
| 2 | Копирование данных | INSERT INTO users_new SELECT ... FROM users |
| 3 | Удаление старой таблицы | DROP TABLE users |
| 4 | Переименование таблицы | ALTER TABLE users_new RENAME TO users |
Эти четыре шага должны быть выполнены на одном дыхании; любое отключение электричества или сбой приложения на полпути приведет к потере данных.
Гарантия обновления без потери данных: Две линии безопасности
Линия безопасности 1: Физическая защита, прямое копирование файла
SQLite по сути представляет собой один-единственный файл. Перед выполнением любых изменений схемы просто скопируйте файл .db в качестве резервной копии.
const fs = require('fs');
fs.copyFileSync('my_project.db', 'my_project_backup.db');
Если что-то пойдет не так, перезапись файла восстановит всё в мгновение ока. Это преимущество, которое не могут предоставить другие крупные базы данных.
Линия безопасности 2: Обертка в Transaction, машина времени базы данных
Оберните все шаги миграции в одну транзакцию Transaction; если какой-либо шаг завершится неудачей, весь процесс будет автоматически отменен (Rollback), как будто ничего и не было.
const Database = require('better-sqlite3');
const db = new Database('my_project.db');
const migrateData = db.transaction(() => {
db.prepare(`
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER NOT NULL DEFAULT 18
)
`).run();
db.prepare(`
INSERT INTO users_new (id, name, age)
SELECT id, name, COALESCE(age, 18) FROM users
`).run();
db.prepare('DROP TABLE users').run();
db.prepare('ALTER TABLE users_new RENAME TO users').run();
});
try {
migrateData();
console.log('Обновление таблицы завершено');
} catch (error) {
console.error('Ошибка обновления, данные безопасно восстановлены:', error.message);
}
«Резервная копия файла + привязка транзакции» — это подушка безопасности для миграции вашей базы данных.
Контролируйте непринужденную SQLite со строгой позиции
Укротите непринужденную
SQLiteс помощью строгой архитектуры на уровне приложения, чтобы наслаждаться молниеносной скоростью разработки и одновременно избегать технического долга в будущем.
Система типов SQLite очень непринужденная, а ALTER TABLE имеет множество ограничений.
Однако, пока вы добросовестно используете проверку типов TypeScript + валидацию Zod + ORM-абстракцию на стороне Node.js в сочетании со стратегией безопасности физического резервного копирования + транзакций, вы можете спокойно наслаждаться высокой эффективностью разработки, которую предлагает SQLite, одновременно подготавливая путь к гладкому переходу на PostgreSQL в будущем.