???? Привет! Сегодня мы поговорим о том, как использовать вложенные транзакции в Bitrix, чтобы обеспечить целостность данных.


???? Тайны Транзакций

Версия main 22.200.0 принесла с собой нечто волшебное - вложенные транзакции в Bitrix! Зачем они нужны? Давайте посмотрим на этот процесс с точки зрения магии.

Untitled

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

???? Для чего нужны транзакции?

  • Транзакции позволяют объединять несколько запросов в единую конструкцию. Она гарантирует, что запросы будут выполнены как единое целое.

  • Транзакции - это нечто большее, чем просто блок кода. Это своего рода защитный механизм, который помогает вам избежать проблем с целостностью данных. Они гарантируют, что ваши операции с базой данных будут либо успешными, либо полностью отменены, если что-то пошло не так.

???? Как это было раньше?

Прежде чем волшебство транзакций появилось в Bitrix, разработчики вынуждены были прибегать к тайным приемам.

Создание таблицы-шпиона, где каждое изменение регистрировалось. Это позволяло восстановить предыдущее состояние при ошибке.

Введение транзакций в Bitrix значительно улучшило этот процесс, предоставив более надежные и эффективные способы гарантировать целостность данных.

???? Волшебство в действии: Как это работает в коде?

<?php

$application = \Bitrix\Main\Application::getInstance();
$connection = $application->getConnection();

try {
    $connection->startTransaction();
  
    // Ваш волшебный код здесь...
  
    $connection->commitTransaction();
} catch (\Exception $e) {
    $connection->rollbackTransaction();
}

Окей, давайте рассмотрим пример использования вложенных транзакций на практике. Допустим, у нас есть сущность, назовем ее ExampleTable. Мы хотим изменить поле NAME у записи с кодом 'example'. При этом, мы также хотим добавить новую запись в другую таблицу - AnotherTable.

<?php

$application = \Bitrix\Main\Application::getInstance();
$connection = $application->getConnection(); // Получаем соединение с базой данных

try {
    // Начинаем транзакцию
    $connection->startTransaction();

    // Получаем из сущности некий объект
    $object = \Example\ExampleTable::query()
        ->where('CODE', 'example')
        ->addSelect('NAME')
        ->fetchObject();
		
    if ($object) {
        $object->set('NAME', 'Пример записи'); // Меняем поле `NAME` у объекта
        $result = $object->save(); // Сохраняем
        if (!$result->isSuccess()) {
            throw new \Bitrix\Main\SystemException('Ошибка в процессе сохранения данных.');
        }
            
        // Пробуем добавить второй элемент в другую таблицу
        $anotherObject = \Example\AnotherTable::createObject();
        $anotherObject->set('TITLE', 'New title');
        $result = $anotherObject->save(); // Сохраняем
        if (!$result->isSuccess()) {
            // В случае ошибки откатываем транзакцию, и первый элемент не сохранится
            throw new \Bitrix\Main\SystemException('Ошибка в процессе сохранения данных.');
        }

        // Успешно завершаем транзакцию
        $connection->commitTransaction();
    } else {
        throw new \Bitrix\Main\SystemException('Объект не найден.');
    }
} catch (\Exception $e) {
    // Если произошла ошибка, отменяем транзакцию
    $connection->rollbackTransaction();
}

В этом кусочке кода транзакция начинается, операции выполняются, и если что-то идет не так, мы можем легко и безопасно откатить все изменения.

???? Что делают методы управления транзакциями?

  • $connection->startTransaction();: Этот метод начинает транзакцию, позволяя группировать несколько операций в единое целое.

  • $connection->commitTransaction();: Вызов этого метода фиксирует все изменения, сделанные в рамках транзакции, и отправляет их в базу данных.

  • $connection->rollbackTransaction();: Если произошла ошибка или необходимо отменить все изменения в рамках транзакции, этот метод откатывает все операции.

????️‍????️ Дополнительные тайны:

  • Вложенные транзакции применимы не только к save(), но и к другим методам ORM, таким как update(), add(). А также, к SQL запросам через $connection->query()

  • Если хотите углубиться в тайны магии транзакций, загляните в официальную документацию Bitrix

???? Завершение:

