InsertIgnore стратегия для безопасной вставки без дублирования

21.11.2025

Проблема дублирования записей

При массовой вставке данных в базу через ORM часто возникает проблема дублирования по уникальным полям. Стандартный метод add() генерирует ошибку при попытке вставить запись с существующим первичным ключом или уникальным индексом. Приходится вручную проверять существование записи через getList() перед вставкой, что неэффективно при массовых операциях.

Стратегия InsertIgnore

С версии 22.0 в Bitrix ORM появилась стратегия InsertIgnore, которая использует SQL-конструкцию INSERT IGNORE. Если запись с таким ключом существует, она игнорируется без ошибки. Это атомарная операция на уровне СУБД, что обеспечивает корректность даже при конкурентных запросах.

Переопределение стратегии в Table-классе

Самый простой способ — переопределить метод getAddStrategy() в вашем Table-классе:

namespace Mycompany\MyModule;

use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Data\AddStrategy;

class UserTokenTable extends DataManager
{
    public static function getTableName()
    {
        return 'b_user_token';
    }
    
    public static function getMap()
    {
        return [
            'ID' => ['data_type' => 'integer', 'primary' => true, 'autocomplete' => true],
            'USER_ID' => ['data_type' => 'integer', 'required' => true],
            'TOKEN' => ['data_type' => 'string', 'required' => true],
        ];
    }
    
    // Переопределяем стратегию для всех операций add()
    protected static function getAddStrategy(): AddStrategy\Contract\AddStrategy
    {
        return new AddStrategy\InsertIgnore(static::getEntity());
    }
}

// Теперь add() использует INSERT IGNORE автоматически
$result = UserTokenTable::add([
    'ID' => 1,
	  'USER' => 1,
    'TOKEN' => 'abc123'
]);

// Если запись существует - игнорируется без ошибки
// $result->isSuccess() вернёт true
// $result->getId() вернёт ID существующей записи

Массовая вставка с InsertIgnore

Для массовой вставки данных используйте метод addMulti():

use Bitrix\MyModule\UserTokenTable;

$tokens = [
    ['ID' => 10, 'USER_ID' => 1, 'TOKEN' => 'token1'],
    ['ID' => 11, 'USER_ID' => 2, 'TOKEN' => 'token2'],
    ['ID' => 12, 'USER_ID' => 3, 'TOKEN' => 'token3'],
    ['ID' => 10, 'USER_ID' => 1, 'TOKEN' => 'token1'], // дубликат - будет проигнорирован
];

// Все записи вставляются одним запросом
$result = UserTokenTable::addMulti($tokens);

// Проверка успешности
if ($result->isSuccess()) {
    // Операция выполнена, дубликаты проигнорированы
}

Указание уникальных полей

По умолчанию InsertIgnore проверяет первичный ключ. Если нужно проверять другие поля, передайте их в конструктор:

protected static function getAddStrategy(): AddStrategy\Contract\AddStrategy
{
    // Проверять уникальность по полям USER_ID и TOKEN
    return new AddStrategy\InsertIgnore(
        static::getEntity(),
        ['USER_ID', 'TOKEN']
    );
}

Проверка изменений базы

Метод isDBChanged() в результате показывает, была ли реально изменена база:

$result = UserTokenTable::add(['USER_ID' => 1, 'TOKEN' => 'abc']);

if ($result->isSuccess()) {
    if ($result->getData()['isDBChanged']) {
        // Запись была вставлена
    } else {
        // Запись уже существовала и была проигнорирована
    }
}

Важные ограничения

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

Когда использовать InsertIgnore

Применяйте InsertIgnore для импорта данных, синхронизации справочников, сохранения логов без дублей, кэширования токенов. Для случаев, когда нужно обновить существующую запись, используйте стратегию Merge вместо InsertIgnore.

Telegram Все советы
Опубликовано 2 недели назад

Похожие советы

Работа с изображениями через Bitrix\Main\File\Image

Разработчики часто используют CFile::ResizeImageGet() для изменения размеров изображений, не подозревая, что эта функция является обёрткой над современным D7 API. Классы Bitrix\Main\File\Image предоставляют прямой доступ к операциям с изображениями, что даёт больше контроля и гибкости.

Основные операции с Image

use Bitrix\Main\File\Image;
use Bitrix\Main\File\Image\Rectangle;
use Bitrix\Main\File\Image\Mask;

$image = new Image('/path/to/image.jpg');
$image->load();

// Получение информации о изображении
$info = $image->getInfo();
echo $info->getWidth() . 'x' . $info->getHeight(); // размеры
echo $info->getMime(); // MIME-тип
echo $info->getFormat(); // Image::FORMAT_JPEG, FORMAT_PNG, etc.

// Изменение размера (пропорционально)
$source = $image->getDimensions();
$destination = new Rectangle(800, 600);
$source->resize($destination, Image::RESIZE_PROPORTIONAL);
$image->resize($source, $destination);

// Сохранение с качеством 85%
$image->save(85);

Аналог unsharpmask из CFile::ResizeImageGet

// CFile::ResizeImageGet по умолчанию применяет sharpen с precision=15
$mask = Mask::createSharpen(15);
$image->filter($mask);

Режимы изменения размера

// RESIZE_PROPORTIONAL - сохраняет пропорции, вписывает в указанный прямоугольник
$source->resize($destination, Image::RESIZE_PROPORTIONAL);

// RESIZE_EXACT - кадрирует изображение по центру до точных размеров
$source->resize($destination, Image::RESIZE_EXACT);

// RESIZE_PROPORTIONAL_ALT - учитывает ориентацию (портрет/ландшафт)
$source->resize($destination, Image::RESIZE_PROPORTIONAL_ALT);

Расширенные возможности

// Поворот и отражение
$image->rotate(90); // поворот на 90°
$image->flipHorizontal(); // зеркальное отражение
$image->autoRotate($exifOrientation); // автокоррекция по EXIF

// Размытие
$image->blur(10); // sigma от 1 до 100

// Водяной знак (изображение)
use Bitrix\Main\File\Image\ImageWatermark;

$watermark = new ImageWatermark('/path/to/watermark.png');
$watermark->setAlignment('right', 'bottom')
    ->setPadding(20)
    ->setAlpha(0.7);
$image->drawWatermark($watermark);

// Сохранение в другой формат
$image->saveAs('/path/to/output.webp', 85, Image::FORMAT_WEBP);

Выбор движка: GD или Imagick

// По умолчанию используется GD
// Для Imagick зарегистрируйте сервис:
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\File\Image\Imagick;

$serviceLocator = ServiceLocator::getInstance();
$serviceLocator->registerByCreator('main.imageEngine', fn() => new Imagick());

Классы Bitrix\Main\File\Image полностью покрывают функциональность CFile::ResizeImageGet(), включая proportional resize, exact crop, sharpen-фильтр и водяные знаки. Для типовых задач resizing проще использовать CFile::ResizeImageGet(), а для сложных сценариев с цепочкой операций — классы напрямую.

Параллельные HTTP-запросы через асинхронный API HttpClient

При интеграции с внешними сервисами часто требуется выполнить несколько HTTP-запросов. Последовательное выполнение приводит к суммированию времени ожидания каждого запроса. Если три API отвечают по 500мс, общее время составит 1.5 секунды. Класс Bitrix\Main\Web\HttpClient поддерживает асинхронное выполнение запросов через curl_multi, что позволяет выполнять их параллельно.

Для асинхронных запросов используется метод sendAsyncRequest(), который возвращает объект Promise. Promise реализует интерфейс Http\Promise\Promise и поддерживает цепочки обработчиков через метод then().

use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Http\Request;
use Bitrix\Main\Web\Uri;

// Важно: для асинхронных запросов требуется CURL
$client = new HttpClient(['useCurl' => true]);

// Список URL для параллельных запросов
$urls = [
    'products' => 'https://api.example.com/products',
    'categories' => 'https://api.example.com/categories',
    'prices' => 'https://api.example.com/prices',
];

$promises = [];

foreach ($urls as $key => $url)
{
    // Создаём PSR-7 совместимый Request
    $request = new Request('GET', new Uri($url));
    
    // sendAsyncRequest() не блокирует выполнение
    $promises[$key] = $client->sendAsyncRequest($request);
}

// wait() блокирует до завершения всех запросов
$responses = $client->wait();

// Обрабатываем результаты
foreach ($promises as $key => $promise)
{
    try
    {
        // wait() на конкретном promise возвращает Response
        $response = $promise->wait();
        $data[$key] = json_decode((string)$response->getBody(), true);
    }
    catch (\Bitrix\Main\Web\Http\ClientException $e)
    {
        // Обработка ошибок сети
        $data[$key] = ['error' => $e->getMessage()];
    }
}

Promise поддерживает callback-функции для обработки успешных и неуспешных запросов:

$promise = $client->sendAsyncRequest($request);

// Регистрируем обработчики до вызова wait()
$promise->then(
    function ($response) {
        // Вызывается при успешном ответе
        // Можно модифицировать и вернуть response
        return $response;
    },
    function ($exception) {
        // Вызывается при ошибке
        // Логируем или обрабатываем исключение
        return $exception;
    }
);

// Запускаем выполнение
$client->wait();

Для POST-запросов с телом используйте Http\FormStream:

use Bitrix\Main\Web\Http\FormStream;

$body = new FormStream(['param1' => 'value1', 'param2' => 'value2']);
$request = new Request('POST', new Uri($url), ['Content-Type' => 'application/x-www-form-urlencoded'], $body);

$promise = $client->sendAsyncRequest($request);

Асинхронный API HttpClient использует curl_multi_exec() под капотом, что обеспечивает истинную параллельность на уровне сетевых операций. Три запроса по 500мс выполнятся примерно за 500мс вместо 1.5 секунд.

Работа с Highload-блоками по имени вместо ID

Продолжаем тему избавления кодовой базы от ID при работе с сущностями Битрикс.

Проблема

Типичный код работы с Highload-блоками содержит жёстко прописанные ID:

<?php
// Антипаттерн: ID зашит в код
$hlblock = HighloadBlockTable::getById(5)->fetch();
$entity = HighloadBlockTable::compileEntity($hlblock);

ID Highload-блока различается между окружениями: на dev-сервере это 5, на production — 12. При переносе кода приходится менять значения вручную или использовать конфигурационные файлы. Ситуация усугубляется, когда ID разбросаны по десяткам файлов проекта.

Решение

Метод HighloadBlockTable::resolveHighloadblock() принимает символьное имя HL-блока вместо числового ID. Имя задаётся при создании блока и остаётся неизменным при переносе между окружениями.

<?php
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Loader;

Loader::includeModule('highloadblock');

// Правильно: используем символьное имя
$hlblock = HighloadBlockTable::resolveHighloadblock('Cities');

if ($hlblock !== null) {
    $entity = HighloadBlockTable::compileEntity($hlblock);
    $entityClass = $entity->getDataClass();
    
    $items = $entityClass::getList([
        'filter' => ['=UF_ACTIVE' => 1]
    ])->fetchAll();
}

Метод compileEntity также поддерживает имя

Символьное имя можно передавать напрямую в compileEntity() — метод внутри вызывает resolveHighloadblock():

<?php
// Компиляция entity по имени — без промежуточных вызовов
$entity = HighloadBlockTable::compileEntity('Cities');
$entityClass = $entity->getDataClass();

$cities = $entityClass::getList()->fetchAll();

Константы для имён HL-блоков

Для централизованного управления именами создайте класс с константами:

<?php
namespace App\Reference;

class HLBlock
{
    public const CITIES = 'Cities';
    public const REGIONS = 'Regions';
    public const COLORS = 'ProductColors';
    public const SIZES = 'ProductSizes';
}

Использование в коде:

<?php
use App\Reference\HLBlock;

$entity = HighloadBlockTable::compileEntity(HLBlock::CITIES);
$entityClass = $entity->getDataClass();

При таком подходе IDE подсказывает доступные HL-блоки, а опечатки выявляются на этапе статического анализа.

Встроенное кеширование

Метод resolveHighloadblock() кеширует результат запроса на 24 часа. Повторные вызовы с тем же именем не обращаются к базе данных:

<?php
// Первый вызов — запрос к БД, результат кешируется
$hlblock1 = HighloadBlockTable::resolveHighloadblock('Cities');

// Повторный вызов — данные из кеша ORM
$hlblock2 = HighloadBlockTable::resolveHighloadblock('Cities');

Валидация имени

Метод проверяет корректность символьного имени регулярным выражением /^[a-z0-9_]+$/i. При несуществующем или некорректном имени возвращается null:

<?php
$hlblock = HighloadBlockTable::resolveHighloadblock('NonExistent');
// $hlblock === null

Итог

Замените числовые ID на символьные имена HL-блоков через resolveHighloadblock(). Код станет переносимым между окружениями без ручных правок, а централизованные константы обеспечат контроль над используемыми справочниками.