🔧 fix(job): perbaiki transaksi bersarang & optimasi batch processing di GenerateClosingBalanceReportJob

- Hapus transaksi bersarang:
  - Pindahkan DB::beginTransaction() & DB::commit() dari processAndSaveClosingBalanceData() ke handle() menggunakan DB::transaction()
  - Cegah error max_lock_per_transaction pada PostgreSQL

- Implementasi batch processing:
  - Tambah metode batchUpdateOrCreate()
  - Ambil data existing dalam satu query via whereIn()
  - Pisahkan data menjadi toInsert & toUpdate
  - Gunakan DB::table()->insert() untuk batch insert
  - Lakukan update individual untuk data yang sudah ada

- Penyederhanaan error handling:
  - Hapus try-catch di processAndSaveClosingBalanceData() karena sudah ditangani di handle()

- Penambahan logging:
  - Tambah log informasi untuk monitoring batch insert/update

- Optimasi performa:
  - Kurangi beban database & mencegah duplikasi data
  - Gunakan pendekatan "delete-first, then insert" seperti ExportStatementJob
This commit is contained in:
Daeng Deni Mardaeni
2025-08-05 13:49:58 +07:00
parent aae0c4ab15
commit 8a6469ecc9

View File

@@ -44,8 +44,7 @@
/**
* Execute the job dengan optimasi performa
*/
public function handle()
: void
public function handle(): void
{
$reportLog = ClosingBalanceReportLog::find($this->reportLogId);
@@ -62,44 +61,41 @@
'report_log_id' => $this->reportLogId
]);
DB::beginTransaction();
// Update status to processing
$reportLog->update([
'status' => 'processing',
'updated_at' => now()
]);
// Step 1: Process and save to database (fast)
$this->processAndSaveClosingBalanceData();
// Gunakan satu transaksi untuk seluruh proses
DB::transaction(function () use ($reportLog) {
// Step 1: Process and save to database (fast)
$this->processAndSaveClosingBalanceData();
// Step 2: Export from database to CSV (fast)
$filePath = $this->exportFromDatabaseToCsv();
// Step 2: Export from database to CSV (fast)
$filePath = $this->exportFromDatabaseToCsv();
// Get record count from database
$recordCount = $this->getProcessedRecordCount();
// Get record count from database
$recordCount = $this->getProcessedRecordCount();
// Update report log with success
$reportLog->update([
'status' => 'completed',
'file_path' => $filePath,
'file_size' => Storage::disk($this->disk)->size($filePath),
'record_count' => $recordCount,
'updated_at' => now()
]);
// Update report log with success
$reportLog->update([
'status' => 'completed',
'file_path' => $filePath,
'file_size' => Storage::disk($this->disk)->size($filePath),
'record_count' => $recordCount,
'updated_at' => now()
]);
DB::commit();
Log::info('Optimized closing balance report generation completed successfully', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'file_path' => $filePath,
'record_count' => $recordCount
]);
Log::info('Optimized closing balance report generation completed successfully', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'file_path' => $filePath,
'record_count' => $recordCount
]);
});
} catch (Exception $e) {
DB::rollback();
Log::error('Error generating optimized closing balance report', [
'account_number' => $this->accountNumber,
'period' => $this->period,
@@ -121,8 +117,7 @@
* Process and save closing balance data to database dengan proteksi duplikasi
* Memproses dan menyimpan data closing balance dengan perlindungan terhadap duplikasi
*/
private function processAndSaveClosingBalanceData()
: void
private function processAndSaveClosingBalanceData(): void
{
$criteria = [
'account_number' => $this->accountNumber,
@@ -130,62 +125,41 @@
'group_name' => $this->groupName
];
DB::beginTransaction();
// HAPUS DB::beginTransaction() - sudah ada di handle()
try {
// Sederhana: hapus data existing terlebih dahulu seperti ExportStatementJob
$this->deleteExistingProcessedData($criteria);
// Sederhana: hapus data existing terlebih dahulu seperti ExportStatementJob
$this->deleteExistingProcessedData($criteria);
// Get opening balance
$runningBalance = $this->getOpeningBalance();
$sequenceNo = 0;
// Get opening balance
$runningBalance = $this->getOpeningBalance();
$sequenceNo = 0;
Log::info('Starting to process closing balance data', [
'opening_balance' => $runningBalance,
'criteria' => $criteria
]);
Log::info('Starting to process closing balance data', [
'opening_balance' => $runningBalance,
'criteria' => $criteria
]);
// Build query yang sederhana tanpa eliminasi duplicate rumit
$query = $this->buildTransactionQuery();
// Build query yang sederhana tanpa eliminasi duplicate rumit
$query = $this->buildTransactionQuery();
// Proses dan insert data langsung seperti ExportStatementJob
$query->chunk($this->chunkSize, function ($transactions) use (&$runningBalance, &$sequenceNo) {
$processedData = $this->prepareProcessedClosingBalanceData($transactions, $runningBalance, $sequenceNo);
// Proses dan insert data dengan batch updateOrCreate untuk efisiensi
$query->chunk($this->chunkSize, function ($transactions) use (&$runningBalance, &$sequenceNo) {
$processedData = $this->prepareProcessedClosingBalanceData($transactions, $runningBalance, $sequenceNo);
if (!empty($processedData)) {
foreach ($processedData as $data) {
ProcessedClosingBalance::updateOrCreate(
[
'account_number' => $data['account_number'],
'period' => $data['period'],
'trans_reference' => $data['trans_reference'],
'amount_lcy' => $data['amount_lcy'],
],
$data
);
}
}
});
if (!empty($processedData)) {
// Gunakan batch processing untuk updateOrCreate
$this->batchUpdateOrCreate($processedData);
}
});
DB::commit();
// HAPUS DB::commit() - akan di-handle di handle()
$recordCount = $this->getProcessedRecordCount();
Log::info('Closing balance data processing completed successfully', [
'final_sequence' => $sequenceNo,
'final_balance' => $runningBalance,
'record_count' => $recordCount
]);
} catch (Exception $e) {
DB::rollback();
Log::error('Error in processAndSaveClosingBalanceData', [
'error' => $e->getMessage(),
'criteria' => $criteria
]);
throw $e;
}
$recordCount = $this->getProcessedRecordCount();
Log::info('Closing balance data processing completed successfully', [
'final_sequence' => $sequenceNo,
'final_balance' => $runningBalance,
'record_count' => $recordCount
]);
}
/**
@@ -601,4 +575,67 @@
'criteria' => $criteria
]);
}
/**
* Batch update or create untuk mengurangi jumlah query dan lock
* Menggunakan pendekatan yang lebih efisien untuk menghindari max_lock_per_transaction
*/
private function batchUpdateOrCreate(array $processedData): void
{
Log::info('Starting batch updateOrCreate', [
'batch_size' => count($processedData)
]);
// Kumpulkan semua trans_reference yang akan diproses
$transReferences = collect($processedData)->pluck('trans_reference')->toArray();
// Ambil data yang sudah ada dalam satu query
$existingRecords = ProcessedClosingBalance::where('account_number', $this->accountNumber)
->where('period', $this->period)
->where('group_name', $this->groupName)
->whereIn('trans_reference', $transReferences)
->get()
->keyBy(function ($item) {
return $item->trans_reference . '_' . $item->amount_lcy;
});
$toInsert = [];
$toUpdate = [];
foreach ($processedData as $data) {
$key = $data['trans_reference'] . '_' . $data['amount_lcy'];
if ($existingRecords->has($key)) {
// Record sudah ada, siapkan untuk update
$existingRecord = $existingRecords->get($key);
$toUpdate[] = [
'id' => $existingRecord->id,
'data' => $data
];
} else {
// Record baru, siapkan untuk insert
$toInsert[] = $data;
}
}
// Batch insert untuk record baru
if (!empty($toInsert)) {
DB::table('processed_closing_balances')->insert($toInsert);
Log::info('Batch insert completed', ['count' => count($toInsert)]);
}
// Batch update untuk record yang sudah ada
if (!empty($toUpdate)) {
foreach ($toUpdate as $updateItem) {
ProcessedClosingBalance::where('id', $updateItem['id'])
->update($updateItem['data']);
}
Log::info('Batch update completed', ['count' => count($toUpdate)]);
}
Log::info('Batch updateOrCreate completed successfully', [
'inserted' => count($toInsert),
'updated' => count($toUpdate)
]);
}
}