Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 105 · После prepared statement и транзакций

MySQL: импорт CSV, частичный откат и восстановление после ошибки

Проверка строк, транзакционные партии, полный откат файла, SAVEPOINT, повтор по хешу и понятные сообщения SQLSTATE с реальными таблицами результата.

Сначала договор импорта, затем цикл запросов

Импорт — это не просто цикл INSERT. Нужно решить, что считать успехом, что делать с ошибочной записью, где фиксировать прогресс и что произойдёт при повторе файла. Эти решения видны пользователю: «принято 3, отклонено 2» и «не принято ничего» — разные полезные результаты.

ПолитикаЧто будет при сбоеСценарий ниже
Проверить и принять хорошие строкиПредыдущие успешные строки остаютсяCSV с отчётом отказов
Подтверждать партииПодтверждённые партии остаются; текущая откатывается2 + 2 + 1 записи
Весь файл атомарноИзменения файла откатываются целикомДубликат на третьей строке
Savepoint для независимой записиРазрешённая к пропуску запись отменяется; хорошие фиксируются в концеCHECK и повтор SKU
Ключ содержимого файлаТот же подтверждённый файл повторно не применяетсяSHA-256 и журнал импортов

Выбор делается до написания catch. Нельзя сначала проглотить все исключения, а потом пытаться по количеству строк понять, что случилось. В примерах исходные CSV полностью показаны и входят в архив. Значения не передаются конкатенацией в SQL: подготовленный запрос принимает их отдельно; смысл полей проверяется отдельно.

Путь данных
  1. CSV → проверенный заголовок и массив полей
  2. Проверка формата и бизнес-ограничений
  3. Подготовленный INSERT/UPDATE в выбранной транзакционной границе
  4. COMMIT либо явный ROLLBACK
  5. Таблица принятых строк, отказов и состояния повторного запуска

Таблицы и ограничения PRIMARY KEY, CHECK, FOREIGN KEY остаются последним рубежом целостности. Предварительная проверка CSV делает сообщение понятнее, но не заменяет ограничение в базе: данные могут поступать и другим путём. Для больших файлов развивайте этот договор в потоковый импорт с ограниченной партией; пять строк здесь нужны, чтобы видеть каждую.

Самостоятельный запуск и исходные CSV

Все полные программы запускаются из CLI на отдельной локальной учебной базе MySQL 8.4/InnoDB с PHP 8.2+ и mysqli/mysqlnd. Контрольный стенд — PHP 8.5.11 и MySQL 8.4.13. Настройка подключения объясняется в первом подключении. В архив уже включены helper и входные CSV: не нужно восстанавливать неизвестные файлы из соседнего примера.

После распаковки задайте пять переменных окружения PHPMAN_MYSQL_HOST, PHPMAN_MYSQL_PORT, PHPMAN_MYSQL_DATABASE, PHPMAN_MYSQL_USER, PHPMAN_MYSQL_PASSWORD и выполните php example.php. Пароль вводится локально и не входит в ZIP. Для собственной учебной базы нужны права чтения/записи, CREATE/DROP и REFERENCES. Результат открывается в result.html, таблицы доступны также в result.json; короткая строка консоли только сообщает, что файлы сохранены.

В коде A — $db, B — $other. Это разные mysqli-соединения к одному локальному серверу, что проверяется по thread_id. Команды чередуются в одном процессе для воспроизводимости. Опыты проверяют поведение сессий, а не HTTP, скорость или тысячу параллельных пользователей.

Принять хорошие строки CSV и объяснить отказы

Поставщик прислал CSV с товарами. Хорошие записи нужно сохранить, а ошибки вернуть с номером записи и понятной причиной. Нулевой остаток — нормальные данные; отрицательное количество и артикул с пробелом правилам этого каталога не подходят.

Исходные данные

input.csv; первая строка — заголовок

skunameqty
TEAЧай2
COFFEEКофе0
BADОтрицательное-1
BAD SKUПробел5
MUGКружка3

Что получится

Принятые товары

skunameqty
COFFEEКофе0
MUGКружка3
TEAЧай2

Отказы по строкам файла

CSV-строкаskuПричина
4BADQuantity: integer 0..999999
5BAD SKUInvalid SKU

Ключевой запрос и его смысл

INSERT INTO mysql107_import_validated (sku,name,qty)
VALUES (?,?,?);

CSV — текстовая таблица с разделителями и кавычками. Не разбирайте её explode(","): запятая может быть частью поля в кавычках. fgetcsv читает одну CSV-запись как массив. В helper явно заданы запятая, двойная кавычка и пустой escape; заголовок строго сравнивается с sku,name,qty. Мы принимаем только UTF-8 без BOM с таким порядком полей.

