При импорте данных из внешних источников (например, при загрузке информации из файлов Excel) разработчики часто сталкиваются с ситуацией, когда в одной ячейке таблицы через разделитель записано несколько значений. Типичный пример — список штрихкодов номенклатуры, разделенный точкой с запятой или двоеточием. Для корректной обработки таких данных в системе 1С необходимо решить две важные задачи: правильно разбить исходную строку на массив подстрок и максимально быстро записать изменения в базу данных без создания избыточной нагрузки на СУБД.
В этой статье мы подробно разберем, как эффективно разделять строки встроенными средствами платформы 1С и функциями БСП, проанализируем частые ошибки при работе с базой данных в цикле и реализуем оптимизированный алгоритм пакетной записи.
Начиная с версии платформы 1С:Предприятие 8.3.6, разработчикам доступна встроенная функция СтрРазделить. Данный метод работает на уровне ядра платформы (на C++), что обеспечивает максимальную скорость выполнения при обработке больших текстовых массивов.
Давайте рассмотрим синтаксис этой функции:
Результат = СтрРазделить(Строка, Разделитель, ВключатьПустые)
Важной особенностью платформенной функции является ее поведение при обработке параметра Разделитель. Если мы передаем в качестве разделителя строку, состоящую из нескольких символов, платформа будет трактовать каждый из этих символов как самостоятельный разделитель. Проанализируем пример:
// Исходная строка содержит разные разделители (двоеточие и точку с запятой)
СтрокаШтрихкодов = "123456789;987654321:555555555";
// Передаем строку разделителей ";:"
МассивШтрихкодов = СтрРазделить(СтрокаШтрихкодов, ";:", Ложь);
// В результате мы получим массив из трех элементов:
// МассивШтрихкодов[0] = "123456789"
// МассивШтрихкодов[1] = "987654321"
// МассивШтрихкодов[2] = "555555555"
Параметр ВключатьПустые со значением Ложь автоматически отсекает пустые строки, которые могли возникнуть из-за случайных двойных разделителей в исходном файле Excel. Это избавляет нас от необходимости писать дополнительные проверки в коде.
Если разработка ведется на более старых версиях платформы или требуется разобрать строку строго по конкретной многосимвольной фразе (например, разделить текст по слову " и "), стандартный метод СтрРазделить нам не подойдет. В этом случае мы должны использовать функцию из общего модуля БСП:
СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(Значение, Разделитель, ПропускатьПустыеСтроки)
Давайте выясним причину разного поведения этих функций. Функция БСП воспринимает параметр Разделитель строго как единую неделимую последовательность символов. Проанализируем код:
// Пример использования функции БСП
СтрокаШтрихкодов = "Ковер_1 и Ковер_2 и Ковер_3";
Разделитель = " и ";
МассивИмен = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаШтрихкодов, Разделитель, Истина);
// Получим массив:
// МассивИмен[0] = "Ковер_1"
// МассивИмен[1] = "Ковер_2"
// МассивИмен[2] = "Ковер_3"
Обратите внимание на логику булевого флага для пустых строк: у БСП параметр называется ПропускатьПустыеСтроки (значение Истина означает исключение пустых элементов), в то время как у платформенной функции СтрРазделить параметр называется ВключатьПустые (для исключения пустых элементов передается Ложь).
После успешного получения массива значений перед разработчиком встает задача сопоставить эти данные с элементами справочника в базе данных и изменить их реквизиты (например, установить флаг Оприходован в значение Истина). Рассмотрим типичную ошибку начинающих разработчиков:
// НЕПРАВИЛЬНЫЙ ВАРИАНТ (Низкая производительность)
Для Шаг = 0 По МассивШтрихкодов.Количество() - 1 Цикл
// Поиск в цикле порождает отдельный запрос к СУБД на каждой итерации
ШтрихкодСсылка = Справочники.ШтрихКодыКовров.НайтиПоКоду(МассивШтрихкодов[Шаг]);
Если ЗначениеЗаполнено(ШтрихкодСсылка) Тогда
ШтрихкодОбъект = ШтрихкодСсылка.ПолучитьОбъект();
ШтрихкодОбъект.Оприходован = Истина;
ШтрихкодОбъект.Записать(); // Запись в цикле без явной транзакции
КонецЕсли;
КонецЦикла;
Давайте разберем по шагам, почему данный код неприемлем для использования в реальных задачах автоматизации:
НайтиПоКоду под капотом выполняет полноценный SQL-запрос к СУБД. Если в массиве содержится 1000 штрихкодов, система сделает 1000 отдельных запросов к базе данных, что приведет к колоссальным временным задержкам на сетевое взаимодействие.Записать() платформа 1С автоматически открывает транзакцию, фиксирует изменения одного объекта и закрывает транзакцию. Запись 1000 элементов по отдельности заставит СУБД выполнить 1000 транзакций, нагружая жесткий диск сервера транзакционными логами.Оприходован = Истина, система все равно получит объект, перезапишет его и вызовет все обработчики событий, тратя ресурсы сервера впустую.Чтобы решить вышеописанные проблемы производительности, мы должны переписать алгоритм. Всю работу с базой данных мы выполним через один запрос, а запись объектов оптимизируем с помощью явного управления транзакциями и отключения лишних проверок.
Посмотрим на пример оптимизированного кода:
// 1. Получаем массив уникальных штрихкодов (исключаем дубли)
МассивШтрихкодов = СтрРазделить(ИсходнаяСтрока, ";", Ложь);
// 2. Выполняем один запрос к базе данных для получения всех нужных элементов
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ШтрихКоды.Ссылка КАК Ссылка
|ИЗ
| Справочник.ШтрихКодыКовров КАК ШтрихКоды
|ГДЕ
| ШтрихКоды.Код В(&МассивШтрихкодов)
| И НЕ ШтрихКоды.Оприходован"; // Выбираем только те, которые еще НЕ оприходованы
Запрос.УстановитьПараметр("МассивШтрихкодов", МассивШтрихкодов);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
// 3. Открываем явную транзакцию для ускорения записи порциями
НачатьТранзакцию();
СчетчикЗаписей = 0;
Пока Выборка.Следующий() Цикл
// Получаем объект для изменения
ШтрихкодОбъект = Выборка.Ссылка.ПолучитьОбъект();
ШтрихкодОбъект.Оприходован = Истина;
// Важный момент: отключаем выполнение тяжелого прикладного кода в модуле объекта,
// так как мы выполняем групповую техническую операцию
ШтрихкодОбъект.ОбменДанными.Загрузка = Истина;
Попытка
ШтрихкодОбъект.Записать();
СчетчикЗаписей = СчетчикЗаписей + 1;
// Каждые 500 записей фиксируем транзакцию и открываем новую,
// чтобы избежать длительных блокировок таблиц СУБД для других пользователей
Если СчетчикЗаписей % 500 = 0 Тогда
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
Исключение
// В случае ошибки отменяем текущую транзакцию и пишем лог
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("ГрупповоеОприходование",
УровеньЖурналаРегистрации.Ошибка, , ,
"Ошибка при записи объекта: " + ОписаниеОшибки());
ВызватьИсключение "Не удалось записать изменения. Операция отменена.";
КонецПопытки;
КонецЦикла;
// Закрываем финальную транзакцию, если она осталась активной
Если ТранзакцияАктивна() Тогда
ЗафиксироватьТранзакцию();
КонецЕсли;
КонецЕсли;
Проанализируем ситуацию, когда количество загружаемых штрихкодов измеряется десятками тысяч. Передача слишком большого массива в параметр запроса В (&МассивШтрихкодов) может замедлить работу оптимизатора SQL-сервера, так как текст запроса разрастается до огромных размеров.
Для решения этой проблемы мы можем предварительно загрузить наш массив во временную таблицу внутри запроса. Для этого сначала преобразуем массив в ТаблицуЗначений, передадим ее в качестве параметра и соединим во временной таблице со справочником. Ниже приведен пример реализации такой схемы:
// Создаем Таблицу Значений для передачи в менеджер временных таблиц
ТаблицаКодов = Новый ТаблицаЗначений;
ТаблицаКодов.Колонки.Добавить("Код", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(150)));
МассивШтрихкодов = СтрРазделить(ИсходнаяСтрока, ";", Ложь);
Для Каждого ТекущийКод Из МассивШтрихкодов Цикл
НоваяСтрока = ТаблицаКодов.Добавить();
НоваяСтрока.Код = СокрЛП(ТекущийКод);
КонецЦикла;
// Используем менеджер временных таблиц для ускорения поиска на стороне СУБД
МенеджерВТ = Новый МенеджерВременных Таблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// Шаг 1. Помещаем таблицу значений во временную таблицу и индексируем поле поиска
Запрос.Текст =
"ВЫБРАТЬ
| ВходныеКоды.Код КАК Код
|ПОМЕСТИТЬ ВТ_Коды
|ИЗ
| &ТаблицаКодов КАК ВходныеКоды
|;
|////////////////////////////////////////////////////////////////////////////////
|ИНДЕКСИРОВАТЬ ВТ_Коды.Код
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ШтрихКоды.Ссылка КАК Ссылка
|ИЗ
| ВТ_Коды КАК ВТ_Коды
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.ШтрихКодыКовров КАК ШтрихКоды
| ПО ВТ_Коды.Код = ШтрихКоды.Код
|ГДЕ
| НЕ ШтрихКоды.Оприходован";
Запрос.УстановитьПараметр("ТаблицаКодов", ТаблицаКодов);
РезультатЗапроса = Запрос.Выполнить();
Использование индексированной временной таблицы гарантирует максимальную производительность поиска на уровне SQL, так как СУБД использует быстрый алгоритм индексного соединения таблиц (Index Spool / Merge Join) вместо последовательного сканирования огромного списка параметров.
Таким образом, комбинируя современные методы разбора строк с пакетными транзакциями, свойством ОбменДанными.Загрузка и механизмами временных таблиц в запросах, мы получаем максимально производительное и отказоустойчивое решение для обработки данных в 1С:Предприятии 8.