Skip to content

9.1 · Persistence topology — 4 logical databases

ตั้งแต่แรกเห็น topology ของ VTRC จะสับสน — "แอปเดียวแต่มี database สี่อัน?" ใช่ครับ สี่อัน ข้ามสอง engine และเป็นเจ้าของโดยสาม service ที่ต่างกัน


ภาพรวม 4 databases

#DatabaseEngineOwnerConnectionRole
DB-1vtrcMariaDBvtrc-apidbapplication state หลัก (users, sessions, roles, leave, withdraw, fund, notification, post, master data)
DB-2vtrc-centralizeMariaDBvtrc-apidb2cross-schema join tables (โดยเฉพาะ centralize/user ที่ bridge ไป HRMI)
DB-3dbHRMI_CenterMSSQLcentralize-api (TypeORM) + cu-central-api (Sequelize)center / dbHR master data — Employee, EmployeeGroup, PaySlip, TimeTemp
DB-4dbHRMI_Center_NBCMSSQLcentralize-apinbcNBC-grouped HR mirror (เงาข้อมูล HRMI สำหรับกลุ่มพนักงาน NBC)

ที่ vtrc-api เห็นแค่ DB-1 กับ DB-2 โดยตรง — DB-3 / DB-4 วิ่งผ่าน cu-central-api ผ่าน centralize bridge (ดู Volume 3.4)

┌──────────────────────────────────────────────────────────────┐
│ vtrc-api (Node.js + Sequelize)                                │
│ ┌─────────────────────────────┐  ┌──────────────────────────┐ │
│ │ db   → vtrc       (MariaDB) │  │ db2  → vtrc-centralize   │ │
│ │      application state      │  │      (MariaDB) bridge    │ │
│ └─────────────────────────────┘  └──────────────────────────┘ │
└──────────────────┬───────────────────────────────────────────┘
                   │ REST/GraphQL

┌──────────────────────────────────────────────────────────────┐
│ centralize-api (NestJS + TypeORM) + cu-central-api            │
│ ┌─────────────────────────────┐  ┌──────────────────────────┐ │
│ │ center → dbHRMI_Center (MSSQL) │  │ nbc  → dbHRMI_Center_NBC (MSSQL) │ │
│ │      HR master data         │  │      NBC mirror           │ │
│ └─────────────────────────────┘  └──────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

DB-1 + DB-2 — connector.js

vtrc-api สร้าง Sequelize instance สองตัวที่ connector.js

8:27:vtrc-api/api/src/connector.js
const db = new Sequelize(DB_NAME, DB_USERNAME, DB_PASSWORD, {
  host: DB_HOST,
  port: DB_PORT,
  dialect: DB_SCHEMA,
  dialectOptions: {
    useUTC: false,
    // timezone: TIME_ZONE
  },
  timezone: TIME_ZONE,
  define: {
    charset: 'utf8mb4',
    collate: 'utf8mb4_unicode_ci'
  },
  pool: {
    max: 100,
    min: 0,
    acquire: 60000,
    idle: 10000
  }
})

DB-2 (db2) มี config เหมือนกันทุกประการ ต่างแค่ env var (DB2_*)

29:48:vtrc-api/api/src/connector.js
const db2 = new Sequelize(DB2_NAME, DB2_USERNAME, DB2_PASSWORD, {
  host: DB2_HOST,
  port: DB2_PORT,
  dialect: DB_SCHEMA,
  dialectOptions: {
    useUTC: false,
    // timezone: TIME_ZONE
  },
  timezone: TIME_ZONE,
  define: {
    charset: 'utf8mb4',
    collate: 'utf8mb4_unicode_ci'
  },
  pool: {
    max: 100,
    min: 0,
    acquire: 60000,
    idle: 10000
  }
})

สิ่งที่ควรสังเกตใน config

ค่าผลกระทบ
dialect: DB_SCHEMAenv var ที่ตั้งเป็น mariadb แต่ชื่อไม่สื่อ — ควรเป็น DB_DIALECT
useUTC: falseเก็บ timestamp ตาม timezone ของ server (Asia/Bangkok) — ถ้าย้าย server ไป timezone อื่น timestamp จะเพี้ยน
timezone: TIME_ZONEAsia/Bangkok — sync กับ useUTC: false
charset: 'utf8mb4'รองรับภาษาไทย + emoji (ถ้าใช้ utf8 แทนจะเก็บภาษาไทยได้ แต่ emoji พัง)
pool.max: 100สูงผิดปกติ — default Sequelize คือ 5; 100 connection ต่อ instance × 2 instance × 4 replica = 800 connection ไปที่ MariaDB
pool.acquire: 60000 (60s)ถ้า pool เต็ม จะรอ 60 วินาทีก่อน throw TimeoutError — request ที่ใช้ DB จะค้างนานก่อน fail
pool.idle: 10000 (10s)connection ที่ไม่ใช้ 10 วินาที → ปิด

Pool sizing — ตัวเลขที่ควนระวัง

pool.max: 100 × 2 connection (db + db2) × 4 replica (APP_NO 1, 2, 3, 5) = 800 connection สูงสุดไปที่ MariaDB