Значения CSV сначала строки. До приведения qty к int проверяется весь текст: 0 или положительное десятичное число до шести цифр. Поэтому «12товаров», пустота и -1 не становятся молча допустимым числом. Артикул по договору состоит из ASCII A–Z, цифр, подчёркивания и дефиса; имя проверяется на непустоту и максимум 60 UTF-8-байт. Это специально более строгая учебная граница, чем VARCHAR(60), который считает символы.

Подготовленный INSERT создаётся один раз; execute передаёт три значения в позиции ?. Они не становятся частью текста SQL. prepared statement отделяет данные от команды, но не решает, допустимы ли отрицательные остатки или неправильный артикул: это отдельная проверка.

В этом коротком файле записи не содержат переносов внутри кавычек, поэтому счётчик совпадает с физическими номерами строк. Для многострочных полей называйте его номером CSV-записи или ведите отдельный учёт физических строк. Пустая строка тоже не обязана быть концом файла; helper читает до false.

Параметры, возвращаемые значения и варианты API: mysqli_prepare, mysqli_stmt_execute. В этой главе используется объектная запись тех же операций; справочник объясняет и процедурную форму. В карточках отдельно указана граница проверки каждого старого примера.

Ошибки и границы применения

Здесь намеренно выбрана политика частичного приёма, и успешные INSERT фиксируются автоматически. Сбой SQL после принятой строки не удалит предыдущие. Дубликат SKU, кодировка, BOM и неожиданное число полей требуют заранее определённого договора; не маскируйте их INSERT IGNORE. Варианты «всё или ничего» и «пропуск с savepoint» разбираются дальше. Для большого файла читайте поток и копите только текущую партию; helper этого маленького опыта хранит пять записей в массиве.

Полная самостоятельная программа PHP и все входные файлы
Принять хорошие строки CSV и объяснить отказыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mysql107-transactions-helper.php';
$db=app107Connect(); $other=null; $created=[]; $blocks=[]; $checks=[]; $diagnostics=[];
try {
app107Create($db,$created,'mysql107_import_validated','sku VARCHAR(20) PRIMARY KEY, name VARCHAR(60) NOT NULL, qty INT NOT NULL CHECK(qty>=0)');
$rows=app107Csv('input.csv',['sku','name','qty']);$rejected=[];$stmt=$db->prepare('INSERT INTO mysql107_import_validated(sku,name,qty) VALUES(?,?,?)');
foreach($rows as $row){
    $values=$row['values'];$reason='';
    if(count($values)!==3){$reason='Expected 3 columns';}
    elseif(!preg_match('/\A[A-Z][A-Z0-9_-]{0,19}\z/D',$values[0])){$reason='Invalid SKU';}
    elseif($values[1]==='' || strlen($values[1])>60){$reason='Name: 1..60 UTF-8 bytes';}
    elseif(!preg_match('/\A(?:0|[1-9][0-9]{0,5})\z/D',$values[2])){$reason='Quantity: integer 0..999999';}
    if($reason!==''){$rejected[]=[(string)$row['line'],$values[0]??'',$reason];continue;}
    $stmt->execute([$values[0],$values[1],(int)$values[2]]);
} $stmt->close();
$accepted=app107Read($db,'SELECT sku,name,qty FROM mysql107_import_validated ORDER BY sku');
$blocks=[app107Block('Принятые товары',['sku','name','qty'],$accepted),app107Block('Отказы по строкам файла',['CSV-строка','sku','Причина'],$rejected)];
$checks=['three_accepted'=>count($accepted)===3,'two_rejected'=>count($rejected)===2,'zero_preserved'=>$accepted[0][2]==='0'];
    app107Report('mysql107-import-validate',$blocks,$checks,$db,$diagnostics);
} finally {
    app107Cleanup($db,$other,$created);
}
echo "mysql107-import-validate: saved result.html and result.json\n";
Ожидаемый результат
mysql107-import-validate: saved result.html and result.json

Программа создаёт показанные данные в своей учебной таблице, выполняет действия и сохраняет result.html/result.json. Архив содержит mysql107-transactions-helper.php, README.txt и нужные входные CSV. Таблицы результата показаны выше; сообщения консоли — только уведомление о сохранённых файлах.

Открыть сохранённый результат реального запуска · Скачать таблицы JSON.

Попробуйте сами

Замените количество у COFFEE с 0 на пустую строку: запись должна попасть в отказ, а не в нулевой остаток. Затем добавьте запятую в название, правильно заключив поле в CSV-кавычки. fgetcsv должен сохранить её как часть одного имени.

Фиксировать импорт небольшими партиями

Импорт большой, и держать одну транзакцию на весь файл неудобно. Разделим его на небольшие партии, сохраняя понятную границу: первые партии уже подтверждены, текущую ещё можно откатить.

Исходные данные

input.csv

skuqty
A1
B2
C3
D4
E5

Что получится

После каждого COMMIT

ПартияСтрок в партииВидно второму соединению
122
224
315

Итог