Таким образом, мы научились использовать волшебство транзакций в Bitrix, обеспечивая целостность данных. Если у вас есть свои волшебные методы или вопросы, делитесь в комментариях!

Комментарии (7)


  1. FanatPHP
    21.11.2023 21:02
    +1

    Два классических замечания к новичковым попыткам начать использовать транзакции:

    Во-первых, Exception ловит только половину возможных причин, по которым выполнение кода может быть прервано.

    Во-вторых, обеспечить целостность данных в случае ошибки - это, конечно, хорошо. Но вот замалчивать ошибку при этом не стоит. Откатив транзакцию, надо проинформировать об ошибке программиста.

    Поэтому

    } catch (\Throwable $e) {
        // Если произошла ошибка, отменяем транзакцию
        $connection->rollbackTransaction();
        throw $e;
    }

    Понятно, что от бессмысленного лепета "'Ошибка в процессе сохранения данных." толку мало, но это, как я понимаю, издержки битрикса. Возможно, когда-нибудь он научится сам кидать нормальные исключения. А пока хоть это пусть выдаёт.


    1. isa3v Автор
      21.11.2023 21:02

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

      В реальных проектах, конечно же, рекомендуется более детально обрабатывать ошибки. Ваши замечания в этом смысле абсолютно верные


  1. SieMax
    21.11.2023 21:02

    Если после startTransaction произойдет непредвиденное завершение работы приложения и до rollbackTransaction мы не доберёмся, что произойдет? Будет ли произведен автоматический откат при следующей инициализации ядра Bitrix?


    1. isa3v Автор
      21.11.2023 21:02
      +1

      Скорее при прекращении текущего соединения.
      MySQL innodb автоматически откатывает все незакомиченные изменения.


    1. FanatPHP
      21.11.2023 21:02

      Это, кстати, хорошее замечание, которое как бы намекает, что в общем случае заключать транзакцию в try-catch в принципе и не нужно - транзакция и так откатится при закрытии соединения. Но учитывая, что явное лучше неявного, то традиционно мы это делаем. Тем более что некоторых обстоятельствах (персистентное соединение с БД, например) надо все-таки, хотя бы попытаться.


  1. FanatPHP
    21.11.2023 21:02
    +1

    Кстати, собрался прочитать текст по ссылке. Тема "вложенности" транзакций в статье не раскрыта от слова совсем.

    Я ещё вчера очень удивился, поскольку при чтении складывается полное впечатление, будто в битрикс добавили поддержку транзакций вообще, а раньше их не было. А слово "вложенные" просто вкралось по ошибке. И только прочитав документацию, понял, о чем речь.

    Сама по себе поддержка транзакций в битриксе была давно (что и неудивительно, поскольку там явно растут ноги из PDO). А появилась поддержка именно "вложенных" транзакций, собранная из SAVEPOINT и палок, что позволило использовать транзакции не только в "вызывающем (конечном) сценарии" (что бы это не значило), "но и в API". В то время как ранее

    ...могла сложиться ситуация, когда конечный сценарий открывает транзакцию, вызываемый API тоже открывает свою транзакцию, в итоге все коммиты и роллбеки перемешиваются непредсказуемым образом.

    То есть новость должна звучать не "В битриксе появилась поддержка транзакций", а "теперь транзакции можно использовать как в конечном приложении, так и в API".


  1. rpsv
    21.11.2023 21:02

    Статья про транзакции, а не про вложенность. Вложенность работает так под капотом:

    $db = \Bitrix\Main\Application::getConnection();
    
    try
    {
        // START TRANSACTION
        $db->startTransaction();
    
            // SAVEPOINT 1
            $db->startTransaction();
    
                // SAVEPOINT 2
                $db->startTransaction();
                
                // -
                $db->commitTransaction();
                
            // ROLLBACK TO SAVEPOINT 1 (внутри кидает TransactionException)
            $db->rollbackTransaction();
    
        // DON'T EXECUTE
        $db->commitTransaction();
    }
    catch (\Bitrix\Main\DB\TransactionException $e)
    {
        // ROLLBACK
        $db->rollbackTransaction();
    }
    catch (Throwable $e)
    {
        // ROLLBACK or ROLLBACK TO SAVEPOINT *
        $db->rollbackTransaction();
        
        throw $e;
    }