Транзакции при записи - откат, границы, что не откатится
Записываем связанные данные так, чтобы не осталось половины: заказ без позиций, запись журнала без заказа или списание без движения по складу.
Решение
Открываем и закрываем транзакцию:
$connection = \Bitrix\Main\Application::getInstance()->getConnection();$connection->startTransaction();try { $id = SyncTable::add($fields)->getId(); SyncItemTable::addMulti($items); $connection->commitTransaction();} catch (\Throwable $e) { $connection->rollbackTransaction(); // без этого транзакция висит открытой throw $e;}Транзакция охватывает связанные записи целиком. Если вторая запись не удалась, первая не должна остаться: именно это и отличает транзакцию от двух подряд идущих вызовов.
Держим границу по смыслу, а не по коду:
- заказ и его позиции - одна транзакция- заказ и запись в журнал обмена - тоже одна- заказ и отправка письма - разные вещи: письмо откатить нельзя- обход тысячи товаров - не транзакция вовсе, а порции по сто штукГраницу транзакции задаёт смысл операции. Всё, что при сбое обязано исчезнуть целиком, входит внутрь; всё остальное - за её пределами.
Выносим наружу то, что не откатывается:
$connection->commitTransaction(); // сначала данные в базе\Bitrix\Main\Mail\Event::send([...]); // и только потом письмоApi::notify($orderId); // и вызов чужого сервиса// внутри транзакции они опасны: откат вернёт базу, но не вернёт письмоПисьма, файлы и вызовы наружу откату не подлежат. Отправленное внутри транзакции письмо уйдёт даже при откате, и покупатель получит уведомление о заказе, которого в базе нет.
Не даём транзакции затянуться:
foreach (array_chunk($ids, 100) as $chunk) { $connection->startTransaction(); foreach ($chunk as $id) { SyncTable::update($id, ['STATUS' => 'sent']); } $connection->commitTransaction(); // порция - своя транзакция}Долгая транзакция держит строки заблокированными. Пока она открыта, чужие процессы ждут те же записи, и обмен из фонового превращается в заметный для покупателей.
Транзакции работают только на движке таблиц, который их поддерживает. На старых установках часть таблиц может остаться на движке без транзакций, и там вызовы отработают молча и без всякого эффекта.
Изменение структуры таблицы внутри транзакции незаметно её закрывает. Команды вроде добавления столбца фиксируют всё сделанное до них, и откат после такой команды возвращает уже не всё.
Вкладывать транзакции друг в друга нельзя. Вторая открытая транзакция не создаёт вложенности, и откат внутренней откатывает всё сразу, что обычно и становится сюрпризом.
Границы транзакций полезно проговорить словами до кода. Вопрос «что обязано исчезнуть целиком, если на середине всё сломается» звучит просто, а ответ на него задаёт и границу, и всё, что придётся вынести наружу.
Типичные проблемы
Записи в базе появляются наполовину.
Связанные записи пишутся без общей транзакции, каждым вызовом отдельно. Их объединяют в одну границу по смыслу операции.
После ошибки база ведёт себя странно.
Исключение вышло наружу, а транзакция так и осталась открытой и висящей дальше. Откат обязательно ставят в обработчик исключения, а не забывают о нём вовсе.
Покупатель получил письмо о несуществующем заказе.
Письмо отправлено прямо внутри транзакции, а сама она потом взяла и откатилась. Отправку письма выносят за границу транзакции, уже после подтверждения самой этой записи.
Обмен подвешивает работу менеджеров.
Транзакция открыта на весь проход по каталогу и держит блокировки. Работу режут на порции, каждая из которых со своей короткой транзакцией.
Откат не откатил ничего.
Таблица лежит на движке, который эти самые транзакции вообще никак не поддерживает. Такие вызовы отрабатывают молча и без эффекта.
Частые вопросы
Когда транзакция действительно нужна?
Когда две и более записи обязаны появиться вместе или не появиться вовсе. Одиночная запись в ней не нуждается.
Оборачивает ли платформа сохранение заказа сама?
Штатное сохранение заказа устроено согласованно, а вот свои дописки рядом - нет. Их границу задают сами.
Что делать с вызовом чужого сервиса?
Ставить его после подтверждения записи или в очередь. Внутри транзакции он ещё и держит её открытой.
Можно ли вложить транзакцию в транзакцию?
Нет: вторая не создаёт вложенности, и откат сработает на всё сразу. Границу планируют одну.
Смежное
- Свои таблицы на ORM - оглавление подтемы
- Своя таблица на ORM: сущность, запросы, изменение структуры - куда именно пишем
- Связи между таблицами ORM: ссылки, выборка, удаление - что обычно пишется вместе
- Ошибки в своём коде: результат вместо исключения, журнал, показ - как сообщить об отказе вызывающему коду
- Очередь обмена со сторонней системой: задания, повторы, сверка - куда девать вызовы наружу
- Ядро D7 - устройство ядра целиком
- Своя сущность от таблицы до админки: слои, права, интерфейс - где эти границы обычно нужны