skuqty
A1
B2
C3
D4
E5

Ключевой запрос и его смысл

START TRANSACTION;
INSERT INTO mysql107_import_batches(sku,qty) VALUES ('A',1);
INSERT INTO mysql107_import_batches(sku,qty) VALUES ('B',2);
COMMIT;
-- Следующая партия — другая транзакция.

В примере пять исходных записей разбиваются array_chunk на партии 2,2,1. Для каждой вызывается begin_transaction, затем повторно используется подготовленный INSERT и только после успешной партии — commit. Во втором реальном соединении после каждого COMMIT выполняется COUNT(*): видны 2, затем 4, затем 5 строк.

Фиксация партиями
  1. A: BEGIN → строки A,B → COMMIT; наблюдатель B видит 2
  2. A: BEGIN → строки C,D → COMMIT; наблюдатель B видит 4
  3. A: BEGIN → строка E → COMMIT; наблюдатель B видит 5

Меньшая партия сокращает длительность удержания её блокировок и объём одного отката. Но это другой бизнес-договор: файл уже не атомарен целиком. Если третья партия упадёт, первые четыре записи останутся. Чтобы безопасно продолжить после сбоя, сохраните идентификатор файла, подтверждённую границу и правило повторной обработки; один номер строки без проверки содержимого файла недостаточен.

Размер 2 выбран, чтобы каждый шаг был виден. Универсального «правильного» количества строк нет: учитываются размер строки, индексы, конкуренция, время транзакции и ограничения запроса. Подготовленный INSERT повторяется, но весь файл не собирается в одну огромную SQL-строку. В нашем маленьком helper файл прочитан в массив; на большом входе накапливают ограниченный буфер и освобождают его после COMMIT.

Параметры, возвращаемые значения и варианты API: mysqli_begin_transaction, mysqli_commit, mysqli_rollback. В этой главе используется объектная запись тех же операций; справочник объясняет и процедурную форму. В карточках отдельно указана граница проверки каждого старого примера.

Ошибки и границы применения

Не объявляйте пять строк успешно импортированными до последнего подтверждения. Не пытайтесь одним rollback отменить уже зафиксированные партии: нужны отдельная компенсирующая операция или staging/публикация версии набора. Автоповтор после потери соединения опасен без ключей: COMMIT мог пройти, хотя подтверждение до PHP не дошло.

Полная самостоятельная программа PHP и все входные файлы
Фиксировать импорт небольшими партиямиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mysql107-transactions-helper.php';
$db=app107Connect(); $other=null; $created=[]; $blocks=[]; $checks=[]; $diagnostics=[];
try {
app107Create($db,$created,'mysql107_import_batches','sku VARCHAR(20) PRIMARY KEY, qty INT NOT NULL');$other=app107Connect();
$rows=app107Csv('input.csv',['sku','qty']);$stmt=$db->prepare('INSERT INTO mysql107_import_batches(sku,qty) VALUES(?,?)');$history=[];
foreach(array_chunk($rows,2) as $index=>$batch){
    $db->begin_transaction();
    try{foreach($batch as $row){[$sku,$qty]=$row['values'];$stmt->execute([$sku,(int)$qty]);}$db->commit();}
    catch(Throwable $error){$db->rollback();throw $error;}
    $seen=app107Read($other,'SELECT COUNT(*) FROM mysql107_import_batches')[0][0];
    $history[]=[(string)($index+1),(string)count($batch),$seen];
}$stmt->close();
$stored=app107Read($db,'SELECT sku,qty FROM mysql107_import_batches ORDER BY sku');
$blocks=[app107Block('После каждого COMMIT',['Партия','Строк в партии','Видно второму соединению'],$history),app107Block('Итог',['sku','qty'],$stored)];
$checks=['three_batches'=>count($history)===3,'visible_after_commits'=>array_column($history,2)===['2','4','5'],'five_stored'=>count($stored)===5,'two_sessions'=>$db->thread_id!==$other->thread_id];
    app107Report('mysql107-import-batches',$blocks,$checks,$db,$diagnostics);
} finally {
    app107Cleanup($db,$other,$created);
}
echo "mysql107-import-batches: saved result.html and result.json\n";
Ожидаемый результат
mysql107-import-batches: saved result.html and result.json

Программа создаёт показанные данные в своей учебной таблице, выполняет действия и сохраняет result.html/result.json. Архив содержит mysql107-transactions-helper.php, README.txt и нужные входные CSV. Таблицы результата показаны выше; сообщения консоли — только уведомление о сохранённых файлах.

Открыть сохранённый результат реального запуска · Скачать таблицы JSON.

Попробуйте сами

Добавьте в пятую запись дубликат A. Третья партия должна откатиться, первые A–D останутся. Затем определите, что пользователь должен увидеть: «4 принято, партия 3 отклонена», а не общий неопределённый результат «ошибка импорта».

