Сегодня мы поговорим про использование транзакций в PDO на PHP. Если вы плохо себе представляете, что такое транзакции, то для лучшего понимания советую сначала прочитать статью «Введение в транзакции в MySQL«.
Чтобы начать транзакцию, необходимо выполнить метод «beginTransaction()» у объекта класса «PDO». Рассмотрим пример на php:
$dsn = 'mysql:dbname=1;host=localhost'; $user = 'root'; $password = ''; $driver = array(PDO :: MYSQL_ATTR_INIT_COMMAND => 'SET NAMES `utf8`'); try { $db = new PDO($dsn, $user, $password, $driver); //создаем новый объект класса PDO для взаимодействия с БД } catch (PDOException $e) { echo 'Подключение не удалось: '. $e->getCode() .'|'. $e->getMessage()); exit(); } $db->beginTransaction(); //Начинаем транзакцию $db->exec("INSERT INTO user VALUES (1, 'Коля')"); $db->exec("INSERT INTO user VALUES (2, 'Алексей')"); $db->exec("INSERT INTO user VALUES (1, 'Иван')"); ... //далее commit() или rollBack()
Чтобы зафиксировать изменения в транзакции, у объекта PDO нужно выполнить метод commit():
$db->commit();
Чтобы отменить изменения (откатить транзакцию), у объекта $db PDO необходимо вызвать метод rollBack():
$db->rollBack();
Обратите внимание, если начать транзакцию и ее не завершить (то есть в рамках скрипта не выполнить ни commit() ни rollback()), то при завершении работы скрипта транзакция откатится автоматически, если не установлено постоянного соединения с БД (не установлен атрибут PDO::ATTR_PERSISTENT => true). Тоже самое произойдет при уничтожении PDO объекта ($db=null) в коде скрипта, в этом случае PDO завершит текущее соединение с БД. Откат транзакции при завершении соединения с БД делает PDO драйвер, это очень удобно при аварийном завершении скриптов.
Транзакции доступны только для таблиц с типом InnoDB. Для MyISAM таблиц транзакции недоступны.
По умолчанию в MySQL включен autocommit. Это означает подтверждение (фиксацию) каждого запроса к БД, это означает, что каждый запрос к базе данных в MySQL по умолчанию является транзакцией. Поэтому вставка данных в таблицы типа InnoDB идет медленнее, чем в таблицы типа MyISAM. При импорте данных и вставке больших объемов информации в таблицы InnoDB следует отключать autocommit и фиксировать изменения, т. е. делать commit не после каждой вставки, а после нескольких вставок (коммитить только после совершения группы запросов).
Обработка ошибок PDO в PHP и откат транзакций при ошибках
По умолчанию в PDO установлен «тихий» режим обработки ошибок (silent mode). Это означает, что при возникновении ошибки в PDO, исключение выброшено не будет и работа скрипта продолжится. Ошибки не будут ловиться с помощью try catch блоков, код ошибки и описание будет возможно получить только с помощью специальных методов у объекта PDO или PDOStatement: errorCode() и errorInfo(). Для того, чтобы ошибки PDO можно было «ловить» в try..catch, нужно изменить режим обработки ошибок с PDO::ERRMODE_SILENT
на PDO::ERRMODE_EXCEPTION
. Внимание: после установки этого режима желательно обрабатывать исключения при каждом запросе к БД, так как при возникновении ошибки остановится работа скрипта и произойдет остановка всего web-приложения. Если вы устанавливайте этот режим, обязательно используйте try..catch блоки в каждом запросе, чтобы ловить ошибки .
Рассмотрим, как изменится логика приложения после включения режима обработки ошибок PDO::ERRMODE_EXCEPTION:
$dsn = 'mysql:dbname=1;host=localhost';$user = 'root';$password = ''; $driver = array(PDO :: MYSQL_ATTR_INIT_COMMAND => 'SET NAMES `utf8`'); try { $db = new PDO($dsn, $user, $password, $driver); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //Устанавливаем режим обработки ошибок ERRMODE_EXCEPTION } catch (PDOException $e) { echo 'Подключение не удалось: '. $e->getCode() .'|'. $e->getMessage(); exit(); } try { $db->beginTransaction(); //Начинаем транзакцию $db->exec("INSERT INTO user VALUES (1, 'Коля')"); $db->exec("INSERT INTO user VALUES (2, 'Алексей')"); $db->exec("INSERT INTO user VALUES (1, 'Иван')"); catch (PDOException $e) { //Ловим ошибку $db->rollBack(); echo 'PDOException: '.$e->getCode() .'|'. $e->getMessage()); exit(); } $db->commit(); //Если все запросы прошли успешно - коммитим
Это простой пример обработки ошибок при использовании транзакций. В реальных приложениях нужно обязательно смотреть код ошибки. Если, например, это отключение от MySQL сервера, то совсем необязательно завершать работу скрипта после отката транзакции. В этом случае можно попытаться переподключиться к SQL серверу через какой то промежуток времени и пробовать заново выполнить текущий запрос или транзакцию. Если это, например, ошибка несовпадения типа данных — то в этом случае конечно нет смысла повторять запрос, можно откатывать транзакцию и завершить работу скрипта. При возникновении определенных ошибок вообще можно не откатывать транзакцию. Вообщем надо смотреть код SQL ошибки — и уже посмотрев решать как дальше поступать.
Рассмотрим пример:
function connect_db() { $dsn = 'mysql:dbname=1;host=localhost'; $user = 'root'; $password = ''; $driver = array(PDO :: MYSQL_ATTR_INIT_COMMAND => 'SET NAMES `utf8`'); try { $db = new PDO($dsn, $user, $password, $driver); //создаем новый объект класса PDO для взаимодействия с БД $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //Устанавливаем режим обработки ошибок ERRMODE_EXCEPTION } catch (PDOException $e) { echo 'Подключение не удалось: '. $e->getCode() .'|'. $e->getMessage()); return false; } return $db; } function doQuery($db, $sql, $count_db = 0) { if($count_db>5) { echo "Кол-во попыток подключения превысило допустимый лимит"; return false; } try { if($db->inTransaction()) { echo "Транзакция уже начата"; return false; } $db->beginTransaction();//Начинаем транзакцию $db->exec($sql); } catch (PDOException $e) { if($db->inTransaction()) $db->rollBack(); if($e->errorInfo[1] >= 2000&&$db=connect_db()) { //если код ошибки > 2000 (это потеря соединения с БД и пр.) то пробуем переподключится и выполнить запрос заново return doQuery($db, $sql, $count_db++); } else { echo 'PDOException: '.$e->getCode() .'|'. $e->getMessage(); return false; } } if($db->inTransaction()) return $db->commit(); }
Обратите внимание на метод:
$db->inTransaction();
Он проверяет, начата ли транзакция или нет. Это очень важно, так как если вызвать метод beginTransaction() в том случае, если транзакция уже начата, или наоборот вызвать метод rollBack() или commit() когда транзакция не еще начата, то в любом из этих случаев вы получите ФАТАЛЬНУЮ ошибку. Да, поэтому всегда проверяйте начата ли транзакция, прежде чем ее завершить, в противном случае вы просто словите ошибку и ваше приложение аварийно завершится.
Статьи по теме: