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 | ไฟล์ | Table | Database | PK | Live? |
|---|---|---|---|---|---|
EmployeeGroup | entities/employeeGroup.entity.ts | emEmplGrup_reStructure | dbHRMI_Center | EmplGrupID (str 36) | Yes |
EmployeeGroupNBC | entities/employeeGroup.nbc.entity.ts | (inherit) | dbHRMI_Center_NBC | (inherit) | Yes |
EmployeeLevel | entities/employeeLevel.entity.ts | emEmplLevel_reStructure | dbHRMI_Center | EmplLevelID (str 36) | Yes |
EmployeeLevelNBC | entities/employeeLevel.nbc.entity.ts | (inherit) | dbHRMI_Center_NBC | (inherit) | Yes |
TechnicalPosition | entities/technicalPosition.entity.ts | emPositionTechnical_reStructure | dbHRMI_Center | PosTechnicalID (str 36) | Yes |
TechnicalPositionNBC | entities/technicalPosition.nbc.entity.ts | (inherit) | dbHRMI_Center_NBC | (inherit) | Yes |
UserEntity | entities/user.entity.ts | auth_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
@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 BaseEntity | active record pattern — ทำให้เรียก EmployeeGroup.find() แบบ static method ได้ |
@PrimaryColumn (ไม่ใช่ @PrimaryGeneratedColumn) | PK เป็น string ที่ HRMI สร้างขึ้น (GUID ความยาว 36) ไม่ใช่ auto-increment |
length: 36 | GUID format — HRMI ใช้ GUID แบบ raw string ไม่ใช่ UUID type ของ MSSQL |
| audit columns | CreatedBy, CreatedDate, ModifiedBy, ModifiedDate — pattern มาตรฐานของ HRMI tables |
IsDeleted, IsInactive | soft delete flags — สองค่าแยกกัน IsDeleted = ลบแล้ว, IsInactive = ระงับชั่วคราว |
SourceDB | multi-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
@Entity({
name: 'emEmplGrup_reStructure',
database: 'dbHRMI_Center_NBC',
})
export class EmployeeGroupNBC extends EmployeeGroup {}- body ว่างเปล่า — สืบทอดทุก column จาก parent
- แตกต่างแค่
databaseใน@Entitydecorator - รูปแบบนี้เป็นการหลีกเลี่ยงข้อจำกัดของ TypeORM ที่ไม่รองรับการลงทะเบียน entity เดียวกันบนหลาย database โดยตรง — ต้องสร้าง subclass แยก
ทุก entity มี SourceDB column
ยืนยันว่าทั้ง 6 live entities มี SourceDB column เหมือนกัน
| Entity | SourceDB | nullable? |
|---|---|---|
EmployeeGroup | ✅ | Yes |
EmployeeGroupNBC | ✅ (inherit) | Yes |
EmployeeLevel | ✅ | No |
EmployeeLevelNBC | ✅ (inherit) | No |
TechnicalPosition | ✅ | No |
TechnicalPositionNBC | ✅ (inherit) | No |
ความแตกต่าง — EmployeeGroup.sourceDB เป็น nullable ส่วน entity อื่นไม่ใช่ บ่งบอกว่าอาจมีข้อมูลเก่าใน emEmplGrup_reStructure ที่ยังไม่มี SourceDB (migrated ไม่ครบ)
ค่าของ SourceDB
จากการวิเคราะห์ SQL ใน employee.service.ts:37-41 พบ mapping ของ SourceDB values
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_rep | 02CS | จุฬาฯ ศรีสะเกษ (CU Si Sa Ket) |
dbHRMI_CU_H_rep | 02 | โรงพยาบาลจุฬาฯ (CU Hospital) |
dbHRMI_RC_C_rep | 03 | สภากาชาดกลาง (RC Central) |
dbHRMI_RC_B_rep | 00 | สภากาชาดธนาคารเลือด (RC Blood) |
dbHRMI_RC_NBC_rep | 07 | สภากาชาดสาขาอื่น (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
-- ตัวอย่าง 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
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 object | Type | Database | entity ครอบ? |
|---|---|---|---|
emOrgUnit_new_reStructure | table | center + NBC | ไม่ม — ใช้ใน raw SQL เท่านั้น |
emOrg | table | center | ไม่ม |
vtrcMainProfile | view | center | ไม่ม |
vtrcAllProfile | view | center | ไม่ม |
centralizedProfileOrdering_reStructure_all_cdb_nbc | table/view | center | ไม่ม |
hrEmpWorkProfile | table | center + NBC | ไม่ม |
emPositionManage_reStructure | table | center + NBC | ไม่ม |
emPosition_reStructure | table | center + NBC | ไม่ม |
emEmplGrup_reStructure | table | center + NBC | ✅ EmployeeGroup |
emEmplLevel_reStructure | table | center + NBC | ✅ EmployeeLevel |
emPositionTechnical_reStructure | table | center + NBC | ✅ TechnicalPosition |
hrEmpBankBook_reStructure | table | center + NBC | ไม่ม |
emBank_reStructure | table | center + NBC | ไม่ม |
emBankBranch_reStructure | table | center + NBC | ไม่ม |
hrTimeTempImport | table | center + NBC | ไม่ม |
uv_i3_PaySlipDT_reStructure | view | center | ไม่ม |
สังเกต — มี entity เพียง 3 จาก 16 tables/views ที่ใช้จริง (รวมเป็น 6 entity files เพราะคู center/NBC)
เหตุผลที่ entity ไม่ครอบหมด
- TypeORM active record method (
find,findOne) ใช้ไม่ได้กับ SQL view — view ไม่มี PK ที่ edit ได้ → ต้องใช้ raw SQL - Cross-database join ใน raw SQL ไม่สามารถ represent ใน entity relation ของ TypeORM ได้
- ทีมเลือกใช้ 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 ไม่ declare | column นั้นจะไม่ถูก 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 มี
SourceDBcolumn — multi-tenant identifier เช่นเดียวกับ cu-central-api - SourceDB values — 5 codes แมปจาก HRMI source database name (
dbHRMI_CU_H_rep→02,dbHRMI_RC_NBC_rep→07) - 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