Откатить весь файл при ошибке одной строки

Файл представляет одну согласованную загрузку. Если артикул повторяется, нельзя оставить половину его строк: нужно сохранить исходное состояние и сообщить, какой запрос остановил импорт.

Исходные данные

До файла

skuqty
BASE9

input.csv

skuqty
A2
B4
A7

Что получится

До импорта

skuqty
BASE9

После отказа

skuqty
BASE9

Причина остановки

Попыток INSERTКодПолитика
31062ROLLBACK whole file

Ключевой запрос и его смысл

START TRANSACTION;
INSERT INTO mysql107_import_atomic(sku,qty) VALUES('A',2);
INSERT INTO mysql107_import_atomic(sku,qty) VALUES('B',4);
INSERT INTO mysql107_import_atomic(sku,qty) VALUES('A',7); -- 1062
ROLLBACK; -- приложение откатывает и успешные A/B

Строка BASE существует до начала транзакции. Внутри одной транзакции вставляются A и B, затем снова A. PRIMARY KEY отклоняет третью команду с 1062. Обработчик вызывает rollback, который отменяет также первые две успешные вставки этой транзакции. BASE остаётся, потому что была подтверждена раньше.

Важная граница: сама ошибка дубликата обычно откатывает ошибочную SQL-команду, а не автоматически всю ранее выполненную работу транзакции. Именно приложение выбирает политику «весь файл» и завершает её rollback. Если после перехвата бездумно дойти до commit, A и B могут сохраниться.

СостояниеСодержимое
До BEGINBASE=9
Внутри транзакции после двух INSERTBASE, A, B видны собственной сессии
Третий INSERTОшибка 1062; приложение выбирает полный откат
После ROLLBACKТолько BASE=9

DDL — создание, изменение и удаление обычных таблиц — выполняется до опыта или в окончательной очистке. Не помещайте CREATE/ALTER в середину импорта, рассчитывая откатить их вместе со строками: такие команды могут неявно завершить транзакцию. Полный архив сначала готовит схему, а затем начинает именно транзакцию данных.

Параметры, возвращаемые значения и варианты API: mysqli_begin_transaction, mysqli_rollback. В этой главе используется объектная запись тех же операций; справочник объясняет и процедурную форму. В карточках отдельно указана граница проверки каждого старого примера.

Ошибки и границы применения

Атомарность файла не равна предварительной проверке всех данных. При больших файлах длинная транзакция может быть дорогой; альтернативой служит загрузка во временный набор приложения с последующей проверкой и коротким переключением активной версии. Не путайте ожидаемый дубликат учебного примера с сетевым сбоем: неизвестные ошибки передаются выше после отката.

Полная самостоятельная программа PHP и все входные файлы
Откатить весь файл при ошибке одной строкиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mysql107-transactions-helper.php';
$db=app107Connect(); $other=null; $created=[]; $blocks=[]; $checks=[]; $diagnostics=[];
try {
app107Create($db,$created,'mysql107_import_atomic','sku VARCHAR(20) PRIMARY KEY, qty INT NOT NULL');
$db->query("INSERT INTO mysql107_import_atomic VALUES('BASE',9)");$before=app107Read($db,'SELECT sku,qty FROM mysql107_import_atomic ORDER BY sku');
$rows=app107Csv('input.csv',['sku','qty']);$stmt=$db->prepare('INSERT INTO mysql107_import_atomic(sku,qty) VALUES(?,?)');$attempted=0;$code=0;
$db->begin_transaction();
try{foreach($rows as $row){++$attempted;[$sku,$qty]=$row['values'];$stmt->execute([$sku,(int)$qty]);}$db->commit();}
catch(mysqli_sql_exception $error){$db->rollback();if($error->getCode()!==1062){throw $error;}$code=$error->getCode();}
$stmt->close();$after=app107Read($db,'SELECT sku,qty FROM mysql107_import_atomic ORDER BY sku');
$blocks=[app107Block('До импорта',['sku','qty'],$before),app107Block('После отказа',['sku','qty'],$after),app107Block('Причина остановки',['Попыток INSERT','Код','Политика'],[[(string)$attempted,(string)$code,'ROLLBACK whole file']])];
$checks=['duplicate_third'=>$attempted===3&&$code===1062,'whole_file_rolled_back'=>$after===$before&&$after===[['BASE','9']]];
    app107Report('mysql107-import-atomic',$blocks,$checks,$db,$diagnostics);
} finally {
    app107Cleanup($db,$other,$created);
}
echo "mysql107-import-atomic: saved result.html and result.json\n";
Ожидаемый результат
mysql107-import-atomic: saved result.html and result.json

Программа создаёт показанные данные в своей учебной таблице, выполняет действия и сохраняет result.html/result.json. Архив содержит mysql107-transactions-helper.php, README.txt и нужные входные CSV. Таблицы результата показаны выше; сообщения консоли — только уведомление о сохранённых файлах.

