При обработке и группировке связанных коллекций данных разработчики нередко сталкиваются с ситуацией, когда подчиненные строки заполняются некорректно: дублируются значения первой строки, данные записываются не в те ветки или иерархия строится с ошибками. Рассмотрим причины такого поведения на практическом примере и разберем оптимальные способы построения иерархических структур в платформе 1С:Предприятие.
Проанализируем фрагмент кода, в котором при попытке создать двухуровневую таблицу значений (вкладывая одну ТаблицаЗначений внутрь другой) постоянно дублируются строки:
Т_М = Новый ТаблицаЗначений;
Т_М.Колонки.Добавить("Номенклатура");
Т_М.Колонки.Добавить("СтавкаНДС");
Т_М.Колонки.Добавить("КоличествоПоступило");
Т_М.Колонки.Добавить("КоличествоПродано");
Т_М.Колонки.Добавить("Сумма");
Т_М.Колонки.Добавить("Разница");
Т_М.Колонки.Добавить("СуммаНДС");
Т_М.Колонки.Добавить("ТаблТЗ");
Для Каждого ТекущаяСтрока Из Т1 Цикл
НайденнаяСтрока = Т_М.Найти(ТекущаяСтрока.Номенклатура, "Номенклатура");
Если НайденнаяСтрока = Неопределено Тогда
СтрокаОсновная = Т_М.Добавить();
СтрокаОсновная.Номенклатура = стр.Номенклатура;
СтрокаОсновная.СтавкаНДС = стр.СтавкаНДС;
СтрокаОсновная.КоличествоПоступило = стр.КоличествоПоступило;
СтрокаОсновная.КоличествоПродано = стр.КоличествоПродано;
СтрокаОсновная.Сумма = стр.Сумма;
СтрокаОсновная.Разница = стр.Разница;
СтрокаОсновная.СуммаНДС = стр.СуммаНДС;
СтрокаОсновная.ТаблТЗ = Новый ТаблицаЗначений;
СтрокаОсновная.ТаблТЗ.Колонки.Добавить("СФПолученный");
Иначе
НайденнаяСтрока = Т1.Найти(ТекущаяСтрока.Номенклатура, "Номенклатура");
КонецЕсли;
СтрокаТаблТЗ = СтрокаОсновная.ТаблТЗ.Добавить();
СтрокаТаблТЗ.СФПолученный = ТекущаяСтрока.СФПолученный;
КонецЦикла;
Выясним причину, почему данные заполняются неверно. В представленном алгоритме допущены две критические ошибки:
стр вместо текущей итерационной переменной ТекущаяСтрока. Если переменная стр была инициализирована где-то выше первой строкой, именно ее значения будут бесконечно копироваться во все новые строки.Иначе переменной СтрокаОсновная не присваивается найденная строка таблицы Т_М. Вместо этого вызывается бессмысленный повторный поиск по исходной таблице Т1. В результате переменная СтрокаОсновная продолжает хранить ссылку на строку из предыдущей итерации, и подчиненные данные добавляются не в найденную группу, а в ту строку, которая была создана последней.Разберем по шагам, как исправить исходный код, сохранив логику работы с коллекциями:
Т_М = Новый ТаблицаЗначений;
Т_М.Колонки.Добавить("Номенклатура");
Т_М.Колонки.Добавить("СтавкаНДС");
Т_М.Колонки.Добавить("КоличествоПоступило");
Т_М.Колонки.Добавить("КоличествоПродано");
Т_М.Колонки.Добавить("Сумма");
Т_М.Колонки.Добавить("Разница");
Т_М.Колонки.Добавить("СуммаНДС");
Т_М.Колонки.Добавить("ТаблТЗ");
Для Каждого ТекущаяСтрока Из Т1 Цикл
НайденнаяСтрока = Т_М.Найти(ТекущаяСтрока.Номенклатура, "Номенклатура");
Если НайденнаяСтрока = Неопределено Тогда
СтрокаОсновная = Т_М.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаОсновная, ТекущаяСтрока);
СтрокаОсновная.ТаблТЗ = Новый ТаблицаЗначений;
СтрокаОсновная.ТаблТЗ.Колонки.Добавить("СФПолученный");
Иначе
// Обязательно перенаправляем ссылку на найденную строку
СтрокаОсновная = НайденнаяСтрока;
КонецЕсли;
СтрокаТаблТЗ = СтрокаОсновная.ТаблТЗ.Добавить();
СтрокаТаблТЗ.СФПолученный = ТекущаяСтрока.СФПолученный;
КонецЦикла;
Хранение объекта ТаблицаЗначений внутри ячейки другой таблицы значений считается устаревшей практикой, доставшейся в наследство от платформы 1С 7.7. В 1С:Предприятие 8 для многоуровневых структур данных предназначен штатный объект ДеревоЗначений.
Рассмотрим преимущества использования дерева значений:
Строки.ДанныеФормыДерево.Посмотрим на пример построения двухуровневой структуры с помощью ДеревоЗначений:
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Номенклатура");
Дерево.Колонки.Добавить("СтавкаНДС");
Дерево.Колонки.Добавить("КоличествоПоступило");
Дерево.Колонки.Добавить("КоличествоПродано");
Дерево.Колонки.Добавить("Сумма");
Дерево.Колонки.Добавить("Разница");
Дерево.Колонки.Добавить("СуммаНДС");
Дерево.Колонки.Добавить("СФПолученный");
// Для быстрого поиска родительских строк используем Соответствие
СоответствиеГрупп = Новый Соответствие;
Для Каждого ТекущаяСтрока Из Т1 Цикл
СтрокаРодитель = СоответствиеГрупп[ТекущаяСтрока.Номенклатура];
Если СтрокаРодитель = Неопределено Тогда
СтрокаРодитель = Дерево.Строки.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаРодитель, ТекущаяСтрока);
СоответствиеГрупп.Вставить(ТекущаяСтрока.Номенклатура, СтрокаРодитель);
КонецЕсли;
СтрокаПодчиненная = СтрокаРодитель.Строки.Добавить();
СтрокаПодчиненная.СФПолученный = ТекущаяСтрока.СФПолученный;
КонецЦикла;
Важный момент оптимизации: Использование объекта Соответствие вместо метода Найти() позволяет осуществлять поиск родительской строки со скоростью $O(1)$, что кардинально ускоряет обработку больших массивов данных.
Если исходные данные получены из нескольких источников или таблиц, наиболее производительный и лаконичный путь — передать эти таблицы в качестве параметров нового запроса, объединить их и рассчитать итоги средствами СУБД.
Разберем пример реализации через язык запросов:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.СтавкаНДС КАК СтавкаНДС,
| Т.КоличествоПоступило КАК КоличествоПоступило,
| Т.КоличествоПродано КАК КоличествоПродано,
| Т.Сумма КАК Сумма,
| Т.Разница КАК Разница,
| Т.СуммаНДС КАК СуммаНДС,
| Т.СФПолученный КАК СФПолученный
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ТаблицаДанных КАК Т
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.СтавкаНДС КАК СтавкаНДС,
| ВТ.КоличествоПоступило КАК КоличествоПоступило,
| ВТ.КоличествоПродано КАК КоличествоПродано,
| ВТ.Сумма КАК Сумма,
| ВТ.Разница КАК Разница,
| ВТ.СуммаНДС КАК СуммаНДС,
| ВТ.СФПолученный КАК СФПолученный
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ
|ИТОГИ
| МАКСИМУМ(СтавкаНДС),
| СУММА(КоличествоПоступило),
| СУММА(КоличествоПродано),
| СУММА(Сумма),
| СУММА(Разница),
| СУММА(СуммаНДС)
|ПО
| Номенклатура";
Запрос.УстановитьПараметр("ТаблицаДанных", Т1);
// Выгрузка результата сразу формирует готовое ДеревоЗначений с итогами
ДеревоРезультат = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Использование конструкции ИТОГИ ... ПО ... с методом Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам) исключает необходимость ручного обхода коллекций в коде, снижает вероятность алгоритмических ошибок и формирует готовую иерархическую структуру данных.