perf: оптимизировано массовое удаление строк
- Добавлен batch delete метод на бэкенде (удаление множества строк за один запрос) - Использование WHERE IN для удаления нескольких строк одним SQL запросом - Добавлен прогресс-бар при удалении большого количества строк - Удаление 1000 строк теперь занимает секунды вместо минут - Добавлена поддержка транзакций для атомарности операций - Оптимизирован размер батчей для баланса производительности и надежности
This commit is contained in:
@@ -569,7 +569,7 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ УДАЛИТЬ
|
// ✅ УДАЛИТЬ (с реальным прогрессом для больших объемов)
|
||||||
document.getElementById('btnDelete').addEventListener('click', async () => {
|
document.getElementById('btnDelete').addEventListener('click', async () => {
|
||||||
if (!table || !currentSchema || !currentTable) return;
|
if (!table || !currentSchema || !currentTable) return;
|
||||||
|
|
||||||
@@ -586,40 +586,106 @@ document.getElementById('btnDelete').addEventListener('click', async () => {
|
|||||||
|
|
||||||
if (!confirm(confirmMsg)) return;
|
if (!confirm(confirmMsg)) return;
|
||||||
|
|
||||||
try {
|
const rowsArray = Array.from(selectedRowsData.values());
|
||||||
let deleted = 0;
|
|
||||||
let errors = 0;
|
// Для очень больших удалений - показываем прогресс
|
||||||
|
const BATCH_SIZE = 1000; // Удаляем по 1000 строк за раз
|
||||||
|
const batches = [];
|
||||||
|
for (let i = 0; i < rowsArray.length; i += BATCH_SIZE) {
|
||||||
|
batches.push(rowsArray.slice(i, i + BATCH_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batches.length > 1) {
|
||||||
|
// Множество батчей - показываем реальный прогресс
|
||||||
|
const modal = createProgressModal(`Удаление ${count} записей...`);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
const progressBar = modal.querySelector('.progress-bar');
|
||||||
|
const progressText = modal.querySelector('.spinner').parentElement;
|
||||||
|
|
||||||
|
let totalDeleted = 0;
|
||||||
|
let totalErrors = 0;
|
||||||
|
let allErrorMessages = [];
|
||||||
|
|
||||||
// Удаляем все выделенные строки из selectedRowsData
|
|
||||||
for (const [key, rowData] of selectedRowsData) {
|
|
||||||
try {
|
try {
|
||||||
await api('/api/table/delete', 'POST', {
|
for (let i = 0; i < batches.length; i++) {
|
||||||
|
const batch = batches[i];
|
||||||
|
|
||||||
|
progressText.innerHTML = `<span class="spinner" style="display: inline-block; animation: spin 1s linear infinite;">⏳</span> Обработка ${i + 1} из ${batches.length} батчей...`;
|
||||||
|
|
||||||
|
const result = await api('/api/table/delete-batch', 'POST', {
|
||||||
schema: currentSchema,
|
schema: currentSchema,
|
||||||
table: currentTable,
|
table: currentTable,
|
||||||
row: rowData
|
rows: batch
|
||||||
});
|
});
|
||||||
deleted++;
|
|
||||||
} catch (e) {
|
totalDeleted += result.deleted;
|
||||||
console.error(`Ошибка удаления строки ${key}:`, e);
|
totalErrors += result.errors;
|
||||||
errors++;
|
if (result.errorMessages) {
|
||||||
|
allErrorMessages.push(...result.errorMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем прогресс
|
||||||
|
const progress = ((i + 1) / batches.length) * 100;
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
|
||||||
selectedRowsData.clear();
|
selectedRowsData.clear();
|
||||||
updateSelectionCounter();
|
updateSelectionCounter();
|
||||||
await table.replaceData();
|
await table.replaceData();
|
||||||
|
|
||||||
if (errors > 0) {
|
if (totalErrors > 0) {
|
||||||
alert(`Удалено строк: ${deleted}\nОшибок: ${errors}`);
|
const errorsToShow = allErrorMessages.slice(0, 10);
|
||||||
|
const moreErrors = allErrorMessages.length > 10
|
||||||
|
? `\n... и еще ${allErrorMessages.length - 10} ошибок`
|
||||||
|
: '';
|
||||||
|
alert(`Удалено строк: ${totalDeleted}\nОшибок: ${totalErrors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`);
|
||||||
} else {
|
} else {
|
||||||
alert(`✓ Удалено строк: ${deleted}`);
|
alert(`✓ Успешно удалено строк: ${totalDeleted}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
document.body.removeChild(modal);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert('Ошибка удаления: ' + e.message);
|
alert('Ошибка удаления: ' + e.message);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Один батч - используем простое модальное окно
|
||||||
|
const modal = createProgressModal('Удаление записей...');
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api('/api/table/delete-batch', 'POST', {
|
||||||
|
schema: currentSchema,
|
||||||
|
table: currentTable,
|
||||||
|
rows: rowsArray
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
|
||||||
|
selectedRowsData.clear();
|
||||||
|
updateSelectionCounter();
|
||||||
|
await table.replaceData();
|
||||||
|
|
||||||
|
if (result.errors > 0) {
|
||||||
|
const errorsToShow = result.errorMessages.slice(0, 10);
|
||||||
|
const moreErrors = result.errorMessages.length > 10
|
||||||
|
? `\n... и еще ${result.errorMessages.length - 10} ошибок`
|
||||||
|
: '';
|
||||||
|
alert(`Удалено строк: ${result.deleted}\nОшибок: ${result.errors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`);
|
||||||
|
} else {
|
||||||
|
alert(`✓ Удалено строк: ${result.deleted}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
console.error(e);
|
||||||
|
alert('Ошибка удаления: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ CSV ==========
|
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ CSV ==========
|
||||||
|
|
||||||
function detectDelimiter(text) {
|
function detectDelimiter(text) {
|
||||||
|
|||||||
@@ -254,4 +254,23 @@ $app->get('/api/fk-values', function (Request $request, Response $response) use
|
|||||||
$response->getBody()->write(json_encode(['values' => $values]));
|
$response->getBody()->write(json_encode(['values' => $values]));
|
||||||
return $response->withHeader('Content-Type', 'application/json');
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: массовое удаление строк (batch delete)
|
||||||
|
$app->post('/api/table/delete-batch', function (Request $request, Response $response) use ($container) {
|
||||||
|
$payload = json_decode((string)$request->getBody(), true);
|
||||||
|
$pdo = $container->get('db');
|
||||||
|
$meta = new \App\MetaService($pdo);
|
||||||
|
$ds = new \App\DataService($pdo);
|
||||||
|
|
||||||
|
$schema = $payload['schema'];
|
||||||
|
$table = $payload['table'];
|
||||||
|
$rows = $payload['rows'] ?? []; // Массив строк для удаления
|
||||||
|
$metaArr = $meta->getTableMeta($schema, $table);
|
||||||
|
|
||||||
|
$result = $ds->deleteMultipleRows($schema, $table, $rows, $metaArr['primaryKey']);
|
||||||
|
$response->getBody()->write(json_encode($result));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
$app->run();
|
$app->run();
|
||||||
|
|||||||
@@ -435,6 +435,115 @@ class DataService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Массовое удаление строк (оптимизированное)
|
||||||
|
*/
|
||||||
|
public function deleteMultipleRows(string $schema, string $table, array $rows, array $pk): array
|
||||||
|
{
|
||||||
|
if (empty($rows)) {
|
||||||
|
return ['deleted' => 0, 'errors' => 0, 'message' => 'No rows provided'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($pk)) {
|
||||||
|
throw new \RuntimeException('No primary key — delete disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("=== МАССОВОЕ УДАЛЕНИЕ: Schema=$schema, Table=$table, Строк=" . count($rows) . " ===");
|
||||||
|
|
||||||
|
$deleted = 0;
|
||||||
|
$errors = 0;
|
||||||
|
$errorMessages = [];
|
||||||
|
|
||||||
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Если PK состоит из одного поля - используем WHERE IN (самый быстрый способ)
|
||||||
|
if (count($pk) === 1) {
|
||||||
|
$pkField = $pk[0];
|
||||||
|
|
||||||
|
// Группируем по батчам (по 500 строк) для избежания превышения лимитов MySQL
|
||||||
|
$batchSize = 500;
|
||||||
|
$batches = array_chunk($rows, $batchSize);
|
||||||
|
|
||||||
|
foreach ($batches as $batchIndex => $batch) {
|
||||||
|
$pkValues = [];
|
||||||
|
|
||||||
|
foreach ($batch as $row) {
|
||||||
|
if (!array_key_exists($pkField, $row)) {
|
||||||
|
$errors++;
|
||||||
|
$errorMessages[] = "Строка не содержит PK поле: $pkField";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pkValues[] = $row[$pkField];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($pkValues)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем батч за один запрос
|
||||||
|
$placeholders = implode(',', array_fill(0, count($pkValues), '?'));
|
||||||
|
$sql = "DELETE FROM `{$schema}`.`{$table}` WHERE `{$pkField}` IN ($placeholders)";
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute($pkValues);
|
||||||
|
|
||||||
|
$deletedInBatch = $stmt->rowCount();
|
||||||
|
$deleted += $deletedInBatch;
|
||||||
|
|
||||||
|
error_log("Батч " . ($batchIndex + 1) . ": удалено $deletedInBatch строк");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Составной PK - удаляем по одной строке (но в одной транзакции)
|
||||||
|
foreach ($rows as $index => $row) {
|
||||||
|
try {
|
||||||
|
$whereParts = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
foreach ($pk as $name) {
|
||||||
|
if (!array_key_exists($name, $row)) {
|
||||||
|
throw new \RuntimeException("Missing PK value: $name");
|
||||||
|
}
|
||||||
|
$whereParts[] = "`$name` = :pk_$name";
|
||||||
|
$params[":pk_$name"] = $row[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = sprintf(
|
||||||
|
"DELETE FROM `%s`.`%s` WHERE %s",
|
||||||
|
$schema, $table,
|
||||||
|
implode(' AND ', $whereParts)
|
||||||
|
);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$deleted += $stmt->rowCount();
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
$errors++;
|
||||||
|
$errorMessages[] = "Строка " . ($index + 1) . ": " . $e->getMessage();
|
||||||
|
|
||||||
|
if ($errors <= 10) {
|
||||||
|
error_log("Ошибка удаления строки " . ($index + 1) . ": " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pdo->commit();
|
||||||
|
error_log("=== УДАЛЕНИЕ ЗАВЕРШЕНО: удалено=$deleted, ошибок=$errors ===");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->pdo->rollBack();
|
||||||
|
error_log("=== КРИТИЧЕСКАЯ ОШИБКА УДАЛЕНИЯ: " . $e->getMessage() . " ===");
|
||||||
|
throw new \RuntimeException('Delete failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'deleted' => $deleted,
|
||||||
|
'errors' => $errors,
|
||||||
|
'errorMessages' => $errorMessages
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private function arrayToCSV(array $headers, array $rows): string
|
private function arrayToCSV(array $headers, array $rows): string
|
||||||
{
|
{
|
||||||
$output = fopen('php://temp', 'r+');
|
$output = fopen('php://temp', 'r+');
|
||||||
|
|||||||
Reference in New Issue
Block a user