Открыть сохранённый результат реального запуска · Скачать таблицы JSON.

Попробуйте сами

Исправьте последнюю строку A,7 на C,7: теперь COMMIT сохранит BASE,A,B,C. Оставьте дубликат, но мысленно уберите rollback из обработчика: объясните, почему одного сообщения «1062» недостаточно для гарантии пустого импорта.

Практический вопрос: Duplicate key error does not cancel/rollback MySQL transaction. Автор ожидает, что повтор ключа отменит всю транзакцию, но после COMMIT остаётся первая вставка. В нашем файле явный rollback отменяет ранее успешные A/B и сохраняет подтверждённую BASE. Ситуация использована для постановки задачи; правила сверены с первичной документацией, код и данные примера написаны отдельно.

Пропустить отдельные ошибки по явной политике

Для независимых товаров разрешён частичный импорт: хорошие строки должны сохраниться, отрицательный остаток и повторный артикул — попасть в отчёт отказов. При этом несколько SQL-команд одной строки можно откатить вместе до её контрольной точки.

Исходные данные

input.csv

skuqty
A2
B-1
C4
A7

Что получится

Принятые строки

skuqty
A2
C4

Пропущенные строки

CSV-строкаskuКод
3B3819
5A1062

Ключевой запрос и его смысл

START TRANSACTION;
SAVEPOINT row_guard;
INSERT INTO mysql107_import_savepoints(sku,qty) VALUES('B',-1);
-- Если это разрешённая к пропуску ошибка 3819:
ROLLBACK TO SAVEPOINT row_guard;
RELEASE SAVEPOINT row_guard;
-- Продолжить остальные строки; COMMIT только в конце файла.
COMMIT;

SAVEPOINT row_guard ставит именованную точку внутри уже открытой транзакции. При известной ошибке ROLLBACK TO SAVEPOINT row_guard отменяет изменения после этой точки, сохраняя более раннюю работу. RELEASE SAVEPOINT удаляет имя точки; это не COMMIT. Лишь последний commit подтверждает все принятые строки.

PHP вызывает savepoint и release_savepoint через mysqli. Для отката до точки отправляет точную SQL-команду ROLLBACK TO SAVEPOINT row_guard. Имя фиксировано кодом; в него не подставляются данные CSV. В каждой итерации точка создаётся заново, поэтому один идентификатор достаточен.

Вход A=2 проходит. B=-1 нарушает CHECK и получает 3819. C=4 проходит. Повтор A получает 1062. Только эти два номера объявлены допустимыми к пропуску; любое другое исключение выходит во внешний catch, который откатывает весь импорт. Это явная политика качества данных, а не catch всех ошибок с продолжением.

После отката до точки транзакция остаётся открыта. Ранее взятые блокировки в общем случае не освобождаются как при полном rollback; отдельные детали вставленных строк отличаются. Поэтому savepoint не является способом бесконечно держать большую транзакцию, «освобождая» её по одной строке.

Параметры, возвращаемые значения и варианты API: mysqli_savepoint, mysqli_release_savepoint, mysqli_rollback. В этой главе используется объектная запись тех же операций; справочник объясняет и процедурную форму. В карточках отдельно указана граница проверки каждого старого примера.

Ошибки и границы применения

Если строки файла зависят друг от друга, частичный приём может нарушить смысл всего набора даже при соблюдении FK. Решение о пропуске должно принимать правило импорта, а не один технический код. CREATE/ALTER/TRUNCATE в середине могут уничтожить границы savepoint через неявный commit. Deadlock способен откатить всю транзакцию: не пытайтесь без проверки продолжить её старые точки.

Полная самостоятельная программа PHP и все входные файлы
Пропустить отдельные ошибки по явной политикеСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mysql107-transactions-helper.php';
$db=app107Connect(); $other=null; $created=[]; $blocks=[]; $checks=[]; $diagnostics=[];
try {
app107Create($db,$created,'mysql107_import_savepoints','sku VARCHAR(20) PRIMARY KEY, qty INT NOT NULL, CONSTRAINT mysql107_import_nonnegative CHECK(qty>=0)');
$rows=app107Csv('input.csv',['sku','qty']);$stmt=$db->prepare('INSERT INTO mysql107_import_savepoints(sku,qty) VALUES(?,?)');$rejects=[];
$db->begin_transaction();
try{
    foreach($rows as $row){
        $db->savepoint('row_guard');
        try{[$sku,$qty]=$row['values'];$stmt->execute([$sku,(int)$qty]);}
        catch(mysqli_sql_exception $error){
            if(!in_array($error->getCode(),[1062,3819],true)){throw $error;}
            $db->query('ROLLBACK TO SAVEPOINT row_guard');
            $rejects[]=[(string)$row['line'],$sku,(string)$error->getCode()];
        }
        $db->release_savepoint('row_guard');
    }
    $db->commit();
}catch(Throwable $error){$db->rollback();throw $error;}
$stmt->close();$stored=app107Read($db,'SELECT sku,qty FROM mysql107_import_savepoints ORDER BY sku');
$blocks=[app107Block('Принятые строки',['sku','qty'],$stored),app107Block('Пропущенные строки',['CSV-строка','sku','Код'],$rejects)];
$checks=['two_saved'=>$stored===[['A','2'],['C','4']],'known_failures'=>array_column($rejects,2)===['3819','1062']];
    app107Report('mysql107-import-savepoints',$blocks,$checks,$db,$diagnostics);
} finally {
    app107Cleanup($db,$other,$created);
}
echo "mysql107-import-savepoints: saved result.html and result.json\n";
Ожидаемый результат
mysql107-import-savepoints: saved result.html and result.json

Программа создаёт показанные данные в своей учебной таблице, выполняет действия и сохраняет result.html/result.json. Архив содержит mysql107-transactions-helper.php, README.txt и нужные входные CSV. Таблицы результата показаны выше; сообщения консоли — только уведомление о сохранённых файлах.

Открыть сохранённый результат реального запуска · Скачать таблицы JSON.

Попробуйте сами

Добавьте к успешному INSERT второй запрос аудита в той же итерации. Если он завершится заранее допустимой ошибкой, rollback-to должен отменить оба изменения текущей строки. Сравните это с простым continue после исключения без savepoint.

Не применять тот же файл повторно

Оператор повторно выбрал тот же файл, а затем прислал действительно обновлённый снимок остатков. Первый повтор должен ничего не менять, новый файл — заменить количества, а повтор старого уже обработанного файла — не откатить каталог назад.

Исходные данные

input.csv

skuqty
A2
B4

changed.csv

skuqty
A5
B4

Что получится

История импорта

ФайлРезультатqty A
input.csvapplied2
input.csvalready imported2
changed.csvapplied5
input.csvalready imported5

Текущие товары

skuqty
A5
B4

Различных принятых файлов

count
2

Ключевой запрос и его смысл

START TRANSACTION;
INSERT INTO mysql107_import_files(file_hash,rows_count) VALUES (?,?);
INSERT INTO mysql107_import_snapshot(sku,qty) VALUES (?,?)
ON DUPLICATE KEY UPDATE qty=?;
COMMIT;

Перед записью вычисляется SHA-256 байтов входного файла. Этот отпечаток — PRIMARY KEY журнала файлов. В одной транзакции сначала вставляется отпечаток, затем применяются строки. Если отпечаток уже есть, 1062 приводит к rollback текущей попытки и результату already imported. При сбое применения новый отпечаток откатится вместе со строками, позволяя осознанно повторить исправленную операцию.

ON DUPLICATE KEY UPDATE используется здесь по артикулу: новый товар вставляется, существующий получает qty из файла. Это снимок количества, не команда «добавить количество». Для нового A=2 получаем 2, для обновлённого A=5 — 5, а не 7. Третий параметр execute содержит новое qty ещё раз; deprecated VALUES(qty) не используется.

Журнал считает два разных содержимых файла. Итоговые строки сверяются непосредственно SELECT, а не суммой affected_rows: для upsert число затронутых строк зависит от вставки, изменения и отсутствия изменения. Имя файла не является ключом: новый экспорт может называться так же, а одинаковые байты — по-разному.

Параметры, возвращаемые значения и варианты API: mysqli_stmt_execute, mysqli_commit, mysqli_rollback. В этой главе используется объектная запись тех же операций; справочник объясняет и процедурную форму. В карточках отдельно указана граница проверки каждого старого примера.

Ошибки и границы применения

Хеш отличает байты, не смысл. BOM, порядок строк или окончания строк меняют его даже при тех же товарах. Это не универсальная дедупликация и не защита от позднего впервые увиденного старого файла; для этого нужна версия/время источника и договор приоритета. Учебный вход доверенный и маленький; в рабочем импорте сначала примените валидацию из предыдущего сценария. Исчезнувшие из снимка товары здесь не удаляются: это отдельное правило, которое нельзя молча выводить из отсутствия строки.

