Featured image of post อย่าเพิ่งหลงกลความง่ายของ SQLite! กับดักชนิดข้อมูลแบบไดนามิกคืออะไร? ทำไม ALTER TABLE ถึงทำงานได้แบบครึ่งๆ กลางๆ? และจะสร้างสถาปัตยกรรมการเขียนโปรแกรมเชิงป้องกันใน Node.js เพื่อการอัปเกรด Schema แบบไร้รอยต่อได้อย่างไร?

อย่าเพิ่งหลงกลความง่ายของ SQLite! กับดักชนิดข้อมูลแบบไดนามิกคืออะไร? ทำไม ALTER TABLE ถึงทำงานได้แบบครึ่งๆ กลางๆ? และจะสร้างสถาปัตยกรรมการเขียนโปรแกรมเชิงป้องกันใน Node.js เพื่อการอัปเกรด Schema แบบไร้รอยต่อได้อย่างไร?

SQLite ใช้ระบบชนิดข้อมูลแบบไดนามิกที่มีความยืดหยุ่นสูง ซึ่งการใส่สตริงลงในคอลัมน์ INTEGER จะไม่แสดงข้อผิดพลาดแต่อย่างใด เรียนรู้เกี่ยวกับกับดัก Type Affinity ของ SQLite ผลกระทบของการไม่มีชนิดข้อมูล Boolean และ Date แบบเนทีฟ ข้อจำกัดของ ALTER TABLE พร้อมกลยุทธ์การอัปเกรดที่ปลอดภัยแบบ 'สี่ขั้นตอนสร้างใหม่และย้ายข้อมูล' ตลอดจนการสร้างโครงสร้างการเขียนโปรแกรมเชิงป้องกันด้วย TypeScript, Zod, และ Prisma

ลองจินตนาการถึงถังขยะรีไซเคิลที่คุณติดป้ายไว้อย่างชัดเจนว่า “สำหรับขวดพลาสติกเท่านั้น” แต่เมื่อมีคนทิ้งเศษกระดาษลงไป มันกลับยอมรับไว้อย่างเงียบๆ โดยไม่มีการประท้วงใดๆ เลยแม้แต่คำเดียว?

นี่คือประสบการณ์ที่ทำให้เสียวสันหลังวาบเมื่อนักพัฒนาพบกับระบบชนิดข้อมูลของ 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 ในอนาคตได้อย่างราบรื่น

Reference

All rights reserved,未經允許不得隨意轉載
ถูกสร้างด้วย Hugo
ธีม Stack ออกแบบโดย Jimmy