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

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

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

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

Глава 112 · После подключения PDO и первых подготовленных запросов

PDO_MYSQL: транзакции, частичный откат и границы COMMIT

Заказ с позициями, отказ доставки, SAVEPOINT, владелец транзакции, неявный COMMIT DDL и фактическая видимость autocommit через два PDO-соединения.

От заявки к общей границе подтверждения

Заказ должен появиться вместе со своими позициями. Если оформление отказало, пользователь должен получить исходный баланс и отсутствие незавершённой записи. Здесь разберём, как выразить этот договор средствами PDO и где MySQL может закончить транзакцию раньше вашего commit.

Операция заказа 501
  1. Одно PDO-соединение → beginTransaction
  2. INSERT заказа 501 и его позиций CUP × 1/TEA × 2
  3. Успех → commit → читать весь заказ
  4. Отказ → rollBack → читать исходное состояние
ЗаписьЧто означает
$dbОдно соединение с MySQL. Его транзакция не распространяется на другое соединение.
$db->beginTransaction()Начать группу; аргументов нет. Возвращает bool, ошибка/вложенный вызов может дать PDOException.
$db->commit()Подтвердить текущую группу. Не отправляет письма и не выполняет внешние HTTP-действия.
$db->rollBack()Отменить незавершённую группу в транзакционном хранилище.
$db->inTransaction()Узнать текущее состояние; не является командой восстановления уже подтверждённых изменений.

Предыдущие главы объясняют подключение и значения параметров и чтение результата. Для первичного знакомства с таблицами начните с структуры базы и связей. Здесь таблицы малы, чтобы увидеть каждое изменение. SQL выбирает нужные строки сам; транзакция не заменяет WHERE, ограничения и права доступа.

Как запустить и увидеть сохранённые таблицы

Самостоятельные архивы этой пары глав рассчитаны на PHP 8.4+ с PDO_MYSQL/mysqlnd и локальный MySQL 8.4 с InnoDB. Используется PDO::connect и драйверный подкласс Pdo\Mysql. Для PHP 8.3 и старше сначала изучите различия конструктора в подключении PDO_MYSQL: getWarningCount и новые имена драйверных констант там недоступны. В PHP 8.5 старые PDO::MYSQL_ATTR_* объявлены устаревшими; здесь используются Pdo\Mysql::ATTR_*. Общие PDO::ATTR_* сохраняют прежние имена.

Настройте отдельную учебную базу и пять переменных окружения PHPMAN_MYSQL_HOST, PHPMAN_MYSQL_PORT, PHPMAN_MYSQL_DATABASE, PHPMAN_MYSQL_USER, PHPMAN_MYSQL_PASSWORD. В архиве находятся example.php, pdo109-transactions-helper.php и README.txt. Команда php example.php создаёт result.html, result.json, observations.json и metrics.json. HTML показывает именно сохранённые значения; JSON позволяет использовать их дальше. Пароль вводится локально и в архив не входит.

Сохранить заказ и его позиции одним подтверждением

Покупатель оформил заказ 501 с двумя позициями. Запись о покупателе без позиций ещё не является готовым заказом. Сохраняем эти изменения одной короткой группой и только после этого показываем подтверждение.

Входные данные

Заявка

ЗаказПокупательПозиции
501АннаTEA × 2; CUP × 1

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

Заказ после COMMIT

order_idcustomerskuqty
501АннаCUP1
501АннаTEA2

Состояние PDO

До beginTransactionПосле beginTransactionПосле commit
falsetruefalse
Как изменяются данные
  1. Заявка 501: Анна, CUP × 1 и TEA × 2
  2. beginTransaction → INSERT заказа и двух позиций
  3. commit → новая выборка JOIN показывает обе позиции

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$db->beginTransaction();
$order->execute([501, 'Анна']);
$line->execute([501, 'TEA', 2]);
$line->execute([501, 'CUP', 1]);
$db->commit();

В переменной $db уже находится подключение PDO. Стрелка -> вызывает метод этого соединения; скобки beginTransaction() и commit() пустые, потому что аргументы им не нужны. Первый вызов начинает транзакцию, последний подтверждает изменения. Между ними $order и $line — подготовленные SQL-запросы: execute получает массив значений в порядке маркеров ?. Номер 501 выбран явно, чтобы в таблице было видно, какие позиции относятся к заказу. В полном коде после commit выполняется новый JOIN и читаются обе сохранённые строки. inTransaction возвращает bool: false до группы, true внутри, false после подтверждения.

