Как правильно сложить значения динамических колонок и заполнить табличную часть в 1С

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

При разработке обработок загрузки данных из внешних источников (например, файлов Excel или сторонних систем) программисты 1С часто сталкиваются с необходимостью динамического обхода колонок таблицы значений. Типичная задача: определить, в каких колонках находятся нужные показатели (например, себестоимость товара), сложить их значения для каждой строки и перенести итоговый результат в табличную часть документа.

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

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

Рассмотрим ситуацию, с которой столкнулся автор темы. У него есть таблица значений тз, полученная в результате импорта. Сначала необходимо определить, в каких колонках содержится ключевое слово "Себестоимость ", и запомнить их имена. Затем требуется обойти строки таблицы, найти соответствующую номенклатуру в базе данных и сложить значения найденных колонок себестоимости.

В первом сообщении темы предлагается следующий код для поиска колонок:


СтрокаСебест = тз.Найти("Себестоимость "); 
Если СтрокаСебест <> Неопределено Тогда 
    Для каждого стрсебес из СтрокаСебест Цикл 
        Счстроки = Счстроки + 1; 
        Если стрсебес = "Себестоимость " тогда 
            МассивКолонок.Добавить("_" + Счстроки); 
        КонецЕсли; 
    КонецЦикла; 
КонецЕсли;

Проанализируем этот фрагмент. Метод таблицы значений Найти() выполняет поиск переданного значения по всей таблице (или по определенным колонкам) и возвращает объект типа СтрокаТаблицыЗначений (либо Неопределено, если совпадений нет). Ошибка заключается в том, что строка таблицы значений не является коллекцией, которую можно перебрать с помощью цикла Для каждого.

Попытка выполнить этот код приведет к аварийному завершению работы программы с ошибкой: «Операция не может быть выполнена, так как объект не является коллекцией».

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


МассивКолонок = Новый Массив;
СтрокаСебест = тз.Найти("Себестоимость ");

Если СтрокаСебест <> Неопределено Тогда
    // Обходим коллекцию колонок таблицы значений
    Для Каждого Колонка Из тз.Колонки Цикл
        // Проверяем значение в найденной строке для текущей колонки
        Если СокрЛП(СтрокаСебест[Колонка.Имя]) = "Себестоимость" Тогда
            МассивКолонок.Добавить(Колонка.Имя);
        КонецЕсли;
    КонецЦикла;
КонецЕсли;

В этом коде мы безопасно обращаемся к свойствам строки по имени колонки СтрокаСебест[Колонка.Имя], что позволяет точно определить программные имена колонок (например, "_10", "_11" и так далее) и добавить их в МассивКолонок.

Динамическое обращение к колонкам через квадратные скобки

Вторая часть задачи — сложить значения этих колонок для каждой строки таблицы и записать результат в табличную часть документа.

Для обращения к значениям колонок, имена которых хранятся в виде строк в массиве, в платформе 1С предусмотрен универсальный механизм — квадратные скобки []. Этот синтаксис позволяет динамически получать доступ к свойствам объектов (строк таблиц значений, структур, соответствий, реквизитов формы) по их строковому представлению.

Разберем пример реализации суммирования во втором цикле:


Для Каждого Стр Из тз Цикл
    ТЧНов = НовыйДок.ТЧ.Добавить();
    
    // Динамический подсчет суммы по найденным колонкам
    Себестоимость = 0;
    Для Каждого ИмяКолонки Из МассивКолонок Цикл
        // Складываем значения, динамически обращаясь к колонке строки
        Себестоимость = Себестоимость + Стр[ИмяКолонки];
    КонецЦикла;
    
    ТЧНов.Себестоимость = Себестоимость;
КонецЦикла;

Благодаря конструкции Стр[ИмяКолонки] код становится гибким. Ему не важно, сколько именно колонок себестоимости было найдено — две, три или десять. Программа автоматически вычислит их сумму для каждой строки.

Критическая оптимизация: избавляемся от запросов в цикле

Теперь обратим внимание на важнейший аспект производительности, который справедливо затронул один из участников обсуждения в Сообщении 3. В исходном коде автора поиск номенклатуры выполняется непосредственно внутри цикла:

НоменСсылка = Справочники.Номенклатура.НайтиПоНаименованию(стр._2, истина);

Использование метода НайтиПоНаименованию() (а также НайтиПоКоду(), НайтиПоРеквизиту() или прямых запросов) внутри цикла по строкам — это классический антипаттерн разработки на платформе 1С. Такая архитектурная ошибка называется проблемой "N+1 запросов".

