Hãy tưởng tượng một thùng rác tái chế mà bạn đã dán nhãn rõ ràng là “Chỉ dành cho chai nhựa”, nhưng khi ai đó ném một mảnh giấy vào, nó vẫn âm thầm chấp nhận mà không một lời phản đối?
Đây là trải nghiệm lạnh sống lưng khi các nhà phát triển gặp hệ thống kiểu dữ liệu của SQLite lần đầu tiên.
Nếu bạn đã quen với phong cách nhân viên hải quan nghiêm ngặt của PostgreSQL (nơi các kiểu dữ liệu không hợp lệ bị từ chối nhập cảnh trực tiếp), sự dễ dãi của SQLite có thể khiến bạn nghi ngờ cuộc sống của mình.
Đáng sợ hơn nữa, khi bạn muốn thay đổi cấu trúc bảng, nó sẽ bảo bạn:
“Không thể sửa đổi bảng trực tiếp. Hãy xây một ngôi nhà mới, chuyển đồ đạc qua, rồi cho nổ tung ngôi nhà cũ đi.”
Dưới Mái Nhà SQLite Chỉ Có 5 Lớp Lưu Trữ
Cho dù bạn có khai báo các tên kiểu dữ liệu lộng lẫy thế nào trong CREATE TABLE (VARCHAR(255), BIGINT, DECIMAL), SQLite dưới tầng ngầm chỉ nhận diện 5 lớp lưu trữ sau đây:
| Lớp Lưu Trữ | Mô tả |
|---|---|
| NULL | Giá trị rỗng |
| INTEGER | Số nguyên (tự động chiếm từ 1 đến 8 byte tùy thuộc vào độ lớn của số) |
| REAL | Số thực (cố định 8 byte) |
| TEXT | Chuỗi ký tự (mặc định mã hóa UTF-8) |
| BLOB | Đối tượng nhị phân lớn (được lưu trữ chính xác như khi nhập) |
Các kiểu dữ liệu bạn đặt trên các cột chỉ đơn thuần là "gợi ý" cho SQLite, không phải "quy tắc bắt buộc".
Điều này được gọi là “Type Affinity” (Kiểu tương thích).
SQLitesẽ cố gắng chuyển đổi dữ liệu của bạn thành kiểu được đề xuất, nhưng nếu không thể, nó sẽ nhét dữ liệu gốc vào mà không đưa ra bất kỳ lỗi nào.
Bạn có thể khai báo một cột age INTEGER trong SQLite rồi chèn chuỗi 'mãi mãi tuổi mười tám'; nó vẫn sẽ chấp nhận một cách vui vẻ.
Ba Cạm Bẫy Kiểu Dữ Liệu Dễ Dẫm Phải Nhất
Cạm bẫy 1: Không có kiểu Boolean gốc
SQLite không có kiểu boolean. True và False chỉ có thể được đại diện bằng các số nguyên 1 và 0.
Khi bạn lấy dữ liệu từ SQLite bằng Node.js, bạn sẽ nhận được số 1 hoặc 0, không phải true hoặc false.
Nếu bạn trực tiếp thực hiện kiểm tra như if (user.is_admin === true), it will never be true.
Cạm bẫy 2: Không có kiểu Date/Time
SQLite không có kiểu ngày/giờ. Bạn chỉ có thể lưu trữ thời gian dưới dạng:
| Phương pháp lưu trữ | Ví dụ | Ưu & Nhược điểm |
|---|---|---|
| TEXT (chuỗi ISO-8601) | '2026-05-19T18:00:00Z' |
Khuyên dùng nhất, dễ đọc, chuyển đổi liền mạch khi chuyển sang PostgreSQL trong tương lai |
| INTEGER (Unix Timestamp) | 1747656000 |
Chiếm ít bộ nhớ, nhưng con người không đọc được |
Không bao giờ lưu trữ ngày tháng dưới các định dạng tùy chỉnh như 2026/5/19 6:00 CH, nếu không việc di chuyển dữ liệu trong tương lai sẽ là một thảm họa.
Cạm bẫy 3: Chèn chuỗi vào cột Integer không báo lỗi
Trong PostgreSQL, việc chèn một chuỗi vào cột INTEGER sẽ báo lỗi ngay lập tức. Nhưng SQLite sẽ chỉ cố gắng chuyển đổi một cách âm thầm, và nếu thất bại, nó chấp nhận nguyên trạng.
Điều này có nghĩa là dữ liệu bẩn có thể âm thầm lẻn vào cơ sở dữ liệu của bạn cho đến một ngày chương trình của bạn bị sập do nhận được một kiểu dữ liệu không mong muốn, lúc đó bạn mới phát hiện ra vấn đề.
Lập Trình Phòng Thủ: Đối Xử Với Một Cơ Sở Dữ Liệu Dễ Dãi Bằng Thái Độ Nghiêm Khắc
Đối mặt với sự dễ dãi của SQLite, bạn phải xây dựng các cơ chế phòng thủ nghiêm ngặt trong phát triển Node.js:
| Cấp độ phòng thủ | Công cụ | Vai trò |
|---|---|---|
| Người gác cổng compile | TypeScript | Bắt các kiểu dữ liệu không chính xác ở giai đoạn viết mã |
| Xác thực đầu vào API | Zod | Xác thực dữ liệu đầu vào một cách nghiêm ngặt (đảm bảo age luôn là số) |
| Ép kiểu dưới tầng ngầm | Prisma / Drizzle ORM | Tự động xử lý sự khác biệt về kiểu dữ liệu giữa SQLite và PostgreSQL |
Chuyển "người gác cổng xác thực dữ liệu" từ lớp cơ sở dữ liệu sang lớp ứng dụng
Node.jslà một chiến lược quan trọng để tận dụng tốc độ phát triển của SQLite trong khi vẫn đảm bảo khả năng mở rộng trong tương lai.
Khi sử dụng một ORM, chỉ cần bạn khai báo type: 'boolean' trong mã của mình, ORM sẽ tự động chuyển đổi thành 1/0 khi lưu vào SQLite, và chuyển ngược lại thành true/false khi đọc, che giấu hoàn hảo sự khác biệt về kiểu dữ liệu bên dưới.
ALTER TABLE Chỉ Là Bán Thành Phẩm: Những Gì Có Thể Và Không Thể Sửa Đổi
Sự hỗ trợ của SQLite cho việc sửa đổi cấu trúc bảng là rất hạn chế:
| Thao tác | Được hỗ trợ |
|---|---|
Thêm cột (ADD COLUMN) |
Có |
Đổi tên cột (RENAME COLUMN) |
Có |
Xóa cột (DROP COLUMN) |
Có (trong các phiên bản mới hơn) |
Đổi tên bảng (RENAME TO) |
Có |
| Thay đổi kiểu cột | Không |
Thêm/Xóa các ràng buộc UNIQUE, NOT NULL |
Không |
| Sửa đổi Khóa chính (Primary Key) | Không |
| Sửa đổi Khóa ngoại (Foreign Key) | Không |
Một khi bạn cần thực hiện bất kỳ sửa đổi nào "không được hỗ trợ",
SQLiteyêu cầu bạn thực hiện chiến lược "tái tạo và di chuyển".
Bốn Bước Tái Tạo & Di Chuyển: Cách Nâng Cấp Bảng Của SQLite
Vì không thể sửa đổi trực tiếp, quy trình tiêu chuẩn được khuyên dùng bởi tài liệu chính thức là xây một ngôi nhà mới, chuyển đồ đạc, nổ tung ngôi nhà cũ và gắn biển số nhà mới:
| Bước | Thao tác | Mô tả |
|---|---|---|
| 1 | Tạo Bảng Mới | CREATE TABLE users_new (...) sử dụng cấu trúc chính xác |
| 2 | Sao Chép Dữ Liệu | INSERT INTO users_new SELECT ... FROM users |
| 3 | Xóa Bảng Cũ | DROP TABLE users |
| 4 | Đổi Tên Bảng | ALTER TABLE users_new RENAME TO users |
Bốn bước này phải được thực hiện liền mạch một mạch; bất kỳ sự cố mất điện hay sập ứng dụng nào giữa chừng đều dẫn đến mất dữ liệu.
Đảm Bảo Nâng Cấp Không Mất Dữ Liệu: Hai Tuyến Phòng Thủ An Toàn
Tuyến phòng thủ 1: Phòng thủ vật lý, Sao chép tệp trực tiếp
SQLite về bản chất chỉ là một tệp đơn độc. Trước khi thực hiện bất kỳ thay đổi schema nào, chỉ cần tạo một bản sao của tệp .db làm bản sao lưu.
const fs = require('fs');
fs.copyFileSync('my_project.db', 'my_project_backup.db');
Nếu mọi việc trục trặc, việc thay thế tệp sẽ khôi phục mọi thứ trong nháy mắt. Đây là một lợi thế mà các cơ sở dữ liệu lớn khác không thể cung cấp.
Tuyến phòng thủ 2: Bao bọc bằng Transaction, Cỗ máy thời gian của cơ sở dữ liệu
Bao bọc tất cả các bước di chuyển bên trong một Transaction duy nhất; nếu bất kỳ bước nào thất bại, toàn bộ quá trình sẽ tự động hoàn tác (Rollback) như thể chưa từng có chuyện gì xảy ra.
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('Nâng cấp bảng thành công');
} catch (error) {
console.error('Nâng cấp thất bại, dữ liệu được khôi phục an toàn:', error.message);
}
"Tệp sao lưu + Ràng buộc bằng Transaction" chính là túi khí an toàn cho việc di chuyển cơ sở dữ liệu của bạn.
Kiểm Soát SQLite Dễ Dãi Bằng Thái Độ Nghiêm Khắc
Chế ngự
SQLitedễ dãi bằng một kiến trúc lớp ứng dụng nghiêm ngặt để tận hưởng tốc độ phát triển nhanh như chớp của nó trong khi vẫn tránh được các khoản nợ kỹ thuật trong tương lai.
Hệ thống kiểu dữ liệu của SQLite rất dễ dãi, và ALTER TABLE cũng có nhiều hạn chế.
Tuy nhiên, chỉ cần bạn thực hiện kiểm tra kiểu TypeScript + xác thực Zod + lớp trừu tượng ORM ở phía Node.js, kết hợp với chiến lược an toàn sao lưu vật lý + Transaction, bạn có thể yên tâm tận hưởng hiệu quả phát triển do SQLite mang lại đồng thời mở đường di chuyển sang PostgreSQL dễ dàng trong tương lai.