Всем привет! Сегодня у нас на повестке дня работа с 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"');

ВАЖНО: методы queryqueryScalar и 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 экранируются.

Поддерживаются следующие преобразования:

  1. ? - преобразование либо к строке, либо к дате (наглядно на примерах);

  2. ?s - преобразование к строке;

  3. ?i - преобразование к целому числу;

  4. ?f - преобразование к дробному числу;

  5. ?# - экранированием имён столбцов

Всевозможные комбинации рассмотрим сразу на примерах:

/**
 * Создаем новый объект и можем использовать его сразу в запросе
 */
$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, добро пожаловать в комментарии, готов их обсудить ;-)

Полезные ссылки

Собрал различные полезности по теме:

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


  1. FanatPHP
    23.12.2024 09:55

    перед запросом на добавление первоначально выполняется запрос на чтение SELECT * FROM ... LIMIT 0,1

    Удивился, полез посмотреть, и действительно, на самом деле выполняется более логичный запрос с LIMIT 0.

    в итоге выполнится ПОДГОТОВЛЕННЫЙ запрос

    Всё-таки, слово "подготовленный" имеет строго определённое значение. Здесь лучше подойдёт "отформатированный".

    Самые важные методы связаны непосредственно с безопасностью и экранированием:

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

    Экранированный SQL

    Судя по всему, автор статьи сам не понимает, что делает метод convertToDb, и почему его всегда следует предпочесть методу forSql. А функция на самом деле интересная. Хотя есть и спорные моменты. Вот эта привычка молча обрезать строку по длине наверняка подпортила нервов не одному поколению программистов: они думали что данные записались норм - ошибок ведь не было - а потом через полгода выплывает, что там какие-то обрезки.

    Также в хелпере есть ряд методов для формирования запросов с префиксом prepare.

    И снова термин, который во всём остальном мире означает одно, а в битриксе - что-то совсем другое. Но при этом что именно другое - в статье не объясняется!

    Транзакции

    И никто не догадался сделать простую онанимку, чтобы писать

    $db->transaction(function () use ($db) {
        $db->queryExecute('UPDATE my_table SET active = "N" WHERE age > 0');
        \Bitrix\Main\SiteTable::update('s1', [
            'ACTIVE' => 'N',
        ]);  
    });

    а не каждый раз всю эту колбасу на пол-экрана.

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


  1. Adgh
    23.12.2024 09:55

    ORM справляется с чтением / записью в колонки с json-native и прочих СУБД-специфичных типов данных, или из коробки только текст, числа и булево?