Задать вопрос
@AlexandrMa

Как объединить запросы в транзакцию?

Класс PDO
$stmt = $db->prepare("UPDATE `tab1` SET `col` = ?");
$stmt->execute($data1);
$stmt->closeCursor();

$stmt = $db->prepare("UPDATE `tab2` SET  `col` = ?");
$stmt->execute($data2);
$stmt->closeCursor();

$stmt = $db->prepare("UPDATE `tab3` SET  `col` = ?");
$stmt->execute($data3);
$stmt->closeCursor();


Задача сделать так, чтобы на время выполнения скрипта, полностью все блокировалось и если один из запросов не выполнится, то все бы откатилось назад. Это реально так сделать с использованием именно такой конструкции кода php?

PS. Вопрос не про запросы типа INSERT INTO ON DUPLICATE KEY UPDATE и также собранные в один запрос.
  • Вопрос задан
  • 1116 просмотров
Подписаться 2 Простой 23 комментария
Решения вопроса 2
ipatiev
@ipatiev Куратор тега PHP
Потомок старинного рода Ипатьевых-Колотитьевых
Поскольку быстрым поиском готовый ответ на Тостере не находится, стоит написать канонический.

Сначала общая информация:

Транзакция служит для обеспечения принципа "всё или ничего", гарантируя, что либо все запросы выполнились без ошибок, либо, если в каком-то из запросов произошла ошибка, то все предыдущие будут отменены, как будто их и не было вовсе. Из чего можно сделать следующие выводы:
  • транзакция не нужна для любого количества запросов на выборку данных, поскольку там нечего откатывать
  • транзакция не нужна для одного запроса на изменение данных (вставка, обновление, удаление) - такой запрос представляет из себя мини-транзакцию, которая сама автоматом откатывается при ошибке
  • не следует путать транзакции с блокировками. Хотя при определённых параметрах транзакции могут выполнять и блокировку, в общем случае это два разных механизма, которые могут выполняться как вместе, так и по отдельности. По умолчанию транзакция НЕ обеспечивает блокировку таблиц, участвующих в запросе


Самым простым вариантом будет заключить запросы между вызовами beginTransaction() и commit(), как показано например в документации к последнему.
$db->beginTransaction();
$db->prepare("UPDATE `tab1` SET `col` = ?")->execute($data1);
$db->prepare("UPDATE `tab2` SET  `col` = ?")->execute($data2);
$db->prepare("UPDATE `tab3` SET  `col` = ?")->execute($data3);
$db->commit();

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

Соответственно, при ошибке в любом из запросов транзакция автоматически откатится. А при успешном выполнении всех запросов транзакция, соответственно, закоммитится.

В редких случаях, когда после отката транзакции предполагается дальнейшее выполнение кода, можно заключить код транзакции в try-catch и откатить её вручную.

Важно помнить некоторые особенности.
  • в mysql не все движки таблиц поддерживают транзакции. впрочем ,учитывая что innodb является движком по умолчанию уже лет 20, это вряд ли будет проблемой
  • запросы на изменение страктуры таблиц автоматически коммитят стартовавшую транзакцию, то есть их следует избегать. Кажется, в новых версиях какие-то уже не коммитят, но я предпочитаю избегать всё равно.


Также желательно помнить, что в любом более-менее сложном коде очень быстро появляются вложенные транзакции, а PDO при попытке стартовать транзакцию при уже открытой, выбросит исключение, что, соответственно, приведёт к откату родительской (и это гораздо лучше поведения MySQL по умолчанию, которая автоматически коммитит старую). И имеет смысл накидать простой кодик, который считает вложенные транзакции, и не стартует, если уже был старт, а при коммите вычитает вложенность, а реально коммитит только если вложенности не осталось. Что-то вроде кода из комментариев к beginTransaction(), подравняв его напильником
class \MyPDO extends \PDO
{
    protected $transactionCounter = 0;

    public function beginTransaction()
    {
        if($this->transactionCounter++ === 0) {
            return parent::beginTransaction();
        }
    }
    public function commit()
    {
        $this->transactionCounter--;
        if($this->transactionCounter === 0) {
            return parent::commit();
        }
    }
    public function rollback()
    {
        $this->transactionCounter = 0;
        return parent::rollback();
    }
}

разместив его либо прямо в PDO, либо в своем враппере.
Ответ написан
Vindicar
@Vindicar
RTFM!
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы