Как найти элемент справочника по двум реквизитам при загрузке данных в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке механизмов загрузки данных из внешних файлов (Excel, CSV) разработчики часто сталкиваются с ситуацией, когда стандартный метод НайтиПоРеквизиту() объекта менеджера справочника не справляется со своей задачей. Этот метод принципиально рассчитан на поиск только по одному значению и возвращает первый попавшийся элемент. Если ключевое поле в базе не уникально (например, реквизит ТипМарка дублируется для разных позиций), поиск вернет некорректную ссылку. Разберем, как правильно реализовать поиск элемента справочника по комбинации полей и избежать типичных ошибок при заполнении реквизитов ссылочного типа.

Анализ типичных ошибок при реализации поиска

Проанализируем распространенные ошибки, которые начинающие разработчики допускают при попытке написать отбор по двум полям:

  1. Передача ссылки вместо строки в параметры запроса. Попытка передать результат функции поиска в строковый параметр запроса (например, спрНоменклатура.НайтиПоКоду(...)) приводит к несоответствию типов или логическим сбоям при сравнении с текстовыми полями.
  2. Попытка присвоить строку в ссылочное поле. Реквизит табличной части Материал имеет тип СправочникСсылка.Номенклатура. Если присвоить ему Выборка.Наименование, значение не запишется, так как платформа не может автоматически привести строковый тип к ссылочному. Присваивать необходимо поле ссылки: Выборка.Ссылка.
  3. Ошибки в цикле обхода выборки. Использование конструкции Выборка.Следующий() одновременно в условии Если и в заголовке цикла Пока Выборка.Следующий() Цикл приводит к тому, что курсор выборки смещается дважды за одну итерацию. В результате каждая вторая найденная запись просто пропускается.
  4. Выполнение запросов в цикле. Вызов объекта Запрос на каждой строке загружаемой таблицы значений (ТЗ) многократно увеличивает нагрузку на СУБД. На небольших файлах это работает, но при объемах в тысячи строк загрузка начнет зависать.

Способ 1. Точечный поиск запросом по двум полям

Если требуется исправить существующий алгоритм с минимальными изменениями, рассмотрим правильное построение запроса с выборкой первого совпадения внутри цикла обработки строк.

Разберем по шагам, как исправить код:

  1. В секции ГДЕ запроса указываем жесткое соответствие обоим реквизитам: ТипМарка и Наименование.
  2. В параметрах запроса передаем строковые значения, очищенные от лишних пробелов с помощью СокрЛП().
  3. Для реквизитов строкового типа неограниченной длины (например, если используется НаименованиеПолное) обязательно применяем функцию ВЫРАЗИТЬ(Поле КАК СТРОКА(Длина)), иначе СУБД выдаст ошибку при сравнении.
  4. Из запроса выбираем именно поле Ссылка и записываем его в реквизит табличной части.

Посмотрим на пример корректного кода для итерации по строкам:


Для Каждого ТекСтрока Из ТЗ Цикл

    ПолныйКод = СокрЛП(СтрЗаменить(Строка(ТекСтрока.ПолныйКод), ".", "/"));
    ВидРабот = СпрВидыРабот.НайтиПоКоду(ПолныйКод, Истина);

    // Подготавливаем строковые параметры для поиска, удаляя пробелы
    ИскомаяМарка = СокрЛП(ТекСтрока.ТипМарка);
    ИскомоеИмя   = СокрЛП(ТекСтрока.НаименованиеМатериала);

    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ 1
        |   Номенклатура.Ссылка КАК Ссылка
        |ИЗ
        |   Справочник.Номенклатура КАК Номенклатура
        |ГДЕ
        |   Номенклатура.ТипМарка = &ТипМарка
        |   И Номенклатура.Наименование = &Наименование";

    Запрос.УстановитьПараметр("ТипМарка", ИскомаяМарка);
    Запрос.УстановитьПараметр("Наименование", ИскомоеИмя);

    Выборка = Запрос.Выполнить().Выбрать();

    Если Выборка.Следующий() И ЗначениеЗаполнено(ВидРабот) Тогда
        
        ТчСтр = ЭтотОбъект.Объект.Материалы.Добавить();
        
        // Присваиваем ссылку, а не строковое наименование
        ТчСтр.Материал   = Выборка.Ссылка;
        ТчСтр.ВидРабот   = ВидРабот;
        ТчСтр.Количество = ТекСтрока.КоличествоПоПроекту;
        ТчСтр.Цена       = ТекСтрока.ЦенаЗаЕдиницу;
        
        Если ЗначениеЗаполнено(ТекСтрока.ЦенаЗаЕдиницу) И ЗначениеЗаполнено(ТекСтрока.КоличествоПоПроекту) Тогда
            ТчСтр.Итого = ТекСтрока.ЦенаЗаЕдиницу * ТекСтрока.КоличествоПоПроекту;
        КонецЕсли;

    Иначе
        стрОшиб = ТЗОшибки.Добавить();
        стрОшиб.СообщОшиб = "НЕ ЗАГРУЖЕН " + ?(ПустаяСтрока(ИскомаяМарка), "'Нет кода'", ИскомаяМарка) 
            + " '" + ИскомоеИмя + "', т.к. позиция не найдена в 1С.";
    КонецЕсли;