Подробные параметры и возвращаемые значения: PDO::beginTransaction(), PDO::commit(), PDO::inTransaction(), PDOStatement::execute().

Ошибки, границы и альтернативы

Таблицы созданы заранее с ENGINE=InnoDB. Одного успешного beginTransaction недостаточно, чтобы сделать нетранзакционное хранилище транзакционным. Здесь связь order_id показана явно; в рабочей схеме добавьте FOREIGN KEY и проверки заявки. Подготовленные запросы отделяют значения от SQL, транзакция объединяет изменения — это разные задачи. Атомарность этой группы не означает, что любые параллельные чтения полностью изолированы: нужные ограничения и блокировки выбираются отдельно. Письмо, платёж или HTTP-вызов не откатываются вместе с INSERT.

Полная самостоятельная программа PHP с входными данными
Сохранить заказ и его позиции одним подтверждениемСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_tx_orders','id INT PRIMARY KEY, customer VARCHAR(40) NOT NULL');
pdo109Create($db,$created,'pdo109_tx_lines','order_id INT NOT NULL, sku VARCHAR(20) NOT NULL, qty INT NOT NULL CHECK(qty>0), PRIMARY KEY(order_id,sku)');
$before=$db->inTransaction(); $db->beginTransaction(); $during=$db->inTransaction();
$order=$db->prepare('INSERT INTO pdo109_tx_orders VALUES(?,?)');$order->execute([501,'Анна']);
$line=$db->prepare('INSERT INTO pdo109_tx_lines VALUES(?,?,?)');
$line->execute([501,'TEA',2]);$line->execute([501,'CUP',1]);
$db->commit();$after=$db->inTransaction();
$rows=pdo109Read($db,'SELECT o.id,o.customer,l.sku,l.qty FROM pdo109_tx_orders o JOIN pdo109_tx_lines l ON l.order_id=o.id ORDER BY l.sku');
$tables=[pdo109Table('Заказ после COMMIT',['order_id','customer','sku','qty'],$rows),pdo109Table('Состояние PDO',['До beginTransaction','После beginTransaction','После commit'],[[$before,$during,$after]])];
$checks=['two_positions'=>$rows===[['501','Анна','CUP','1'],['501','Анна','TEA','2']],'transaction_states'=>!$before && $during && !$after];
    pdo109Report('pdo109-order-commit',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-order-commit: saved result.html and result.json\n";
Ожидаемый результат
pdo109-order-commit: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

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

Небольшое продолжение

Замените количество CUP на 3 и прочитайте новую итоговую строку CUP / 3. Затем попробуйте qty=0: CHECK отклонит позицию, finally отменит ещё открытую группу. Для человекочитаемого отказа развейте следующий пример с catch. Не переносите CREATE TABLE внутрь этой группы.

Отменить списание, когда оформление заказа прервалось

Из баланса уже сняты 2500 копеек и создано бронирование 701, но проверка доставки отказала. Читателю нужен конкретный ответ: восстановятся ли деньги и останется ли бронирование?

Входные данные

До оформления

Баланс, коп.Списание, коп.Бронирований
1000025000

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

После обработанного отказа

Баланс, коп.БронированийТранзакция открыта
100000false
Как изменяются данные
  1. Баланс 10000 → незавершённое списание 2500 → бронирование 701
  2. Проверка доставки: отказ → catch → rollBack
  3. Новая выборка: баланс 10000; бронирований 0

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

if ($db->inTransaction()) {
    $db->rollBack();
}

Полная программа намеренно вызывает DomainException после двух успешных изменений. Это воспроизводимый отказ бизнес-правила, а не имитация аварии MySQL. catch(Throwable $error) охватывает и ошибки базы, и ошибки приложения в этой группе. Сначала inTransaction проверяет состояние, затем rollBack отменяет незавершённые изменения. Только известный учебный отказ превращается в обычный результат; всё неожиданное повторно выбрасывается через throw $error. Новый SELECT возвращает баланс 10000 и ноль бронирований. Если бы проверка просто вернула false, код сам должен был бы превратить её в выбранный отказ или явно выполнить rollback.

Подробные параметры и возвращаемые значения: PDO::rollBack(), PDO::inTransaction(), PDOStatement::execute().

Ошибки, границы и альтернативы

Перехваченное исключение само по себе не означает, что ваша операция уже отменена. В долгоживущем процессе незакрытая транзакция может продолжать удерживать блокировки. Проверка inTransaction предотвращает вторичную ошибку «нет активной транзакции», но не возвращает уже подтверждённые изменения. Ошибку самого rollback тоже нельзя скрывать в производственном журнале: надо сохранить исходную причину и отдельно отметить невозможность отмены. В примере соединение живое, откат действительно проверяется чтением данных.

Полная самостоятельная программа PHP с входными данными
Отменить списание, когда оформление заказа прервалосьСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_tx_credit','id INT PRIMARY KEY, cents INT NOT NULL');
pdo109Create($db,$created,'pdo109_tx_booking','id INT PRIMARY KEY, address VARCHAR(80) NOT NULL');
$db->exec('INSERT INTO pdo109_tx_credit VALUES(1,10000)');$caught=false;
try {
    $db->beginTransaction();
    $debit=$db->prepare('UPDATE pdo109_tx_credit SET cents=cents-? WHERE id=1 AND cents>=?');$debit->execute([2500,2500]);
    if ($debit->rowCount()!==1) { throw new DomainException('Недостаточный баланс'); }
    $insert=$db->prepare('INSERT INTO pdo109_tx_booking VALUES(?,?)');$insert->execute([701,'Москва']);
    throw new DomainException('Учебный отказ: способ доставки недоступен');
} catch (Throwable $error) {
    if ($db->inTransaction()) { $db->rollBack(); }
    if (!$error instanceof DomainException || $error->getMessage()!=='Учебный отказ: способ доставки недоступен') { throw $error; }
    $caught=true;
}
$balance=(string)$db->query('SELECT cents FROM pdo109_tx_credit WHERE id=1')->fetchColumn();
$count=(string)$db->query('SELECT COUNT(*) FROM pdo109_tx_booking')->fetchColumn();
$tables=[pdo109Table('После обработанного отказа',['Баланс, коп.','Бронирований','Транзакция открыта'],[[$balance,$count,$db->inTransaction()]])];
$checks=['expected_refusal'=>$caught,'money_restored'=>$balance==='10000','booking_removed'=>$count==='0','transaction_closed'=>!$db->inTransaction()];
    pdo109Report('pdo109-exception-rollback',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-exception-rollback: saved result.html and result.json\n";
Ожидаемый результат
pdo109-exception-rollback: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

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

Небольшое продолжение

Перенесите учебный throw перед INSERT бронирования: после отката результат останется 10000/0. Уберите только throw и добавьте commit после INSERT: получите 7500/1. Не смешивайте оба варианта в одном try без явного решения, что является успешным завершением.

Отказаться от промокода и сохранить сам заказ

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

Входные данные

Выбор покупателя

ЗаказСумма, коп.Промокод
8015000OLD: скидка1000, срок истёк

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

Заказ без невалидной скидки

order_idtotal_centscoupon
8015000NULL

Граница отмены

Промокод отклонёнПосле ROLLBACK TO транзакция активна
truetrue
Как изменяются данные
  1. Заказ 801:5000/NULL → SAVEPOINT optional_coupon
  2. Скидка OLD:4000 → отказ → ROLLBACK TO optional_coupon
  3. Заказ 801:5000/NULL → RELEASE → общий COMMIT

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$db->exec('SAVEPOINT optional_coupon');
$apply->execute([1000, 'OLD']);
// Бизнес-проверка отклонила промокод:
$db->exec('ROLLBACK TO SAVEPOINT optional_coupon');
$db->exec('RELEASE SAVEPOINT optional_coupon');
$db->commit();

SAVEPOINT optional_coupon ставится после создания заказа за 5000 копеек. UPDATE временно меняет сумму на 4000 и coupon на OLD. После отказа ROLLBACK TO SAVEPOINT возвращает 5000 и NULL, а внешняя транзакция остаётся активной. RELEASE SAVEPOINT удаляет саму точку; он ничего не подтверждает. commit выполняет владелец всей операции. У PDO нет отдельного универсального метода savepoint: здесь используется exec с фиксированным SQL MySQL. Имя точки является SQL-идентификатором, поэтому его не заменяют маркером значения ?.

Подробные параметры и возвращаемые значения: PDO::exec(), PDO::inTransaction(), PDO::commit().

Ошибки, границы и альтернативы

Этот вариант допустим лишь когда бизнес-правило разрешает продолжать без скидки. Если цена является условием согласия покупателя, надо отменить весь заказ. Savepoint не является вложенной независимой транзакцией. Откат к точке не обещает освобождения всех взятых после неё блокировок; они могут удерживаться до конца внешней группы. COMMIT и полный ROLLBACK удаляют все точки. После deadlock сервер может отменить всю транзакцию: попытка ROLLBACK TO уже не восстанавливает её.

Полная самостоятельная программа PHP с входными данными
Отказаться от промокода и сохранить сам заказСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_tx_promo','order_id INT PRIMARY KEY, total_cents INT NOT NULL, coupon VARCHAR(20) NULL');
$db->beginTransaction();$db->exec('INSERT INTO pdo109_tx_promo VALUES(801,5000,NULL)');
$db->exec('SAVEPOINT optional_coupon');$caught=false;
try {
    $apply=$db->prepare('UPDATE pdo109_tx_promo SET total_cents=total_cents-?,coupon=? WHERE order_id=801');$apply->execute([1000,'OLD']);
    throw new DomainException('Промокод истёк');
} catch (DomainException $error) {
    if ($error->getMessage()!=='Промокод истёк') { throw $error; }
    $db->exec('ROLLBACK TO SAVEPOINT optional_coupon');$caught=true;
}
$active=$db->inTransaction();$db->exec('RELEASE SAVEPOINT optional_coupon');$db->commit();
$rows=pdo109Read($db,'SELECT order_id,total_cents,coupon FROM pdo109_tx_promo');
$tables=[pdo109Table('Заказ без невалидной скидки',['order_id','total_cents','coupon'],$rows),pdo109Table('Граница отмены',['Промокод отклонён','После ROLLBACK TO транзакция активна'],[[$caught,$active]])];
$checks=['base_order_retained'=>$rows===[['801','5000',null]],'optional_rejected'=>$caught,'outer_still_active'=>$active,'committed'=>!$db->inTransaction()];
    pdo109Report('pdo109-savepoint',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-savepoint: saved result.html and result.json\n";
Ожидаемый результат
pdo109-savepoint: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

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

Небольшое продолжение

Сделайте промокод действительным: уберите только учебный отказ и сохраните UPDATE, RELEASE и commit. Ожидаются 801/4000/OLD. Для нескольких независимых купонов не создавайте произвольные имена точек из пользовательских строк: используйте заранее заданные имена и явную политику.

Назначить одного владельца транзакции для нескольких функций

Оформление задачи вызывает несколько функций. Если каждая сама начинает и подтверждает транзакцию, одна функция может завершить группу раньше вызывающего кода. Явно назначаем владельца — внешнюю операцию.

Входные данные

Группа действий

Первый запускВторой запуск
Собрать + Проверить → отменаУпаковать + Передать → подтверждение

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

Первая попытка отменена владельцем

Вложенный begin отклонёнВнешняя активнаДо rollbackПосле rollback
truetrue20

Вторая попытка подтверждена

idtitle
3Упаковать
4Передать
Как изменяются данные
  1. Вызывающий код: begin → функция 1 → функция 2
  2. Функции: execute; владение транзакцией не меняют
  3. Владелец решает: отменить обе задачи или подтвердить обе

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$db->beginTransaction();
pdo109AddTask($db, 3, 'Упаковать');
pdo109AddTask($db, 4, 'Передать');
$db->commit();

Функция pdo109AddTask получает то же PDO-соединение аргументом, проверяет наличие активной транзакции и только добавляет строку. Она не вызывает commit или rollback. Во время первой попытки второй beginTransaction намеренно вызывает PDOException: вложенный вызов не создаёт ещё один независимый уровень. Внешняя группа остаётся активной и содержит две задачи; её rollback убирает обе. Второй запуск с теми же функциями подтверждает задачи 3 и 4. Простой фрагмент показывает именно второй путь; определение функции и отказ первого пути находятся в полном коде.

Подробные параметры и возвращаемые значения: PDO::beginTransaction(), PDO::inTransaction(), PDO::rollBack(), PDO::commit().

Ошибки, границы и альтернативы

beginTransaction при уже активной транзакции выбрасывает исключение независимо от ERRMODE. Тихий счётчик «вложенности» в PHP не создаёт соответствующих транзакций в MySQL. Для локального частичного отката нужны реальные savepoint, а для окончательного решения — один владелец. Наша функция требует внешней группы, что видно в её контракте. Другой допустимый API — функция, которая всегда владеет всей транзакцией и отказывается вызываться внутри чужой; смешивать эти договоры опасно.

Полная самостоятельная программа PHP с входными данными
Назначить одного владельца транзакции для нескольких функцийСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_tx_tasks','id INT PRIMARY KEY, title VARCHAR(40) NOT NULL');
function pdo109AddTask(PDO $db,int $id,string $title): void {
    if (!$db->inTransaction()) { throw new LogicException('Caller must own the transaction'); }
    $stmt=$db->prepare('INSERT INTO pdo109_tx_tasks VALUES(?,?)');$stmt->execute([$id,$title]);
}
$db->beginTransaction();pdo109AddTask($db,1,'Собрать');$nestedRejected=false;
try { $db->beginTransaction(); } catch (PDOException $error) { $nestedRejected=true; }
$stillActive=$db->inTransaction();pdo109AddTask($db,2,'Проверить');
$pending=(string)$db->query('SELECT COUNT(*) FROM pdo109_tx_tasks')->fetchColumn();$db->rollBack();
$left=(string)$db->query('SELECT COUNT(*) FROM pdo109_tx_tasks')->fetchColumn();
$db->beginTransaction();pdo109AddTask($db,3,'Упаковать');pdo109AddTask($db,4,'Передать');$db->commit();
$rows=pdo109Read($db,'SELECT id,title FROM pdo109_tx_tasks ORDER BY id');
$tables=[pdo109Table('Первая попытка отменена владельцем',['Вложенный begin отклонён','Внешняя активна','До rollback','После rollback'],[[$nestedRejected,$stillActive,$pending,$left]]),pdo109Table('Вторая попытка подтверждена',['id','title'],$rows)];
$checks=['nested_rejected'=>$nestedRejected,'outer_survived'=>$stillActive,'outer_cancelled_both'=>$pending==='2' && $left==='0','functions_do_not_commit'=>$rows===[['3','Упаковать'],['4','Передать']]];
    pdo109Report('pdo109-nested-owner',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-nested-owner: saved result.html and result.json\n";
Ожидаемый результат
pdo109-nested-owner: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

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

Небольшое продолжение

Вызовите pdo109AddTask до beginTransaction: получите LogicException и ни одной новой задачи. Затем передайте в функцию другое PDO-соединение: это другой контекст, и внешняя транзакция первого соединения на него не распространяется. Для рабочего сервиса передавайте одно соединение явно.

Увидеть, почему CREATE TABLE разрывает откат заказа

После обновления остатков импорт создаёт служебную таблицу. Разработчик ожидает, что catch отменит всё, но количество уже стало 3. Показанный опыт объясняет, почему проверка inTransaction не может «починить» такую последовательность.

Входные данные

Изменение

qty доqty во времяDDL
53Создать отдельную обычную учебную таблицу

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

Обычный CREATE TABLE внутри транзакции

До CREATE активнаПосле CREATE активнаrollback вызванqty после проверкиСтрок в новой таблице
truefalsefalse30
Как изменяются данные
  1. qty5 → begin → UPDATE qty3 ещё можно отменить
  2. Обычный CREATE TABLE → неявное подтверждение
  3. inTransaction=false → guard ничего не откатывает → qty3 сохранён

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$db->beginTransaction();
$db->exec('UPDATE pdo109_tx_ddl_value SET qty=3 WHERE id=1');
// Следующий вызов выполняет обычный CREATE TABLE:
pdo109Create($db, $created, 'pdo109_tx_ddl_created', 'id INT PRIMARY KEY', false);
$after = $db->inTransaction(); // false: UPDATE уже зафиксирован

В полном примере исходная обычная таблица имеет qty=5. После beginTransaction меняем его на 3. Следующий pdo109Create выполняет обычный CREATE TABLE без TEMPORARY. На MySQL он неявно подтверждает предшествующую транзакцию. После него inTransaction=false; условие отката пропускает rollBack. Повторное чтение показывает 3 и существование новой пустой таблицы. Ноль строк в новой таблице означает, что SELECT успешно обратился к существующей таблице, а не что таблица отсутствует. Cleanup в конце отдельно удаляет обе созданные нами таблицы.

Подробные параметры и возвращаемые значения: PDO::beginTransaction(), PDO::inTransaction(), PDO::rollBack(), PDO::exec().

Ошибки, границы и альтернативы

Миграции структуры и изменения заказа разделяйте. Обобщение «любая DDL всегда делает COMMIT» неточно: CREATE TEMPORARY TABLE и DROP TEMPORARY TABLE не делают этот implicit commit, но создание/удаление временной таблицы не отменяется ROLLBACK. ALTER TABLE временной таблицы имеет другие правила. Здесь проверен именно обычный CREATE TABLE. Не вызывайте rollBack без проверки после него: получите вторичное исключение, а исходное изменение всё равно не исчезнет. Atomic DDL MySQL описывает атомарность самой DDL, а не присоединение её к транзакции заказа.

Полная самостоятельная программа PHP с входными данными
Увидеть, почему CREATE TABLE разрывает откат заказаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_tx_ddl_value','id INT PRIMARY KEY, qty INT NOT NULL',false);
$db->exec('INSERT INTO pdo109_tx_ddl_value VALUES(1,5)');
$db->beginTransaction();$db->exec('UPDATE pdo109_tx_ddl_value SET qty=3 WHERE id=1');$before=$db->inTransaction();
pdo109Create($db,$created,'pdo109_tx_ddl_created','id INT PRIMARY KEY',false);
$after=$db->inTransaction();$rollbackCalled=false;
if ($db->inTransaction()) { $db->rollBack();$rollbackCalled=true; }
$qty=(string)$db->query('SELECT qty FROM pdo109_tx_ddl_value WHERE id=1')->fetchColumn();
$exists=(string)$db->query('SELECT COUNT(*) FROM pdo109_tx_ddl_created')->fetchColumn();
$tables=[pdo109Table('Обычный CREATE TABLE внутри транзакции',['До CREATE активна','После CREATE активна','rollback вызван','qty после проверки','Строк в новой таблице'],[[$before,$after,$rollbackCalled,$qty,$exists]])];
$checks=['began_transaction'=>$before,'ddl_ended_transaction'=>!$after,'guard_skipped_rollback'=>!$rollbackCalled,'update_was_committed'=>$qty==='3','new_table_exists'=>$exists==='0'];
    pdo109Report('pdo109-ddl-boundary',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-ddl-boundary: saved result.html and result.json\n";
Ожидаемый результат
pdo109-ddl-boundary: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

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

Небольшое продолжение

Переместите создание второй таблицы до beginTransaction, затем явно вызовите rollBack после UPDATE: qty снова должен стать 5. Так отделяется подготовка схемы от операции с данными. Не тренируйте этот опыт на таблицах сайта: архив использует только свои pdo109_tx_* имена.

Практический вопрос: Cannot commit transaction with PDO connection in Laravel. Импорт SQL-дампа создал таблицы, но последующий commit сообщил об отсутствии транзакции. Наш пример намеренно ограничен одной своей DDL-командой на MySQL 8.4; поведение MariaDB из вопроса не выдаётся за выполненный тест. Постановка задачи взята из обсуждения; решение и данные написаны отдельно, семантика сверена по документации PHP/MySQL.

Проверить, когда выключенный autocommit подтверждает изменения

В старом приложении выключили autocommit для импорта, а потом включили «чтобы вернуть настройки». Такая смена сама может подтвердить накопленные изменения. Два соединения позволяют увидеть момент публикации данных.

Входные данные

Общая обычная таблица

idqtyНаблюдатель B
15autocommit включён; отдельный SELECT каждый раз

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

Одна правка, два PDO-соединения

A после UPDATEB до включения autocommitB после включения autocommit
454
Как изменяются данные
  1. A: autocommit=false; qty 5→4 → A видит 4
  2. B: отдельный SELECT до подтверждения → видит 5
  3. A: autocommit=true → COMMIT; новый SELECT B → видит 4

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$db->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
$db->exec('UPDATE pdo109_tx_autocommit SET qty=4 WHERE id=1');
// Смена false → true подтверждает незавершённую транзакцию:
$db->setAttribute(PDO::ATTR_AUTOCOMMIT, true);

A выключает PDO::ATTR_AUTOCOMMIT и меняет qty с 5 на 4. A читает собственное незавершённое изменение, B с обычным autocommit пока читает 5. Вызов setAttribute(PDO::ATTR_AUTOCOMMIT,true) переводит MySQL из 0 в 1 и фиксирует незавершённую транзакцию. Следующий отдельный SELECT у B уже показывает 4. Реальные CONNECTION_ID A и B сохраняются в наблюдениях, поэтому это два соединения, а не два имени одной переменной. Последовательность воспроизводится в одном процессе и ничего не утверждает о нагрузочной производительности.

Подробные параметры и возвращаемые значения: PDO::setAttribute(), PDO::getAttribute(), PDO::rollBack().

Ошибки, границы и альтернативы

setAttribute возвращает bool успешности настройки, но не заменяет понимание её эффекта. Для отдельной бизнес-операции обычно проще beginTransaction/commit/rollBack при стандартном autocommit. Если B держит собственную длинную REPEATABLE READ транзакцию со снимком, его повторное чтение может отличаться от этого опыта. В примере B не открывает такую группу. Не делайте переключение autocommit универсальной функцией очистки незавершённой работы: сначала явно выберите rollback либо commit.

Полная самостоятельная программа PHP с входными данными
Проверить, когда выключенный autocommit подтверждает измененияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_tx_autocommit','id INT PRIMARY KEY, qty INT NOT NULL',false);
$db->exec('INSERT INTO pdo109_tx_autocommit VALUES(1,5)');$observer=pdo109Connect();$others[]=$observer;
$idA=(string)$db->query('SELECT CONNECTION_ID()')->fetchColumn();$idB=(string)$observer->query('SELECT CONNECTION_ID()')->fetchColumn();
$db->setAttribute(PDO::ATTR_AUTOCOMMIT,false);$db->exec('UPDATE pdo109_tx_autocommit SET qty=4 WHERE id=1');
$self=(string)$db->query('SELECT qty FROM pdo109_tx_autocommit WHERE id=1')->fetchColumn();
$otherBefore=(string)$observer->query('SELECT qty FROM pdo109_tx_autocommit WHERE id=1')->fetchColumn();
$db->setAttribute(PDO::ATTR_AUTOCOMMIT,true);
$otherAfter=(string)$observer->query('SELECT qty FROM pdo109_tx_autocommit WHERE id=1')->fetchColumn();
$tables=[pdo109Table('Одна правка, два PDO-соединения',['A после UPDATE','B до включения autocommit','B после включения autocommit'],[[$self,$otherBefore,$otherAfter]])];
$checks=['two_real_connections'=>$idA!==$idB,'own_uncommitted_read'=>$self==='4','observer_before'=>$otherBefore==='5','enabling_commits'=>$otherAfter==='4','autocommit_restored'=>(bool)$db->getAttribute(PDO::ATTR_AUTOCOMMIT)];
$observations=[['ID A',$idA],['ID B',$idB]];
    pdo109Report('pdo109-autocommit',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-autocommit: saved result.html and result.json\n";
Ожидаемый результат
pdo109-autocommit: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

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

Небольшое продолжение

Замените переключение в true явным rollBack, а уже затем восстановите autocommit: B должен прочитать 5. Не выполняйте новый UPDATE между rollback и восстановлением настройки, иначе начнёте другую транзакцию. Для сравнения снимков перейдите к готовому примеру двух SQL-соединений в главе конкуренции.

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

Не всякая ошибка отменяет одинаковый объём работы. Для InnoDB deadlock отменяет всю выбранную сервером транзакцию. Lock wait timeout по умолчанию отменяет только ожидавшую команду; настройка innodb_rollback_on_timeout меняет эту границу. Поэтому приложение не должно продолжать произвольный остаток операции после любого исключения.

СитуацияТехнический ориентирОсмысленное действие
Конфликт уникальностиSQLSTATE 23000;MySQL 1062Проверить договор повторов/уникальности; не повторять тот же INSERT бесконечно.
Взаимная блокировкаSQLSTATE 40001;MySQL 1213Ограниченно повторить всю допустимую транзакцию после отката, с повторным чтением условий.
Истекло ожидание блокировкиобычно HY000;MySQL 1205Установить судьбу всей операции; в нашем договоре явно отменить оставшуюся группу, затем решать вопрос повтора.
Потерян ответ COMMITсоединение оборвалосьНеизвестно, успел ли сервер подтвердить. Проверить устойчивый ключ операции на новом соединении.

Повтор имеет ограничение числа попыток и допустим только для повторяемой операции. Уведомление и внешний платёж не помещайте внутрь автоматически повторяемого блока без отдельного договора. Практический ключ повторной заявки и два настоящих SQL-соединения разобраны отдельно. PDO меняет API вызова, но не правила InnoDB.

Проверять результат операции, а не только отсутствие исключения

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

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

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

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

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

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