Формирование запросов

В Bitrix Framework есть два инструмента для работы с SQL-запросами.

SqlHelper. Отвечает за экранирование данных и форматирование SQL-выражений.

SqlExpression. Позволяет строить безопасные запросы через плейсхолдеры и автоматически защищает от инъекций.

SqlHelper. Экранирование и базовые операции

ORM автоматически использует SqlHelper для стандартных операций. Обращайтесь к SqlHelper напрямую, если нужно:

  • выполнить сложный SQL-запрос,

  • оптимизировать производительность запросов.

Получить SqlHelper:

$helper = \Bitrix\Main\Application::getConnection()->getSqlHelper();
        

Как экранировать данные

Экранировать названия столбцов:

$helper->quote('id'); // `id`
        $helper->quote('table_name.id'); // `table_name`.`id`
        $helper->quote('не ` безопасная " строка'); // `не  безопасная " строка`
        

Экранировать значения:

$safeValue = $helper->forSql('не " безопасная \' строка'); // не \" безопасная \' строка
        

Как конвертировать данные для SQL

Общий метод convertToDb обрамляет строки в кавычки, NULL преобразует в 'NULL':

$helper->convertToDb('не " безопасная \' строка'); // 'не \" безопасная \' строка'
        $helper->convertToDb(null); // 'NULL'
        $helper->convertToDb(123); // '123'
        

Указать тип поля:

$helper->convertToDb('строка', new \Bitrix\Main\ORM\Fields\FloatField('column_name')); // 0
        $helper->convertToDb(123, new \Bitrix\Main\ORM\Fields\TextField('column_name')); // '123'
        

Как работать со строками и числами

Конвертировать в строку. Метод преобразует NULL в пустую строку:

$helper->convertToDbString('строка'); // 'строка'
        $helper->convertToDbString(null); // ''
        $helper->convertToDbString(123); // '123'
        

Обрезать строку до нужной длины:

$helper->convertToDbString('Длинная строка', 10); // 'Длинная и '
        

Конвертировать для текстовых полей. Аналогично convertToDbString, но без указания длины:

$helper->convertToDbText('строка'); // 'строка'
        $helper->convertToDbText(null); // ''
        $helper->convertToDbText(123); // '123'
        

Как конвертировать данные для работы с БД

Конвертировать бинарные данные:

$binaryData = base64_decode('iVBORw0KGgoAAA==');
        $helper->convertToDbBinary($binaryData); // 'PNG\r\n\Z\n\0\0'
        

Конвертировать в целое число:

$helper->convertToDbInteger('строка'); // 0
        $helper->convertToDbInteger(null); // 0
        $helper->convertToDbInteger(123); // 123
        $helper->convertToDbInteger(123.456); // 123
        

Ограничить размер числа в байтах:

$helper->convertToDbInteger(10_000_000_000, 2); // 32767 (макс. для 2 байт)
        $helper->convertToDbInteger(10_000_000_000, 4); // 2147483647 (макс. для 4 байт)
        

Конвертировать в число с плавающей точкой:

$helper->convertToDbFloat('строка'); // '0'
        $helper->convertToDbFloat(123.456); // '123.456'
        

Указать количество знаков после запятой:

$helper->convertToDbFloat(123.456789, 1); // '123.5'
        

Как выполнять операции с датами и временем

Конвертировать дату:

$helper->convertToDbDate(new \Bitrix\Main\Type\Date('01.01.2015')); // '2015-01-01'
        

Конвертировать время:

$helper->convertToDbDateTime(new \Bitrix\Main\Type\DateTime('01.01.2015 01:23:45')); // '2015-01-01 01:23:45'
        

Два последних метода — convertToDbDate и convertToDbDateTime — принимают null или объекты Date/DateTime. При других типах данных выбрасывают исключение.

Получить формат даты для текущей БД:

$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')
        

Эти методы преобразуют данные для совместимости в PostgreSQL. В MySQL они не требуются:

$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-конструкции в качестве параметров.

Как совершать пакетные операции с данными

prepareMerge() — добавляет или обновляет одну запись:

[ $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', 10)
        ON DUPLICATE KEY UPDATE `CNT` = `CNT` + 10
        

prepareMergeValues() — добавляет или обновляет несколько записей:

$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`)
        VALUES (1, 's1', 'counter_name', 1),(2, 's1', 'counter_name', 1),(2, 's1', 'another_counter', 1)
        ON DUPLICATE KEY UPDATE `CNT` = `CNT` + 1
        

prepareMergeSelect() — добавляет или обновляет из подзапроса:

$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),
            ],
        );
        

Генерирует запрос:

INSERT INTO `b_user_counter` (`USER_ID`,`SITE_ID`,`CODE`,`CNT`)
        (SELECT * FROM my_counters)
        ON DUPLICATE KEY UPDATE `CNT` = `CNT` + 10
        

prepareMergeMultiple() — полностью заменяет записи:

$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],
            ],
        );
        

Генерирует запросы, может разделить на несколько при большом объеме данных:

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)
        

SqlExpression. Безопасное построение запросов

SqlExpression использует специальные метки — плейсхолдеры. При компиляции запроса плейсхолдеры заменяются на экранированные значения для предотвращения SQL-инъекций.

  • ? — автоматическое преобразование.

  • ?s — строка.

  • ?i — целое число.

  • ?f — число с плавающей точкой.

  • ?# — имя столбца.

  • ?v — подстановка значений VALUES для запросов типа INSERT и UPDATE.

Создать новый объект:

$sql = new SqlExpression('SELECT * FROM b_user');
        $result = Application::getConnection()->query($sql);
        

Получить готовый SQL-запрос можно двумя способами:

echo $sql->compile(); // Явный вызов компиляции
        echo (string)$sql;    // Неявное преобразование в строку
        

Создать новое SQL-выражение с использованием SqlExpression для безопасного формирования запроса:

$sql = new SqlExpression(
            // Шаблон SQL-запроса с плейсхолдерами для безопасной подстановки параметров
            'SELECT * FROM ?# WHERE (ID = ?i OR ID > ?f) AND `NAME` = ?s AND DATE_REGISTER > ?',
            
            // Параметры, которые будут подставлены вместо плейсхолдеров:
            
            'b_user',       // ?# — имя таблицы (экранируется как идентификатор)
            
            1,              // ?i — целочисленное значение (ID = 1)
            
            1.23,           // ?f — число с плавающей точкой (ID > 1.23)
            
            'admin',        // ?s — строковое значение (NAME = 'admin', с экранированием)
            
            new \Bitrix\Main\Type\Date('01.01.2024') // ? — объект даты (DATE_REGISTER > '2024-01-01')
        );
        

Результат:

SELECT * FROM `b_user` WHERE (ID = 1 OR ID > 1.23) AND `NAME` = 'admin' AND DATE_REGISTER > '2024-01-01'
        

Передача NULL преобразует в NULL все плейсхолдеры, кроме ?#:

$sql = new SqlExpression(
            'SELECT * FROM ?# WHERE ID = ?i OR NAME = ?',
            null,
            null,
            null
        );
        

Результат:

SELECT * FROM `` WHERE ID = NULL OR NAME = NULL
        

Для дат используйте базовый плейсхолдер ? для автоматического форматирования. Для строкового представления — ?s:

$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')
        );
        

Результат:

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')