ทำความรู้จักกับ Migrations และปัญหาที่มือใหม่มักเจอ
ในการพัฒนาซอฟต์แวร์ เรามักต้องเก็บข้อมูลลงใน Database (ฐานข้อมูล หรือที่เก็บข้อมูลของแอป) เมื่อแอปมีการอัปเดต เรามักต้องปรับเปลี่ยนโครงสร้างตารางข้อมูลตามไปด้วย Migration (กระบวนการจัดการการเปลี่ยนแปลงโครงสร้างฐานข้อมูล) จึงเป็นเครื่องมือที่เข้ามาช่วยให้เราบันทึกการเปลี่ยนแปลงเหล่านี้ไว้ในรูปแบบไฟล์โค้ด เพื่อให้เราย้อนกลับหรืออัปเดตโครงสร้างฐานข้อมูลได้อย่างปลอดภัยและเป็นระเบียบ
สำหรับมือใหม่ ปัญหาที่พบบ่อยคือการลืมนำไฟล์เหล่านี้ติดตัวไปตอนที่เรา Deploy (เอาโค้ดขึ้นเซิร์ฟเวอร์จริง หรือการทำให้โปรแกรมของเราพร้อมใช้งานบนอินเทอร์เน็ต) ทำให้แอปบนเซิร์ฟเวอร์รันไม่ได้เพราะหาไฟล์คำสั่งจัดการฐานข้อมูลไม่เจอ การต้องคอยก๊อปปี้โฟลเดอร์ migrations/ ไปวางไว้ข้างๆ โปรแกรมทุกครั้งเป็นเรื่องยุ่งยากและมีโอกาสเกิดข้อผิดพลาดสูงมาก
Miglite เวอร์ชัน 0.8.0 เข้ามาแก้ปัญหานี้ด้วยแนวคิดการ Ship (ขนส่งหรือรวมไฟล์เข้าด้วยกัน) โดยการฝังไฟล์เหล่านี้เข้าไปใน Binary (ไฟล์โปรแกรมที่คอมไพล์แล้ว หรือไฟล์ที่รันได้โดยตรงเหมือน .exe ใน Windows) ทำให้เราไม่ต้องกังวลเรื่องการลืมเอาไฟล์ไปวางที่เซิร์ฟเวอร์อีกต่อไป เพราะทุกอย่างรวมอยู่ในไฟล์เดียวจบครบในตัวเดียวครับ
การฝังไฟล์ Migration เข้าไปใน Binary
ในภาษา Go เรามีเครื่องมือที่ชื่อว่า go:embed ซึ่งช่วยให้เราสามารถดึงไฟล์ภายนอก เช่น ไฟล์ SQL เข้ามาเก็บไว้ในหน่วยความจำของโปรแกรมตอนที่มันทำงานได้ การใช้งานนี้ทำให้เราไม่ต้องมีโฟลเดอร์แยกต่างหากในเครื่องเซิร์ฟเวอร์อีกต่อไป โปรแกรมจะอ่านไฟล์จากภายในตัวมันเองแทนการมองหาไฟล์บนฮาร์ดดิสก์
ลองมาดูตัวอย่างการเขียนโค้ดเพื่อฝังไฟล์เหล่านี้เข้าไปในโปรแกรมของเรากันครับ นี่คือวิธีการตั้งค่าพื้นฐานที่ทำให้ Miglite รู้จักไฟล์ที่ถูกฝังอยู่ข้างในตัวมันเอง แทนที่จะไปหาจากโฟลเดอร์ข้างนอกเหมือนแบบเดิม
//go:embed migrations/*.sql
var migrationFS embed.FS
func main() {
// กำหนดให้ใช้ไฟล์ที่ฝังอยู่ในตัวโปรแกรม
mig, err := miglite.NewAuto(func(cfg *miglite.Config) {
cfg.Migrations.Path = "migrations"
})
// เชื่อมต่อกับฐานข้อมูลและสั่งรันการอัปเดต
mig.SetFS(migrationFS).SetSqlDB(db)
mig.Up(command.UpOption{Yes: true})
}
ในตัวอย่างนี้ บรรทัดแรกคือคำสั่งพิเศษที่บอกให้ Go นำไฟล์ SQL ทั้งหมดในโฟลเดอร์มาเก็บไว้ในตัวแปร migrationFS ส่วน mig.SetFS(migrationFS) คือการบอกให้ไลบรารีไปอ่านไฟล์จากหน่วยความจำแทนการอ่านจากโฟลเดอร์ปกติ ผลลัพธ์คือเมื่อเราสั่งคอมไพล์โปรแกรม เราจะได้ไฟล์เดียวที่รวมทั้งตัวแอปและโครงสร้างฐานข้อมูลไปใช้งานได้ทันที
การใช้ Flag --db เพื่อเลือกฐานข้อมูลที่ต้องการ
บ่อยครั้งที่นักพัฒนาต้องการทดสอบโปรแกรมกับฐานข้อมูลหลายที่ เช่น ฐานข้อมูลสำหรับทดสอบ หรือฐานข้อมูลจำลองจากข้อมูลจริง การแก้ไขไฟล์ตั้งค่าทุกครั้งเป็นเรื่องที่เสียเวลาและเสี่ยงต่อการลืมเปลี่ยนกลับ การใช้ Flag (ตัวเลือกเสริมสำหรับคำสั่ง) จึงเป็นทางออกที่สะดวกและปลอดภัยกว่ามากครับ
ใน Miglite v0.5.0 เป็นต้นมา เราสามารถใช้คำสั่ง --db เพื่อระบุชื่อฐานข้อมูลที่ต้องการจัดการได้ทันทีโดยไม่ต้องไปแก้ไฟล์ตั้งค่าหลัก สิ่งนี้มีประโยชน์มากตอนที่เราต้องการรันคำสั่ง SQL (ภาษาที่ใช้คุยกับฐานข้อมูล) เฉพาะกิจบนฐานข้อมูลที่เรากำหนดเอง
# ตรวจสอบสถานะของฐานข้อมูลที่ระบุ
miglite --db my_test_db status
# รันคำสั่ง SQL บนฐานข้อมูลที่ต้องการโดยไม่ต้องแก้ไฟล์ Config
miglite exec --db my_test_db --yes "SELECT * FROM users;"
บรรทัดแรกคือการสั่งให้เช็คสถานะการอัปเดตบนฐานข้อมูลที่ชื่อว่า my_test_db เท่านั้น บรรทัดที่สองคือการรันคำสั่ง SQL ตรงๆ ลงไปในฐานข้อมูลดังกล่าวโดยข้ามขั้นตอนการยืนยัน ผลลัพธ์ที่ได้คือเราสามารถจัดการฐานข้อมูลหลายตัวได้จากคำสั่งเดียวโดยไม่ต้องกังวลว่าจะไปกระทบกับค่าตั้งค่าถาวรของโปรแกรมครับ
การทำงานของ exec แบบ Transaction เดียว
Transaction (ชุดคำสั่งที่ต้องสำเร็จทั้งหมดหรือล้มเหลวทั้งหมด) คือหัวใจสำคัญของการจัดการข้อมูลที่ปลอดภัย ในเวอร์ชันก่อนหน้านี้ exec จะรันคำสั่งทีละบรรทัด หากคำสั่งที่ห้าพัง คำสั่งที่หนึ่งถึงสี่จะถูกบันทึกลงไปแล้ว ทำให้ข้อมูลอาจจะค้างอยู่ในสถานะไม่สมบูรณ์ แต่ตอนนี้ Miglite ปรับปรุงใหม่ให้รันคำสั่งทั้งหมดในชุดเดียวแล้วครับ
การทำงานแบบใหม่นี้จะแบ่งคำสั่ง SQL ออกเป็นส่วนๆ และรันภายใต้ Transaction เดียวกัน หากมีคำสั่งไหนผิดพลาด ระบบจะทำการ Rollback (การย้อนกลับสถานะข้อมูลให้เหมือนก่อนเริ่มรันคำสั่ง) ให้โดยอัตโนมัติ ทำให้มั่นใจได้ว่าฐานข้อมูลของเราจะไม่พังเพราะคำสั่งที่รันไม่จบ
ตัวอย่างการใช้งานคือการนำไฟล์ SQL ที่มีคำสั่งหลายบรรทัดมาสั่งรันผ่านคำสั่ง exec ของ Miglite ซึ่งระบบจะจัดการเรื่องความปลอดภัยให้เราเองโดยไม่ต้องเขียนโค้ดเพิ่ม วิธีนี้เหมาะมากสำหรับการนำเข้าข้อมูลเริ่มต้น (Seed data) ที่เราต้องการให้แน่ใจว่าข้อมูลทุกอย่างต้องเข้าฐานข้อมูลไปพร้อมกันอย่างสมบูรณ์
การปรับปรุงภายในเพื่อความเสถียร
การพัฒนาซอฟต์แวร์ที่ดีต้องมีการปรับปรุงโครงสร้างภายในหรือที่เรียกว่า Refactor (การปรับปรุงโค้ดโดยไม่เปลี่ยนการทำงาน) อยู่เสมอ ในเวอร์ชัน 0.7.0 ทางทีมผู้พัฒนาได้แยกส่วนการทำงานออกจากค่าคงที่ของระบบ ทำให้การรันคำสั่งต่างๆ มีความอิสระและแม่นยำมากขึ้นกว่าเดิมครับ
จุดที่สำคัญสำหรับมือใหม่คือการแก้ไขเรื่อง up --skip-err ที่เคยถูกละเลยไป ตอนนี้หากมีการรัน Migration แล้วเกิดข้อผิดพลาด ระบบจะข้ามไฟล์นั้นไปและรันไฟล์ถัดไปให้จนจบ แต่จะคืนค่าสถานะเป็น "ผิดพลาด" ให้เราทราบ ซึ่งช่วยให้การรันสคริปต์ใน CI/CD (ระบบทดสอบและส่งโค้ดขึ้นเซิร์ฟเวอร์อัตโนมัติ) ทำได้ง่ายขึ้นและตรวจสอบได้จริง
นอกจากนี้ การจัดการเรื่อง Connection (การเชื่อมต่อกับฐานข้อมูล) ก็ทำได้ดีขึ้น โดย Miglite จะไม่ปิดการเชื่อมต่อที่เราส่งเข้าไปเองอีกต่อไป ทำให้เราสามารถนำตัวเชื่อมต่อเดิมไปใช้งานต่อได้หลังจากรัน Migration เสร็จสิ้น ซึ่งเป็นสิ่งที่นักพัฒนาที่ต้องดูแลแอปพลิเคชันระยะยาวควรรู้ไว้นะครับ เพราะมันช่วยลดปัญหาการเชื่อมต่อฐานข้อมูลหลุดโดยไม่ตั้งใจได้ดีมาก
สรุป: การเริ่มต้นใช้งาน Miglite ในโปรเจกต์ของคุณ
การอัปเกรดมาใช้ Miglite v0.8.0 นั้นแทบไม่มีผลกระทบกับโค้ดเดิมของคุณเลย เพราะไม่มีการเปลี่ยนคำสั่งหรือรูปแบบไฟล์ตั้งค่า สิ่งที่เปลี่ยนคือความสะดวกในการจัดการไฟล์และการทำงานที่เสถียรขึ้น หากคุณกำลังมองหาเครื่องมือจัดการฐานข้อมูลที่เบาและใช้งานง่าย ผมแนะนำให้ลองเริ่มจากโปรเจกต์เล็กๆ ของตัวเองก่อนครับ
สมมติว่าคุณกำลังทำโปรเจกต์เว็บแอปพลิเคชันส่วนตัว ให้ลองเปลี่ยนจากการวางโฟลเดอร์ migrations/ ไว้ข้างๆ โปรแกรม มาลองใช้ go:embed เพื่อฝังไฟล์เหล่านี้เข้าไปดูครับ คุณจะพบว่าขั้นตอนการ Deploy ของคุณจะง่ายขึ้นมาก เพราะไม่ต้องคอยกังวลว่าไฟล์สำคัญจะหายไปตอนอัปโหลดขึ้นเซิร์ฟเวอร์อีกต่อไป
จำไว้ว่าการเป็นโปรแกรมเมอร์ที่ดีไม่ใช่แค่การเขียนโค้ดให้รันได้ แต่คือการใช้เครื่องมือที่ช่วยให้การทำงานของเรามีประสิทธิภาพและเกิดข้อผิดพลาดน้อยที่สุด Miglite เป็นตัวอย่างที่ดีของเครื่องมือที่ช่วยลดความยุ่งยากในงานพื้นฐาน เพื่อให้คุณมีเวลาไปโฟกัสกับการเขียนฟีเจอร์ใหม่ๆ ที่สนุกและท้าทายกว่าเดิมครับ
ที่มา: miglite v0.8.0: ship migrations inside the binary — DEV Community