Полная самостоятельная программа PHP и все входные файлы
Не применять тот же файл повторноСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mysql107-transactions-helper.php';
$db=app107Connect(); $other=null; $created=[]; $blocks=[]; $checks=[]; $diagnostics=[];
try {
app107Create($db,$created,'mysql107_import_files','file_hash CHAR(64) PRIMARY KEY, rows_count INT NOT NULL');
app107Create($db,$created,'mysql107_import_snapshot','sku VARCHAR(20) PRIMARY KEY, qty INT NOT NULL');
function import107(mysqli $db,string $filename): string {
    $hash=hash_file('sha256',__DIR__.'/'.$filename);$rows=app107Csv($filename,['sku','qty']);
    $db->begin_transaction();
    try{
        try{$s=$db->prepare('INSERT INTO mysql107_import_files(file_hash,rows_count) VALUES(?,?)');$s->execute([$hash,count($rows)]);$s->close();}
        catch(mysqli_sql_exception $error){if($error->getCode()!==1062){throw $error;}$db->rollback();return 'already imported';}
        $s=$db->prepare('INSERT INTO mysql107_import_snapshot(sku,qty) VALUES(?,?) ON DUPLICATE KEY UPDATE qty=?');
        foreach($rows as $row){[$sku,$qty]=$row['values'];$s->execute([$sku,(int)$qty,(int)$qty]);}$s->close();
        $db->commit();return 'applied';
    }catch(Throwable $error){$db->rollback();throw $error;}
}
$history=[];
foreach(['input.csv','input.csv','changed.csv','input.csv'] as $filename){
    $status=import107($db,$filename);$qty=app107Read($db,"SELECT qty FROM mysql107_import_snapshot WHERE sku='A'")[0][0];$history[]=[$filename,$status,$qty];
}
$stored=app107Read($db,'SELECT sku,qty FROM mysql107_import_snapshot ORDER BY sku');$count=app107Read($db,'SELECT COUNT(*) FROM mysql107_import_files')[0][0];
$blocks=[app107Block('История импорта',['Файл','Результат','qty A'],$history),app107Block('Текущие товары',['sku','qty'],$stored),app107Block('Различных принятых файлов',['count'],[[$count]])];
$checks=['two_applied'=>array_column($history,1)===['applied','already imported','applied','already imported'],'latest_a_five'=>$stored===[['A','5'],['B','4']],'two_hashes'=>$count==='2'];
    app107Report('mysql107-import-replay',$blocks,$checks,$db,$diagnostics);
} finally {
    app107Cleanup($db,$other,$created);
}
echo "mysql107-import-replay: saved result.html and result.json\n";
Ожидаемый результат
mysql107-import-replay: saved result.html and result.json

Программа создаёт показанные данные в своей учебной таблице, выполняет действия и сохраняет result.html/result.json. Архив содержит mysql107-transactions-helper.php, README.txt и нужные входные CSV. Таблицы результата показаны выше; сообщения консоли — только уведомление о сохранённых файлах.

Открыть сохранённый результат реального запуска · Скачать таблицы JSON.

Попробуйте сами

Скопируйте input.csv под другим именем без изменения байтов: должен получиться already imported. Затем поменяйте порядок двух строк: хеш изменится. Определите, нужен ли вашему приложению байтовый ключ или нормализованное содержимое с явной версией поставщика.

Отделить понятную ошибку от внутренней диагностики

Пользователю нужен понятный отказ, разработчику — точная причина. Показывать сырой текст MySQL прямо на странице нельзя: там могут оказаться имена схемы, значения и внутреннее устройство запроса.

Исходные данные

Родители

id
1

Существующие товары

skuparent_id
A1

Что получится

Безопасный ответ приложения

СценарийКодSQLSTATEСообщениеАвтоповтор
duplicate106223000Артикул уже существуетno
parent145223000Родительская запись не найденаno
column105442S22Ошибка структуры запроса; передано разработчикуno

Исходная запись сохранена

skuparent_id
A1

Ключевой запрос и его смысл

try {
    $statement->execute($values);
} catch (mysqli_sql_exception $error) {
    $code = $error->getCode();
    $state = $error->getSqlState();
    // Из известных кодов строится безопасное сообщение; сырой текст остаётся в журнале.
}

В строгом режиме mysqli_report(MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT) сбой SQL становится mysqli_sql_exception. getCode() возвращает номер ошибки MySQL, getSqlState() — пятисимвольную категорию SQLSTATE. Дубликат и отсутствующий родитель здесь оба имеют 23000, но действия пользователя различаются: одного SQLSTATE недостаточно.

ОшибкаЧто произошлоДействие
1062 / 23000Нарушен уникальный артикулПоказать конфликт и предложить согласованный вариант исправления.
1452 / 23000Нет нужной родительской записи FKУточнить справочник/порядок импорта.
1054 / 42S22Код запросил неизвестный столбецПередать разработчику; повтор теми же данными запрос не исправит.

Программа действительно выполняет три ошибочных запроса над собственными таблицами, распознаёт конкретные код и состояние и сохраняет публичную таблицу безопасных сообщений. В отдельном локальном diagnostics.json остаётся server_message с контекстом. Этот файл не выводится в публичный результат; в рабочем сервисе журнал должен лежать вне доступного каталога сайта и иметь ограниченный доступ. Данные учебные, реквизитов соединения в журнале нет.

