Skip to content

9.4 · Schema management + syncModelToTable

VTRC ไม่มี migration tool — ไม่มี sequelize-cli, ไม่มี golang-migrate, ไม่มี flyway ใช้วิธี auto-sync ทุกครั้งที่ boot บทนี้เล่าว่ามันทำงานอย่างไร และทำไมถึงเป็น risk ใหญ่


กลไก — syncModelToTable()

101:135:vtrc-api/api/src/lib/model.js
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
}

วิธีอ่าน

  1. รันตอน index.js:104 หลัง db.authenticate() สำเร็จ
  2. Loop ผ่าน syncModels (hardcoded list ของชื่อ model ที่จะ sync)
  3. เรียก Models[modelName].sync({ force: false }) — Sequelize จะ:
    • ถ้าตารางไม่มี → CREATE TABLE
    • ถ้าตารางมีแล้ว → ไม่ทำอะไร (เพราะ force: false ไม่ได้ใช้ alter: true)
  4. ถ้ามี default data (ใน defaultData import) → 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 — ปัญหาอีกชั้น

17:99:vtrc-api/api/src/lib/model.js
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

5:15:vtrc-api/api/src/lib/model.js
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

113:124:vtrc-api/api/src/lib/model.js
        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

javascript
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

เดาได้ว่าทีมตัดสินใจแบบนี้เพราะ:

  1. "อยาก deploy เร็ว" — ไม่ต้องรัน migration script ก่อน deploy
  2. "กลัว migration พัง" — migration ที่ fail กลางคันทำให้ DB ค้าง
  3. "ไม่คุ้นเคย" — Sequelize CLI ต้องเรียนรู้ additional

แต่ trade-off คือ risk ที่สะสม — schema drift, audit ไม่ได้, rollback ยาก, multi-environment ไม่ reproducible


สิ่งที่ควรจะเป็น (modernization target)

ปัจจุบันควรจะเป็น
syncModelToTable() auto-syncMigration tool เช่น sequelize-cli, umzug, หรือ golang-migrate
Hardcoded syncModels arrayAuto-discover จาก model registry
Manual ALTER TABLE ผ่าน phpMyAdminMigration files ที่ review ได้, commit ใน git
ไม่มี rollbackMigration ที่มี up + down
Default data seeding inlineSeed files แยกต่างหาก, รันเฉพาะตอน setup ครั้งแรก

เคล็ดลับตอนทำงานจริง

ถ้าเพิ่ม column แล้ว query fail "Unknown column"

  • เช็กใน DB จริงว่า column ขึ้นไหม — DESCRIBE TableName
  • ถ้าไม่ขึ้น → manual ALTER TABLE TableName ADD COLUMN ...
  • แล้วเพิ่มชื่อ model ใน syncModels array (ถ้ายังไม่มี)

ถ้า 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


ไป 8.5 ERD — Identity & Access