ลองจินตนาการถึงถังขยะรีไซเคิลที่คุณติดป้ายไว้อย่างชัดเจนว่า “สำหรับขวดพลาสติกเท่านั้น” แต่เมื่อมีคนทิ้งเศษกระดาษลงไป มันกลับยอมรับไว้อย่างเงียบๆ โดยไม่มีการประท้วงใดๆ เลยแม้แต่คำเดียว?
นี่คือประสบการณ์ที่ทำให้เสียวสันหลังวาบเมื่อนักพัฒนาพบกับระบบชนิดข้อมูลของ SQLite เป็นครั้งแรก
หากคุณคุ้นเคยกับสไตล์เจ้าหน้าที่ศุลกากรที่เข้มงวดของ PostgreSQL (ที่ซึ่งชนิดข้อมูลที่ไม่ถูกต้องจะถูกปฏิเสธการเข้าเมืองโดยตรง) ความง่ายๆ สบายๆ ของ SQLite อาจทำให้คุณตั้งคำถามกับชีวิตได้
ที่น่ากลัวกว่านั้นคือ เมื่อคุณต้องการ แก้ไขโครงสร้างตาราง มันจะบอกคุณว่า:
“ตารางไม่สามารถแก้ไขได้โดยตรง โปรดสร้างบ้านใหม่ ย้ายเฟอร์นิเจอร์ไป แล้วระเบิดบ้านหลังเก่าทิ้งซะ”
ภายใต้เบื้องลึก SQLite มี Storage Classes เพียง 5 แบบเท่านั้น
ไม่ว่าคุณจะประกาศชื่อชนิดข้อมูลที่หรูหราเพียงใดใน CREATE TABLE (VARCHAR(255), BIGINT, DECIMAL) แต่ SQLite ภายใต้เบื้องหลังจะรู้จักเพียง 5 storage classes เหล่านี้เท่านั้น:
| Storage Class | คำอธิบาย |
|---|---|
| NULL | ค่าว่าง |
| INTEGER | จำนวนเต็ม (จะใช้พื้นที่ 1 ถึง 8 ไบต์โดยอัตโนมัติตามขนาดของข้อมูล) |
| REAL | เลขทศนิยม (ขนาดคงที่ 8 ไบต์) |
| TEXT | สตริง (ใช้การเข้ารหัสแบบ UTF-8 เป็นค่าเริ่มต้น) |
| BLOB | ข้อมูลไบนารีขนาดใหญ่ (จัดเก็บตรงตามที่ป้อนเข้ามา) |
ชนิดข้อมูลที่คุณตั้งค่าให้กับคอลัมน์นั้น เป็นเพียง "คำแนะนำ" สำหรับ SQLite เท่านั้น ไม่ใช่ "กฎข้อบังคับ"
สิ่งนี้เรียกว่า “Type Affinity” โดย
SQLiteจะพยายามแปลงข้อมูลของคุณให้เป็นชนิดข้อมูลที่แนะนำ แต่ถ้าทำไม่ได้ มันก็จะใส่ข้อมูลเดิมลงไปทันทีโดยไม่มีการแจ้งข้อผิดพลาดใดๆ
คุณสามารถประกาศคอลัมน์ age INTEGER ใน SQLite แล้วใส่สตริง 'สิบแปดตลอดกาล' ลงไปได้ และมันก็จะยอมรับด้วยความยินดี
กับดักชนิดข้อมูล 3 ประการที่ง่ายที่สุดในการก้าวพลาด
กับดักที่ 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 |
ใช้พื้นที่น้อย แต่คนอ่านไม่เข้าใจ |
อย่าจัดเก็บวันที่ในรูปแบบที่กำหนดเอง เช่น 2026/5/19 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 โดยพื้นฐานแล้วเป็นเพียงไฟล์ไฟล์หนึ่ง ก่อนที่จะดำเนินการเปลี่ยนแปลง Schema ใดๆ เพียงแค่คัดลอกไฟล์ .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 ก็มีข้อจำกัดมากมาย
อย่างไรก็ตาม ตราบใดที่คุณใช้ การตรวจสอบชนิดข้อมูล TypeScript + การตรวจสอบ Zod + เลเยอร์ ORM ในฝั่ง Node.js ควบคู่ไปกับกลยุทธ์ความปลอดภัยของ การสำรองไฟล์ทางกายภาพ + Transaction คุณก็สามารถเพลิดเพลินกับประสิทธิภาพการพัฒนาที่ได้รับจาก SQLite ได้อย่างปลอดภัย ในขณะเดียวกันก็ปูทางไปสู่การย้ายไปยัง PostgreSQL ในอนาคตได้อย่างราบรื่น