3.3.6 · Entities + SourceDB tenancy
บทนี้รวบรวม TypeORM entity ทั้ง 7 ตัวของ centralize-api (6 live + 1 dead) พร้อมอธิบายรูปแบบ multi-tenant sharding ผ่าน column SourceDB ที่เหมือนกับ cu-central-api (Volume 3.2)
ภาพรวม entity
| Entity | ไฟล์ | Table | Database | PK | Live? |
|---|---|---|---|---|---|
EmployeeGroup | centralize-api/src/entities/employeeGroup.entity.ts | emEmplGrup_reStructure | dbHRMI_Center | EmplGrupID (str 36) | Yes |
EmployeeGroupNBC | centralize-api/src/entities/employeeGroup.nbc.entity.ts | (inherit) | dbHRMI_Center_NBC | (inherit) | Yes |
EmployeeLevel | centralize-api/src/entities/employeeLevel.entity.ts | emEmplLevel_reStructure | dbHRMI_Center | EmplLevelID (str 36) | Yes |
EmployeeLevelNBC | centralize-api/src/entities/employeeLevel.nbc.entity.ts | (inherit) | dbHRMI_Center_NBC | (inherit) | Yes |
TechnicalPosition | centralize-api/src/entities/technicalPosition.entity.ts | emPositionTechnical_reStructure | dbHRMI_Center | PosTechnicalID (str 36) | Yes |
TechnicalPositionNBC | centralize-api/src/entities/technicalPosition.nbc.entity.ts | (inherit) | dbHRMI_Center_NBC | (inherit) | Yes |
UserEntity | centralize-api/src/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
centralize-api/src/entities/employeeGroup.entity.ts:1-43
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
@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 ได้ (ใช้จริงใน EmployeeGroupRepository) |
@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 (บท 3.3.4) |
| ไม่มี index | ไม่มี @Index() — index อยู่ที่ HRMI schema โดยตรง ไม่ได้ define ใน entity |
EmployeeGroupNBC — empty subclass
centralize-api/src/entities/employeeGroup.nbc.entity.ts:1-8
import { Entity } from 'typeorm';
import { EmployeeGroup } from './employeeGroup.entity';
@Entity({
name: 'emEmplGrup_reStructure',
database: 'dbHRMI_Center_NBC',
})
export class EmployeeGroupNBC extends EmployeeGroup {}- body ว่างเปล่า — สืบทอดทุก column จาก parent
- แตกต่างแค่
databaseใน@Entitydecorator - รูปแบบนี้เป็นการหลีกเลี่ยงข้อจำกัดของ TypeORM ที่ไม่รองรับการลงทะเบียน entity เดียวกันบนหลาย database โดยตรง — ต้องสร้าง subclass แยก
EmployeeLevelNBC (centralize-api/src/entities/employeeLevel.nbc.entity.ts:1-8) และ TechnicalPositionNBC (centralize-api/src/entities/technicalPosition.nbc.entity.ts:1-8) ใช้ pattern เดียวกันทั้งคู่
ทุก entity มี SourceDB column
ยืนยันว่าทั้ง 6 live entities มี SourceDB column เหมือนกัน แต่ nullability ไม่เหมือนกัน
| Entity | SourceDB | nullable? | ไฟล์ |
|---|---|---|---|
EmployeeGroup | ✅ | Yes | centralize-api/src/entities/employeeGroup.entity.ts:41-42 |
EmployeeGroupNBC | ✅ (inherit) | Yes | (inherit) |
EmployeeLevel | ✅ | Yes | centralize-api/src/entities/employeeLevel.entity.ts:41-42 |
EmployeeLevelNBC | ✅ (inherit) | Yes | (inherit) |
TechnicalPosition | ✅ | Yes | centralize-api/src/entities/technicalPosition.entity.ts:41-42 |
TechnicalPositionNBC | ✅ (inherit) | Yes | (inherit) |
หมายเหตุ — v1 เอกสารบอกว่า EmployeeLevel/TechnicalPosition SourceDB เป็น non-nullable แต่โค้ดปัจจุบันประกาศ nullable: true ทั้งสอง → แก้ไขเอกสาร v1 ในจุดนี้ ความจริงคือ ทุก entity SourceDB nullable บ่งชี้ว่าอาจมีข้อมูลเก่าที่ยังไม่มี SourceDB (migrated ไม่ครบ)
ค่าของ SourceDB
จากการวิเคราะห์ SQL ใน centralize-api/src/modules/employee/employee.service.ts:36-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'⚠️ จุดที่สับสน — SourceDB ใน raw table เก็บชื่อ database เต็ม (dbHRMI_CU_H_rep) แต่ query param ที่ client ส่งมาใช้รหัส 2 หลัก (02) → ที่ service layer ต้อง map กลับด้วย CASE WHEN ใน SQL หรือใน logic ฝั่ง JS
เปรียบเทียบกับ cu-central-api
รูปแบบ SourceDB sharding เดียวกันกับ cu-central-api (Volume 3.2) — ทุก HRMI table มี SourceDB column และทุก association ต้อง scope ด้วย WHERE X.SourceDB = Y.SourceDB
→ บ่งชี้ว่าทั้งสอง service อ่าน HRMI schema เดียวกันที่ออกแบบโดยทีม CU/RC ไม่ใช่ pattern ที่ทีม VTRC คิดเอง
UserEntity — dead code
centralize-api/src/entities/user.entity.ts:1-47
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, ...
}(รายละเอียดเต็มในบท 3.3.1 §"รวบรวม dead code")
สรุป — UserEntity เป็น dead code ที่ copy มาจาก Postgres starter template ไม่ได้ register ใน centralize-api/src/entities/index.ts:1-6 ไม่ได้ 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 จึงใช้น้อย
→ บท 3.3.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
ย้ำจากบท 3.3.2 — synchronize: false ใน AppModule TypeORM module config (centralize-api/src/app.module.ts:33,52) หมายความว่า 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 เงียบ ๆ
Entity loading — bug ที่ไม่พัง
centralize-api/src/entities/index.ts:1-6 export ทั้ง 6 entity files
export * from './technicalPosition.entity';
export * from './technicalPosition.nbc.entity';
export * from './employeeGroup.entity';
export * from './employeeGroup.nbc.entity';
export * from './employeeLevel.entity';
export * from './employeeLevel.nbc.entity';UserEntity ไม่ได้ export → ไม่ถูก load
แต่ใน AppModule ที่ centralize-api/src/app.module.ts:35,54 ใช้ glob pattern ไม่ใช้ index.ts
center: __dirname + '/**/*.entity{.ts,.js}'
nbc: __dirname + '/**/*.nbc.entity{.ts,.js}'glob ของ center match ทั้ง *.entity.ts และ *.nbc.entity.ts → entity NBC ถูก load เข้า connection center ด้วย (บท 3.3.2) และ user.entity.ts ก็ match glob → ถูก load เข้า TypeORM จริง แต่เพราะ @Entity('auth_user') ไม่มีใน HRMI ในตอน runtime ที่มีการ query จริง ๆ จะ fail
→ UserEntity ไม่ใช่ dead ในระดับ TypeORM register แต่ dead ในระดับ table จริง — เรียกใช้ method active record ของ UserEntity จะ throw QueryFailedError ตอน runtime เพราะ table auth_user ไม่มี
สรุปประเด็นสำคัญ
- 7 entities รวม (6 live + 1 dead
UserEntity) - รูปแบบซ้ำ — 3 entity × 2 databases ผ่าน subclass empty body ที่ override แค่
@Entity({ database }) - ทุก live entity มี
SourceDBcolumn nullable — multi-tenant identifier เช่นเดียวกับ cu-central-api (แก้ไขจาก v1 ที่บอก SourceDB ของEmployeeLevel/TechnicalPositionเป็น non-nullable) - 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- glob bug —
user.entity.tsถูก load เข้า TypeORM จริง (ไม่ใช่ dead ในระดับ register) แต่ tableauth_userไม่มี → query จะ fail ตอน runtime