Skip to content

5.6 · Entities + SourceDB tenancy

บทนี้รวบรวม TypeORM entity ทั้ง 7 ตัวของ centralize-api (6 live + 1 dead) พร้อมอธิบายรูปแบบ multi-tenant sharding ผ่าน column SourceDB ที่เหมือนกับ cu-central-api (Volume 4.5)


ภาพรวม entity

Entityไฟล์TableDatabasePKLive?
EmployeeGroupentities/employeeGroup.entity.tsemEmplGrup_reStructuredbHRMI_CenterEmplGrupID (str 36)Yes
EmployeeGroupNBCentities/employeeGroup.nbc.entity.ts(inherit)dbHRMI_Center_NBC(inherit)Yes
EmployeeLevelentities/employeeLevel.entity.tsemEmplLevel_reStructuredbHRMI_CenterEmplLevelID (str 36)Yes
EmployeeLevelNBCentities/employeeLevel.nbc.entity.ts(inherit)dbHRMI_Center_NBC(inherit)Yes
TechnicalPositionentities/technicalPosition.entity.tsemPositionTechnical_reStructuredbHRMI_CenterPosTechnicalID (str 36)Yes
TechnicalPositionNBCentities/technicalPosition.nbc.entity.ts(inherit)dbHRMI_Center_NBC(inherit)Yes
UserEntityentities/user.entity.tsauth_user(not registered)userId (uuid)No — dead

รูปแบบที่ซ้ำ — 3 entity × 2 databases = 6 live entity

ทั้งสาม (EmployeeGroup, EmployeeLevel, TechnicalPosition) มีรูปแบบเดียวกัน

  • class หลัก declare @Entity({ database: 'dbHRMI_Center' })
  • class ลูก inherit class หลัก แล้ว override แค่ @Entity({ database: 'dbHRMI_Center_NBC' })
  • ทั้งคู่อ้างถึง table ชื่อเดียวกันใน database คนละตัว → สะท้อนข้อมูลระหว่าง 2 databases

EmployeeGroup — ตัวอย่าง canonical

3:43:centralize-api/src/entities/employeeGroup.entity.ts
@Entity({
  name: 'emEmplGrup_reStructure',
  database: 'dbHRMI_Center',
})
export class EmployeeGroup extends BaseEntity {
  @PrimaryColumn({ name: 'EmplGrupID', length: 36 })
  emplGrupID: string;

  @Column({ name: 'EmplGrupCode', length: 50 })
  emplGrupCode: string;

  @Column({ name: 'EmplGrupName', length: 50 })
  emplGrupName: string;

  @Column({ name: 'EmplGrupNameEng', length: 50, nullable: true })
  emplGrupNameEng?: string;

  @Column({ name: 'Remark', length: 500, nullable: true })
  remark?: string;

  @Column({ name: 'CreatedBy', length: 36, nullable: true })
  createdBy?: string;

  @Column({ name: 'CreatedDate', nullable: true })
  createdDate?: Date;

  @Column({ name: 'ModifiedBy', length: 36, nullable: true })
  modifiedBy?: string;

  @Column({ name: 'ModifiedDate', nullable: true })
  modifiedDate?: Date;

  @Column({ name: 'IsDeleted' })
  isDeleted?: boolean;

  @Column({ name: 'IsInactive' })
  isInactive?: boolean;

  @Column({ name: 'SourceDB', length: 50, nullable: true })
  sourceDB?: string;
}

สิ่งที่ควรทราบจาก entity นี้

หัวข้อรายละเอียด
@Entity({ database: 'dbHRMI_Center' })decorator ระบุ database ที่ entity อยู่ → TypeORM จะ route query ไป connection ที่ register ชื่อ 'center' ใน AppModule
extends BaseEntityactive record pattern — ทำให้เรียก EmployeeGroup.find() แบบ static method ได้
@PrimaryColumn (ไม่ใช่ @PrimaryGeneratedColumn)PK เป็น string ที่ HRMI สร้างขึ้น (GUID ความยาว 36) ไม่ใช่ auto-increment
length: 36GUID format — HRMI ใช้ GUID แบบ raw string ไม่ใช่ UUID type ของ MSSQL
audit columnsCreatedBy, CreatedDate, ModifiedBy, ModifiedDate — pattern มาตรฐานของ HRMI tables
IsDeleted, IsInactivesoft delete flags — สองค่าแยกกัน IsDeleted = ลบแล้ว, IsInactive = ระงับชั่วคราว
SourceDBmulti-tenant column (ดู §"SourceDB tenancy" ด้านล่าง)
ไม่มี relationไม่มี @ManyToOne, @OneToMany — flat entity เพราะ relation ทำใน raw SQL ที่ service layer (บท 5.4)
ไม่มี indexไม่มี @Index() — index อยู่ที่ HRMI schema โดยตรง ไม่ได้ define ใน entity

