Использование транзакций в PDO на PHP

Использование транзакций в PDO на PHPСегодня мы поговорим про использование транзакций в 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() когда транзакция не еще начата, то в любом из этих случаев вы получите ФАТАЛЬНУЮ ошибку. Да, поэтому всегда проверяйте начата ли транзакция, прежде чем ее завершить, в противном случае вы просто словите ошибку и ваше приложение аварийно завершится.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *