refactor: [performance] N+1 쿼리 3건 배치 조회로 최적화
- WorkOrderService.getMaterials(): 기존 BOM 루프 내 find() x2 제거 → 루프 전 bomItemsMap/bomChildItemsMap 일괄 사전 로드 - OrderService.createWorkOrderFromOrder(): 루프 내 DB 쿼리 x2 제거 → item_code→id, process_items 사전 배치 조회 - OrderService.checkBendingStockForOrder(): 루프 내 StockService 호출 제거 → Stock 배치 조회 후 맵 참조
This commit is contained in:
@@ -1236,7 +1236,7 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
}
|
||||
}
|
||||
|
||||
// item_code → item_id 매핑 구축 (fallback용)
|
||||
// item_code → item_id 매핑 구축 (fallback용 — N+1 방지를 위해 사전 일괄 조회)
|
||||
$codeToIdMap = [];
|
||||
if (! empty($bomItemIds)) {
|
||||
$codeToIdRows = DB::table('items')
|
||||
@@ -1250,6 +1250,38 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
}
|
||||
}
|
||||
|
||||
// order_items의 item_code로 추가 매핑 사전 구축 (루프 내 DB 조회 방지)
|
||||
$orderItemCodes = $order->items->pluck('item_code')->filter()->unique()->values()->all();
|
||||
$unmappedCodes = array_diff($orderItemCodes, array_keys($codeToIdMap));
|
||||
if (! empty($unmappedCodes)) {
|
||||
$extraRows = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('code', $unmappedCodes)
|
||||
->whereNull('deleted_at')
|
||||
->select('id', 'code')
|
||||
->get();
|
||||
foreach ($extraRows as $row) {
|
||||
$codeToIdMap[$row->code] = $row->id;
|
||||
}
|
||||
}
|
||||
|
||||
// 사전 매핑된 item_id에 대한 process_items도 일괄 조회
|
||||
$allResolvedIds = array_values(array_unique(array_merge(
|
||||
array_keys($itemProcessMap),
|
||||
array_values($codeToIdMap)
|
||||
)));
|
||||
$unmappedProcessIds = array_diff($allResolvedIds, array_keys($itemProcessMap));
|
||||
if (! empty($unmappedProcessIds)) {
|
||||
$extraProcessItems = DB::table('process_items')
|
||||
->whereIn('item_id', $unmappedProcessIds)
|
||||
->where('is_active', true)
|
||||
->select('item_id', 'process_id')
|
||||
->get();
|
||||
foreach ($extraProcessItems as $pi) {
|
||||
$itemProcessMap[$pi->item_id] = $pi->process_id;
|
||||
}
|
||||
}
|
||||
|
||||
// order_items를 공정별로 그룹화 (BOM item_id → process 매핑 활용)
|
||||
$itemsByProcess = [];
|
||||
foreach ($order->items as $orderItem) {
|
||||
@@ -1268,31 +1300,11 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. fallback: item_code로 items 마스터 조회 → process_items 매핑
|
||||
// 3. fallback: 사전 구축된 맵에서 item_code → process 매핑 (N+1 제거)
|
||||
if ($processId === null && $orderItem->item_code) {
|
||||
$resolvedId = $codeToIdMap[$orderItem->item_code] ?? null;
|
||||
if (! $resolvedId) {
|
||||
$resolvedId = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $orderItem->item_code)
|
||||
->whereNull('deleted_at')
|
||||
->value('id');
|
||||
if ($resolvedId) {
|
||||
$codeToIdMap[$orderItem->item_code] = $resolvedId;
|
||||
}
|
||||
}
|
||||
if ($resolvedId && isset($itemProcessMap[$resolvedId])) {
|
||||
$processId = $itemProcessMap[$resolvedId];
|
||||
} elseif ($resolvedId) {
|
||||
// process_items에서도 조회
|
||||
$pi = DB::table('process_items')
|
||||
->where('item_id', $resolvedId)
|
||||
->where('is_active', true)
|
||||
->value('process_id');
|
||||
if ($pi) {
|
||||
$processId = $pi;
|
||||
$itemProcessMap[$resolvedId] = $pi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1893,16 +1905,22 @@ public function checkBendingStockForOrder(int $orderId): array
|
||||
return [];
|
||||
}
|
||||
|
||||
$stockService = app(StockService::class);
|
||||
// 배치 조회로 N+1 방지 (루프 내 개별 Stock 조회 제거)
|
||||
$bendingItemIds = $bendingItems->pluck('id')->all();
|
||||
$stocksMap = \App\Models\Tenants\Stock::where('tenant_id', $tenantId)
|
||||
->whereIn('item_id', $bendingItemIds)
|
||||
->get()
|
||||
->keyBy('item_id');
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($bendingItems as $item) {
|
||||
$neededQty = $itemQtyMap[$item->id];
|
||||
$stockInfo = $stockService->getAvailableStock($item->id);
|
||||
$stock = $stocksMap->get($item->id);
|
||||
|
||||
$availableQty = $stockInfo ? (float) $stockInfo['available_qty'] : 0;
|
||||
$reservedQty = $stockInfo ? (float) $stockInfo['reserved_qty'] : 0;
|
||||
$stockQty = $stockInfo ? (float) $stockInfo['stock_qty'] : 0;
|
||||
$availableQty = $stock ? (float) $stock->available_qty : 0;
|
||||
$reservedQty = $stock ? (float) $stock->reserved_qty : 0;
|
||||
$stockQty = $stock ? (float) $stock->stock_qty : 0;
|
||||
$shortfallQty = max(0, $neededQty - $availableQty);
|
||||
|
||||
$result[] = [
|
||||
|
||||
@@ -1486,6 +1486,35 @@ public function getMaterials(int $workOrderId): array
|
||||
->keyBy('id');
|
||||
}
|
||||
|
||||
// ── Step 1.5: 기존 BOM용 item_id 일괄 사전 로드 (N+1 방지) ──
|
||||
$bomParentItemIds = $workOrder->items->pluck('item_id')->filter()->unique()->values()->all();
|
||||
$bomItemsMap = collect();
|
||||
$bomChildItemsMap = collect();
|
||||
if (! empty($bomParentItemIds)) {
|
||||
$bomItemsMap = \App\Models\Items\Item::where('tenant_id', $tenantId)
|
||||
->whereIn('id', $bomParentItemIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
// BOM 자식 item_id 수집 및 배치 조회
|
||||
$allChildIds = [];
|
||||
foreach ($bomItemsMap as $parentItem) {
|
||||
if (! empty($parentItem->bom)) {
|
||||
foreach ($parentItem->bom as $bomEntry) {
|
||||
if (! empty($bomEntry['child_item_id'])) {
|
||||
$allChildIds[] = $bomEntry['child_item_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! empty($allChildIds)) {
|
||||
$bomChildItemsMap = \App\Models\Items\Item::where('tenant_id', $tenantId)
|
||||
->whereIn('id', array_unique($allChildIds))
|
||||
->get()
|
||||
->keyBy('id');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 2: 유니크 자재 목록 수집 ──
|
||||
// 키: dynamic_bom → "{item_id}_{woItem_id}", 기존 BOM → "{item_id}"
|
||||
$uniqueMaterials = [];
|
||||
@@ -1527,12 +1556,11 @@ public function getMaterials(int $workOrderId): array
|
||||
continue; // dynamic_bom이 있으면 기존 BOM fallback 건너뜀
|
||||
}
|
||||
|
||||
// 기존 BOM 로직 (하위 호환)
|
||||
// 기존 BOM 로직 (하위 호환) — 사전 로드된 맵 사용
|
||||
$materialItems = [];
|
||||
|
||||
if ($woItem->item_id) {
|
||||
$item = \App\Models\Items\Item::where('tenant_id', $tenantId)
|
||||
->find($woItem->item_id);
|
||||
$item = $bomItemsMap[$woItem->item_id] ?? null;
|
||||
|
||||
if ($item && ! empty($item->bom)) {
|
||||
foreach ($item->bom as $bomItem) {
|
||||
@@ -1543,8 +1571,7 @@ public function getMaterials(int $workOrderId): array
|
||||
continue;
|
||||
}
|
||||
|
||||
$childItem = \App\Models\Items\Item::where('tenant_id', $tenantId)
|
||||
->find($childItemId);
|
||||
$childItem = $bomChildItemsMap[$childItemId] ?? null;
|
||||
|
||||
if (! $childItem) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user