EmployeeGroupNBC — empty subclass

4:8:centralize-api/src/entities/employeeGroup.nbc.entity.ts
@Entity({
  name: 'emEmplGrup_reStructure',
  database: 'dbHRMI_Center_NBC',
})
export class EmployeeGroupNBC extends EmployeeGroup {}
  • body ว่างเปล่า — สืบทอดทุก column จาก parent
  • แตกต่างแค่ database ใน @Entity decorator
  • รูปแบบนี้เป็นการหลีกเลี่ยงข้อจำกัดของ TypeORM ที่ไม่รองรับการลงทะเบียน entity เดียวกันบนหลาย database โดยตรง — ต้องสร้าง subclass แยก

ทุก entity มี SourceDB column

ยืนยันว่าทั้ง 6 live entities มี SourceDB column เหมือนกัน

EntitySourceDBnullable?
EmployeeGroupYes
EmployeeGroupNBC✅ (inherit)Yes
EmployeeLevelNo
EmployeeLevelNBC✅ (inherit)No
TechnicalPositionNo
TechnicalPositionNBC✅ (inherit)No

ความแตกต่าง — EmployeeGroup.sourceDB เป็น nullable ส่วน entity อื่นไม่ใช่ บ่งบอกว่าอาจมีข้อมูลเก่าใน emEmplGrup_reStructure ที่ยังไม่มี SourceDB (migrated ไม่ครบ)

ค่าของ SourceDB

จากการวิเคราะห์ SQL ใน employee.service.ts:37-41 พบ mapping ของ SourceDB values

sql
WHEN orgMain.SourceDB = 'dbHRMI_CU_SC_rep' THEN '02CS'
WHEN orgMain.SourceDB = 'dbHRMI_CU_H_rep'  THEN '02'
WHEN orgMain.SourceDB = 'dbHRMI_RC_C_rep'  THEN '03'
WHEN orgMain.SourceDB = 'dbHRMI_RC_B_rep'  THEN '00'
WHEN orgMain.SourceDB = 'dbHRMI_RC_NBC_rep' THEN '07'
SourceDB rawรหัสที่ได้ความหมาย
dbHRMI_CU_SC_rep02CSจุฬาฯ ศรีสะเกษ (CU Si Sa Ket)
dbHRMI_CU_H_rep02โรงพยาบาลจุฬาฯ (CU Hospital)
dbHRMI_RC_C_rep03สภากาชาดกลาง (RC Central)
dbHRMI_RC_B_rep00สภากาชาดธนาคารเลือด (RC Blood)
dbHRMI_RC_NBC_rep07สภากาชาดสาขาอื่น (RC NBC)

SourceDB คือ multi-tenant identifier

SourceDB ทำหน้าที่เป็น tenant identifier — ทุก row ใน HRMI table ติดแท็กว่า row นี้มาจาก source database ไหน (CU/RC แต่ละสาขา) เมื่อ query ข้อมูลต้องกรองด้วย SourceDB ของ tenant ที่ต้องการ

ตัวอย่าง — ถ้า vtrc-api ต้องการข้อมูล employee ของโรงพยาบาลจุฬาฯ เท่านั้น → ต้องส่ง sourceDB=02 เป็น query param แล้ว centralize-api จะกรองใน WHERE clause

sql
-- ตัวอย่าง pattern ใน service
WHERE emp.SourceDB = 'dbHRMI_CU_H_rep'

เปรียบเทียบกับ cu-central-api

รูปแบบ SourceDB sharding เดียวกันกับ cu-central-api (Volume 4.5) — ทุก HRMI table มี SourceDB column และทุก association ต้อง scope ด้วย WHERE X.SourceDB = Y.SourceDB

→ บ่งชี้ว่าทั้งสอง service อ่าน HRMI schema เดียวกันที่ออกแบบโดยทีม CU/RC ไม่ใช่ pattern ที่ทีม VTRC คิดเอง


UserEntity — dead code

1:48:centralize-api/src/entities/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';

@Entity('auth_user')
@Unique('my_unique_auth_user', ['userName', 'email'])
export class UserEntity {
  @PrimaryGeneratedColumn('uuid')
  userId: string;
  // ... userName, password, firstName, lastName, email, userLevel, ...
}

(รายละเอียดเต็มในบท 5.1 §"รวบรวม dead code")

สรุป — UserEntity เป็น dead code ที่ copy มาจาก Postgres starter template ไม่ได้ register ใน entities/index.ts ไม่ได้ import ที่ไหน table auth_user ไม่มีใน HRMI


HRMI tables/views ที่ entity อ้างถึง