КонецЦикла;

Способ 2. Оптимальный: пакетный поиск через временную таблицу

Выполнять запросы внутри цикла считается антипаттерном в разработке на платформе 1С:Предприятие. Наиболее производительным решением является передача всей загруженной таблицы значений в запрос и выполнение одного левого соединения со справочником.

Рассмотрим подробнее реализацию этого метода:

  1. Колонкам таблицы значений назначаются явные типы с помощью объекта ОписаниеТипов (без этого таблицу значений нельзя передать в запрос).
  2. Таблица передается в запрос и помещается во временную таблицу ВТ_ДанныеФайла.
  3. Выполняется ЛЕВОЕ СОЕДИНЕНИЕ со справочником номенклатуры по обоим реквизитам.
  4. Обход осуществляется уже по готовому результату соединения.

Посмотрим на пример пакетной обработки данных:


// 1. Формируем типизированную таблицу для передачи в запрос
МассивТипСтрока = Новый Массив;
МассивТипСтрока.Добавить(Тип("Строка"));
КвалификаторыСтроки = Новый КвалификаторыСтроки(150);
ТипСтрока = Новый ОписаниеТипов(МассивТипСтрока, , КвалификаторыСтроки);

МассивТипЧисло = Новый Массив;
МассивТипЧисло.Добавить(Тип("Число"));
КвалификаторыЧисла = Новый КвалификаторыЧисла(15, 2);
ТипЧисло = Новый ОписаниеТипов(МассивТипЧисло, , , КвалификаторыЧисла);

ТЗДляЗапроса = Новый ТаблицаЗначений;
ТЗДляЗапроса.Колонки.Добавить("ТипМарка", ТипСтрока);
ТЗДляЗапроса.Колонки.Добавить("НаименованиеМатериала", ТипСтрока);
ТЗДляЗапроса.Колонки.Добавить("КоличествоПоПроекту", ТипЧисло);
ТЗДляЗапроса.Колонки.Добавить("ЦенаЗаЕдиницу", ТипЧисло);
ТЗДляЗапроса.Колонки.Добавить("ПолныйКод", ТипСтрока);

Для Каждого Стр Из ТЗ Цикл
    ЗаполнитьЗначенияСвойств(ТЗДляЗапроса.Добавить(), Стр);
КонецЦикла;

// 2. Выполняем один запрос ко всей таблице
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Данные.ТипМарка КАК ТипМарка,
    |   Данные.НаименованиеМатериала КАК НаименованиеМатериала,
    |   Данные.КоличествоПоПроекту КАК КоличествоПоПроекту,
    |   Данные.ЦенаЗаЕдиницу КАК ЦенаЗаЕдиницу,
    |   Данные.ПолныйКод КАК ПолныйКод
    |ПОМЕСТИТЬ ВТ_ДанныеФайла
    |ИЗ
    |   &ТЗДанные КАК Данные
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |   ВТ.ТипМарка КАК ТипМарка,
    |   ВТ.НаименованиеМатериала КАК НаименованиеМатериала,
    |   ВТ.КоличествоПоПроекту КАК Количество,
    |   ВТ.ЦенаЗаЕдиницу КАК Цена,
    |   ВТ.ПолныйКод КАК ПолныйКод,
    |   Номенклатура.Ссылка КАК НоменклатураСсылка
    |ИЗ
    |   ВТ_ДанныеФайла КАК ВТ
    |   ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Номенклатура
    |       ПО ВТ.ТипМарка = Номенклатура.ТипМарка
    |       И ВТ.НаименованиеМатериала = Номенклатура.Наименование";