MariaDB default max_connections คือ 151 — ถ้าทุก replica ใช้ pool เต็มพร้อมกัน → connection หมด → query fail หมด

ปกติไม่เจอเพราะ traffic ไม่ได้พุ่งพร้อมกันจนเต็ม pool แต่ตอน batch job รัน (cron statLog ที่ตี 00:30) อาจจะใกล้ limit


DB-3 + DB-4 — MSSQL ฝั่ง centralize

centralize-api เป็น NestJS ที่ใช้ TypeORM เชื่อม MSSQL สองตัว

centralize-api/src/app.module.ts
├── TypeOrmModule.forRootAsync({ name: 'center' }) → dbHRMI_Center
└── TypeOrmModule.forRootAsync({ name: 'nbc' })    → dbHRMI_Center_NBC

Entity ระบุ database โดยตรง ใน decorator

3:6:centralize-api/src/entities/employeeGroup.entity.ts
@Entity({ name: 'emEmplGrup_reStructure', database: 'dbHRMI_Center' })
export class EmployeeGroup {
    // ...
}

Entity ตัวเดียวกันมีสอง variant — employeeGroup.entity.ts (Center) กับ employeeGroup.nbc.entity.ts (NBC) — เพื่อ mirror ข้อมูลในสอง physical database

ทำไมต้องมี NBC mirror

พนักงานกลุ่ม NBC (ส่วนงานที่ไม่ใช่ส่วนกลาง) มี data store แยกใน HRMI ฝั่ง Chula — ทำให้ต้อง query จาก database ที่ถูกต้องตามกลุ่มพนักงาน

centralize-api เลือก connection ตาม EmpGroupID ของพนักงานแต่ละคน — logic อยู่ที่ service layer ของ centralize


ใครเป็นเจ้าของอะไร — ownership matrix

ข้อมูลOwner (write)Reader (read)หมายเหตุ
Users / Sessions / Rolesvtrc-apivtrc-apiเขียน / อ่านเองทั้งคู่
Leave / Withdraw / Fundvtrc-apivtrc-apiapplication state เต็มตัว
centralize User bridgevtrc-apivtrc-apicross-schema join table
Employee / EmployeeGroupHRMI (Chula)centralize-api / cu-central-apiVTRC ไม่เคยเขียน HRMI
PaySlip (uvi3PaySlipHD/DT)HRMIcentralize-apiเป็น SQL Server VIEW ไม่ใช่ตารางจริง
TimeTemp (shift/time)HRMIcentralize-apiเช่นกัน

กฎเหล็ก — VTRC ไม่เคย write กลับไปที่ HRMI ทุก write ไปที่ vtrc หรือ vtrc-centralize เท่านั้น


ปัญหาของ topology นี้

1. Cross-schema write ที่ไม่มี transactional boundary

vtrc-api write ทั้ง db และ db2 — แต่ Sequelize ไม่มี cross-database transaction (เพราะเป็น connection คนละตัว)

ถ้า flow นึง write ที่ db แล้ว write ที่ db2 ต่อ — ถ้า db2 fail ข้อมูลใน db ค้างอยู่ (orphan)

ดูรายละเอียดเพิ่มใน บท 8.11 Data integrity risks

2. Schema drift ระหว่าง entity กับ database จริง

vtrc-api ใช้ syncModelToTable() รันทุก boot (auto-sync) — ทำให้ code เป็น source of truth, database ตาม centralize-api ตั้ง synchronize: false — ทำให้ database เป็น source of truth, code ตาม

ถ้า dev แก้ entity ใน vtrc-api แล้ว deploy → schema เปลี่ยนอัตโนมัติ โดยไม่มี migration review

3. สาม model สำหรับแนวคิด "User" เดียวกัน

  • vtrc-api/models/centralize/user/user.js — UUID PK (MariaDB)
  • centralize-api/entities/user.entity.ts — UUID PK (auth_user table, MSSQL)
  • cu-central-api/models/hrmi/employee/employee.jsEmpID string PK (MSSQL)

map กันด้วย refId / EmpCode — ไม่มี canonical employee ID เดียว

ดูรายละเอียดใน บท 8.9 Identity across databases


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

ถ้าต้อง debug "ข้อมูลหาย"

เช็ก database ตามลำดับ ownership — ถ้า leave record หาย ดูที่ vtrc.LeaveDocument ก่อน (owner), ถ้า slip หาย ดูที่ dbHRMI_Center.uvi3PaySlipHD (HRMI)

ถ้า query ได้คนละคนกับที่คาด

อย่าลืมว่า identity map ผ่าน refId / EmpCode — UUID ของ vtrc ไม่ใช่ key เดียวกับ EmpCode ของ HRMI ต้อง join ผ่าน centralize/user ก่อน

ถ้าเจอ connection limit error

ER_CON_COUNT_ERROR — เช็ก SHOW PROCESSLIST ใน MariaDB ดูว่ามี connection ค้างกี่ตัว ปกติเกิดจาก pool.max: 100 ตั้งสูงเกินไป + หลาย replica


ไป 8.2 Model registration + Models global