จากการวิเคราะห์ raw SQL ใน service ทุกตัว พบว่า centralize-api อ้างถึง MSSQL objects เหล่านี้

MSSQL objectTypeDatabaseentity ครอบ?
emOrgUnit_new_reStructuretablecenter + NBCไม่ม — ใช้ใน raw SQL เท่านั้น
emOrgtablecenterไม่ม
vtrcMainProfileviewcenterไม่ม
vtrcAllProfileviewcenterไม่ม
centralizedProfileOrdering_reStructure_all_cdb_nbctable/viewcenterไม่ม
hrEmpWorkProfiletablecenter + NBCไม่ม
emPositionManage_reStructuretablecenter + NBCไม่ม
emPosition_reStructuretablecenter + NBCไม่ม
emEmplGrup_reStructuretablecenter + NBCEmployeeGroup
emEmplLevel_reStructuretablecenter + NBCEmployeeLevel
emPositionTechnical_reStructuretablecenter + NBCTechnicalPosition
hrEmpBankBook_reStructuretablecenter + NBCไม่ม
emBank_reStructuretablecenter + NBCไม่ม
emBankBranch_reStructuretablecenter + NBCไม่ม
hrTimeTempImporttablecenter + NBCไม่ม
uv_i3_PaySlipDT_reStructureviewcenterไม่ม

สังเกต — มี entity เพียง 3 จาก 16 tables/views ที่ใช้จริง (รวมเป็น 6 entity files เพราะคู center/NBC)

เหตุผลที่ entity ไม่ครอบหมด

  1. TypeORM active record method (find, findOne) ใช้ไม่ได้กับ SQL view — view ไม่มี PK ที่ edit ได้ → ต้องใช้ raw SQL
  2. Cross-database join ใน raw SQL ไม่สามารถ represent ใน entity relation ของ TypeORM ได้
  3. ทีมเลือกใช้ raw SQL เป็นหลัก เพราะต้องการ control ทุก query → entity จึงใช้น้อย

→ บท 5.4 ที่อธิบายว่า service layer ใช้ raw SQL เป็นหลักมีเหตุผลสนับสนุนที่นี่


ขาด — index declaration

ไม่มี entity ไหน declare @Index() ใด ๆ — index ทั้งหมดอยู่ใน HRMI schema โดยตรง (สร้างโดยทีม CU/RC)

ผล — ถ้า dev ของ centralize-api อยากทราบว่ามี index บน (EmplGrupCode, SourceDB) หรือไม่ ต้องไป inspect HRMI database โดยตรง — ไม่มี document ใน repo

โดยทั่วไป HRMI tables ที่ใช้บ่อย (emOrgUnit, vtrcMainProfile) ควรมี index บน SourceDB และ EmpCode — แต่ไม่มีทางยืนยันจาก entity definition ได้


synchronize: false — TypeORM ไม่ alter schema

ย้ำจากบท 5.2 — synchronize: false ใน AppModule TypeORM module config หมายความว่า entity definition เป็นเพียง read model ที่ใช้สำหรับ active record method และ type inference แต่ ไม่มีผลในการสร้างหรือแก้ไข table ใน HRMI

หาก entity definition ผิดจาก schema HRMI จริง

กรณีผล
entity declare column ที่ HRMI ไม่มSELECT จะ fail → 500 error
HRMI มี column ที่ entity ไม่ declarecolumn นั้นจะไม่ถูก select → ข้อมูลหาย
type ผิด (เช่น entity บอก string แต่จริงคือ Date)TypeScript type ผิด อาจ runtime error

→ ความเสี่ยงนี้อยู่ที่ว่าใครเป็นคน sync entity definition กับ HRMI schema ปัจจุบัน — ถ้าไม่มีกระบวนการ จะ drift เงียบ ๆ


สรุปประเด็นสำคัญ

  • 7 entities รวม (6 live + 1 dead UserEntity)
  • รูปแบบซ้ำ — 3 entity × 2 databases ผ่าน subclass empty body ที่ override แค่ @Entity({ database })
  • ทุก live entity มี SourceDB column — multi-tenant identifier เช่นเดียวกับ cu-central-api
  • SourceDB values — 5 codes แมปจาก HRMI source database name (dbHRMI_CU_H_rep02, dbHRMI_RC_NBC_rep07)
  • entity ครอบแค่ 3/16 tables/views ที่ใช้จริง — ส่วนใหญ่ใช้ raw SQL เพราะ cross-database join และ view ไม่ติดกับ active record
  • ไม่มี index declaration — index ทั้งหมดอยู่ใน HRMI schema ที่ควบคุมโดยทีม CU/RC
  • synchronize: false ป้องกัน TypeORM ไปแก้ HRMI schema — entity เป็นแค่ read model