9.11 · Data integrity risks
บทนี้รวม risk ที่เกี่ยวกับ data integrity — cross-schema write, soft-delete inconsistency, PII columns ที่ไม่ encrypt, master data drift, type mismatch
เป็น chapter ที่ ops / DBA / security ควรอ่านก่อน maintain ระบบ
Risk 1 — Cross-schema write ไม่มี transactional boundary
vtrc-api write ทั้ง db (vtrc schema) และ db2 (vtrc-centralize schema) — แต่ Sequelize ไม่ cross-database transaction ได้
flow ตัวอย่าง:
1. INSERT vtrc-centralize.Users (...) ← commit แล้ว
2. INSERT vtrc.Session (...) ← ถ้า fail ตรงนี้
3. INSERT vtrc.LoginSession (...) ← ไม่ได้ทำ
ผล: Users row ค้างอยู่ แต่ไม่มี Session → user "exist" แต่ login ไม่ได้ขอบเขต
flow ที่เกี่ยม cross-schema write:
- user registration / migration
- login (create Session + update Users.token)
- profile update (UserDivision + audit log)
วิธีแก้ปัจจุบัน
controller ใช้ try/catch + manual cleanup ในบางจุด แต่ไม่ครบทุก flow
สิ่งที่ควรจะเป็น
- saga pattern (outbox + compensating action)
- หรือทำให้ทุก write อยู่ใน schema เดียวกัน (consolidate)
Risk 2 — Soft-delete inconsistency
บาง model มี isDeleted (BOOLEAN), บาง model มี isActive (BOOLEAN/INTEGER), บาง model ไม่มีเลย
| Model | Field | Type |
|---|---|---|
Hospital | isDeleted | BOOLEAN |
MasterApprover | isActive | BOOLEAN |
Role | isActive | BOOLEAN |
StudyLeaveRequest | isDeleted + isCancel | BOOLEAN × 2 |
Employee (HRMI) | IsDeleted + IsInactive | BOOLEAN × 2 |
LeaveDocument | (ไม่มี — ใช้ status CANCEL) | - |
WDHospital | (ไม่มี — ใช้ status CANCEL) | - |
Session | (ไม่มี — ใช้ expireDate) | - |
ผลกระทบ:
- query "active records เท่านั้น" ต้องเขียนต่างกันทุก model
- บาง model ใช้สอง flag (
isDeleted+isCancel) → logic ซับซ้อน - ไม่มี default scope ใน Sequelize → dev ลืม filter ได้
Risk 3 — PII columns ไม่มี encryption at rest
PII (Personally Identifiable Information) ที่เก็บเป็น plaintext ใน DB:
| Column | Type | Sensitivity |
|---|---|---|
Users.token | TEXT | JWT ปัจจุบัน |
Users.refreshToken | TEXT | refresh token |
Users.clientInfo | TEXT | user agent / IP |
Users.refId | TEXT | EmpCode |
WDHospital.withdrawIdCard | STRING | บัตรประชาชน 13 หลัก |
WDHospital.empCode | STRING | รหัสพนักงาน |
Session.tokenCD | TEXT | Chula SSO token |
Session.refreshTokenCD | STRING | Chula refresh token |
auth_user.password | STRING | hashed (ดี) |
ถ้า database dump รั่ว → PII ทั้งหมดอ่านได้ทันที
สิ่งที่ควรจะเป็น
- column-level encryption สำหรับ PII (เช่น
withdrawIdCardควร encrypt) - หรือใช้ MariaDB's
AES_ENCRYPT()function - หรือใช้ field-level encryption ใน ORM layer
Risk 4 — Money type audit
field เงินทั้งหมดควรเป็น DECIMAL ตรวจสอบครบแล้ว → ผ่าน
| Domain | Field | Type | ผ่าน |
|---|---|---|---|
| Withdraw | totalWithdraw / totalCurrentWithdraw / previousCurrentWithdraw | DECIMAL(12,2) | ใช่ |
| Slip | totalPrice / totalSlipWithdraw | DECIMAL(12,2) | ใช่ |
| Export | totalCurrentWithdraw | DECIMAL(12,2) | ใช่ |
| Study | empSalary | DECIMAL(12,2) | ใช่ |
ข้อดี — ไม่มี precision error แบบ FLOAT
Risk 5 — Master data drift ระหว่าง Center กับ NBC
HRMI มีสอง database — dbHRMI_Center และ dbHRMI_Center_NBC — เก็บ entity เดียวกันแบบ parallel
dbHRMI_Center.emEmplGrup_reStructure EmplGrupID = 'abc-123'
dbHRMI_Center_NBC.emEmplGrup_reStructure EmplGrupID = 'abc-123' (mirror?)ถ้า HRMI update Center แต่ลืม sync NBC → ผู้อ่านข้อมูลผ่าน NestJS center vs nbc connection (หรือผ่าน cu-central ตาม sourceDb จริง เช่น dbHRMI_*_rep) อาจได้ผลต่างกัน
VTRC ไม่เป็นเจ้าของ sync ระหว่าง Center ↔ NBC → ไม่สามารถ enforce ได้
Risk 6 — JSON-encoded fields ไม่ enforce schema
| Column | Type | เก็บอะไร |
|---|---|---|
Users.role | TEXT (JSON via getter/setter) | ["ADMIN","STAFF"] |
Session.userSession | TEXT (JSON via getter/setter) | object ที่ cache ฝั่ง client |
WDExport.wdHospitalIds | TEXT (manual JSON) | ["uuid1","uuid2"] |
WDHistory.payload | JSON | object |
ผลกระทบ:
- ไม่มี schema validation → ใส่อะไรก็ได้
- parse error ถ้า JSON corrupt → runtime crash
- query field ข้างใน JSON ไม่ได้โดยตรง (MySQL มี
JSON_EXTRACTแต่ Sequelize wrapper ไม่ได้ใช้)
Risk 7 — Duplicated columns ที่ silent override
WDHospital.coaCodeName และ WDHospital.isJuristic declare สองครั้งในไฟล์เดียวกัน (ดู บท 8.7)
Sequelize merge attribute declarations — declaration ตัวหลังชนะ → ตัวแรกหายไปอย่างเงียบ
ถ้า declaration ทั้งสองเหมือนกัน → ไม่มีผล ถ้าต่างกัน (เช่น allowNull ต่างกัน) → behavior ผิดจากที่คาด
Risk 8 — Type mismatch ข้าม relation
| Column | Type ที่ใช้ | Type ที่ควรจะเป็น |
|---|---|---|
MasterApprover.userId | STRING | UUID |
FundRequest.userId | STRING | UUID |
FundRequest.createdBy | STRING | UUID |
MasterApprover.createdBy | STRING | UUID |
HRMI EmplGrupID | STRING(36) | native UUID (uniqueidentifier) |
ผลกระทบ:
- index ใหญ่กว่าที่ควร
- join ข้าม type ต้อง cast → slow
- ถ้ามี typo ใน UUID → silent fail (ไม่ validate format)
Risk 9 — syncModelToTable schema drift
(รายละเอียดใน บท 8.4)
- ไม่มี migration → ถ้า dev ลืม manual ALTER → field ใน code มี แต่ field ใน DB ไม่มี
- ไม่มี rollback → ถ้าแก้ผิด ต้อง manual กลับ
- multi-env ไม่ reproducible → dev / UAT / prod schema อาจจะต่างกัน
Risk 10 — expireDate ไม่ enforce
Session.expireDate เป็น DATE แต่ไม่มี scheduled cleanup job → session หมดอายุแล้วยังค้างใน DB
ผลกระทบ:
- table โตเรื่อยๆ (ตาม user login)
- query
WHERE expireDate > NOW()ต้อง scan หลาย row - privacy — session data (รวม token) ค้างนานเกินจำเป็น
Risk 11 — Logical FK ไม่ enforce
ทุก relation ข้าม schema เป็น logical FK (ไม่มี DB constraint) — เช่น Session.userId ไม่มี FK ไป Users.userId
ผลกระทบ:
- ถ้า user ถูกลบ → session ยังค้าง (orphan)
- migration / cleanup ต้อง manual cascade
- referential integrity ไม่ guaranteed
Risk 12 — Hardcoded business UUID
KEY_PLASMA = '47837e2d-7788-4a1a-8e24-a90abf6742f4',
KEY_HR = ['305A814A-CEAC-449B-BF92-CCAEC3475B1A', '5A764C0F-6C0D-46C5-83E0-9B94C755BFE2'],business key ติดอยู่ใน code → ถ้า org structure เปลี่ยนต้องแก้ code + deploy
ผลกระทบ:
- UUID เฉพาะที่ติดกับ environment (prod / UAT อาจจะต่างกัน)
- ถ้า import config ผิด environment → business logic พัง
Summary — severity matrix
| Risk | Severity | Blast radius | ทางแก้ |
|---|---|---|---|
| 1 Cross-schema write | สูง | user data inconsistent | saga / consolidate |
| 2 Soft-delete inconsistency | กลาง | query ผิด | มี default scope |
| 3 PII plaintext | สูง | data leak | column encryption |
| 5 Master drift Center ↔ NBC | กลาง | query ผิดบางพนักงาน | sync script |
| 6 JSON fields | กลาง | corrupt JSON crash | JSON schema validation |
| 7 Duplicated columns | ต่ำ | silent bug | code review |
| 8 Type mismatch | ต่ำ | slow query | migration type cast |
| 9 syncModelToTable drift | สูง | schema mismatch | migration tool |
| 10 expireDate ไม่ cleanup | กลาง | table โต + privacy | cron cleanup |
| 11 Logical FK | กลาง | orphan rows | add FK constraint |
| 12 Hardcoded UUID | ต่ำ | ติด env | env var |
รายละเอียด scorecard อยู่ใน บท 8.12
เคล็ดลับตอนทำงานจริง
ก่อน deploy schema change
- เช็กว่ามี JSON field ที่กระทบไหม
- เช็กว่า migration script รันบน dev / UAT / prod ครบไหม
- backup DB ก่อน manual ALTER
ถ้าเจอ data inconsistent ใน production
- อย่าลบ row ที่ orphan ทันที — backup ก่อน
- เช็ก audit log ก่อนว่า row นั้นเคยใช้ไหม
- แก้ที่ root cause (transaction bug ฯลฯ) ไม่ใช่แค่ patch ข้อมูล
ห้าม trust logical FK
query ที่ join logical FK ต้อง handle null / not found case เอง