При разработке механизмов загрузки данных из внешних файлов (Excel, CSV) разработчики часто сталкиваются с ситуацией, когда стандартный метод НайтиПоРеквизиту() объекта менеджера справочника не справляется со своей задачей. Этот метод принципиально рассчитан на поиск только по одному значению и возвращает первый попавшийся элемент. Если ключевое поле в базе не уникально (например, реквизит ТипМарка дублируется для разных позиций), поиск вернет некорректную ссылку. Разберем, как правильно реализовать поиск элемента справочника по комбинации полей и избежать типичных ошибок при заполнении реквизитов ссылочного типа.
Проанализируем распространенные ошибки, которые начинающие разработчики допускают при попытке написать отбор по двум полям:
спрНоменклатура.НайтиПоКоду(...)) приводит к несоответствию типов или логическим сбоям при сравнении с текстовыми полями.Материал имеет тип СправочникСсылка.Номенклатура. Если присвоить ему Выборка.Наименование, значение не запишется, так как платформа не может автоматически привести строковый тип к ссылочному. Присваивать необходимо поле ссылки: Выборка.Ссылка.Выборка.Следующий() одновременно в условии Если и в заголовке цикла Пока Выборка.Следующий() Цикл приводит к тому, что курсор выборки смещается дважды за одну итерацию. В результате каждая вторая найденная запись просто пропускается.Запрос на каждой строке загружаемой таблицы значений (ТЗ) многократно увеличивает нагрузку на СУБД. На небольших файлах это работает, но при объемах в тысячи строк загрузка начнет зависать.Если требуется исправить существующий алгоритм с минимальными изменениями, рассмотрим правильное построение запроса с выборкой первого совпадения внутри цикла обработки строк.
Разберем по шагам, как исправить код:
ГДЕ запроса указываем жесткое соответствие обоим реквизитам: ТипМарка и Наименование.СокрЛП().НаименованиеПолное) обязательно применяем функцию ВЫРАЗИТЬ(Поле КАК СТРОКА(Длина)), иначе СУБД выдаст ошибку при сравнении.Ссылка и записываем его в реквизит табличной части.Посмотрим на пример корректного кода для итерации по строкам:
Для Каждого ТекСтрока Из ТЗ Цикл
ПолныйКод = СокрЛП(СтрЗаменить(Строка(ТекСтрока.ПолныйКод), ".", "/"));
ВидРабот = СпрВидыРабот.НайтиПоКоду(ПолныйКод, Истина);
// Подготавливаем строковые параметры для поиска, удаляя пробелы
ИскомаяМарка = СокрЛП(ТекСтрока.ТипМарка);
ИскомоеИмя = СокрЛП(ТекСтрока.НаименованиеМатериала);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Номенклатура.Ссылка КАК Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.ТипМарка = &ТипМарка
| И Номенклатура.Наименование = &Наименование";
Запрос.УстановитьПараметр("ТипМарка", ИскомаяМарка);
Запрос.УстановитьПараметр("Наименование", ИскомоеИмя);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() И ЗначениеЗаполнено(ВидРабот) Тогда
ТчСтр = ЭтотОбъект.Объект.Материалы.Добавить();
// Присваиваем ссылку, а не строковое наименование
ТчСтр.Материал = Выборка.Ссылка;
ТчСтр.ВидРабот = ВидРабот;
ТчСтр.Количество = ТекСтрока.КоличествоПоПроекту;
ТчСтр.Цена = ТекСтрока.ЦенаЗаЕдиницу;
Если ЗначениеЗаполнено(ТекСтрока.ЦенаЗаЕдиницу) И ЗначениеЗаполнено(ТекСтрока.КоличествоПоПроекту) Тогда
ТчСтр.Итого = ТекСтрока.ЦенаЗаЕдиницу * ТекСтрока.КоличествоПоПроекту;
КонецЕсли;
Иначе
стрОшиб = ТЗОшибки.Добавить();
стрОшиб.СообщОшиб = "НЕ ЗАГРУЖЕН " + ?(ПустаяСтрока(ИскомаяМарка), "'Нет кода'", ИскомаяМарка)
+ " '" + ИскомоеИмя + "', т.к. позиция не найдена в 1С.";
КонецЕсли;
КонецЦикла;
Выполнять запросы внутри цикла считается антипаттерном в разработке на платформе 1С:Предприятие. Наиболее производительным решением является передача всей загруженной таблицы значений в запрос и выполнение одного левого соединения со справочником.
Рассмотрим подробнее реализацию этого метода:
ОписаниеТипов (без этого таблицу значений нельзя передать в запрос).ВТ_ДанныеФайла.ЛЕВОЕ СОЕДИНЕНИЕ со справочником номенклатуры по обоим реквизитам.Посмотрим на пример пакетной обработки данных:
// 1. Формируем типизированную таблицу для передачи в запрос
МассивТипСтрока = Новый Массив;
МассивТипСтрока.Добавить(Тип("Строка"));
КвалификаторыСтроки = Новый КвалификаторыСтроки(150);
ТипСтрока = Новый ОписаниеТипов(МассивТипСтрока, , КвалификаторыСтроки);
МассивТипЧисло = Новый Массив;
МассивТипЧисло.Добавить(Тип("Число"));
КвалификаторыЧисла = Новый КвалификаторыЧисла(15, 2);
ТипЧисло = Новый ОписаниеТипов(МассивТипЧисло, , , КвалификаторыЧисла);
ТЗДляЗапроса = Новый ТаблицаЗначений;
ТЗДляЗапроса.Колонки.Добавить("ТипМарка", ТипСтрока);
ТЗДляЗапроса.Колонки.Добавить("НаименованиеМатериала", ТипСтрока);
ТЗДляЗапроса.Колонки.Добавить("КоличествоПоПроекту", ТипЧисло);
ТЗДляЗапроса.Колонки.Добавить("ЦенаЗаЕдиницу", ТипЧисло);
ТЗДляЗапроса.Колонки.Добавить("ПолныйКод", ТипСтрока);
Для Каждого Стр Из ТЗ Цикл
ЗаполнитьЗначенияСвойств(ТЗДляЗапроса.Добавить(), Стр);
КонецЦикла;
// 2. Выполняем один запрос ко всей таблице
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| Данные.ТипМарка КАК ТипМарка,
| Данные.НаименованиеМатериала КАК НаименованиеМатериала,
| Данные.КоличествоПоПроекту КАК КоличествоПоПроекту,
| Данные.ЦенаЗаЕдиницу КАК ЦенаЗаЕдиницу,
| Данные.ПолныйКод КАК ПолныйКод
|ПОМЕСТИТЬ ВТ_ДанныеФайла
|ИЗ
| &ТЗДанные КАК Данные
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.ТипМарка КАК ТипМарка,
| ВТ.НаименованиеМатериала КАК НаименованиеМатериала,
| ВТ.КоличествоПоПроекту КАК Количество,
| ВТ.ЦенаЗаЕдиницу КАК Цена,
| ВТ.ПолныйКод КАК ПолныйКод,
| Номенклатура.Ссылка КАК НоменклатураСсылка
|ИЗ
| ВТ_ДанныеФайла КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Номенклатура
| ПО ВТ.ТипМарка = Номенклатура.ТипМарка
| И ВТ.НаименованиеМатериала = Номенклатура.Наименование";
Запрос.УстановитьПараметр("ТЗДанные", ТЗДляЗапроса);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Проверяем, найден ли элемент в справочнике
Если ЗначениеЗаполнено(Выборка.НоменклатураСсылка) Тогда
ТчСтр = ЭтотОбъект.Объект.Материалы.Добавить();
ТчСтр.Материал = Выборка.НоменклатураСсылка;
ТчСтр.Количество = Выборка.Количество;
ТчСтр.Цена = Выборка.Цена;
Если ЗначениеЗаполнено(Выборка.Цена) И ЗначениеЗаполнено(Выборка.Количество) Тогда
ТчСтр.Итого = Выборка.Цена * Выборка.Количество;
КонецЕсли;
Иначе
стрОшиб = ТЗОшибки.Добавить();
стрОшиб.СообщОшиб = "НЕ ЗАГРУЖЕН " + Выборка.ТипМарка + " '" + Выборка.НаименованиеМатериала + "'";
КонецЕсли;
КонецЦикла;
Если справочник Номенклатура содержит умеренное количество позиций (до 30 000–50 000 записей), сопоставление можно перенести в оперативную память. Это исключит как запросы в цикле, так и создание временных таблиц.
Выясним причину эффективности этого подхода: обращение к хэш-таблице в памяти через метод Получить() объекта Соответствие происходит практически мгновенно.
Разберем алгоритм кэширования:
Ссылка, ТипМарка и Наименование.Соответствие, где в качестве ключа формируем составную строку, например: ВРег(СокрЛП(ТипМарка)) + "|" + ВРег(СокрЛП(Наименование)).Посмотрим на пример кэширования:
// 1. Формируем кэш справочника в памяти
КэшНоменклатуры = Новый Соответствие;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.ТипМарка КАК ТипМарка,
| Номенклатура.Наименование КАК Наименование
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ПометкаУдаления";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Очищаем и приводим к верхнему регистру для нечувствительности к регистру
Ключ = ВРег(СокрЛП(Выборка.ТипМарка)) + "|" + ВРег(СокрЛП(Выборка.Наименование));
КэшНоменклатуры.Вставить(Ключ, Выборка.Ссылка);
КонецЦикла;
// 2. Использование кэша при обработке строк
Для Каждого ТекСтрока Из ТЗ Цикл
// Удаляем неразрывные пробелы, часто встречающиеся в Excel
ЧистаяМарка = СтрЗаменить(СокрЛП(ТекСтрока.ТипМарка), Символы.НПП, " ");
ЧистоеИмя = СтрЗаменить(СокрЛП(ТекСтрока.НаименованиеМатериала), Символы.НПП, " ");
КлючПоиска = ВРег(ЧистаяМарка) + "|" + ВРег(ЧистоеИмя);
НайденнаяСсылка = КэшНоменклатуры.Получить(КлючПоиска);
Если НайденнаяСсылка <> Неопределено Тогда
ТчСтр = ЭтотОбъект.Объект.Материалы.Добавить();
ТчСтр.Материал = НайденнаяСсылка;
// Заполнение остальных колонок
Иначе
// Обработка ошибки поиска
КонецЕсли;
КонецЦикла;
Важное замечание: При выгрузке из Excel в строках часто присутствуют неразрывные пробелы (Символы.НПП, код 160). Обычная функция СокрЛП() их не удаляет. Поэтому перед сравнением строк или составлением ключа обязательно используйте замену СтрЗаменить(Строка, Символы.НПП, " "), чтобы гарантировать точное совпадение данных.