При разработке обработок загрузки данных из внешних источников (например, файлов Excel или сторонних систем) программисты 1С часто сталкиваются с необходимостью динамического обхода колонок таблицы значений. Типичная задача: определить, в каких колонках находятся нужные показатели (например, себестоимость товара), сложить их значения для каждой строки и перенести итоговый результат в табличную часть документа.
В этой статье мы подробно разберем, как решить эту задачу изящно и быстро, исправим скрытые ошибки в алгоритмах обхода коллекций, а также проведем глубокую оптимизацию кода для предотвращения зависаний системы при обработке больших массивов данных.
Рассмотрим ситуацию, с которой столкнулся автор темы. У него есть таблица значений тз, полученная в результате импорта. Сначала необходимо определить, в каких колонках содержится ключевое слово "Себестоимость ", и запомнить их имена. Затем требуется обойти строки таблицы, найти соответствующую номенклатуру в базе данных и сложить значения найденных колонок себестоимости.
В первом сообщении темы предлагается следующий код для поиска колонок:
СтрокаСебест = тз.Найти("Себестоимость ");
Если СтрокаСебест <> Неопределено Тогда
Для каждого стрсебес из СтрокаСебест Цикл
Счстроки = Счстроки + 1;
Если стрсебес = "Себестоимость " тогда
МассивКолонок.Добавить("_" + Счстроки);
КонецЕсли;
КонецЦикла;
КонецЕсли;
Проанализируем этот фрагмент. Метод таблицы значений Найти() выполняет поиск переданного значения по всей таблице (или по определенным колонкам) и возвращает объект типа СтрокаТаблицыЗначений (либо Неопределено, если совпадений нет). Ошибка заключается в том, что строка таблицы значений не является коллекцией, которую можно перебрать с помощью цикла Для каждого.
Попытка выполнить этот код приведет к аварийному завершению работы программы с ошибкой: «Операция не может быть выполнена, так как объект не является коллекцией».
Чтобы исправить это поведение и корректно определить имена колонок, содержащих нужное нам значение, мы должны обойти коллекцию колонок таблицы значений. Давайте посмотрим на правильный вариант реализации этой логики:
МассивКолонок = Новый Массив;
СтрокаСебест = тз.Найти("Себестоимость ");
Если СтрокаСебест <> Неопределено Тогда
// Обходим коллекцию колонок таблицы значений
Для Каждого Колонка Из тз.Колонки Цикл
// Проверяем значение в найденной строке для текущей колонки
Если СокрЛП(СтрокаСебест[Колонка.Имя]) = "Себестоимость" Тогда
МассивКолонок.Добавить(Колонка.Имя);
КонецЕсли;
КонецЦикла;
КонецЕсли;
В этом коде мы безопасно обращаемся к свойствам строки по имени колонки СтрокаСебест[Колонка.Имя], что позволяет точно определить программные имена колонок (например, "_10", "_11" и так далее) и добавить их в МассивКолонок.
Вторая часть задачи — сложить значения этих колонок для каждой строки таблицы и записать результат в табличную часть документа.
Для обращения к значениям колонок, имена которых хранятся в виде строк в массиве, в платформе 1С предусмотрен универсальный механизм — квадратные скобки []. Этот синтаксис позволяет динамически получать доступ к свойствам объектов (строк таблиц значений, структур, соответствий, реквизитов формы) по их строковому представлению.
Разберем пример реализации суммирования во втором цикле:
Для Каждого Стр Из тз Цикл
ТЧНов = НовыйДок.ТЧ.Добавить();
// Динамический подсчет суммы по найденным колонкам
Себестоимость = 0;
Для Каждого ИмяКолонки Из МассивКолонок Цикл
// Складываем значения, динамически обращаясь к колонке строки
Себестоимость = Себестоимость + Стр[ИмяКолонки];
КонецЦикла;
ТЧНов.Себестоимость = Себестоимость;
КонецЦикла;
Благодаря конструкции Стр[ИмяКолонки] код становится гибким. Ему не важно, сколько именно колонок себестоимости было найдено — две, три или десять. Программа автоматически вычислит их сумму для каждой строки.
Теперь обратим внимание на важнейший аспект производительности, который справедливо затронул один из участников обсуждения в Сообщении 3. В исходном коде автора поиск номенклатуры выполняется непосредственно внутри цикла:
НоменСсылка = Справочники.Номенклатура.НайтиПоНаименованию(стр._2, истина);
Использование метода НайтиПоНаименованию() (а также НайтиПоКоду(), НайтиПоРеквизиту() или прямых запросов) внутри цикла по строкам — это классический антипаттерн разработки на платформе 1С. Такая архитектурная ошибка называется проблемой "N+1 запросов".
Выясним причину, почему это плохо. Каждый вызов метода НайтиПоНаименованию() заставляет платформу формировать и отправлять отдельный запрос к СУБД (MS SQL, PostgreSQL и др.). Если в вашей таблице значений содержится 1000 строк, система выполнит 1000 последовательных сетевых запросов к базе данных. Это приводит к колоссальным потерям времени на сетевые задержки и транзакционные издержки СУБД, вызывая глухое зависание интерфейса.
Выработаем надежный и профессиональный шаблон оптимизации с использованием объекта Соответствие (Map) и пакетного запроса. Наша цель — получить все данные из базы за один запрос.
Разберем этот процесс по шагам:
Номенклатура.Соответствие, где ключом будет наименование номенклатуры, а значением — ссылка на элемент справочника. Поиск в объекте Соответствие в оперативной памяти выполняется мгновенно (сложность алгоритма близка к O(1)).Посмотрим на пример оптимизированного кода, объединяющего все наши доработки:
// Шаг 1. Собираем список уникальных наименований для поиска
МассивИмен = Новый Массив;
Для Каждого Стр Из тз Цикл
// Предположим, что наименование хранится в колонке _2
ИмяДляПоиска = СокрЛП(Стр._2);
Если ЗначениеЗаполнено(ИмяДляПоиска) И МассивИмен.Найти(ИмяДляПоиска) = Неопределено Тогда
МассивИмен.Добавить(ИмяДляПоиска);
КонецЕсли;
КонецЦикла;
// Шаг 2. Выполняем ОДИН запрос к базе данных для поиска всех позиций
КэшНоменклатуры = Новый Соответствие;
Если МассивИмен.Количество() > 0 Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Наименование В(&МассивИмен)";
Запрос.УстановитьПараметр("МассивИмен", МассивИмен);
// Шаг 3. Заполняем кэш (Соответствие) полученными данными
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Next() Цикл
// Ключ — наименование (строка), Значение — ссылка на элемент
КэшНоменклатуры.Вставить(Выборка.Наименование, Выборка.Ссылка);
КонецЦикла;
КонецЕсли;
// Шаг 4. Основной цикл заполнения табличной части без лишних обращений к БД
Для Каждого Стр Из тз Цикл
ИмяНоменклатуры = СокрЛП(Стр._2);
НоменСсылка = КэшНоменклатуры[ИмяНоменклатуры];
// Если номенклатура не найдена, можно пропустить строку или создать новую позицию
Если НоменСсылка = Неопределено Тогда
// Здесь может быть логика создания нового элемента или логирования ошибки
Продолжить;
КонецЕсли;
// Добавляем строку в табличную часть документа
ТЧНов = НовыйДок.ТЧ.Добавить();
ТЧНов.Номенклатура = НоменСсылка;
// Суммируем себестоимость динамически
Себестоимость = 0;
Для Каждого ИмяКолонки Из МассивКолонок Цикл
Себестоимость = Себестоимость + Стр[ИмяКолонки];
КонецЦикла;
ТЧНов.Себестоимость = Себестоимость;
КонецЦикла;
Этот код отработает в десятки и сотни раз быстрее исходного варианта на реальных базах данных за счет сокращения сетевых транзакций с СУБД.
При промышленной разработке обмена данными и переноса таблиц следует учитывать еще несколько важных моментов.
Риски поиска по наименованию. Метод поиска по наименованию крайне ненадежен. Наименования номенклатуры в реальных базах данных могут дублироваться, содержать невидимые символы, лишние пробелы или отличаться регистром букв. Рекомендуется по возможности использовать более жесткие идентификаторы для сопоставления:
Альтернативный метод заполнения больших объемов через Загрузить(). Если в документе создаются тысячи строк, метод табличной части Добавить() может работать медленно, поскольку при каждом добавлении строки платформа обновляет связанные механизмы отображения интерфейса и пересчитывает итоги. В таких случаях эффективнее подготовить отдельную таблицу значений на сервере с точно такой же структурой колонок, как и у табличной части документа, быстро заполнить её в памяти, а затем загрузить в документ единым методом НовыйДок.ТЧ.Загрузить(ПодготовленнаяТЗ). Метод Загрузить() реализован на уровне платформы на языке C++ и работает кратно быстрее построчного заполнения.