6.6 · Entities + persistence patterns
บทนี้เจาะลึก entities ของ benefit-api และ persistence pattern — โดยเฉพาะ paymentTransaction ซึ่งเป็น entity เอกภาพ (entity กลาง) ที่ embed employee snapshot เข้าไปเต็ม (denormalization สูง)
Entity registry — src/entities/
| Entity | Schema | Table | Role |
|---|---|---|---|
paymentTransaction.entity.ts | vtrc | WDHospitals | transaction หลัก (เบิกค่ารักษา) |
wdHospitals.entity.ts | vtrc | WDHospitals | mirror entity (same table) |
masterWelfareType.entity.ts | vtrc | MasterWelfareType | lookup |
masterApprovers.entity.ts | vtrc | MasterApprovers | lookup |
masterCOA.entity.ts | vtrc | MasterCOA | lookup |
masterFmis.entity.ts | vtrc | MasterFmis | lookup |
masterBank.entity.ts | vtrc | MasterBank | lookup |
wdExportPaymentTransaction.entity.ts | benefit | WDExportPaymentTransaction | export batch |
wdExportPaymentDetail.entity.ts | benefit | WDExportPaymentDetail | export detail |
hospitalsTaskResult.entity.ts | benefit | HospitalsTaskResult | cron task log |
studyLeave.entity.ts | benefit | StudyLeave | study leave request |
inheritance.entity.ts | benefit | Inheritance | inheritance request |
funeral.entity.ts | benefit | Funeral | funeral request |
approveDisaster.entity.ts | benefit | ApproveDisaster | disaster request |
disasterTaskResult.entity.ts | benefit | DisasterTaskResult | cron task log |
studyLeaveTaskResult.entity.ts | benefit | StudyLeaveTaskResult | cron task log |
inheritanceTaskResult.entity.ts | benefit | InheritanceTaskResult | cron task log |
funeralTaskResult.entity.ts | benefit | FuneralTaskResult | cron task log |
fileBankTransaction.entity.ts | benefit | FileBankTransaction | uploaded file metadata |
paymentTransaction.entity.ts — entity เอกภาพ
import { Column, Entity } from 'typeorm';
@Entity('WDHospitals', { schema: 'vtrc' })
export class paymentTransaction {
@Column({ type: 'nvarchar', length: 21, primary: true })
wdHospitalId: string;
@Column({ type: 'nvarchar', length: 50 })
docUUID: string;Entity นี้มีประมาณ 90 fields — รวมข้อมูลทั้ง transaction และ employee snapshot:
Field categories
| Category | ตัวอย่าง fields | จำนวนประมาณ |
|---|---|---|
| Identity | wdHospitalId, docUUID, refDocUUID | 5 |
| Employee snapshot | empCode, empFirstName, empLastName, idCard, orgCode, positionName, levelName, salary, bankAccountNo, bankCode | ~30 |
| Hospital/medical | hospitalCode, hospitalName, patientName, patientType, icd10, admissionDate, dischargeDate | ~15 |
| Money | totalAmount, payAmount, benefitAmount, receiptAmount, vatAmount | ~10 |
| Workflow state | status, orderNo, noteApprove, createdByProgram, updatedByProgram, createdAt, updatedAt | ~10 |
| Reference | bankAccountId, branchCode, fmisDocNo, fmisStatus, docTypeCode | ~15 |
| Audit/legacy | isHistory, createdFrom, version | ~5 |
Denormalization
แต่ละ row ของ WDHospitals ฝัง employee ข้อมูลทั้งหมด (empFirstName, empLastName, idCard, salary, bankAccountNo, …) — เป็น snapshot ณ เวลาที่สร้าง transaction
ผล:
- ข้อมูล employee เก่า ๆ ใน transaction ไม่ update เมื่อ employee profile เปลี่ยน (intended — เก็บประวัติ)
- แต่ทำให้ row ใหญ่ (~90 columns) — query ที่ select
*(เยอะมากใน codebase) กิน memory - ข้อมูล PII กระจายอยู่ทุก row → leak แค่ไม่กี่ row = รั่วไหลใหญ่
Persistence patterns
1. Entity Manager by schema
// app.module.ts — 3 connection registrations
TypeOrmModule.forRoot({ name: 'main', ... }),
TypeOrmModule.forRoot({ name: 'vtrc', ... }),
TypeOrmModule.forRoot({ name: 'hrmi', ... }),Service ฉีด EntityManager ตาม schema ที่ต้องการ:
@Injectable()
export class PaymentsService {
constructor(
@InjectConnection('vtrc') private vtrcEntityManager: EntityManager,
@InjectConnection('main') private mainEntityManager: EntityManager,
@InjectConnection('hrmi') private hrmiEntityManager: EntityManager,
) {}
}2. Cross-schema writes — vtrcEntityManager writes to vtrc.WDHospitals
// payments.service.ts:568
await vtrcEntityManager.query(
`UPDATE WDHospitals
SET status='CANCELDOC', updatedAt='${currentDate}'
WHERE wdHospitalId='${payment.docUUID}'`,
);benefit-api เขียนตรงไปยัง vtrc.WDHospitals — ตารางเดียวกันกับที่ vtrc-api ใช้ — เป็นการละเมิด bounded context ของระบบเดิม
ดู 6.8 S2 (architecture debt)
3. Repository pattern (occasional)
// payments.service.ts:218
const result = await this.paymentTransactionRepository.find({
where: { docUUID: id },
});บาง service ใช้ @InjectRepository(paymentTransaction) — ปลอดภัยกว่า raw SQL (auto-parameterized)
4. Transaction — ไม่ใช้
ค้นหา .transaction(async ( ทั่ว codebase → พบเพียง 2 จุด ใน exportpaymentreport.service.ts ทั้งหมด ไม่มีการใช้ DB transaction ครอบ multi-statement writes
ตัวอย่าง multi-statement write ที่ไม่ใช้ transaction:
payments.service.ts:560-580 — สร้าง WDHospitals row ใหม่ + update row เดิมเป็น CANCELDOC ถ้า step 2 fail → inconsistent state
Cross-schema write map
benefit-api writes to:
├── vtrc.WDHospitals ← payments, exportPayments (CRUD)
├── vtrc.MasterApprovers ← dropdown (read)
├── vtrc.MasterWelfareType ← (read)
├── vtrc.MasterCOA ← (read)
├── vtrc.MasterFmis ← (read)
├── vtrc.MasterBank ← (read)
├── benefit.WDExportPayment* ← exportPayments (CRUD)
├── benefit.StudyLeave ← (CRUD)
├── benefit.Inheritance ← (CRUD)
├── benefit.Funeral ← (CRUD)
└── benefit.*TaskResult ← cron (append-only log)
benefit-api reads from:
└── dbHRMI_Center.* ← cron (pull HRMI data)สรุป: benefit-api เป็น second writer ไปยัง vtrc.WDHospitals — คู่กับ vtrc-api → race condition risk, no transaction isolation ข้าม service
wdHospitals.entity.ts — duplicate mapping
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('WDHospitals', { schema: 'vtrc' })
export class wdHospitals {
@PrimaryColumn({ type: 'nvarchar', length: 21 })
wdHospitalId: string;มี entity class สองตัว (paymentTransaction และ wdHospitals) map ไปยังตารางเดียวกัน vtrc.WDHospospitals — duplicate definition ที่อาจเกิดจาก refactor ครั้งก่อน
TypeORM migrations
.env:46: TYPEORM_MIGRATIONS=false — migrations ปิดอยู่ schema changes ทั้งหมดทำด้วยมือที่ production DB → drift risk