При разработке обработок загрузки данных или интеграции отчетов с документами в «1С:Предприятие 8» программисты часто сталкиваются с задачей переноса информации из плоских или сводных таблиц во внутренние табличные части документов. Особую сложность вызывают случаи, когда структура колонок исходного отчета (например, разбивка по цветам, размерам или характеристикам) должна быть спроецирована на конкретные реквизиты табличной части документа (например, СебестБелый, СебестМатов и так далее).
В этой статье мы подробно разберем, как правильно организовать циклы для решения этой задачи, устраним типичные ошибки новичков, связанные с производительностью поиска данных, и рассмотрим наиболее профессиональные архитектурные подходы.
В исходном примере на форуме у разработчика есть таблица значений (ТЗ), полученная из отчета, а также два массива:
Попытки сопоставить эти массивы «в лоб» через вложенные циклы с использованием жестких счетчиков часто приводят к логическим ошибкам, когда данные записываются не в те колонки или перезаписывают друг друга. Выясним, как решить эту задачу изящно и надежно.
---
Если структура табличной части документа уже жестко задана в конфигураторе (созданы реквизиты СебестБелый, СебестМатов и т.д.), мы можем применить обход колонок таблицы значений с анализом их содержимого. Рассмотрим пример, в котором программа анализирует имена колонок или их значения и на основании этого принимает решение о заполнении реквизитов.
Этот способ основан на использовании функции СтрНайти для поиска ключевых слов («белый», «матов») в именах колонок таблицы значений:
// Предположим, что ДокументОбъект уже создан или получен для редактирования
ТЧНов = ДокументОбъект.ТЧ.Добавить();
ТЧНов.Номенклатура = НайденнаяНоменклатура;
Для Каждого Колонка Из ТЗ.Колонки Цикл
ИмяКолонки = Колонка.Имя;
ЗначениеЯчейки = СтрокаТЗ[ИмяКолонки];
// Если ячейка пустая, пропускаем ее
Если Не ЗначениеЗаполнено(ЗначениеЯчейки) Тогда
Продолжить;
КонецЕсли;
// Проверяем вхождение ключевых слов в имя колонки
Если СтрНайти(ВРег(ИмяКолонки), "БЕЛЫЙ") > 0 Тогда
ТЧНов.СебестБелый = ЗначениеЯчейки;
ИначеЕсли СтрНайти(ВРег(ИмяКолонки), "МАТОВ") > 0 Тогда
ТЧНов.СебестМатов = ЗначениеЯчейки;
КонецЕсли;
КонецЦикла;
Преимущества: Простота реализации. Код понятен и легко поддается отладке.
Недостатки: Код остается жестким. Если в конфигурацию добавится новый цвет (например, «Глянцевый» или «Красный»), разработчику придется вручную дописывать новые блоки ИначеЕсли в программный код.
---
В платформе «1С:Предприятие 8» реализован мощный механизм обращения к свойствам объектов через квадратные скобки. Вместо написания громоздких конструкций Если ... ИначеЕсли мы можем собирать имя реквизита табличной части динамически прямо в цикле.
Давайте разберем по шагам, как это реализовать, используя массив соответствия цветов:
// Подготовим массив соответствия цветов реквизитам
// Ключ - это имя колонки в ТЗ, Значение - суффикс реквизита в ТЧ документа
СоответствиеЦветов = Новый Соответствие;
СоответствиеЦветов.Вставить("МДФ_Белый", "Белый");
СоответствиеЦветов.Вставить("МДФ_Матов", "Матов");
Для Каждого СтрТЗ Из ТЗ Цикл
НоваяСтрокаТЧ = ДокументОбъект.ТЧ.Добавить();
НоваяСтрокаТЧ.Номенклатура = ПолучитьНоменклатуру(СтрТЗ._2); // Ниже разберем, как делать это быстро
Для Каждого КлючИЗначение Из СоответствиеЦветов Цикл
ИмяКолонкиТЗ = КлючИЗначение.Ключ;
СуффиксРеквизита = КлючИЗначение.Значение;
// Формируем имя реквизита ТЧ динамически, например "СебестБелый"
ИмяРеквизитаТЧ = "Себест" + СуффиксРеквизита;
// Безопасное чтение значения из ТЗ
ЗначениеПоказателя = СтрТЗ[ИмяКолонкиТЗ];
Если ЗначениеЗаполнено(ЗначениеПоказателя) Тогда
// Записываем данные в реквизит ТЧ через квадратные скобки
НоваяСтрокаТЧ[ИмяРеквизитаТЧ] = Число(ЗначениеПоказателя);
КонецЕсли;
КонецЦикла;
КонецЦикла;
Чтобы сделать алгоритм абсолютно безопасным, мы можем перед записью программно проверять, существует ли вообще такой реквизит в метаданных нашей табличной части. Это предотвратит аварийное завершение программы в случае изменения структуры конфигурации:
ИмяРеквизитаТЧ = "Себест" + СуффиксРеквизита;
МетаданныеТЧ = ДокументОбъект.Метаданные().ТабличныеЧасти.ТЧ;
Если МетаданныеТЧ.Реквизиты.Найти(ИмяРеквизитаТЧ) <> Неопределено Тогда
НоваяСтрокаТЧ[ИмяРеквизитаТЧ] = Число(ЗначениеПоказателя);
Иначе
ЗаписьЖурналаРегистрации("ЗагрузкаДанных", УровеньЖурналаРегистрации.Предупреждение, , ,
"Не найден реквизит ТЧ: " + ИмяРеквизитаТЧ);
КонецЕсли;
---
Посмотрим на код автора из первого сообщения темы:
НоменСсылка = Справочники.Номенклатура.НайтиПоРеквизиту("НаименованиеПолное", стр._2);
Вызов метода НайтиПоРеквизиту внутри цикла по строкам таблицы значений — это критическая ошибка, которая в разы снижает производительность системы. Если в таблице отчета будет 1000 строк, система выполнит 1000 отдельных запросов к базе данных. Это классическая проблема N+1.
Как сделать правильно: Перед запуском цикла обработки данных необходимо собрать все уникальные наименования номенклатуры, одним запросом получить их ссылки из базы данных, поместить результат во временное соответствие в оперативной памяти и затем моментально обращаться к нему внутри цикла.
Проанализируем оптимальный шаблон загрузки:
// Шаг 1. Собираем уникальные наименования из ТЗ
МассивНаименований = Новый Массив;
Для Каждого Стр Из ТЗ Цикл
Если ЗначениеЗаполнено(Стр._2) Тогда
МассивНаименований.Добавить(Стр._2);
КонецЕсли;
КонецЦикла;
// Очищаем массив от дублей
ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивНаименований);
// Шаг 2. Получаем ссылки одним запросом
СоответствиеНоменклатуры = Новый Соответствие;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.НаименованиеПолное КАК НаименованиеПолное
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.НаименованиеПолное В(&МассивНаименований)";
Запрос.УстановитьПараметр("МассивНаименований", МассивНаименований);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
СоответствиеНоменклатуры.Вставить(Выборка.НаименованиеПолное, Выборка.Ссылка);
КонецЦикла;
// Шаг 3. Основной цикл заполнения документа с быстрым поиском по соответствию
Для Каждого СтрТЗ Из ТЗ Цикл
// Получаем ссылку из соответствия в ОЗУ - это происходит мгновенно
НоменСсылка = СоответствиеНоменклатуры[СтрТЗ._2];
Если НоменСсылка = Неопределено Тогда
// Обработка ситуации, когда номенклатура не найдена в базе
Продолжить;
КонецЕсли;
ТЧНов = ДокументОбъект.ТЧ.Добавить();
ТЧНов.Номенклатура = НоменСсылка;
// Код заполнения колонок...
КонецЦикла;
---
В ходе обсуждения автор упомянул, что перед обработкой он удаляет «лишние строки» из исходной ТЗ. При работе с таблицами значений важно помнить золотое правило программирования в 1С: никогда не удаляйте строки из коллекции внутри цикла Для каждого по этой же самой коллекции.
При удалении строки индексы последующих строк смещаются вверх, из-за чего цикл просто пропустит следующую за удаленной строку. Чтобы этого избежать, используйте один из двух правильных подходов:
Для Индекс = -(ТЗ.Количество() - 1) По 0 Цикл
ТекущийИндекс = -Индекс;
СтрокаТЗ = ТЗ[ТекущийИндекс];
Если СтрокаТЗ.Показатель = 0 Тогда
ТЗ.Удалить(СтрокаТЗ);
КонецЕсли;
КонецЦикла;
НайтиСтроки(): Находим ненужные строки по отбору и удаляем их из таблицы в отдельном цикле.---
В сообщении №15 участники форума справедливо отмечают, что проектирование табличной части документа с колонками под каждый цвет (СебестБелый, СебестМатов, РентГлянц) — это неудачное архитектурное решение (антипаттерн).
Если бизнес-процессы компании предусматривают регулярное появление новых видов отделки, цветов или материалов, правильным подходом будет перепроектирование структуры метаданных:
Характеристика (ссылка на справочник характеристик или цвета) и один реквизит Себестоимость (тип Число).Посмотрим, как упростится и станет универсальным код заполнения ТЧ документа при такой правильной структуре:
// Каждому цвету теперь соответствует отдельная строка в документе
Для Каждого СтрТЗ Из ТЗ Цикл
НоменСсылка = СоответствиеНоменклатуры[СтрТЗ._2];
Если НоменСсылка = Неопределено Тогда
Продолжить;
КонецЕсли;
Для Каждого ИмяКолонки Из МассивКолонок Цикл
ЗначениеСебестоимости = СтрТЗ[ИмяКолонки];
Если ЗначениеЗаполнено(ЗначениеСебестоимости) Тогда
НоваяСтрока = ДокументОбъект.ТЧ.Добавить();
НоваяСтрока.Номенклатура = НоменСсылка;
// Находим или получаем ссылку на элемент справочника Цвета/Характеристики
НоваяСтрока.Цвет = ПолучитьСсылкаНаЦветПоИмениКолонки(ИмяКолонки);
НоваяСтрока.Себестоимость = Число(ЗначениеСебестоимости);
КонецЕсли;
КонецЦикла;
КонецЦикла;
При таком подходе база данных будет работать оптимально, а появление нового цвета в отчетах поставщиков не потребует привлечения программиста для изменения конфигурации и написания нового кода.