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,未經允許不得隨意轉載
使用 Hugo 建立
主題 StackJimmy 設計