9.1 · Persistence topology — 4 logical databases
ตั้งแต่แรกเห็น topology ของ VTRC จะสับสน — "แอปเดียวแต่มี database สี่อัน?" ใช่ครับ สี่อัน ข้ามสอง engine และเป็นเจ้าของโดยสาม service ที่ต่างกัน
ภาพรวม 4 databases
| # | Database | Engine | Owner | Connection | Role |
|---|---|---|---|---|---|
| DB-1 | vtrc | MariaDB | vtrc-api | db | application state หลัก (users, sessions, roles, leave, withdraw, fund, notification, post, master data) |
| DB-2 | vtrc-centralize | MariaDB | vtrc-api | db2 | cross-schema join tables (โดยเฉพาะ centralize/user ที่ bridge ไป HRMI) |
| DB-3 | dbHRMI_Center | MSSQL | centralize-api (TypeORM) + cu-central-api (Sequelize) | center / db | HR master data — Employee, EmployeeGroup, PaySlip, TimeTemp |
| DB-4 | dbHRMI_Center_NBC | MSSQL | centralize-api | nbc | NBC-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
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_*)
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_SCHEMA | env var ที่ตั้งเป็น mariadb แต่ชื่อไม่สื่อ — ควรเป็น DB_DIALECT |
useUTC: false | เก็บ timestamp ตาม timezone ของ server (Asia/Bangkok) — ถ้าย้าย server ไป timezone อื่น timestamp จะเพี้ยน |
timezone: TIME_ZONE | Asia/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_NBCEntity ระบุ database โดยตรง ใน decorator
@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 / Roles | vtrc-api | vtrc-api | เขียน / อ่านเองทั้งคู่ |
| Leave / Withdraw / Fund | vtrc-api | vtrc-api | application state เต็มตัว |
| centralize User bridge | vtrc-api | vtrc-api | cross-schema join table |
| Employee / EmployeeGroup | HRMI (Chula) | centralize-api / cu-central-api | VTRC ไม่เคยเขียน HRMI |
| PaySlip (uvi3PaySlipHD/DT) | HRMI | centralize-api | เป็น SQL Server VIEW ไม่ใช่ตารางจริง |
| TimeTemp (shift/time) | HRMI | centralize-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_usertable, MSSQL)cu-central-api/models/hrmi/employee/employee.js—EmpIDstring 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