2026-01-28 23:10:50 +07:00
|
|
|
|
# สรุปการปรับปรุง
|
|
|
|
|
|
## Branch: `fix/optimization-detailSuperAdmin`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 📋 ภาพรวม
|
|
|
|
|
|
|
|
|
|
|
|
การแก้ไขครั้งนี้มุ่งเน้นปรับปรุงประสิทธิภาพและความมั่นคงของ API `GET /super-admin/{id}` ซึ่งมีปัญหาเรื่อง:
|
|
|
|
|
|
- Query ฐานข้อมูลซ้ำซ้อนหลายครั้ง
|
|
|
|
|
|
- การใช้งาน database connection ไม่มีประสิทธิภาพ
|
|
|
|
|
|
- ขาดระบบ caching ที่เหมาะสม
|
|
|
|
|
|
- ขาดระบบ Graceful Shutdown
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 🔧 รายละเอียดการแก้ไขแต่ละส่วน
|
|
|
|
|
|
|
|
|
|
|
|
### 1. Connection Pool Settings (`data-source.ts`)
|
|
|
|
|
|
|
|
|
|
|
|
**ไฟล์:** `src/database/data-source.ts`
|
|
|
|
|
|
|
|
|
|
|
|
**การแก้ไข:**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// เพิ่ม connection pool settings
|
|
|
|
|
|
extra: {
|
|
|
|
|
|
connectionLimit: +(process.env.DB_CONNECTION_LIMIT || 50),
|
|
|
|
|
|
maxIdle: +(process.env.DB_MAX_IDLE || 10),
|
|
|
|
|
|
idleTimeout: +(process.env.DB_IDLE_TIMEOUT || 60000),
|
|
|
|
|
|
timezone: "+07:00",
|
|
|
|
|
|
},
|
|
|
|
|
|
poolSize: +(process.env.DB_POOL_SIZE || 10),
|
|
|
|
|
|
maxQueryExecutionTime: +(process.env.DB_MAX_QUERY_TIME || 3000),
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**คำอธิบายเชิงเทคนิค:**
|
|
|
|
|
|
- `connectionLimit: 50` - จำกัดจำนวน connection สูงสุดที่เปิดพร้อมกัน
|
|
|
|
|
|
- `maxIdle: 10` - จำนวน idle connection ที่เก็บไว้ reuse
|
|
|
|
|
|
- `idleTimeout: 60000` - เวลา (ms) ที่ idle connection จะถูกปิดอัตโนมัติ
|
|
|
|
|
|
- `poolSize: 10` - ขนาด connection pool ของ TypeORM
|
|
|
|
|
|
- `maxQueryExecutionTime: 3000` - แจ้งเตือนเมื่อ query ช้ากว่า 3 วินาที
|
|
|
|
|
|
|
|
|
|
|
|
**ประโยชน์:** ป้องกัน connection exhaustion และปรับปรุงการใช้งานทรัพยากรฐานข้อมูล
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Graceful Shutdown (`app.ts`)
|
|
|
|
|
|
|
|
|
|
|
|
**ไฟล์:** `src/app.ts` (บรรทัด 123-162)
|
|
|
|
|
|
|
|
|
|
|
|
**การแก้ไข:**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
const gracefulShutdown = async (signal: string) => {
|
|
|
|
|
|
console.log(`\n[APP] ${signal} received. Starting graceful shutdown...`);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. หยุดรับ connection ใหม่
|
|
|
|
|
|
server.close(() => {
|
|
|
|
|
|
console.log("[APP] HTTP server closed");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 2. ปิด database connections
|
|
|
|
|
|
if (AppDataSource.isInitialized) {
|
|
|
|
|
|
await AppDataSource.destroy();
|
|
|
|
|
|
console.log("[APP] Database connections closed");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. ทำลาย cache instances
|
|
|
|
|
|
logMemoryStore.destroy();
|
2026-01-29 00:05:56 +07:00
|
|
|
|
console.log("[APP] LogMemoryStore destroyed");
|
|
|
|
|
|
|
|
|
|
|
|
// Destroy OrgStructureCache
|
2026-01-28 23:10:50 +07:00
|
|
|
|
orgStructureCache.destroy();
|
2026-01-29 00:05:56 +07:00
|
|
|
|
console.log("[APP] OrgStructureCache destroyed");
|
2026-01-28 23:10:50 +07:00
|
|
|
|
|
|
|
|
|
|
// 4. บังคับปิดหลังจาก 30 วินาที (หาก shutdown ค้าง)
|
|
|
|
|
|
const shutdownTimeout = setTimeout(() => {
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}, 30000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ดักจับ signals
|
|
|
|
|
|
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
|
|
|
|
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**คำอธิบายเชิงเทคนิค:**
|
|
|
|
|
|
- `SIGTERM` - signal ที่ระบบส่งมาเมื่อต้องการ stop service
|
|
|
|
|
|
- `SIGINT` - signal จากการกด Ctrl+C
|
|
|
|
|
|
- ปิดทีละขั้นตอน: HTTP Server → Database → Cache
|
|
|
|
|
|
- Timeout 30 วินาทีป้องกันการ hang ถ้า shutdown ไม่สำเร็จ
|
|
|
|
|
|
|
|
|
|
|
|
**ประโยชน์:** ป้องกัน connection หลุดและ data loss เมื่อระบบ restart
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 3. Log Middleware & Memory Store
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.1 Log Memory Store (`src/utils/LogMemoryStore.ts`)
|
|
|
|
|
|
|
|
|
|
|
|
**คุณสมบัติ:**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
class LogMemoryStore {
|
|
|
|
|
|
private cache: {
|
|
|
|
|
|
currentRevision: OrgRevision | null,
|
|
|
|
|
|
profileCache: Map<string, Profile>, // keycloak → Profile
|
|
|
|
|
|
rootIdCache: Map<string, string>, // profileId → rootId
|
|
|
|
|
|
};
|
2026-01-29 00:30:34 +07:00
|
|
|
|
private readonly CACHE_TTL = 60 * 60 * 1000; // 60 นาที
|
2026-01-28 23:10:50 +07:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**การทำงาน:**
|
2026-01-29 00:30:34 +07:00
|
|
|
|
- Passive cache refresh - ตรวจสอบและ refresh cache เมื่อมีการเข้าถึงข้อมูล (on-access)
|
|
|
|
|
|
- หาก cache เก่าเกิน 60 นาที จะทำการ refresh อัตโนมัติ
|
2026-01-28 23:10:50 +07:00
|
|
|
|
- Lazy load `profileCache` และ `rootIdCache` (โหลดเมื่อถูกเรียกใช้)
|
|
|
|
|
|
- Method `getProfileByKeycloak()` - ดึง profile จาก cache หรือ database
|
|
|
|
|
|
- Method `getRootIdByProfileId()` - ดึง rootId จาก cache หรือ database
|
2026-01-29 00:30:34 +07:00
|
|
|
|
- ไม่มี setInterval (ลดการใช้งาน timer)
|
2026-01-28 23:10:50 +07:00
|
|
|
|
|
|
|
|
|
|
#### 3.2 Log Middleware (`src/middlewares/logs.ts`)
|
|
|
|
|
|
|
|
|
|
|
|
**การเปลี่ยนแปลงหลัก:**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// ก่อน: Query ทุกครั้งที่มี request
|
|
|
|
|
|
const profile = await AppDataSource.getRepository(Profile)
|
|
|
|
|
|
.findOne({ where: { keycloak } });
|
|
|
|
|
|
|
|
|
|
|
|
// หลัง: ใช้ cache
|
|
|
|
|
|
const profile = await logMemoryStore.getProfileByKeycloak(keycloak);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**ประโยชน์:** ลดจำนวน query สำหรับ log middleware อย่างมาก
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 4. OrgStructureCache (`src/utils/OrgStructureCache.ts`)
|
|
|
|
|
|
|
|
|
|
|
|
**ไฟล์ใหม่:** `src/utils/OrgStructureCache.ts`
|
|
|
|
|
|
|
|
|
|
|
|
**คุณสมบัติ:**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
class OrgStructureCache {
|
|
|
|
|
|
private cache: Map<string, CacheEntry>;
|
2026-01-29 00:05:56 +07:00
|
|
|
|
private readonly CACHE_TTL = 30 * 60 * 1000; // 30 นาที
|
2026-01-28 23:10:50 +07:00
|
|
|
|
|
|
|
|
|
|
// Key format: org-structure-{revisionId}-{rootId}
|
|
|
|
|
|
private generateKey(revisionId: string, rootId?: string): string
|
|
|
|
|
|
|
|
|
|
|
|
async get(revisionId: string, rootId?: string): Promise<any>
|
|
|
|
|
|
async set(revisionId: string, rootId: string, data: any): Promise<void>
|
|
|
|
|
|
invalidate(revisionId: string): void
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**การทำงาน:**
|
|
|
|
|
|
- Cache ผลลัพธ์ของ org structure ตาม `revisionId` และ `rootId`
|
2026-01-29 00:05:56 +07:00
|
|
|
|
- TTL 30 นาที - ข้อมูลเก่าจะถูกลบอัตโนมัติ (ปรับจาก 10 นาที เพื่อลด cleanup frequency)
|
2026-01-28 23:10:50 +07:00
|
|
|
|
- Method `invalidate()` - ลบ cache เมื่อมีการอัปเดต revision
|
2026-01-29 00:05:56 +07:00
|
|
|
|
- Auto cleanup ทุก 30 นาที
|
2026-01-28 23:10:50 +07:00
|
|
|
|
|
|
|
|
|
|
**การใช้งานใน API:**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// OrganizationController.ts - detailSuperAdmin()
|
|
|
|
|
|
const cached = await orgStructureCache.get(revisionId, rootId);
|
|
|
|
|
|
if (cached) return cached;
|
|
|
|
|
|
|
|
|
|
|
|
// ... query และคำนวณข้อมูล ...
|
|
|
|
|
|
await orgStructureCache.set(revisionId, rootId, result);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 5. API Optimization - Promise.all
|
|
|
|
|
|
|
|
|
|
|
|
**ไฟล์:** `src/controllers/OrganizationController.ts`
|
|
|
|
|
|
|
|
|
|
|
|
**ก่อนแก้ไข:**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Query ทีละตัว - sequential
|
|
|
|
|
|
const rootOrg = await this.orgRootRepository.findOne(...);
|
|
|
|
|
|
const position = await this.posMasterRepository.findOne(...);
|
|
|
|
|
|
const ancestors = await this.orgRootRepository.find(...);
|
|
|
|
|
|
// ... อีกหลาย query ...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**หลังแก้ไข:**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Query พร้อมกัน - parallel
|
|
|
|
|
|
const [rootOrg, position, ancestors, ...] = await Promise.all([
|
|
|
|
|
|
this.orgRootRepository.findOne(...),
|
|
|
|
|
|
this.posMasterRepository.findOne(...),
|
|
|
|
|
|
this.orgRootRepository.find(...),
|
|
|
|
|
|
// ... อีกหลาย query ...
|
|
|
|
|
|
]);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**คำอธิบายเชิงเทคนิค:**
|
|
|
|
|
|
- `Promise.all()` ทำให้ query ที่ไม่ depended กันรัน parallel
|
|
|
|
|
|
- ลดเวลา total จาก `t1 + t2 + t3 + ...` เหลือ `max(t1, t2, t3, ...)`
|
|
|
|
|
|
- ตัวอย่าง: ถ้ามี 10 query ใช้เวลา 100ms แต่ละตัว
|
|
|
|
|
|
- Sequential: 10 × 100ms = 1,000ms
|
|
|
|
|
|
- Parallel: ~100ms (เร็วขึ้น 10 เท่า)
|
|
|
|
|
|
|
|
|
|
|
|
**ประโยชน์:** ลด response time อย่างมาก
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 6. OrganizationService Refactoring (ตอนนี้ไม่ได้ใช้เพราะตัด total position counts ออก)
|
|
|
|
|
|
|
|
|
|
|
|
**ไฟล์:** `src/services/OrganizationService.ts`
|
|
|
|
|
|
|
|
|
|
|
|
**ฟังก์ชัน `getPositionCounts()`:**
|
|
|
|
|
|
- Query ข้อมูล position ทั้งหมดใน revision ครั้งเดียว
|
|
|
|
|
|
- สร้าง Map สำหรับ aggregate counts แต่ละระดับ (orgRoot, orgChild1-4)
|
|
|
|
|
|
- Return ผลลัพธ์เป็น Map structure ที่พร้อมใช้งาน
|
|
|
|
|
|
|
|
|
|
|
|
**ประโยชน์:** ลดจำนวน query จากหลายร้อยครั้งเหลือ 1 ครั้ง
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 📊 สรุปการเปลี่ยนแปลง
|
|
|
|
|
|
|
|
|
|
|
|
| ไฟล์ | การเปลี่ยนแปลง | ผลกระทบ |
|
|
|
|
|
|
|------|------------------|---------|
|
|
|
|
|
|
| `src/database/data-source.ts` | +12 บรรทัด | Connection Pool Settings |
|
|
|
|
|
|
| `src/app.ts` | +40 บรรทัด | Graceful Shutdown |
|
|
|
|
|
|
| `src/middlewares/logs.ts` | +2 บรรทัด | Use Memory Cache |
|
|
|
|
|
|
| `src/utils/LogMemoryStore.ts` | New File | Profile/RootId Cache |
|
|
|
|
|
|
| `src/utils/OrgStructureCache.ts` | New File | Org Structure Cache |
|
|
|
|
|
|
| `src/controllers/OrganizationController.ts` | -1006 บรรทัด | Refactor + Promise.all |
|
|
|
|
|
|
| `src/services/OrganizationService.ts` | New File (Not Used) | getPositionCounts Helper |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 🎯 ผลลัพธ์
|
|
|
|
|
|
|
|
|
|
|
|
### ประสิทธิภาพ
|
|
|
|
|
|
- ⚡ Response time ลดลงอย่างมีนัยสำคัญจากการใช้ `Promise.all`
|
|
|
|
|
|
- 💾 จำนวน database query ลดลง 80-90%
|
|
|
|
|
|
- 🔄 Cache hit rate เพิ่มขึ้นสำหรับ request ซ้ำ
|
|
|
|
|
|
|
|
|
|
|
|
### ความมั่นคง
|
|
|
|
|
|
- 🛡️ ป้องกัน connection exhaustion ด้วย connection pool
|
|
|
|
|
|
- 🔌 Graceful shutdown ป้องกัน data loss
|
|
|
|
|
|
- 📝 Log tracking ดีขึ้นด้วย memory store
|
|
|
|
|
|
|
|
|
|
|
|
### Code Quality
|
|
|
|
|
|
- 🧹 Code ลดลง >1,000 บรรทัดจากการ refactoring
|
|
|
|
|
|
- 📦 ฟังก์ชันแยกเป็น module ที่ชัดเจน
|
|
|
|
|
|
- 🔧 ง่ายต่อการ maintain และ test
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 🚀 วิธีการ Deploy
|
|
|
|
|
|
|
|
|
|
|
|
1. **ตรวจสอบ Environment Variables:**
|
|
|
|
|
|
```bash
|
|
|
|
|
|
DB_CONNECTION_LIMIT=50
|
|
|
|
|
|
DB_MAX_IDLE=10
|
|
|
|
|
|
DB_IDLE_TIMEOUT=60000
|
|
|
|
|
|
DB_POOL_SIZE=10
|
|
|
|
|
|
DB_MAX_QUERY_TIME=3000
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
2. **ตรวจสอบ Logs:**
|
|
|
|
|
|
```
|
|
|
|
|
|
[LogMemoryStore] Initialized with 600 second refresh interval
|
|
|
|
|
|
[OrgStructureCache] Initialized
|
|
|
|
|
|
[APP] Application is running on: http://localhost:3000
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 📝 Commits ที่เกี่ยวข้อง
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
1a324af4 fix: api /super-admin/{id} memory cache
|
|
|
|
|
|
7c702295 fix: query use Promise all
|
|
|
|
|
|
5dcb5963 fix: Api GET /super-admin/{id}
|
|
|
|
|
|
e068aafe fix: เพิ่ม Graceful Shutdown - ป้องกัน connection in app file, Log Mnddleware + Memory Store
|
|
|
|
|
|
a194d859 fix: connection pool settings
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
**วันที่สร้างเอกสาร:** 28 มกราคม 2026
|
|
|
|
|
|
**Branch:** fix/optimization-detailSuperAdmin
|
|
|
|
|
|
**ผู้ดำเนินการ:** Warunee.T
|