Skip to content

6.6 · Entities + persistence patterns

บทนี้เจาะลึก entities ของ benefit-api และ persistence pattern — โดยเฉพาะ paymentTransaction ซึ่งเป็น entity เอกภาพ (entity กลาง) ที่ embed employee snapshot เข้าไปเต็ม (denormalization สูง)


Entity registry — src/entities/

EntitySchemaTableRole
paymentTransaction.entity.tsvtrcWDHospitalstransaction หลัก (เบิกค่ารักษา)
wdHospitals.entity.tsvtrcWDHospitalsmirror entity (same table)
masterWelfareType.entity.tsvtrcMasterWelfareTypelookup
masterApprovers.entity.tsvtrcMasterApproverslookup
masterCOA.entity.tsvtrcMasterCOAlookup
masterFmis.entity.tsvtrcMasterFmislookup
masterBank.entity.tsvtrcMasterBanklookup
wdExportPaymentTransaction.entity.tsbenefitWDExportPaymentTransactionexport batch
wdExportPaymentDetail.entity.tsbenefitWDExportPaymentDetailexport detail
hospitalsTaskResult.entity.tsbenefitHospitalsTaskResultcron task log
studyLeave.entity.tsbenefitStudyLeavestudy leave request
inheritance.entity.tsbenefitInheritanceinheritance request
funeral.entity.tsbenefitFuneralfuneral request
approveDisaster.entity.tsbenefitApproveDisasterdisaster request
disasterTaskResult.entity.tsbenefitDisasterTaskResultcron task log
studyLeaveTaskResult.entity.tsbenefitStudyLeaveTaskResultcron task log
inheritanceTaskResult.entity.tsbenefitInheritanceTaskResultcron task log
funeralTaskResult.entity.tsbenefitFuneralTaskResultcron task log
fileBankTransaction.entity.tsbenefitFileBankTransactionuploaded file metadata

paymentTransaction.entity.ts — entity เอกภาพ

1:10:benefit-api/src/entities/paymentTransaction.entity.ts
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จำนวนประมาณ
IdentitywdHospitalId, docUUID, refDocUUID5
Employee snapshotempCode, empFirstName, empLastName, idCard, orgCode, positionName, levelName, salary, bankAccountNo, bankCode~30
Hospital/medicalhospitalCode, hospitalName, patientName, patientType, icd10, admissionDate, dischargeDate~15
MoneytotalAmount, payAmount, benefitAmount, receiptAmount, vatAmount~10
Workflow statestatus, orderNo, noteApprove, createdByProgram, updatedByProgram, createdAt, updatedAt~10
ReferencebankAccountId, branchCode, fmisDocNo, fmisStatus, docTypeCode~15
Audit/legacyisHistory, 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

ts
// app.module.ts — 3 connection registrations
TypeOrmModule.forRoot({ name: 'main',   ... }),
TypeOrmModule.forRoot({ name: 'vtrc',   ... }),
TypeOrmModule.forRoot({ name: 'hrmi',   ... }),

Service ฉีด EntityManager ตาม schema ที่ต้องการ:

ts
@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

ts
// 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)

ts
// 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

1:5:benefit-api/src/entities/wdHospitals.entity.ts
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


ไปต่อ