9.4 · Schema management + syncModelToTable
VTRC ไม่มี migration tool — ไม่มี sequelize-cli, ไม่มี golang-migrate, ไม่มี flyway ใช้วิธี auto-sync ทุกครั้งที่ boot บทนี้เล่าว่ามันทำงานอย่างไร และทำไมถึงเป็น risk ใหญ่
กลไก — syncModelToTable()
const syncModelToTable = async () => {
const models = syncModels // Object.keys(Models)
const skipModel = ['db', 'Op', 'Sequelize']
const syncTotal = models.length // - skipModel.length
const defaultData = masterData
console.log(`## Syncing model to table total [${syncTotal}]. ##`)
// Sync each models.
for (const i in models) {
const modelName = models[i]
if (skipModel.indexOf(modelName) === -1) {
await Models[modelName].sync({ force: false }).then(synced => {
if (typeof defaultData[modelName] !== 'undefined') {
// Skip when data more than one record.
Models[modelName].findAll({ limit: 1 }).then(rows => {
if (rows.length === 0) {
// Insert default data to table.
Models[modelName].bulkCreate(defaultData[modelName]).then(added => {
console.log(`Data from model "${modelName}" has been added ${defaultData[modelName].length} record(s).`)
return added
})
}
return null
})
}
console.log(`# -- Model#${+i + 1} "${modelName}" has been synced. -- #`)
return synced
}).catch(e => {
console.error('Sync model to table ERROR: ', e)
})
}
}
return 1
}วิธีอ่าน
- รันตอน
index.js:104หลังdb.authenticate()สำเร็จ - Loop ผ่าน
syncModels(hardcoded list ของชื่อ model ที่จะ sync) - เรียก
Models[modelName].sync({ force: false })— Sequelize จะ:- ถ้าตารางไม่มี →
CREATE TABLE - ถ้าตารางมีแล้ว → ไม่ทำอะไร (เพราะ
force: falseไม่ได้ใช้alter: true)
- ถ้าตารางไม่มี →
- ถ้ามี default data (ใน
defaultDataimport) → seed ถ้าตารางว่าง
force: false หมายความว่าอะไร
| ค่า | พฤติกรรม |
|---|---|
{ force: false } (ปัจจุบัน) | ถ้ามีตารางอยู่แล้ว ไม่ทำอะไร — แม้ว่า model จะเพิ่ม column ใหม่ |
{ force: true } | DROP TABLE แล้ว CREATE ใหม่ — ลบข้อมูลทั้งหมด |
{ alter: true } | ALTER TABLE ให้ตรงกับ model — เพิ่ม column ใหม่, เปลี่ยน type, ฯลฯ |
ผลคือ — ถ้า dev เพิ่ม column ใน model แล้ว deploy ลง production, column ใหม่จะไม่ขึ้น
การ "Add column" ทำอย่างไรในทางปฏิบัติ
เพราะ sync({ force: false }) ไม่ ALTER — dev ต้อง manual ALTER TABLE ใน phpMyAdmin หรือ DB client โดยตรง
workflow จริง:
1. dev เพิ่ม field ใน model (Sequelize)
2. dev ไป manual ALTER TABLE ใน production DB
3. dev deploy code ใหม่
4. boot ครั้งถัดไป sync จะ skip (force: false)ผลกระทบ:
- ไม่มี audit trail — ใครแก้อะไรตอนไหน ไม่มี record
- ไม่มี rollback — ถ้าแก้ผิด ต้อง manual กลับ
- ต้องทำ manual ทุก environment (dev / UAT / prod) — ไม่ reproducible
- ถ้า dev ลืม manual ALTER → field ใน code มี แต่ field ใน DB ไม่มี → query fail ตอน runtime
Hardcoded sync list — ปัญหาอีกชั้น
const syncModels = [
'Gallery',
'Policy',
// ... 80 model names ...
'Strategy',
'Output'
]syncModels เป็น array ชื่อ model ที่จะ sync — เขียนเอง ไม่ได้ dynamic จาก Object.keys(Models)
ผลกระทบ:
- ถ้า dev เพิ่ม model ใหม่ → ต้องไปเพิ่มชื่อใน array นี้ด้วย ไม่งั้นตารางไม่ถูกสร้าง
- ถ้าลบ model ออกจาก array → ตารางเดิมยังอยู่ (orphan table)
- comment
// Object.keys(Models)บนบรรทัด 102 คือเดิมใช้ dynamic แล้วเปลี่ยนมาเป็น hardcoded ทีหลัง (เหตุผลไม่ชัด — อาจจะกัน sync บาง model)
createDatabases() — สร้าง database ก่อน sync
const createDatabases = async (db) => {
console.log('DB_MORE_DATABASE', DB_MORE_DATABASE);
if (DB_MORE_DATABASE.length > 0) {
for (const dbName of DB_MORE_DATABASE) {
console.log(`## Creating database "${dbName}". ##`)
await db.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``)
}
}
return 1
}createDatabases(db2) รันตอน index.js:114 — สร้าง database ใน DB_MORE_DATABASE env var (รายการ database name คั่นด้วย comma) ถ้ายังไม่มี
ใช้ backtick ครอบชื่อ database (\${dbName}`) เพื่อกัน SQL injection ถ้าชื่อมี special char — **แต่** ถ้า DB_MORE_DATABASEenv var ผู้ไม่หวังดีตั้งเป็นvtrc`; DROP DATABASE vtrc`` ก็จะรันได้
Default data seeding
if (typeof defaultData[modelName] !== 'undefined') {
// Skip when data more than one record.
Models[modelName].findAll({ limit: 1 }).then(rows => {
if (rows.length === 0) {
// Insert default data to table.
Models[modelName].bulkCreate(defaultData[modelName]).then(added => {
console.log(`Data from model "${modelName}" has been added ${defaultData[modelName].length} record(s).`)
return added
})
}
return null
})
}ถ้ามี entry ใน defaultData[modelName] → ถ้าตารางว่าง (เช็กจาก findAll({ limit: 1 })) → bulk insert default records
ปกติเป็น master data เช่น role defaults, config defaults
Bug ใน seeding logic — fire-and-forget
Models[modelName].findAll({ limit: 1 }).then(rows => { ... })ไม่มี await ใน outer chain — sync loop ไม่รอ seeding เสร็จก่อนเริ่ม sync model ถัดไป
ผลคือ — ถ้าระหว่าง seeding model A, sync model B รันพร้อมกัน → race condition ได้ โชคดีที่ sync ส่วนใหญ่ไม่มี dependency ข้าม model จึงไม่ค่อยเจอ
ฝั่ง centralize — synchronize: false
centralize-api (NestJS + TypeORM) ตั้งค่าตรงกันข้าม
centralize-api/src/app.module.ts
TypeOrmModule.forRootAsync({
name: 'center',
...,
synchronize: false, // ← ไม่ auto-sync
logging: ['error', 'query']
})TypeORM อ่าน entity definitions แต่ไม่ alter schema ตาม — schema ของ HRMI เป็นของ Chula แก้ไม่ได้
ผลกระทบ — centralize-api ต้องอ่าน schema ตามที่ HRMI กำหนด แม้ว่า entity จะไม่ตรงก็ตาม
Schema drift risk
ถ้า model definition ใน code ไม่ตรงกับ schema จริงใน DB:
| กรณี | ผลกระทบ |
|---|---|
| เพิ่ม field ใน code, แต่ DB ยังไม่มี | query SELECT → error "Unknown column" |
| เพิ่ม field ใน DB, แต่ code ยังไม่มี | query ส่งค่า default → ข้อมูลใหม่ใน DB ไม่ถูกอ่าน |
| เปลี่ยน type ใน code, DB ยังเดิม | type cast error หรือ silent truncate |
| เปลี่ยน ENUM ใน code, DB ยังเดิม | INSERT ค่าใหม่ → error "Data truncated for column" |
เพราะไม่มี migration ที่บังคับ — drift สะสมอย่างเงียบ จนเจอตอน runtime แล้ว error
ทำไมไม่ใช้ migration tool
เดาได้ว่าทีมตัดสินใจแบบนี้เพราะ:
- "อยาก deploy เร็ว" — ไม่ต้องรัน migration script ก่อน deploy
- "กลัว migration พัง" — migration ที่ fail กลางคันทำให้ DB ค้าง
- "ไม่คุ้นเคย" — Sequelize CLI ต้องเรียนรู้ additional
แต่ trade-off คือ risk ที่สะสม — schema drift, audit ไม่ได้, rollback ยาก, multi-environment ไม่ reproducible
สิ่งที่ควรจะเป็น (modernization target)
| ปัจจุบัน | ควรจะเป็น |
|---|---|
syncModelToTable() auto-sync | Migration tool เช่น sequelize-cli, umzug, หรือ golang-migrate |
Hardcoded syncModels array | Auto-discover จาก model registry |
Manual ALTER TABLE ผ่าน phpMyAdmin | Migration files ที่ review ได้, commit ใน git |
| ไม่มี rollback | Migration ที่มี up + down |
| Default data seeding inline | Seed files แยกต่างหาก, รันเฉพาะตอน setup ครั้งแรก |
เคล็ดลับตอนทำงานจริง
ถ้าเพิ่ม column แล้ว query fail "Unknown column"
- เช็กใน DB จริงว่า column ขึ้นไหม —
DESCRIBE TableName - ถ้าไม่ขึ้น → manual
ALTER TABLE TableName ADD COLUMN ... - แล้วเพิ่มชื่อ model ใน
syncModelsarray (ถ้ายังไม่มี)
ถ้า boot ช้า
syncModels มี ~80 entries แต่ละ entry ใช้ await ... .sync() — ถ้ามีหลาย model ตารางใหม่ boot จะช้ามาก ปกติ sync ที่ตารางมีอยู่แล้วเสร็จในไม่กี่ ms
อย่า deploy model ใหม่ตอน traffic สูง
boot ครั้งแรกของ replica ใหม่จะ sync ทุก model — ถ้าตารางใหญ่ (เช่น LeaveDocument ที่มีล้าน row) sync check จะช้า
อย่าใช้ force: true
ถ้าเปลี่ยนจาก force: false เป็น force: true → ลบข้อมูลทั้งหมด ทุกตารางใน syncModels