Видимый фрагмент PHP — схема обработчика, где statement и values созданы ранее; это не отдельная запускаемая программа. Полный архив ниже содержит создание таблиц, три реальные ошибки, чтение сохранённой записи и очистку. Неизвестное исключение не заменяется молчаливым «успехом».

Параметры, возвращаемые значения и варианты API: mysqli_report, mysqli_stmt_sqlstate, mysqli_stmt_errno. В этой главе используется объектная запись тех же операций; справочник объясняет и процедурную форму. В карточках отдельно указана граница проверки каждого старого примера.

Ошибки и границы применения

Не объявляйте все ошибки класса 23000 повторяемыми и не выполняйте бесконечный retry. Deadlock/lock timeout требуют другого ограниченного алгоритма повторения всей бизнес-транзакции; потеря соединения около COMMIT оставляет неопределённость, решаемую ключом операции. Ошибка имени колонки требует исправления кода. Записи журнала тоже могут содержать пользовательские данные: ограничивайте их состав и срок хранения.

Полная самостоятельная программа PHP и все входные файлы
Отделить понятную ошибку от внутренней диагностикиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/mysql107-transactions-helper.php';
$db=app107Connect(); $other=null; $created=[]; $blocks=[]; $checks=[]; $diagnostics=[];
try {
app107Create($db,$created,'mysql107_import_error_parent','id INT PRIMARY KEY');
app107Create($db,$created,'mysql107_import_error_items','sku VARCHAR(20) PRIMARY KEY, parent_id INT NOT NULL, CONSTRAINT mysql107_import_parent_fk FOREIGN KEY(parent_id) REFERENCES mysql107_import_error_parent(id)');
$db->query('INSERT INTO mysql107_import_error_parent VALUES(1)');$db->query("INSERT INTO mysql107_import_error_items VALUES('A',1)");
$cases=[['duplicate',"INSERT INTO mysql107_import_error_items VALUES('A',1)",1062,'23000','Артикул уже существует'],['parent',"INSERT INTO mysql107_import_error_items VALUES('B',999)",1452,'23000','Родительская запись не найдена'],['column','SELECT missing_column FROM mysql107_import_error_items',1054,'42S22','Ошибка структуры запроса; передано разработчику']];
$rows=[];$diagnostics=[];$all=true;
foreach($cases as [$context,$sql,$expected,$state,$safe]){
    try{$db->query($sql);throw new RuntimeException('Expected SQL error missing');}
    catch(mysqli_sql_exception $error){
        if($error->getCode()!==$expected || $error->getSqlState()!==$state){throw $error;}
        $rows[]=[$context,(string)$error->getCode(),$error->getSqlState(),$safe,'no'];
        // Local private evidence. This file is deliberately not linked as a public result.
        $diagnostics[]=['context'=>$context,'code'=>$error->getCode(),'sqlstate'=>$error->getSqlState(),'server_message'=>$error->getMessage()];
    }
}
$stored=app107Read($db,'SELECT sku,parent_id FROM mysql107_import_error_items');
$blocks=[app107Block('Безопасный ответ приложения',['Сценарий','Код','SQLSTATE','Сообщение','Автоповтор'],$rows),app107Block('Исходная запись сохранена',['sku','parent_id'],$stored)];
$public=json_encode($blocks,JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR);
$hasRaw=false;foreach($diagnostics as $record){if(str_contains($public,$record['server_message'])){$hasRaw=true;}}
$checks=['three_actual_errors'=>count($rows)===3,'only_original_row'=>$stored===[['A','1']],'no_raw_server_message_in_public'=>!$hasRaw];
    app107Report('mysql107-import-errors',$blocks,$checks,$db,$diagnostics);
} finally {
    app107Cleanup($db,$other,$created);
}
echo "mysql107-import-errors: saved result.html and result.json\n";
Ожидаемый результат
mysql107-import-errors: saved result.html and result.json

Программа создаёт показанные данные в своей учебной таблице, выполняет действия и сохраняет result.html/result.json. Архив содержит mysql107-transactions-helper.php, README.txt и нужные входные CSV. Таблицы результата показаны выше; сообщения консоли — только уведомление о сохранённых файлах.

Открыть сохранённый результат реального запуска · Скачать таблицы JSON.

Попробуйте сами

Для третьего случая исправьте имя столбца и убедитесь, что больше нет причины выводить этот отказ. Затем разработайте безопасное сообщение для неизвестной ошибки с идентификатором события; сырой текст должен оставаться только в защищённом журнале, а не попадать в JSON API или HTML.

Что расширять в рабочем импорте

Добавьте явно выбранный источник, версию набора, хранение отчёта, предел размера и правила доступа. Для большого экспорта/чтения нужен ограниченный или потоковый результат. Для повторной бизнес-заявки используйте ключ операции, а не слепой повтор INSERT после любой ошибки. Проверяйте нужное конечное состояние, а не только отсутствие исключения.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.