Prevent lost updates with optimistic concurrency checks
This commit is contained in:
@@ -173,15 +173,17 @@ $app->post('/api/table/update', function (Request $request, Response $response)
|
||||
$schema = $payload['schema'];
|
||||
$table = $payload['table'];
|
||||
$row = $payload['row'];
|
||||
$originalRow = $payload['originalRow'] ?? null;
|
||||
$metaArr = $meta->getTableMeta($schema, $table);
|
||||
|
||||
$result = $ds->updateRow($schema, $table, $row, $metaArr['columns'], $metaArr['primaryKey']);
|
||||
$result = $ds->updateRow($schema, $table, $row, $metaArr['columns'], $metaArr['primaryKey'], $originalRow);
|
||||
$response->getBody()->write(json_encode($result));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
} catch (\Exception $e) {
|
||||
$error = ['error' => true, 'message' => $e->getMessage()];
|
||||
$response->getBody()->write(json_encode($error));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
$status = ((int)$e->getCode() === 409 || str_starts_with($e->getMessage(), 'CONFLICT:')) ? 409 : 400;
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus($status);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ async function api(url, method = 'GET', body) {
|
||||
|
||||
if (!res.ok) {
|
||||
// Сервер вернул JSON с ошибкой
|
||||
throw new Error(data.message || data.error || 'Неизвестная ошибка');
|
||||
const rawMessage = data.message || data.error || 'Неизвестная ошибка';
|
||||
const cleanMessage = String(rawMessage).replace(/^CONFLICT:\s*/i, '');
|
||||
throw new Error(cleanMessage);
|
||||
}
|
||||
|
||||
// Проверяем на ошибку в успешном ответе
|
||||
|
||||
@@ -806,7 +806,8 @@ async function showEditModal(selectedRows) {
|
||||
await api('/api/table/update', 'POST', {
|
||||
schema: currentSchema,
|
||||
table: currentTable,
|
||||
row: updatedRow
|
||||
row: updatedRow,
|
||||
originalRow: row
|
||||
});
|
||||
updated++;
|
||||
} catch (err) {
|
||||
|
||||
@@ -566,7 +566,7 @@ async function selectTable(schema, tableName, restoreState = false) {
|
||||
});
|
||||
|
||||
// Функция сохранения строки
|
||||
async function saveRow(rowPos, rowData, rowElement) {
|
||||
async function saveRow(rowPos, rowData, rowElement, originalRowData = null) {
|
||||
console.log('💾 === СОХРАНЕНИЕ ===');
|
||||
console.log(' Data:', rowData);
|
||||
|
||||
@@ -581,7 +581,8 @@ async function selectTable(schema, tableName, restoreState = false) {
|
||||
const result = await api('/api/table/update', 'POST', {
|
||||
schema: currentSchema,
|
||||
table: currentTable,
|
||||
row: rowData
|
||||
row: rowData,
|
||||
originalRow: originalRowData || rowData
|
||||
});
|
||||
|
||||
console.log('✅ СОХРАНЕНО:', result);
|
||||
@@ -694,9 +695,11 @@ async function selectTable(schema, tableName, restoreState = false) {
|
||||
clearTimeout(dirtyRows.get(rowPos).timeout);
|
||||
}
|
||||
|
||||
const originalRowData = { ...rowData, [cell.getField()]: oldValue };
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
console.log('⏰ Автосохранение...');
|
||||
await saveRow(rowPos, rowData, row);
|
||||
await saveRow(rowPos, rowData, row, originalRowData);
|
||||
}, 2000);
|
||||
|
||||
dirtyRows.set(rowPos, { data: rowData, element: row, timeout: timeout });
|
||||
|
||||
@@ -209,7 +209,7 @@ private function formatPDOError(\PDOException $e, string $schema, string $table,
|
||||
}
|
||||
|
||||
|
||||
public function updateRow(string $schema, string $table, array $row, array $columns, array $pk): array
|
||||
public function updateRow(string $schema, string $table, array $row, array $columns, array $pk, ?array $originalRow = null): array
|
||||
{
|
||||
$schema = $this->validateIdentifier($schema, 'schema');
|
||||
$table = $this->validateIdentifier($table, 'table');
|
||||
@@ -248,6 +248,22 @@ public function updateRow(string $schema, string $table, array $row, array $colu
|
||||
$params[":pk_$name"] = $row[$name];
|
||||
}
|
||||
|
||||
$trackedColumns = [];
|
||||
if (is_array($originalRow)) {
|
||||
foreach ($columns as $c) {
|
||||
$name = $this->validateIdentifier((string)$c['COLUMN_NAME'], 'column');
|
||||
if (in_array($name, $pk, true)) {
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists($name, $originalRow)) {
|
||||
continue;
|
||||
}
|
||||
$whereParts[] = "`$name` <=> :orig_$name";
|
||||
$params[":orig_$name"] = $originalRow[$name];
|
||||
$trackedColumns[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
"UPDATE `%s`.`%s` SET %s WHERE %s",
|
||||
$schema, $table,
|
||||
@@ -262,6 +278,44 @@ public function updateRow(string $schema, string $table, array $row, array $colu
|
||||
$stmt->execute($params);
|
||||
$rowCount = $stmt->rowCount();
|
||||
error_log("Rows updated: $rowCount");
|
||||
|
||||
if ($rowCount === 0 && !empty($trackedColumns)) {
|
||||
$pkWhere = [];
|
||||
$selectParams = [];
|
||||
foreach ($pk as $name) {
|
||||
$name = $this->validateIdentifier((string)$name, 'column');
|
||||
$pkWhere[] = "`$name` = :pk_$name";
|
||||
$selectParams[":pk_$name"] = $row[$name];
|
||||
}
|
||||
|
||||
$selectedColumns = array_merge($pk, $trackedColumns);
|
||||
$selectSql = sprintf(
|
||||
"SELECT %s FROM `%s`.`%s` WHERE %s LIMIT 1",
|
||||
implode(', ', array_map(fn($col) => "`$col`", $selectedColumns)),
|
||||
$schema,
|
||||
$table,
|
||||
implode(' AND ', $pkWhere)
|
||||
);
|
||||
$selectStmt = $this->pdo->prepare($selectSql);
|
||||
$selectStmt->execute($selectParams);
|
||||
$currentRow = $selectStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($currentRow === false) {
|
||||
throw new \RuntimeException('CONFLICT: Запись была удалена другим пользователем.', 409);
|
||||
}
|
||||
|
||||
foreach ($trackedColumns as $name) {
|
||||
$expected = $originalRow[$name] ?? null;
|
||||
$actual = $currentRow[$name] ?? null;
|
||||
if ($expected != $actual) {
|
||||
throw new \RuntimeException(
|
||||
'CONFLICT: Запись была изменена в другой сессии. Обновите таблицу и повторите.',
|
||||
409
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['updated' => $rowCount];
|
||||
} catch (\PDOException $e) {
|
||||
error_log("UPDATE FAILED: " . $e->getMessage());
|
||||
|
||||
Reference in New Issue
Block a user