Выясним причину, почему это плохо. Каждый вызов метода НайтиПоНаименованию() заставляет платформу формировать и отправлять отдельный запрос к СУБД (MS SQL, PostgreSQL и др.). Если в вашей таблице значений содержится 1000 строк, система выполнит 1000 последовательных сетевых запросов к базе данных. Это приводит к колоссальным потерям времени на сетевые задержки и транзакционные издержки СУБД, вызывая глухое зависание интерфейса.

Выработаем надежный и профессиональный шаблон оптимизации с использованием объекта Соответствие (Map) и пакетного запроса. Наша цель — получить все данные из базы за один запрос.

Разберем этот процесс по шагам:

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

Посмотрим на пример оптимизированного кода, объединяющего все наши доработки:


// Шаг 1. Собираем список уникальных наименований для поиска
МассивИмен = Новый Массив;
Для Каждого Стр Из тз Цикл
    // Предположим, что наименование хранится в колонке _2
    ИмяДляПоиска = СокрЛП(Стр._2);
    Если ЗначениеЗаполнено(ИмяДляПоиска) И МассивИмен.Найти(ИмяДляПоиска) = Неопределено Тогда
        МассивИмен.Добавить(ИмяДляПоиска);
    КонецЕсли;
КонецЦикла;

// Шаг 2. Выполняем ОДИН запрос к базе данных для поиска всех позиций
КэшНоменклатуры = Новый Соответствие;
Если МассивИмен.Количество() > 0 Тогда
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Номенклатура.Ссылка КАК Ссылка,
        |	Номенклатура.Наименование КАК Наименование
        |ИЗ
        |	Справочник.Номенклатура КАК Номенклатура
        |ГДЕ
        |	Номенклатура.Наименование В(&МассивИмен)";
    
    Запрос.УстановитьПараметр("МассивИмен", МассивИмен);
    
    // Шаг 3. Заполняем кэш (Соответствие) полученными данными
    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Next() Цикл
        // Ключ — наименование (строка), Значение — ссылка на элемент
        КэшНоменклатуры.Вставить(Выборка.Наименование, Выборка.Ссылка);
    КонецЦикла;
КонецЕсли;

// Шаг 4. Основной цикл заполнения табличной части без лишних обращений к БД
Для Каждого Стр Из тз Цикл
    ИмяНоменклатуры = СокрЛП(Стр._2);
    НоменСсылка = КэшНоменклатуры[ИмяНоменклатуры];
    
    // Если номенклатура не найдена, можно пропустить строку или создать новую позицию
    Если НоменСсылка = Неопределено Тогда
        // Здесь может быть логика создания нового элемента или логирования ошибки
        Продолжить;
    КонецЕсли;
    
    // Добавляем строку в табличную часть документа
    ТЧНов = НовыйДок.ТЧ.Добавить();
    ТЧНов.Номенклатура = НоменСсылка;
    
    // Суммируем себестоимость динамически
    Себестоимость = 0;
    Для Каждого ИмяКолонки Из МассивКолонок Цикл
        Себестоимость = Себестоимость + Стр[ИмяКолонки];
    КонецЦикла;
    
    ТЧНов.Себестоимость = Себестоимость;
КонецЦикла;

Этот код отработает в десятки и сотни раз быстрее исходного варианта на реальных базах данных за счет сокращения сетевых транзакций с СУБД.

Дополнительные рекомендации по оптимизации и безопасности

При промышленной разработке обмена данными и переноса таблиц следует учитывать еще несколько важных моментов.

Риски поиска по наименованию. Метод поиска по наименованию крайне ненадежен. Наименования номенклатуры в реальных базах данных могут дублироваться, содержать невидимые символы, лишние пробелы или отличаться регистром букв. Рекомендуется по возможности использовать более жесткие идентификаторы для сопоставления:

Альтернативный метод заполнения больших объемов через Загрузить(). Если в документе создаются тысячи строк, метод табличной части Добавить() может работать медленно, поскольку при каждом добавлении строки платформа обновляет связанные механизмы отображения интерфейса и пересчитывает итоги. В таких случаях эффективнее подготовить отдельную таблицу значений на сервере с точно такой же структурой колонок, как и у табличной части документа, быстро заполнить её в памяти, а затем загрузить в документ единым методом НовыйДок.ТЧ.Загрузить(ПодготовленнаяТЗ). Метод Загрузить() реализован на уровне платформы на языке C++ и работает кратно быстрее построчного заполнения.

← На главную