Skip to content

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 ไม่มีเลย

ModelFieldType
HospitalisDeletedBOOLEAN
MasterApproverisActiveBOOLEAN
RoleisActiveBOOLEAN
StudyLeaveRequestisDeleted + isCancelBOOLEAN × 2
Employee (HRMI)IsDeleted + IsInactiveBOOLEAN × 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:

ColumnTypeSensitivity
Users.tokenTEXTJWT ปัจจุบัน
Users.refreshTokenTEXTrefresh token
Users.clientInfoTEXTuser agent / IP
Users.refIdTEXTEmpCode
WDHospital.withdrawIdCardSTRINGบัตรประชาชน 13 หลัก
WDHospital.empCodeSTRINGรหัสพนักงาน
Session.tokenCDTEXTChula SSO token
Session.refreshTokenCDSTRINGChula refresh token
auth_user.passwordSTRINGhashed (ดี)

ถ้า 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 ตรวจสอบครบแล้ว → ผ่าน

DomainFieldTypeผ่าน
WithdrawtotalWithdraw / totalCurrentWithdraw / previousCurrentWithdrawDECIMAL(12,2)ใช่
SliptotalPrice / totalSlipWithdrawDECIMAL(12,2)ใช่
ExporttotalCurrentWithdrawDECIMAL(12,2)ใช่
StudyempSalaryDECIMAL(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

ColumnTypeเก็บอะไร
Users.roleTEXT (JSON via getter/setter)["ADMIN","STAFF"]
Session.userSessionTEXT (JSON via getter/setter)object ที่ cache ฝั่ง client
WDExport.wdHospitalIdsTEXT (manual JSON)["uuid1","uuid2"]
WDHistory.payloadJSONobject

ผลกระทบ:

  • ไม่มี 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

ColumnType ที่ใช้Type ที่ควรจะเป็น
MasterApprover.userIdSTRINGUUID
FundRequest.userIdSTRINGUUID
FundRequest.createdBySTRINGUUID
MasterApprover.createdBySTRINGUUID
HRMI EmplGrupIDSTRING(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

153:155:vtrc-api/api/src/config.js
  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

RiskSeverityBlast radiusทางแก้
1 Cross-schema writeสูงuser data inconsistentsaga / consolidate
2 Soft-delete inconsistencyกลางquery ผิดมี default scope
3 PII plaintextสูงdata leakcolumn encryption
5 Master drift Center ↔ NBCกลางquery ผิดบางพนักงานsync script
6 JSON fieldsกลางcorrupt JSON crashJSON schema validation
7 Duplicated columnsต่ำsilent bugcode review
8 Type mismatchต่ำslow querymigration type cast
9 syncModelToTable driftสูงschema mismatchmigration tool
10 expireDate ไม่ cleanupกลางtable โต + privacycron cleanup
11 Logical FKกลางorphan rowsadd FK constraint
12 Hardcoded UUIDต่ำติด envenv 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 เอง


ไป 8.12 Scorecard + modernization target