Запрос.УстановитьПараметр("ТЗДанные", ТЗДляЗапроса);
Выборка = Запрос.Выполнить().Выбрать();

Пока Выборка.Следующий() Цикл
    
    // Проверяем, найден ли элемент в справочнике
    Если ЗначениеЗаполнено(Выборка.НоменклатураСсылка) Тогда
        
        ТчСтр = ЭтотОбъект.Объект.Материалы.Добавить();
        ТчСтр.Материал   = Выборка.НоменклатураСсылка;
        ТчСтр.Количество = Выборка.Количество;
        ТчСтр.Цена       = Выборка.Цена;
        
        Если ЗначениеЗаполнено(Выборка.Цена) И ЗначениеЗаполнено(Выборка.Количество) Тогда
            ТчСтр.Итого = Выборка.Цена * Выборка.Количество;
        КонецЕсли;
        
    Иначе
        стрОшиб = ТЗОшибки.Добавить();
        стрОшиб.СообщОшиб = "НЕ ЗАГРУЖЕН " + Выборка.ТипМарка + " '" + Выборка.НаименованиеМатериала + "'";
    КонецЕсли;

КонецЦикла;

Способ 3. Кэширование данных через объект «Соответствие»

Если справочник Номенклатура содержит умеренное количество позиций (до 30 000–50 000 записей), сопоставление можно перенести в оперативную память. Это исключит как запросы в цикле, так и создание временных таблиц.

Выясним причину эффективности этого подхода: обращение к хэш-таблице в памяти через метод Получить() объекта Соответствие происходит практически мгновенно.

Разберем алгоритм кэширования:

  1. Перед циклом загрузки выполняем один запрос, выбирающий поля Ссылка, ТипМарка и Наименование.
  2. Заполняем коллекцию Соответствие, где в качестве ключа формируем составную строку, например: ВРег(СокрЛП(ТипМарка)) + "|" + ВРег(СокрЛП(Наименование)).
  3. При чтении строк из файла формируем аналогичный составной строковый ключ и запрашиваем ссылку из кэша.

Посмотрим на пример кэширования:


// 1. Формируем кэш справочника в памяти
КэшНоменклатуры = Новый Соответствие;

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Номенклатура.Ссылка КАК Ссылка,
    |   Номенклатура.ТипМарка КАК ТипМарка,
    |   Номенклатура.Наименование КАК Наименование
    |ИЗ
    |   Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |   НЕ Номенклатура.ПометкаУдаления";

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // Очищаем и приводим к верхнему регистру для нечувствительности к регистру
    Ключ = ВРег(СокрЛП(Выборка.ТипМарка)) + "|" + ВРег(СокрЛП(Выборка.Наименование));
    КэшНоменклатуры.Вставить(Ключ, Выборка.Ссылка);
КонецЦикла;

// 2. Использование кэша при обработке строк
Для Каждого ТекСтрока Из ТЗ Цикл
    
    // Удаляем неразрывные пробелы, часто встречающиеся в Excel
    ЧистаяМарка = СтрЗаменить(СокрЛП(ТекСтрока.ТипМарка), Символы.НПП, " ");
    ЧистоеИмя   = СтрЗаменить(СокрЛП(ТекСтрока.НаименованиеМатериала), Символы.НПП, " ");
    
    КлючПоиска = ВРег(ЧистаяМарка) + "|" + ВРег(ЧистоеИмя);
    НайденнаяСсылка = КэшНоменклатуры.Получить(КлючПоиска);
    
    Если НайденнаяСсылка <> Неопределено Тогда
        ТчСтр = ЭтотОбъект.Объект.Материалы.Добавить();
        ТчСтр.Материал = НайденнаяСсылка;
        // Заполнение остальных колонок
    Иначе
        // Обработка ошибки поиска
    КонецЕсли;

КонецЦикла;

Важное замечание: При выгрузке из Excel в строках часто присутствуют неразрывные пробелы (Символы.НПП, код 160). Обычная функция СокрЛП() их не удаляет. Поэтому перед сравнением строк или составлением ключа обязательно используйте замену СтрЗаменить(Строка, Символы.НПП, " "), чтобы гарантировать точное совпадение данных.

← На главную