ยกระดับโค้ดฐานข้อมูลจากสคริปต์แก้ไขด้วยมือหลังบ้าน (Ad-hoc Manual Fix) สู่สถาปัตยกรรม Database-as-Code (DbaC) ที่สามารถเก็บใน Git, ทำ Peer Review, และรันผ่าน CI/CD Pipeline แบบอัตโนมัติ 100% พร้อมไขความลับกลไก Dual-Migration Strategy: V__*.sql (Versioned สำหรับตาราง) vs R__*.sql (Repeatable สำหรับ Stored Units) ขจัดปัญหา Schema Drift อย่างเด็ดขาด!
Phase 3 เริ่มที่คำถามว่าโค้ดในฐานข้อมูลจะอยู่กับทีมได้อย่างไร — เก็บใน Git รีวิวได้ และ deploy ผ่าน pipeline ไม่ใช่ให้คนแอบเข้าไปแก้บน production ด้วยมือ
แนวคิดการบริหารโครงสร้างและโปรแกรมยูนิตในฐานข้อมูลให้เหมือนซอร์สโค้ดซอฟต์แวร์ เก็บใน Git ทำ code review และสั่ง deploy ผ่าน CI/CD
เครื่องมือที่อ่านไฟล์สคริปต์ SQL ตามลำดับ แล้วไปรันอัปเดตบนฐานข้อมูลเป้าหมายให้อัตโนมัติ พร้อมบันทึกประวัติไว้
สคริปต์แบบ Versioned ใช้กับโครงสร้างตาราง — รันครั้งเดียว ห้ามแก้ไฟล์เดิม ส่วน Repeatable ใช้กับ stored code — รันใหม่ให้อัตโนมัติทุกครั้งที่ไฟล์ถูกแก้
โดยรวมวันนี้คือการจัดการสคริปต์ฐานข้อมูลด้วย Flyway แยกโครงสร้างตารางกับ stored code ออกจากกัน แล้วนำเข้าสู่ CI/CD เพื่อขจัดปัญหาคนแอบไปแก้ฐานข้อมูลด้วยมือ (schema drift)
ช่วยให้ทีมโปรแกรมเมอร์หลายคนทำงานบนฐานข้อมูลเดียวกันได้อย่างเป็นระบบ โดยไม่ต้องกลัวว่าโค้ดจะทับกันหรือหายไป
หลังจากที่เราได้สร้าง Stored Units ประสิทธิภาพสูงทั้ง 4 เสาหลัก (UDFs, Procedures, Triggers, Materialized Views) สำเร็จแล้ว คำถามคลาสสิกที่สถาปนิกและผู้บริหารมักจะถามเสมอคือ "เราจะดูแลโค้ดพวกนี้อย่างไรเมื่อมีนักพัฒนา 20 คน? จะควบคุมไม่ให้คนแอบเข้าไปแก้ฟังก์ชันบน Production ได้อย่างไร? และจะกู้คืนระบบกลับสู่ State เดิมได้เร็วแค่ไหนหาก Deploy พัง?"
ทีมพัฒนาระบบการเงินมีนักพัฒนา 15 คน และมี Environment 4 ชั้น (Dev, SIT, UAT, Production) ในคืนวันศุกร์ เกิด Bug ในการปัดเศษทศนิยมของฟังก์ชัน fn_calculate_tax() บน Production นักพัฒนาอาวุโสใช้สิทธิ DB Admin แอบเปิด DBeaver เข้าไปแก้โค้ดบน Production โดยตรงจนหายดี แต่ ลืมนำโค้ดที่แก้กลับมาบันทึกลง Git Repository:
ต้นตอของวิกฤตนี้คือ "การปล่อยให้ฐานข้อมูลเป็น State ลึกลับที่ไม่ผ่าน Version Control" ทางออกเดียวในระดับ Enterprise คือการเปลี่ยนกระบวนทัศน์มาเป็น Database-as-Code (DbaC) ด้วยเครื่องมือ Migration อย่าง Flyway หรือ Liquibase ครับ!
| มิติเปรียบเทียบสถาปัตยกรรม | Versioned Migration (V__*.sql) | Repeatable Migration (R__*.sql) |
|---|---|---|
| ลักษณะการตั้งชื่อไฟล์ | V1__create_tables.sql, V2__add_idx.sql |
R__01_functions.sql, R__02_procedures.sql |
| เป้าหมายวัตถุในฐานข้อมูล | Stateful Objects (ตาราง, คอลัมน์, Foreign Keys) | Stateless/Code Objects (UDFs, Procedures, Triggers, Views) |
| ความถี่ในการรัน (Execution Frequency) | รันเพียง 1 ครั้งถ้วน ตามลำดับเวอร์ชัน | รันใหม่ ทุกครั้งที่สคริปต์ใน Git มีการเปลี่ยนแปลง Checksum |
| ลำดับการทำงาน (Execution Order) | รันก่อนเสมอ เรียงตามหมายเลขเวอร์ชัน (V1 ➔ V2 ➔ V3) | รันทีหลังสุด หลังจาก Versioned Migrations สำเร็จทั้งหมดแล้ว |
| ข้อกำหนดทางไวยากรณ์ (Syntax Rule) | คำสั่ง DDL ปกติ (ห้ามรันซ้ำ) | ต้องเป็น Idempotent (เช่น CREATE OR REPLACE) |
| ผลเมื่อแก้เนื้อหาในไฟล์เดิม | 🚨 Validation Failed: Checksum Mismatch! | ✅ คำนวณ Checksum ใหม่และ Deploy อัตโนมัติ |
"ทุกท่านครับ การแอบเปิด GUI เข้าไปแก้ Stored Procedure บน Production DB คือฝันร้ายที่ทำลายระบบ CI/CD และเป็นสาเหตุอันดับ 1 ของ Deployment Failures!
ทางออกระดับสถาปัตยกรรมคือการใช้ Database-as-Code ผ่าน Flyway โดยแยกยุทธศาสตร์ออกเป็น 2 ส่วน: Versioned Migrations (V__) สำหรับโครงสร้างตารางที่ไม่เปลี่ยนบ่อย และ Repeatable Migrations (R__) สำหรับ Stored Units ซึ่งรันใหม่ให้อัตโนมัติทุกครั้งที่มีการแก้โค้ดใน Git!
ผลลัพธ์คือการขจัดปัญหา Schema Drift อย่างเด็ดขาด สามารถกู้คืนระบบกลับสู่ State ใดๆ ในประวัติศาสตร์ได้ภายใน 3 วินาที และทำให้การปล่อยโค้ดฐานข้อมูลเป็นเรื่องอัตโนมัติ 100% บน CI/CD Pipeline ครับ"
ลองจำลองสถานการณ์การแก้ไขไฟล์ใน Git Repository หรือการแอบแก้ไขฐานข้อมูลหลังบ้าน เพื่อดูว่า Flyway Engine จะตอบสนองอย่างไร และจะเกิด Schema Drift หรือไม่
การทำ Database-as-Code ต้องเริ่มตั้งแต่การเข้าใจโครงสร้างไดเรกทอรีของสคริปต์, การตั้งชื่อไฟล์ที่ถูกต้องตามมาตรฐาน Flyway, และการเขียนสคริปต์ Stored Units ใน Repeatable Migration ให้มีคุณสมบัติ Idempotent (รันซ้ำกี่ครั้งก็ได้ผลลัพธ์ถูกต้องเสมอ):
db/migration/
├── V1__create_core_tables.sql <-- [1] Versioned: รันครั้งแรกครั้งเดียว ห้ามแก้ไฟล์นี้ตลอดกาล
├── V2__add_orders_indexes.sql <-- [2] Versioned: รันต่อจาก V1 ตามลำดับหมายเลขเวอร์ชัน
├── R__01_udf_calculations.sql <-- [3] Repeatable: รันใหม่ทุกครั้งที่มีการแก้โค้ด UDF ใน Git
├── R__02_stored_procedures.sql <-- [4] Repeatable: รันใหม่ทุกครั้งที่มีการแก้ Procedure ใน Git
└── R__03_audit_triggers.sql <-- [5] Repeatable: รันใหม่ทุกครั้งที่มีการแก้ Trigger ใน Git
__) คือตัวคั่นระหว่างประเภท/เวอร์ชันกับคำอธิบายสคริปต์ ห้ามใช้ขีดเดียวเด็ดขาด!
-- db/migration/V1__init_schema.sql
-- 🎯 กฎเหล็ก: Versioned Migration ต้องขึ้นต้นด้วย V__ และรันเพียงครั้งเดียวตามลำดับถาวร
-- [1] สร้างตารางบัญชีเงินฝาก
CREATE TABLE accounts (
account_id BIGSERIAL PRIMARY KEY,
account_no VARCHAR(20) UNIQUE NOT NULL,
balance NUMERIC(15,2) NOT NULL DEFAULT 0.00,
status VARCHAR(10) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- [2] สร้างตารางบันทึกประวัติธุรกรรม
CREATE TABLE transaction_logs (
log_id BIGSERIAL PRIMARY KEY,
account_id BIGINT REFERENCES accounts(account_id),
amount NUMERIC(15,2) NOT NULL,
type VARCHAR(20) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
flyway_schema_history ห้ามใครกลับมาแก้ไขไฟล์นี้อีกเด็ดขาด!
-- db/migration/R__01_stored_units.sql
-- 🎯 กฎเหล็ก: Repeatable Migration ต้องขึ้นต้นด้วย R__ และทุกคำสั่งต้องเป็น Idempotent (รันซ้ำได้ไม่พัง)
-- [1] Stored Function แบบ Idempotent (ใช้ CREATE OR REPLACE)
CREATE OR REPLACE FUNCTION fn_get_account_balance(p_account_id BIGINT)
RETURNS NUMERIC
LANGUAGE sql
STABLE
AS $$
SELECT balance FROM accounts WHERE account_id = p_account_id;
$$;
-- [2] Stored Procedure แบบ Idempotent พร้อม Explicit Transaction Control
CREATE OR REPLACE PROCEDURE pr_deposit_funds(
p_account_id BIGINT,
p_amount NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE accounts SET balance = balance + p_amount WHERE account_id = p_account_id;
INSERT INTO transaction_logs (account_id, amount, type) VALUES (p_account_id, p_amount, 'DEPOSIT');
COMMIT;
END $$;
R__01 เปลี่ยนไป และจะรันไฟล์นี้ใหม่อัตโนมัติบนทุก Environment ทันที!
เปรียบเทียบโค้ดเดิมที่สร้างไฟล์ Versioned ใหม่ไปเรื่อยๆ จนซอร์สโค้ดกระจัดกระจาย กับโค้ดใหม่ที่จัดการผ่าน Repeatable Migration รวมศูนย์:
-- ❌ หายนะของการแก้ Stored Procedure ด้วย Versioned Migration:
-- V3__update_proc_v1.sql
-- V4__update_proc_v2.sql
-- V5__fix_bug_proc.sql
-- V6__add_param_proc.sql
-- ผลลัพธ์: มีไฟล์สคริปต์สะสมเป็นร้อยๆ ไฟล์ใน Git!
-- นักพัฒนาคนใหม่เข้ามา ไม่รู้ว่าไฟล์ไหนคือ Logic ล่าสุดที่รันอยู่บน Production
-- และประวัติ Git History ของแต่ละฟังก์ชันกระจัดกระจายข้ามหลายไฟล์จน Review ไม่ได้!
-- db/migration/R__02_core_financial_procedures.sql
-- 🎯 ใช้ Repeatable Migration รวม Stored Units ที่เกี่ยวข้องไว้ในไฟล์เดียว
-- เมื่อแก้ Bug ให้แก้ในไฟล์นี้ตรงๆ Flyway จะคำนวณ Checksum ใหม่และ Deploy ให้อัตโนมัติ!
-- [Statement 1] จัดการกรณีที่มีการเปลี่ยน Parameter Signature ด้วย DROP IF EXISTS
DROP PROCEDURE IF EXISTS pr_transfer_funds_enterprise(BIGINT, BIGINT, NUMERIC);
-- [Statement 2] ประกาศ Stored Procedure ฉบับล่าสุด (Idempotent 100%)
CREATE OR REPLACE PROCEDURE pr_transfer_funds_enterprise(
p_src_acc BIGINT,
p_dst_acc BIGINT,
p_amount NUMERIC
)
LANGUAGE plpgsql
AS $$
DECLARE
v_src_bal NUMERIC;
BEGIN
-- [บรรทัดที่ 1-3] ตรวจสอบยอดเงินต้นทางพร้อมล็อกแถว
SELECT balance INTO v_src_bal FROM accounts WHERE account_id = p_src_acc FOR UPDATE;
IF v_src_bal < p_amount THEN
RAISE EXCEPTION 'ยอดเงินในบัญชีต้นทางไม่เพียงพอ (คงเหลือ: %, ต้องการ: %)', v_src_bal, p_amount;
END IF;
-- [บรรทัดที่ 4-6] ทำการหักเงินและเพิ่มเงินเชิงเซต
UPDATE accounts SET balance = balance - p_amount WHERE account_id = p_src_acc;
UPDATE accounts SET balance = balance + p_amount WHERE account_id = p_dst_acc;
-- [บรรทัดที่ 7-8] บันทึก Transaction Logs
INSERT INTO transaction_logs(account_id, amount, type) VALUES (p_src_acc, -p_amount, 'TRANSFER_OUT');
INSERT INTO transaction_logs(account_id, amount, type) VALUES (p_dst_acc, p_amount, 'TRANSFER_IN');
-- 🎯 [บรรทัดที่ 9] สั่ง COMMIT ปิด Transaction ภายใน Procedure
COMMIT;
END $$;
แบบทดสอบวัดระดับความเข้าใจ Day 11: Database-as-Code with Flyway / Liquibase Migration Mechanics (Versioned vs Repeatable, Schema Drift, Checksum Validation, Idempotency) และข้อสอบสะสมทบทวนครบทั้ง Days 1–10 คลิกเลือกคำตอบ ก, ข, ค, หรือ ง เพื่อดูผลลัพธ์และคำอธิบายอย่างละเอียดทันที
บทเรียน Day 11: Database-as-Code with Flyway / Liquibase Migration Mechanics ได้ยกระดับการจัดการซอร์สโค้ดฐานข้อมูลเข้าสู่ยุค Database-as-Code โดยการแบ่งสคริปต์ออกเป็น Versioned Migrations (V__*.sql) สำหรับโครงสร้าง DDL และ Repeatable Migrations (R__*.sql) สำหรับ Stored Units (UDFs, Procedures, Triggers) ซึ่งช่วยขจัดปัญหา Schema Drift และทำให้การ Deploy สคริปต์ฐานข้อมูลเข้าสู่ระบบ CI/CD เป็นเรื่องอัตโนมัติ 100%
ในวันถัดไป เราจะไปเจาะลึกการสร้างระบบทดสอบอัตโนมัติ (Automated Database Unit Testing) โดยการเขียน Unit Test สำหรับ Stored Units ด้วย pgTAP และการจำลอง Ephemeral Database Container สำหรับรัน Integration Tests ใน CI/CD Pipeline ด้วย Testcontainers เพื่อการันตีว่า Stored Units ทุกตัวปราศจาก Bug ก่อน Deploy ลง Production ครับ!