9.9 · Identity across databases
ปัญหาใหญ่อันดับต้นของ VTRC data layer — "User" มีหลายนิยาม ข้าม 3 services และ 3 databases ไม่มี canonical employee ID เดียวที่ใช้ได้ทุกที่
บทนี้ map ความสัมพันธ์ของ identity ทั้งหมด + อธิบายว่าทำไม join ข้าม service ถึงยาก
3 model สำหรับแนวคิด "User"
| # | Service | Database | Table | PK | Type | Format |
|---|---|---|---|---|---|---|
| 1 | vtrc-api | vtrc-centralize (MariaDB) | Users | userId | UUID | 550e8400-e29b-41d4-a716-446655440000 |
| 2 | centralize-api | dbHRMI_Center (MSSQL) | auth_user | userId | UUID | same format |
| 3 | centralize-api | dbHRMI_Center (MSSQL) | emEmplGrup_reStructure และ siblings | EmplGrupID / EmpID | STRING(36) | string UUID-as-string |
| 4 | cu-central-api | MSSQL | Employee | EmpID (PK) + EmpCode | STRING(36) / STRING(50) | EmpID = stable PK; EmpCode = business code |
(ไม่ใช่ 3 จริง — มีทั้ง EmpID stable PK และ EmpCode business key)
Identity graph
┌─────────────────────────────────────────────────────────────┐
│ vtrc-centralize.Users (MariaDB) │
│ ├── userId (UUID) │
│ ├── personId (UUID — secondary identity) │
│ ├── param (STRING — cache ของ EmpCode) │
│ ├── refId (TEXT — link ไป HRMI EmpCode) │
│ └── sourceDb (STRING — HRMI SourceDB เช่น `dbHRMI_*_rep`; ไม่ใช่ `'vtrc'\|'nbc'`) │
└────────────────┬───────────────────────────────────────────┘
│
│ refId === EmpCode (string match)
▼
┌─────────────────────────────────────────────────────────────┐
│ dbHRMI_Center (MSSQL) — owned by HRMI/Chula │
│ │
│ emEmplGrup_reStructure │
│ ├── EmplGrupID (PK, STRING 36) │
│ ├── EmplGrupCode (STRING 50 — business code) │
│ ├── EmplGrupName (STRING 50) │
│ ├── IsDeleted / IsInactive (BOOLEAN) │
│ └── SourceDB (STRING 50) │
│ │
│ emEmplLevel_reStructure │
│ ├── EmplLevelID (PK, STRING 36) │
│ └── EmplLevelCode / EmplLevelName │
│ │
│ (Employee + slip + time attendance tables) │
│ │
└─────────────────────────────────────────────────────────────┘
▲
│ userId === auth_user.userId (UUID match)
│
┌────────────────┴───────────────────────────────────────────┐
│ dbHRMI_Center.auth_user (MSSQL) — owned by centralize-api │
│ ├── userId (PK, UUID) │
│ ├── userName (STRING — login name) │
│ ├── password (STRING — hashed) │
│ ├── email / firstName / lastName │
│ ├── userLevel (UUID — link ไป EmplLevel) │
│ └── forgetPassword (UUID — token) │
└─────────────────────────────────────────────────────────────┘Mapping — ทำไมถึงยาก
vtrc-api → HRMI
vtrc-centralize.Users.refId (TEXT) เก็บค่า EmpCode (string) ของ HRMI
refId: {
type: Sequelize.TEXT,
allowNull: true
},ใช้ตอน query "user คนนี้มีเงินเดือนเท่าไหร่":
1. vtrc-api อ่าน User.refId → ได้ EmpCode
2. vtrc-api เรียก cu-central-api (ผ่าน centralize bridge) → "fetch payslip by EmpCode"
3. cu-central-api query HRMI WHERE EmpCode = ?
4. ได้ slip data กลับมาปัญหา:
refIdเป็น TEXT ไม่ enforce format → อาจจะมีค่าผิดได้- ถ้า EmpCode เปลี่ยน (พนักงานย้ายสังกัด)
refIdค้างที่ค่าเดิม → slip query ไม่เจอ - ถ้า EmpCode ซ้ำใน HRMI (ข้อมูลผิด) → vtrc จะ merge คนละคนเป็นคนเดียวโดยไม่ได้ตั้งใจ
vtrc-api → cu-central-api auth
vtrc-centralize.Users.userId (UUID) อาจจะตรงกับ identity ฝั่ง cu-central — แต่ไม่ guarantee
ปัญหา — UUID generate ฝั่ง vtrc ตอน register, ฝั่ง HR/cu-central มี identity ของตัวเอง ไม่มี mechanism ที่ sync ทั้งสอง → UUID อาจจะไม่ตรงกัน
NestJS
centralize-apiก็มีauth_userentity แต่เป็น dead code และ caller จาก vtrc-api ยังไม่ยืนยัน — ดู Volume 5.9
HRMI UUID เป็น string ไม่ใช่ native UUID
@PrimaryColumn({ name: 'EmplGrupID', length: 36 })
emplGrupID: string;TypeORM declare เป็น string length 36 — เก็บ UUID ในรูปแบบ string ไม่ใช่ native UUID type ของ MSSQL (uniqueidentifier)
ผลกระทบ:
- index size ใหญ่กว่า native UUID
- performance ตอน join ช้ากว่า
- ถ้าย้ายไปใช้ framework อื่น (เช่น Go +
uuid.UUID) ต้อง cast string → UUID
sourceDb field — multi-tenant flag
sourceDb: {
type: Sequelize.STRING,
allowNull: true,
}sourceDb เป็น STRING ว่างได้ — ไม่ใช่ enum ปิด 'vtrc' | 'nbc'
ค่าจริงที่โค้ดอ้างเป็น ชื่อ SourceDB ของ HRMI เช่น:
dbHRMI_CU_SC_rep— ดูการเปรียบเทียบในcu-central-api/main-api/src/resolvers/payrollSlip.js(workingProfile.sourceDb === 'dbHRMI_CU_SC_rep')dbHRMI_RC_B— ตัวอย่างที่ frontend ส่งในบางหน้า (เช่น recruit dashboard)
ค่านี้มาจากโปรไฟล์ HR (currentProfile มักเป็นรูปแบบ empId:sourceDb) แล้วถูกเก็บลง Users.sourceDb / ใช้ตอน query ข้ามระบบ
@Column({ name: 'SourceDB', length: 50, nullable: true })
sourceDB?: string;ฝั่ง HRMI / TypeORM ก็มีฟิลด์ SourceDB เป็น string ความยาวจำกัด — sync กันในเชิงความหมาย (tenant/source ของระเบียน) ไม่ใช่รหัสสั้น vtrc/nbc
ผลกระทบ — query HR data ต้องส่ง sourceDb ที่ถูกต้องตามเรคอร์ดพนักงาน (เช่น dbHRMI_*_rep) ไม่ใช่สมมติแค่สองค่า vtrc/nbc
3 PK strategies
| Strategy | ใช้ที่ไหน | ข้อดี | ข้อเสีย |
|---|---|---|---|
Native UUID (Sequelize.UUID) | vtrc-api MariaDB | index เล็ก, fast compare | ไม่ human-readable |
| UUID-as-string (STRING 36) | centralize-api MSSQL | portable ข้าม engine | index ใหญ่, slower |
| Business code (EmpCode string) | HRMI MSSQL | human-readable | เปลี่ยนได้ตาม org policy |
ผลกระทบตอน query
"ดูข้อมูล user คนนี้ทั้งหมด"
ต้อง join ข้าม 3 database ที่ไม่มี cross-DB relation:
Step 1: vtrc-centralize.Users → refId (EmpCode)
Step 2: cu-central-api (live bridge) → fetch Employee by EmpCode
Step 3: cu-central-api → fetch PaySlip by EmpCode
Step 4: cu-central-api → fetch TimeAttendance by EmpCode
Step 5: vtrc.LeaveDocument → WHERE empCode = ?
Step 6: vtrc.WDHospital → WHERE empCode = ?หมายเหตุ: NestJS centralize-api มี REST ที่อ่าน HRMI ได้เช่นกัน แต่ caller จาก vtrc-api ใน workspace นี้ยังไม่ยืนยัน — live path คือ END_POINT_CENTRALIZE → cu-central-api
ทุก step เป็น network call → latency สะสม
"audit ว่า user คนนี้เคยทำอะไรบ้าง"
เพราะทุก domain เก็บ empCode แยก → audit log กระจายอยู่ 6+ table
ถ้าพนักงานย้ายกอง EmpCode เปลี่ยน → historical data ยังอยู่ใน code เดิม แต่ user ปัจจุบันไม่เห็น
"ลบ user แล้ว data ตามหรือเปล่า"
ไม่มี cascade — ถ้าลบ Users row ที่ vtrc-centralize:
Session.userIdค้าง (orphan)LeaveDocument.empCodeยังอยู่ (แต่ user ไม่มา login ได้แล้ว)- HRMI ไม่กระทบ (VTRC ไม่เคยลบ HRMI data)
canonical employee ID — ไม่มี
VTRC ไม่มี canonical employee ID ที่ stable ข้าม service — ทุก reference เป็น "best effort mapping":
| Field | Stable? | เปลี่ยนได้ไหม |
|---|---|---|
vtrc.User.userId (UUID) | stable | ไม่เปลี่ยน |
vtrc-centralize.Users.refId (EmpCode) | semi-stable | เปลี่ยนตอนย้ายกอง |
HRMI EmpID (STRING(36) PK) | stable | ไม่เปลี่ยน |
HRMI EmpCode (business STRING(50)) | unstable | เปลี่ยนตาม org policy |
modernization target
สิ่งที่ควรจะเป็น:
| ปัจจุบัน | ควรจะเป็น |
|---|---|
| 3 PK strategies | 1 canonical employee ID (แนะนำ employeeId UUID ที่ stable) |
refId TEXT mapping | canonical ID relation ที่ enforce |
| EmpCode ใน domain tables | employeeId FK ที่ cascade |
| ไม่มี audit trail ข้าม domain | event sourcing / outbox pattern |
รายละเอียดอยู่ใน บท 8.12 และ Volume 15 (Modernization)
เคล็ดลับตอนทำงานจริง
ถ้าได้ "user not found" ตอน query HRMI data
- เช็ก
vtrc.User.refId— ถ้า null/empty → map ไม่สำเร็จ - เช็ก
sourceDb— ใช้ค่า SourceDB จริงจากโปรไฟล์ (เช่นdbHRMI_CU_SC_rep) เมื่อ query/branch ตามต้นทาง HRMI; NestJS connection ชื่อnbcชี้dbHRMI_Center_NBCเป็นคนละชั้นกับสตริงsourceDbบน Users
ถ้า slip ไม่ตรงกับ user
- EmpCode อาจจะเปลี่ยน → query HRMI ด้วย
EmpID(stable) แทน - refId อาจจะเก่า → manual sync
อย่า trust UUID match ข้าม vtrc ↔ centralize auth_user
UUID อาจจะไม่ตรงกัน → ใช้ flow verify ผ่าน login แทน
ถ้าจะ implement "ลบ user"
ต้อง manual cascade — Session, LeaveDocument, WDHospital, Notification, etc. ที่ reference user ต้อง update/cleanup ทีละอัน