Всем привет! Сегодня у нас на повестке дня работа с SQL-запросами, базами данных, какие есть варианты и как вообще правильно с ними работать в рамках BitrixFramework
.
Разберем основы конфигурации, как подключать несколько БД на один проект, делать безопасные запросы и не тревожиться на счет инъекций.
Не стоит пугаться AI-шной картинки, это то как искусственный интеллект видит ER диаграмму. Материал писался исключительно белковой нейронкой ;-)
Конфигурируем БД
Первое, с чего начинаем, это конфигурация. В момент установки БУС-ика или Б24 мастер настройки у вас все спросит и сам пропишет нужные данные в файл конфигурации /bitrix/.settings.php
. Посмотрим, что находится в секции connections
:
return [
// ...
'connections' => [
'value' => [
'default' => [
'className' => \Bitrix\Main\DB\MysqliConnection::class,
'host' => 'localhost',
'database' => 'busik',
'login' => 'db_user',
'password' => '***',
'options' => 2,
],
],
'readonly' => true,
],
];
Ключ className
задаёт класс соединения, который будет создаваться, а далее весь массив настроек передаётся в конструктор соответствующего класса.
Продукт гарантирует корректную работу для СУБД:
Bitrix\Main\DB\MysqliConnection
Bitrix\Main\DB\PgsqlConnection
В зависимости от ваших потребностей можно использовать также движки:
Bitrix\Main\DB\MssqlConnection
Bitrix\Main\DB\OracleConnection
И несколько key-value
движков:
Bitrix\Main\Data\HsphpReadConnection
Bitrix\Main\Data\MemcacheConnection
Bitrix\Main\Data\MemcachedConnection
Bitrix\Main\Data\RedisConnection
Полный список доступных параметров можно посмотреть в самих классах различных движков и в документации: https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&LESSON_ID=2795#connections
Еще одно подключение к БД
При необходимости, а также непреодолимом желании, можно добавлять несколько соединений с БД. Для этого достаточно дополнить секцию connections
в файле конфигурации bitrix/.settings.php
:
return [
// ...
'connections' => [
'value' => [
'default' => [
'className' => \Bitrix\Main\DB\MysqliConnection::class,
'host' => 'localhost',
'database' => 'busik',
'login' => 'db_user',
'password' => '***',
'options' => 2,
],
'redis' => [
'className' => \Bitrix\Main\Data\RedisConnection::class,
'host' => 'rediska',
'port' => '12345',
],
],
'readonly' => true,
],
];
Далее получить объект соединения можно через фасад приложения:
// по умолчанию `default`
$db = \Bitrix\Main\Application::getConnection();
// обращаемся по имени
$db = \Bitrix\Main\Application::getConnection('default');
$redis = \Bitrix\Main\Application::getConnection('redis');
Connection
После того как БД сконфигурирована, рассмотрим варианты взаимодействия с объектом соединения.
/**
* @var \Bitrix\Main\DB\Connection $db
*/
$db = \Bitrix\Main\Application::getConnection();
/**
* Простой запрос
*/
$resultIterator = $db->query('SELECT `ID`, `NAME` FROM b_user');
/**
* Запрос с лимитом, в итоге выполнится запрос: SELECT `ID`, `NAME` FROM b_user LIMIT 0,10
*/
$resultIterator = $db->query('SELECT `ID`, `NAME` FROM b_user', 10);
/**
* Запрос с лимитом и отступом, в итоге выполнится запрос: SELECT `ID`, `NAME` FROM b_user LIMIT 5,10
*/
$resultIterator = $db->query('SELECT `ID`, `NAME` FROM b_user', 5, 10);
/**
* Итератор по результатам запроса
*/
foreach ($resultIterator as $row)
{
$id = (int)$row['ID'];
$name = (int)$row['NAME'];
// ...
}
/**
* Получаем значение первого столбца в выборке, в итоге выполниться запрос: SELECT `ID`, `NAME` FROM b_user LIMIT 0,1
* Конструкция LIMIT добавится в запрос, а вот секция SELECT не модифицируется!
*/
$id = $db->queryScalar('SELECT `ID`, `NAME` FROM b_user');
/**
* Выполнение запроса без получения результата, актуально для C*UD запросов
*/
$db->queryExecute('UPDATE b_user SET ACTIVE = "Y" WHERE DATE_REGISTER > "2024-01-01"');
ВАЖНО: методы query
, queryScalar
и queryExecute
принимают аргумент binds
, но этот аргумент не относится к подготовке запроса и защите от SQL-инъекции, как например PDOStatement::execute
! Данный аргумент нужен для трекинга запросов, об этом поговорим далее. Для защиты от SQL инъекций нужно использовать класс SqlExpression
или SqlHelper
, об этом также поговорим далее.
Для выполнения INSERT запросов есть специальные методы, которые уже подготавливают значения перед выполнением запроса и позволяют избежать SQL-инъекций:
/**
* @var \Bitrix\Main\DB\Connection $db
*/
$db = \Bitrix\Main\Application::getConnection();
/**
* Единичное добавление, в итоге выполнится ПОДГОТОВЛЕННЫЙ запрос:
* INSERT INTO `my_table`(`NAME`, `CONTENT`) VALUES ('habr', 'про \" базы \'')
*/
$insertedId = $db->add('my_table', [
'NAME' => 'habr',
'CONTENT' => 'про " базы \'',
]);
/**
* Множественное добавления строк, в итоге выполнится ПОДГОТОВЛЕННЫЙ запрос:
* INSERT INTO `my_table` (`NAME`, `CONTENT`) VALUES ('habr one', 'про \" базы \''), ('habr two', '\';SELECT * FROM b_user WHERE ID = 1')
*/
$lastInsertedId = $db->addMulti('my_table', [
[
'NAME' => 'habr one',
'CONTENT' => 'про " базы \'',
],
[
'NAME' => 'habr two',
'CONTENT' => "';SELECT * FROM b_user WHERE ID = 1",
],
]);
Помимо подготовки значений, методы add
и addMulti
также проверяют столбцы таблицы и исключают из запроса те, которых не существует:
/**
* В итоге выполниться ПОДГОТОВЛЕННЫЙ запрос:
* INSERT INTO `b_user`(`NAME`) VALUES ('habr')
*/
$insertedId = $db->add('b_user', [
'NAME' => 'habr',
'NOT_EXISTS_COLUMN' => 'что я тут делаю?',
]);
Побочный эффект таких проверок: перед запросом на добавление первоначально выполняется запрос на чтение SELECT * FROM ... LIMIT 0,1
. В коде данной логики используется статический кеш, поэтому запрос столбцов выполняется 1 раз за хит.
Стоит упомянуть что классы-наследники Bitrix\Main\DB\Connection
поддерживают также DDL-методы. Подробно останавливаться на них не будем, т.к. сами по себе методы простые и используются редко, поэтому ограничимся лишь перечислением доступных методов:
createTable
createIndex
createPrimaryIndex
truncateTable
renameTable
dropColumn
dropTable
SqlTracker
Для отладки SQL-запросов можно использовать Bitrix\Main\Diag\SqlTracker
, внутри себя он будет собирать тайминги, трейс и дополнительную сопутствующую информацию.
Изолировано для конкретного куска кода использовать трекинг можно так:
<?php
/**
* Начинаем и сбрасываем отладку (если вдруг она была начата до этого)
*/
$tracker = \Bitrix\Main\Application::getConnection()->startTracker(true);
/**
* Исследуемый код
*/
$USER->Update(1, [
'PHONE_NUMBER' => '+7-900-000-00-00',
]);
/**
* Результаты
*/
foreach ($tracker->getQueries() as $query)
{
print_r([
// выполненный запрос
$query->getSql(),
// время выполнения
$query->getTime(),
// стэк до места выполнения запроса
$query->getTrace(),
// значение глобальной переменной BX_STATE (не используется в новом коде)
$query->getState(),
// ид ноды в случае работы БД в кластере
$query->getNode(),
// содержимое аргумента $binds передаваемое в методах Connection::query*
$query->getBinds(),
]);
}
Result
При выполнении запросов на чтение (в том числе и через ORM), возвращает объект результата запроса \Bitrix\Main\DB\Result
. Это еще не сами данные, поэтому разберемся как их читать.
Самое простое, это использовать объект как итератор:
$resultIterator = \Bitrix\Main\Application::getConnection()->query('SELECT * FROM b_user');
/**
* Работаем как с итератором
*/
foreach ($resultIterator as $row)
{
$id = $row['ID'];
}
/**
* Равносильно записи выше
*/
while ($row = $resultIterator->fetch())
{
$id = $row['ID'];
}
Метод fetch
под капотом использует ряд модификаций и преобразований, чтобы удобнее было работать с данными. Если по каким-то причинам нужно получить сырые данные, сделать это можно с помощью метода fetchRaw
.
Рассмотрим пример, чтобы понять разницу:
$resultIterator = \Bitrix\Main\Application::getConnection()->query('SELECT ID, ACTIVE, DATE_REGISTER FROM b_user');
while ($row = $resultIterator->fetch())
{
/**
* [ID] => 1
* [ACTIVE] => Y
* [DATE_REGISTER] => Bitrix\Main\Type\DateTime Object
*/
print_r($row);
break;
}
while ($row = $resultIterator->fetchRaw())
{
/**
* [ID] => 2
* [ACTIVE] => Y
* [DATE_REGISTER] => 2021-12-29 09:50:14
*/
print_r($row);
break;
}
Столбец DATE_REGISTER
был преобразован в объект Bitrix\Main\Type\DateTime
, т.к. перед выполнением чтения, объект запроса обратился к хелперу и получил список необходимых конвертеров (эту механику можно подглядеть в Bitrix\Main\DB\Result::__construct
).
При необходимости, и непреодолимом желании, можно добавить свои конвертеры столбцов (column converters) и модификаторы выборки (fetch modifiers):
$resultIterator = \Bitrix\Main\Application::getConnection()->query('SELECT ID, ACTIVE, DATE_REGISTER FROM b_user');
/**
* Конвертор работает только с одним столбцом
*/
$resultIterator->setConverters([
'DATE_REGISTER' => static fn($value) => $value ? strtotime($value) : null,
]);
/**
* Модификатор работает со строкой в целом
*/
$resultIterator->addFetchDataModifier(static function(array $row) {
$row['ACTIVE_BOOL'] = $row['ACTIVE'] === 'Y';
return $row;
});
foreach ($resultIterator as $row)
{
/**
* [ID] => 1
* [ACTIVE] => Y
* [DATE_REGISTER] => 1640771366
* [ACTIVE_BOOL] => true
*/
print_r($row);
break;
}
Также есть ряд вспомогательных методов:
/**
* Кол-во строк в запросе
*/
$count = $resultIterator->getSelectedRowsCount();
/**
* Объект драйвера
* В случае с Mysql будет экземпляр класса \mysqli_result
*/
$dbResource = $resultIterator->getResource();
/**
* Список выбранных столбцов
*/
$selectedFields = $resultIterator->getFields();
SqlHelper
Вся логика подготовки SQL перед запросами лежит на классе Bitrix\Main\DB\SqlHelper
, а точнее его конкретных реализациях для работы с конкретными базами. Работы с данным хелпером спрятана внутрь ORM и Connection классов, но при необходимости можно обратиться к нему напрямую.
Самые важные методы связаны непосредственно с безопасностью и экранированием:
/**
* Хелпер, адаптированый под конкретную базу
*/
$helper = \Bitrix\Main\Application::getConnection()->getSqlHelper();
/**
* Экранирование столбцов
*/
$helper->quote('id'); // `id`
$helper->quote('table_name.id'); // `table_name`.`id`
$helper->quote('не ` безопасная " строка'); // `не безопасная " строка`
/**
* Экранированное ЗНАЧЕНИЕ
*/
$safeValue = $helper->forSql('не " безопасная \' строка'); // не \" безопасная \' строка
/**
* Экранированный SQL
*/
$safeSql = $helper->convertToDb('не " безопасная \' строка'); // 'не \" безопасная \' строка'
Есть ряд методов для работы с датами:
/**
* Получаем формат даты, корректный для текущей БД
*/
$helper->formatDate('DD.MM.YYYY HH:MI'); // %d.%m.%Y %H:%i
/**
* Получаем функцию преобразования столбца в конкретный формат
*/
$helper->formatDate('DD.MM.YYYY HH:MI', $helper->quote('column_name')); // DATE_FORMAT(`column_name`, '%d.%m.%Y %H:%i')
$helper->formatDate('DD.MM.YYYY HH:MI', $helper->convertToDb('2024-01-01')); // DATE_FORMAT('2024-01-01', '%d.%m.%Y %H:%i')
/**
* Добавить секунды к указанной дате
*/
$helper->addSecondsToDateTime(60); // DATE_ADD(NOW(), INTERVAL 60 SECOND)
$helper->addSecondsToDateTime(60, $helper->quote('column')); // DATE_ADD(`column`, INTERVAL 60 SECOND)
$helper->addSecondsToDateTime(60, $helper->convertToDb('2024-01-01')); // DATE_ADD('2024-01-01', INTERVAL 60 SECOND)
/**
* Добавить дни к указанной дате
*/
$helper->addDaysToDateTime(60); // DATE_ADD(NOW(), INTERVAL 60 DAY)
$helper->addDaysToDateTime(60, $helper->quote('column')); // DATE_ADD(`column`, INTERVAL 60 DAY)
$helper->addDaysToDateTime(60, $helper->convertToDb('2024-01-01')); // DATE_ADD('2024-01-01', INTERVAL 60 DAY)
И для работы с SQL-функциями:
/**
* Функции текущей даты и времени
*/
$helper->getCurrentDateFunction(); // CURDATE()
$helper->getCurrentDateTimeFunction(); // NOW()
$helper->getDatetimeToDateFunction($helper->quote('column_name')); // DATE(`column_name`)
$helper->getDatetimeToDateFunction($helper->convertToDb('2024-01-01')); // DATE('2024-01-01')
/**
* Методы ниже для MySQL не производит никаких преобразований, т.к. она и так работает :)
* Оба примера ниже приведены для PgSQL для наглядности преобразований:
*/
$helper->getCharToDateFunction(date('Y-m-d H:i:s')); // timestamp '2024-01-01 00:00:00'
$helper->getDateToCharFunction($helper->quote('column_name')); // TO_CHAR([column_name], 'YYYY-MM-DD HH24:MI:SS')
/**
* Функция подстроки
*/
$helper->getSubstrFunction($helper->quote('column_name'), 1); // SUBSTR(`column_name`, 1)
$helper->getSubstrFunction($helper->quote('column_name'), 1, 10); // SUBSTR(`column_name`, 1, 10)
/**
* Функция конкатенации (принимает неограниченное число аргументов)
*/
$helper->getConcatFunction(); // пустая строка ;)
$helper->getConcatFunction(1, 2, 3); // CONCAT(1, 2, 3)
$helper->getConcatFunction(
$helper->quote('column_name'),
$helper->convertToDb('delimiter'),
$helper->quote('another_column'),
); // CONCAT(`column_name`, 'delimiter', `another_column`)
/**
* Проверка на NULL
*/
$helper->getIsNullFunction($helper->quote('column_name'), 1); // IFNULL(`column_name`, 1)
$helper->getIsNullFunction($helper->quote('column_name'), $helper->convertToDb('value')); // IFNULL(`column_name`, 'value')
/**
* Длинна строки
*/
$helper->getLengthFunction($helper->quote('column_name')); // LENGTH(`column_name`)
/**
* Рандом
*/
$helper->getRandomFunction(); // rand()
/**
* Хеширование
*/
$helper->getSha1Function($helper->quote('column_name')); // sha1(`column_name`)
/**
* Полнотекстовый поиск
*/
$helper->getMatchFunction($helper->quote('column_name'), $helper->convertToDb('value')); // MATCH (`column_name`) AGAINST ('value' IN BOOLEAN MODE)
ВАЖНО: методы, описанные выше (и даты и функции) используют аргументы как есть, поэтому их необходимо экранировать вызывающему коду. Необходимо это для возможности указывать целые SQL-конструкции в той или иной функции.
Также в хелпере есть ряд методов для формирования запросов с префиксом prepare
. Так или иначе эти методы используют друг друга, детально рассмотрим методы для мерджинга:
/**
* Пытаемся добавить новую запись, но в случае конфликтов по `primaryFields` выполняем обновление указанных полей.
* ВАЖНО: в данном случае MySQL никак не использует в запросе `primaryFields`, но подразумевается, что на указанные поля добавлен UNIQUE INDEX.
*
* В итоге получим такой запрос:
* INSERT INTO `b_user_counter` (`USER_ID`, `SITE_ID`, `CODE`, `CNT`)
* VALUES (1, 's1', 'counter_name', 10)
* ON DUPLICATE KEY UPDATE `CNT` = `CNT` + 10
*/
[ $sql ] = $helper->prepareMerge(
tableName: 'b_user_counter',
primaryFields: [
'USER_ID',
'SITE_ID',
'CODE',
],
insertFields: [
'USER_ID' => 1,
'SITE_ID' => 's1',
'CODE' => 'counter_name',
'CNT' => 10,
],
updateFields: [
'CNT' => new \Bitrix\Main\DB\SqlExpression('?# + ?i', 'CNT', 10),
],
);
/**
* Аналогично пытаемся добавить новые записи, но данные берем из списка.
*
* В итоге получим такой запрос:
* INSERT INTO `b_user_counter` (`USER_ID`,`SITE_ID`,`CODE`,`CNT`)
* values (1, 's1', 'counter_name', 1),(2, 's1', 'counter_name', 1),(2, 's1', 'another_counter', 1)
* ON DUPLICATE KEY UPDATE `CNT` = `CNT` + 1"
*/
$sql = $helper->prepareMergeValues(
tableName: 'b_user_counter',
primaryFields: [
'USER_ID',
'SITE_ID',
'CODE',
],
insertRows: [
[
'USER_ID' => 1,
'SITE_ID' => 's1',
'CODE' => 'counter_name',
'CNT' => 1,
],
[
'USER_ID' => 2,
'SITE_ID' => 's1',
'CODE' => 'counter_name',
'CNT' => 1,
],
[
'USER_ID' => 2,
'SITE_ID' => 's1',
'CODE' => 'another_counter',
'CNT' => 1,
],
],
updateFields: [
'CNT' => new \Bitrix\Main\DB\SqlExpression('?# + ?i', 'CNT', 1),
],
);
/**
* Аналогично пытаемся добавить новую запись, но данные берем из подзапроса.
*
* В итоге получим такой запрос:
* INSERT INTO `b_user_counter` (`USER_ID`,`SITE_ID`,`CODE`,`CNT`)
* (SELECT * FROM my_counters)
* ON DUPLICATE KEY UPDATE `CNT` = `CNT` + 10
*/
$sql = $helper->prepareMergeSelect(
tableName: 'b_user_counter',
primaryFields: [
'USER_ID',
'SITE_ID',
'CODE',
],
selectFields: [
'USER_ID',
'SITE_ID',
'CODE',
'CNT',
],
select: '(SELECT * FROM my_counters)',
updateFields: [
'CNT' => new \Bitrix\Main\DB\SqlExpression('?# + ?i', 'CNT', 10),
],
);
/**
* Пытаемся добавить новую запись, но в случае конфликтов заменяем её на указанную.
* В отличие от предыдущих методов, тут записи в случае конфликтов просто переписываются, без возможности гибкого апдейта
*
* В итоге получим такой запрос:
* REPLACE INTO `b_user_counter` (`USER_ID`, `SITE_ID`, `CODE`, `CNT`)
* VALUES (1, 's1', 'counter_name', 5), (2, 's1', 'counter_name', 10), (2, 's1', 'another_counter', 15)
*/
$sqlQueries = $helper->prepareMergeMultiple(
tableName: 'b_user_counter',
primaryFields: [
'USER_ID',
'SITE_ID',
'CODE',
],
insertRows: [
[
'USER_ID' => 1,
'SITE_ID' => 's1',
'CODE' => 'counter_name',
'CNT' => 5,
],
[
'USER_ID' => 2,
'SITE_ID' => 's1',
'CODE' => 'counter_name',
'CNT' => 10,
],
[
'USER_ID' => 2,
'SITE_ID' => 's1',
'CODE' => 'another_counter',
'CNT' => 15,
],
],
);
/**
* В ответ мы получаем массив запросов, т.к. в случае превышения максимального размера, хелпер сам разделит запрос на части.
*/
foreach ($sqlQueries as $sql)
{
$db->query($sql);
}
Хелпер также содержит множество технических методов по типу конвертации значения из/в тип базы данных (см. пачку запросов с префиксом convert*
. Использовать их в клиентском коде вам скорее всего не придётся, т.к. лучше использовать высокоуровневые инструменты: SqlExpression
и ORM.
SqlExpression
Для более удобной работы с хелпером и в целом работы с запросами, существует класс Bitrix\Main\DB\SqlExpression
. Механика максимально проста: в SQL-запрос выставляются плейсхолдеры, которые при формировании SQL экранируются.
Поддерживаются следующие преобразования:
?
- преобразование либо к строке, либо к дате (наглядно на примерах);?s
- преобразование к строке;?i
- преобразование к целому числу;?f
- преобразование к дробному числу;?#
- экранированием имён столбцов
Всевозможные комбинации рассмотрим сразу на примерах:
/**
* Создаем новый объект и можем использовать его сразу в запросе
*/
$sql = new SqlExpression('SELECT * FROM b_user');
$result = Application::getConnection()->query($sql);
/**
* Получить SQL-запрос можно двумя способами
*/
echo $sql->compile();
// равносильно
echo (string)$sql;
/**
* Плейсхолдер внутри проверяет тип значения и выполняет необходимые преобразования
* Ниже представлен список поддерживаемых плейсхолдеров.
*
* В итоге получим запрос:
* SELECT * FROM `b_user` WHERE (ID = 1 OR ID > 1.23) AND `NAME` = 'admin' AND DATE_REGISTER > '2024-01-01'
*/
$sql = new SqlExpression(
'SELECT * FROM ?# WHERE (ID = ?i OR ID > ?f) AND `NAME` = ?s AND DATE_REGISTER > ?',
'b_user',
1.23,
1.23,
'admin',
new \Bitrix\Main\Type\Date('01.01.2024'),
);
/**
* В случае, если мы указываем NULL значение, все плейсхолдеры кроме ?# преобразуются к NULL значению.
*
* В итоге получим запрос:
* SELECT * FROM `` WHERE ID = NULL OR NAME = NULL
*/
$sql = new SqlExpression(
'SELECT * FROM ?# WHERE ID = ?i OR NAME = ?',
null,
null,
null,
);
/**
* Для преобразования дат используется базовый плейсхолдер ?
* В случае если по каким-то причинам нам нужно именно строковое представление даты, то нужно использовать модификатор ?s
*
* В итоге получим запрос:
* WHERE (DATE = '2024-01-01' OR DATE_TIME = '2024-01-01 00:00:00')
* AND (DATE = '01.01.2024' OR DATE_TIME = '01.01.2024 00:00:00')
*/
$sql = new SqlExpression(
'
WHERE (DATE = ? OR DATE_TIME = ?)
AND (DATE = ?s OR DATE_TIME = ?s)
',
new \Bitrix\Main\Type\Date('01.01.2024'),
new \Bitrix\Main\Type\DateTime('01.01.2024'),
new \Bitrix\Main\Type\Date('01.01.2024'),
new \Bitrix\Main\Type\DateTime('01.01.2024'),
);
Помимо формирования запросов, SqlExpression
можно также использовать в фильтрах ORM: https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&LESSON_ID=2244#ExpressionField
Транзакции
Для работы с транзакциями нужно использовать объект подключения Bitrix\Main\DB\Connection
, причем внутри транзакции можно использовать как запросы с помощью Connection::query
,так и ORM-сущности, потому что внутри они также сваливаются к работе с тем же самым Connection::query
.
$db = \Bitrix\Main\Application::getConnection();
try
{
$db->startTransaction();
$db->queryExecute('UPDATE my_table SET active = "N" WHERE age > 0');
\Bitrix\Main\SiteTable::update('s1', [
'ACTIVE' => 'N',
]);
$db->commitTransaction();
}
catch (Throwable $e)
{
$db->rollbackTransaction();
throw $e;
}
ВАЖНО: таблеты ORM могут использовать другое соединение, переопределенное через метод DataManager::getConnectionName
. В данном случае транзакция открывается в рамках конкретной БД.
Помимо обычных транзакций, поддерживаются также и вложенные транзакции. Механика работы точно такая же, но есть ньюанс решения проблемы частичного отката:
use Bitrix\Main\Application;
use Bitrix\Main\DB\Connection;
use Bitrix\Main\DB\SqlExpression;
use Bitrix\Main\DB\TransactionException;
function updateAccounts(int $userId, Connection $db)
{
try
{
$db->startTransaction();
// DataManager::update
$db->commitTransaction();
}
catch (Throwable $e)
{
$db->rollbackTransaction();
throw $e;
}
}
function updateOrders(int $userId, Connection $db)
{
try
{
$db->startTransaction();
// Connection::queryExecute
$db->commitTransaction();
}
catch (Throwable $e)
{
$db->rollbackTransaction();
throw $e;
}
}
$db = Application::getConnection();
try
{
$db->startTransaction();
updateOrders($userId, $db);
updateAccounts($userId, $db);
$db->commitTransaction();
}
catch (TransactionException $e)
{
/**
* Тут нам нужно решить, что делать с упавшей вложенной транзакцией.
* Скорее всего, вам нужно откатить всю транзакция целиком и вызывать очередной ROLLBACK
*/
$db->rollbackTransaction();
}
catch (Throwable $e)
{
$db->rollbackTransaction();
throw $e;
}
ORM
По работе с ORM у нас есть достаточно полная документация, поэтому переписывать её смысла не вижу, просто оставлю ссылку на неё. В случае если у вас есть вопросы/темы касаемо ORM, добро пожаловать в комментарии, готов их обсудить ;-)
Полезные ссылки
Собрал различные полезности по теме: