На практике импорт восьми тысяч строк через цикл PriceTable::add() упирается в таймаут PHP, а PriceTable::addMulti($rows) падает на duplicate key при втором прогоне. ORM D7 умеет три массовые стратегии - addMulti, addMergeMulti и addInsertIgnoreMulti - но выбор неочевиден: где нужен upsert, где пропуск дублей, и почему onBeforeAdd вдруг молчит. Ниже матрица сценариев для 1С-Битрикс: Управление сайтом и шаблон пакетного импорта, который вы прогоните на staging дважды без сюрпризов.
Три сценария на одном CSV: addMulti - чистый append одним SQL; addInsertIgnoreMulti - повторный прогон без ошибок, дубли пропущены; addMergeMulti - upsert, строка обновляется при конфликте unique. Merge и InsertIgnore никогда не вызывают события ORM. При autoincrement и $ignoreEvents=false ядро само разбивает bulk на отдельные add() с E_USER_NOTICE. Пакеты по 500-1000 строк, unique index в миграции, валидация до bulk.
Речь про коробочную CMS 1С-Битрикс — не про CRM-портал Bitrix24. ORM (Object-Relational Mapping) - слой D7, который связывает PHP-класс *Table с таблицей MySQL. Если *Table ещё нет - начните с гайда по DataManager и своей таблице (B71): getMap, createDbTable, одиночный add. Здесь - массовые операции поверх готовой схемы.
Выберите стратегию массовой вставки под сценарий импорта
Одиночный Table::add() удобен для единичных записей, но импорт каталога, синхронизация справочника или лог токенов требуют пакетов. Три метода решают разные задачи - путать их опасно.
| Метод | SQL-эквивалент | Когда использовать | События ORM |
|---|---|---|---|
| addMulti | Один INSERT на пакет | Чистый append: первичная загрузка, нет риска дублей | Да, если $ignoreEvents=false и нет autoincrement-ловушки |
| addMergeMulti | INSERT ON DUPLICATE KEY UPDATE | Синхронизация: цена, остаток, статус меняются при повторе | Никогда |
| addInsertIgnoreMulti | INSERT IGNORE | Идемпотентный лог: повторная вставка того же ключа - тихий skip | Никогда |
Делайте: перед кодом назовите сценарий - append, skip или upsert. Не делайте: не оборачивайте getList+update в цикл на десятки тысяч строк, если хватает addMergeMulti.
Настройте addMulti: пакеты, $ignoreEvents и ловушка autoincrement
DataManager::addMulti($rows, $ignoreEvents = false) по умолчанию использует стратегию Insert - один SQL на пакет. Типичная ошибка: в getMap() поле ID с autoincrement, а $ignoreEvents=false — ядро откатывается к циклу отдельных add() и шлёт E_USER_NOTICE "Insert query was forced to multiple separate queries". События onBeforeAdd сработают, но скорость пропадёт.
- Разбейте CSV на пакеты по 500-1000 строк - меньше риск упереться в max_allowed_packet MySQL.
- Решите про события: нужен onBeforeAdd - терпите отдельные запросы; нужна скорость - передайте $ignoreEvents=true и вынесите валидацию до bulk (см. гайд по событиям ORM).
- Вызовите $result = PriceTable::addMulti($chunk, true) и проверьте $result->isSuccess().
- Зафиксируйте в комментарии к методу импорта: "события отключены намеренно, валидация в validateRow()".
- Прогоните импорт на staging и убедитесь, что в логе PHP нет forced separate queries, если ждали один SQL.
$chunks = array_chunk($rows, 500);
foreach ($chunks as $chunk) {
$result = \Mycompany\Module\PriceTable::addMulti($chunk, true);
if (!$result->isSuccess()) {
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
}
Делайте: явно выбирайте trade-off "скорость vs onBeforeAdd". Не делайте: не ждите все сгенерированные ID после bulk с autoincrement - ядро их не вернёт пакетом.
Внедрите addMergeMulti для синхронизации справочника (upsert)
Trait AddMergeTrait добавляет addMerge() и addMergeMulti(). Под капотом - INSERT ON DUPLICATE KEY UPDATE: при конфликте по primary или unique обновляются не-PK колонки. События ORM не вызываются даже при $ignoreEvents=false - это зафиксировано в официальном API.
Без PK или unique index в таблице получите NotSupportedException. Индекс создайте в миграции до первого импорта. Для Highload-блоков массовый merge тоже возможен, но множественные UF-поля требуют осторожности - см. гайд по HL и D7 ORM.
use Bitrix\Main\ORM\Data\AddStrategy\Trait\AddMergeTrait;
class PriceTable extends DataManager
{
use AddMergeTrait;
// getMap() с unique на EXTERNAL_ID ...
}
PriceTable::addMergeMulti($rows);
Альтернатива - переопределить getAddStrategy() и вернуть Merge с кастомным набором unique-полей, если конфликт не по PK. Стратегия InsertIgnore в ядре доступна с main 22.0.
Делайте: merge для ежедневной синхронизации цен и остатков из ERP. Не делайте: не используйте merge, если при повторе строку нужно оставить как была - тогда InsertIgnore.
Примените addInsertIgnoreMulti, когда дубль надо пропустить без ошибки
addInsertIgnoreMulti() генерирует INSERT IGNORE: строка с уже существующим unique-ключом не вставляется и не обновляется. Удобно для лога токенов, одноразовых событий, идемпотентных webhook-записей. В Result смотрите isDBChanged - была ли реальная вставка.
Разовый вызов trait-метода vs глобальный getAddStrategy(): trait - для точечного импорта; переопределение getAddStrategy() на InsertIgnore - когда весь класс по умолчанию терпит дубли (например, очередь с составным unique USER_ID+TOKEN).
use Bitrix\Main\ORM\Data\AddStrategy\Trait\AddInsertIgnoreTrait;
class TokenLogTable extends DataManager
{
use AddInsertIgnoreTrait;
}
TokenLogTable::addInsertIgnoreMulti($rows);
Делайте: ignore для "вставить, если ещё нет". Не делайте: не ждите обновления полей при конфликте - для этого addMergeMulti.
Оберните пакеты в транзакцию и отличите bulk от цикла add
Каждый пакет addMulti сам по себе атомарен на уровне одного SQL, но импорт из двадцати пакетов без транзакции оставит половину строк при обрыве на пятнадцатом. Обёртка Connection::startTransaction() описана в гайде по транзакциям D7 ORM: откройте транзакцию, прогоните пакеты, commit при успехе всех Result, rollback при первой ошибке.
Схема импорта:
validate CSV → array_chunk(500) → startTransaction → addMergeMulti пакетами → isSuccess на каждый → commitTransaction / rollbackTransaction
Цикл add() внутри одной транзакции на десять тысяч строк - десять тысяч отдельных INSERT и нагрузка на binlog. Bulk с $ignoreEvents=true - десяток SQL-запросов. Разница в минутах на больших объёмах.
Делайте: транзакцию на весь импорт или на логическую партию (например, один склад). Не делайте: не держите транзакцию часами - укладывайтесь в таймаут MySQL и PHP.
Проверьте чек-лист перед выкладкой на production
- Убедитесь, что в таблице есть PK или unique index под выбранную стратегию merge/ignore.
- Задокументируйте отказ от onBeforeAdd, если используете merge, ignore или addMulti с $ignoreEvents=true.
- Ограничьте размер пакета и проверьте max_allowed_packet на staging.
- Прогоните импорт дважды: первый раз - вставка, второй - ожидаемое поведение (обновление или skip без duplicate key).
- Сверьте лог PHP: нет неожиданных E_USER_NOTICE про separate queries.
- Для HL с множественными UF не полагайтесь на голый addMulti - используйте collection->save() по рекомендациям HL-гайда.
Нужен разбор импорта под ваш модуль - обсудим задачу. Похожие кейсы смотрите в портфолио.
Автор: Максим Мольков, разработчик 1С-Битрикс.
Источники: DataManager::addMulti, AddMergeTrait, AddInsertIgnoreTrait, операции ORM и события.
Частые вопросы
Чем addMergeMulti отличается от addInsertIgnoreMulti?
addMergeMulti обновляет не-PK поля при конфликте unique - это upsert для синхронизации. addInsertIgnoreMulti пропускает дубль без ошибки и без обновления - для идемпотентных логов. Выберите merge, если второй прогон должен подтянуть новые цены; ignore - если повторная строка не нужна вообще.
Вызывается ли onBeforeAdd при addMulti?
Только если $ignoreEvents=false и ядро не ушло в forced separate queries из-за autoincrement. При $ignoreEvents=true события не сработают. addMerge и addInsertIgnore никогда не вызывают onBeforeAdd - вынесите проверки в validateRow() до bulk.
Нужен ли unique index для merge и ignore?
Да. Без primary или unique index ORM бросит NotSupportedException. Создайте индекс в миграции до импорта и убедитесь, что конфликтующие поля совпадают с бизнес-ключом (SKU, EXTERNAL_ID, пара USER_ID+TOKEN).
Как импортировать 10 000 строк без таймаута?
Разбейте на пакеты по 500-1000 строк, используйте addMulti или addMergeMulti с $ignoreEvents=true, запускайте из CLI или агента с увеличенным max_execution_time. Оберните пакеты в транзакцию и не смешивайте с тяжёлым getList+update в цикле.
Чем addMulti в транзакции отличается от цикла add()?
addMulti отправляет один INSERT на пакет - меньше round-trip к MySQL. Цикл add() - тысячи отдельных INSERT и срабатывание событий на каждую строку. В одной транзакции bulk быстрее и короче binlog, но события при $ignoreEvents=true отключены - это осознанный выбор.
Можно ли вызвать addMerge через addMulti и getAddStrategy()?
Да: переопределите getAddStrategy() и верните Merge или InsertIgnore с нужными unique-полями - тогда обычный addMulti($rows) пойдёт по выбранной стратегии. Для разовых сценариев проще trait-методы addMergeMulti() и addInsertIgnoreMulti() без смены поведения всего класса.
Было полезно?
Поставьте лайк, если статья помогла разобраться.
Поделиться статьёй
Отправьте тому, кому это пригодится.
Можно поделиться и фрагментом: выделите текст статьи.