Featured image of post 别被 SQLite 的随性骗了!动态类型陷阱是什么?为什么 ALTER TABLE 是半残的?如何在 Node.js 中做好防御性编程实现无痛数据表升级?

别被 SQLite 的随性骗了!动态类型陷阱是什么?为什么 ALTER TABLE 是半残的?如何在 Node.js 中做好防御性编程实现无痛数据表升级?

SQLite 采用动态弱类型系统,字符串塞进 INTEGER 字段竟然不会报错。了解 SQLite 的类型亲和性陷阱、缺乏原生 Boolean 与 Date 类型的影响,以及 ALTER TABLE 的限制与「重建搬移四步曲」的安全升级策略,搭配 TypeScript、Zod、Prisma 等工具建立防御性编程架构。

想象一个资源回收桶,你明明在上面贴了“宝特瓶专用”的标签,结果别人丢了一张纸片进去,它居然默默收下,连一声抗议都没有?

这就是开发者第一次遇见 SQLite 类型系统时的惊悚体验。

如果你习惯了 PostgreSQL 那种严格的海关作风(类型不对直接拒绝入境),SQLite 的随性程度可能会让你怀疑人生。

更恐怖的是,当你想 修改数据表结构 时,它会告诉你:

“数据表改不了,请盖一栋新房子,把家具搬过去,然后把旧房子炸掉。”

SQLite 底层只有 5 种存储类别

不管你在 CREATE TABLE 时宣告了什么华丽的类型名称(VARCHAR(255)BIGINTDECIMAL),SQLite 底层只认得这 5 种存储类别

存储类别 说明
NULL 空值
INTEGER 整数(依数字大小自动占用 1 到 8 bytes)
REAL 浮点数(固定 8 bytes)
TEXT 字符串(默认 UTF-8 编码)
BLOB 二进制数据(原封不动存储)

你在字段上设置的类型,对 SQLite 来说 只是一个“建议”,而不是“强制规定”

这叫做 “类型亲和性(Type Affinity)”SQLite 会尝试把你的数据转成建议的类型,但如果转不了,它照样把原始数据塞进去,完全不报错。

你可以在 SQLite 宣告一个 age INTEGER 字段,然后塞入字符串 '永远的十八岁',它会欣然接受。

三个最容易踩坑的类型陷阱

陷阱 1:没有原生 Boolean

SQLite 没有布尔类型TrueFalse 只能用整数 10 代替。

当你用 Node.jsSQLite 捞出数据时,拿到的会是数字 10,不是 truefalse

如果你直接拿去做 if (user.is_admin === true) 的判断,永远不会成立。

陷阱 2:没有 Date/Time 类型

SQLite 没有日期时间类型。你只能把时间存成:

存储方式 范例 优缺点
TEXT(ISO-8601 字符串) '2026-05-19T18:00:00Z' 最推荐,可读性高,未来搬到 PostgreSQL 可无缝转换
INTEGER(Unix Timestamp) 1747656000 占用空间小,但人类不可读

千万不要存像 2026/5/19 下午六点 这种自创格式,否则未来做数据迁移时会是一场灾难。

陷阱 3:字符串塞进整数字段不会报错

PostgreSQL,你把字符串塞进 INTEGER 字段会直接报错。但 SQLite 只会默默尝试转换,转不了就原样收下。

这意味着你的数据库里 可能悄悄混进了脏数据,直到某天你的程序因为拿到非预期的类型而崩溃,才会发现问题。

防御性编程:用严格的态度对待随性的数据库

面对 SQLite 的随性,在 Node.js 开发中必须建立起 严格的防御机制

防御层级 工具 作用
编译期挡错 TypeScript 在代码阶段就挡下错误的类型
API 入口验证 Zod 接收数据时严格验证(确认 age 一定是数字)
底层类型转换 Prisma / Drizzle ORM 自动处理 SQLite 和 PostgreSQL 之间的类型差异

“数据验证的守门员” 从数据库层移到 Node.js 应用层,这是同时驾驭 SQLite 开发速度与未来扩展性的关键策略。

使用 ORM 时,你只要在程序里宣告 type: 'boolean',ORM 存入 SQLite 时自动转成 1/0,拿出来时自动转回 true/false,完美遮蔽底层的类型差异。

ALTER TABLE 是半残的:什么能改、什么不能改

SQLite 对数据表结构的修改支持非常有限:

操作 是否支持
新增字段(ADD COLUMN 支持
重新命名字段(RENAME COLUMN 支持
删除字段(DROP COLUMN 支持 (较新版本)
重新命名数据表(RENAME TO 支持
更改字段类型 不支持
增加/移除 UNIQUENOT 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);
}

“备份文件 + Transaction 绑定” 就是你的数据库迁移的安全气囊。

用严格的态度驾驭随性的 SQLite

严格的应用层架构 来驾驭随性的 SQLite,享受它极速开发的优势,同时避开未来的技术债。

SQLite 的类型系统很随性,ALTER TABLE 也有诸多限制。

但只要在 Node.js 端做好 TypeScript 类型检查 + Zod 验证 + ORM 抽象层,再搭配 物理备份 + Transaction 的安全策略,你就能安心享受 SQLite 带来的开发效率,同时为未来可能搬移至 PostgreSQL 铺好后路。

Reference

All rights reserved,未經允許不得隨意轉載
Built with Hugo
主题 StackJimmy 设计