При разработке прикладных решений на платформе 1С:Предприятие часто возникает задача: обойти табличную часть одного объекта (например, документа или справочника) и на основе этих данных создать новые элементы в другом справочнике. Проблема усложняется, когда строки табличной части содержат повторяющиеся аналитики (например, этапы производства, группы товаров или склады), и нам необходимо сгруппировать данные перед созданием новых объектов.
В этой статье мы подробно разберем пять различных способов реализации такой группировки и обхода. Мы проанализируем достоинства и недостатки каждого подхода, а также рассмотрим важные архитектурные нюансы производительности, которые часто вызывают жаркие споры в сообществе разработчиков.
Это классический, наиболее простой алгоритм, не требующий создания дополнительных тяжелых объектов. Суть метода заключается в том, что мы предварительно сортируем коллекцию по группировочному полю, после чего обходим ее в цикле, сравнивая значение текущей строки с предыдущим обработанным значением.
Рассмотрим пример реализации такого обхода:
// Шаг 1. Получаем табличную часть и сортируем её по ключевому полю
ТаблицаОбхода = СсылкаНаОбъект.ТабличнаяЧасть.Выгрузить();
ТаблицаОбхода.Сортировать("НомерЭтапа Возр");
// Шаг 2. Инициализируем переменную для хранения предыдущего значения
НомерПредыдущегоЭтапа = Неопределено;
// Шаг 3. Запускаем цикл обхода
Для Каждого СтрокаТЧ Из ТаблицаОбхода Цикл
Если СтрокаТЧ.НомерЭтапа <> НомерПредыдущегоЭтапа Тогда
// Сюда мы попадаем только при смене этапа (появлении нового уникального значения)
// Выполняем действия по созданию нового элемента справочника
НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
НовыйЭлемент.Наименование = "Этап № " + СтрокаТЧ.НомерЭтапа;
НовыйЭлемент.Записать();
// Фиксируем новый ключ группировки
НомерПредыдущегоЭтапа = СтрокаТЧ.НомерЭтапа;
КонецЕсли;
// Здесь можно выполнять обработку детальных строк для текущего этапа,
// если это необходимо для логики заполнения.
КонецЦикла;
Преимущества: Минимальное потребление оперативной памяти (ОЗУ), отсутствие сложных объектов, высокая скорость выполнения на небольших и средних коллекциях.
Недостатки: Требует обязательной предварительной сортировки. Код может стать перегруженным, если логика обработки детальных записей внутри одной группы достаточно сложна.
Если для создания новых элементов справочника нам требуются только уникальные значения группировочных колонок, а детальные строки анализировать не нужно, самым элегантным решением будет использование встроенного метода Свернуть объекта ТаблицаЗначений.
Разберем этот лаконичный алгоритм:
// 1. Выгружаем табличную часть в Таблицу Значений на сервере
ТЗОбхода = СсылкаНаОбъект.ТабличнаяЧасть.Выгрузить();
// 2. Сворачиваем таблицу по интересующей нас колонке.
// Все дублирующиеся строки будут удалены, останутся только уникальные.
ТЗОбхода.Свернуть("НомерЭтапа");
// 3. Обходим полученную коллекцию уникальных значений
Для Каждого СтрокаТЧ Из ТЗОбхода Цикл
НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
НовыйЭлемент.Наименование = "Этап № " + СтрокаТЧ.НомерЭтапа;
НовыйЭлемент.Записать();
КонецЦикла;
Преимущества: Очень высокая скорость написания кода и его отличная читаемость. Метод Свернуть реализован на уровне платформы на языке C++ и работает чрезвычайно быстро.
Недостатки: Этот метод полностью уничтожает детальные строки табличной части. Если в процессе создания элемента справочника вам необходимо перенести туда подчиненную табличную часть (например, состав операций этапа), этот метод вам не подойдет.
Этот подход часто предлагают разработчики, привыкшие решать любые задачи группировки данных с помощью СУБД. Суть метода заключается в передаче таблицы значений в качестве параметра в запрос, помещении её во временную таблицу и выполнении запроса с предложением ИТОГИ ПО.
Посмотрим, как выглядит программный код для такой реализации:
ТЗСпр = ЭлементСправочника.ТабличнаяЧасть.Выгрузить();
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТЗСпр", ТЗСпр);
Запрос.Текст =
"ВЫБРАТЬ
| ТЗСпр.НомерЭтапа КАК НомерЭтапа,
| ТЗСпр.НомерОперации КАК НомерОперации
|ПОМЕСТИТЬ ВТ_ТабЧасть
|ИЗ
| &ТЗСпр КАК ТЗСпр
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ТабЧасть.НомерЭтапа КАК НомерЭтапа,
| ВТ_ТабЧасть.НомерОперации КАК НомерОперации
|ИЗ
| ВТ_ТабЧасть КАК ВТ_ТабЧасть
|ИТОГИ ПО
| НомерЭтапа";
ВыборкаЭтапов = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаЭтапов.Следующий() Цикл
// Создаем головной элемент для этапа
НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
НовыйЭлемент.Наименование = "Этап № " + ВыборкаЭтапов.НомерЭтапа;
// Получаем подчиненные детальные записи (операции этого этапа)
ДетальнаяВыборка = ВыборкаЭтапов.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ДетальнаяВыборка.Следующий() Цикл
// Здесь мы можем заполнить подчиненную табличную часть нового элемента
НоваяСтрока = НовыйЭлемент.Операции.Добавить();
НоваяСтрока.Операция = ДетальнаяВыборка.НомерОперации;
КонецЦикла;
НовыйЭлемент.Записать();
КонецЦикла;
Важный архитектурный анализ! Несмотря на кажущуюся универсальность, этот метод имеет серьезные скрытые издержки производительности. Давайте проанализируем, что происходит на физическом уровне, когда мы передаем таблицу значений в запрос:
tempdb) и производит физическую запись строк (транзакции INSERT). Это создает нагрузку на жесткие диски сервера СУБД.Использовать этот метод для обработки стандартных табличных частей документов (которые обычно содержат от 10 до 500 строк) — это серьезная архитектурная ошибка. Затраты времени на сериализацию и сетевой оверхед в сотни раз превышают время выполнения аналогичного кода в оперативной памяти.
Когда этот способ оправдан? Использовать запрос оправдано только в двух случаях: во-первых, если объем таблицы превышает десятки тысяч строк (когда мощности СУБД по сортировке начинают превосходить однопоточный встроенный язык 1С); во-вторых, если в запросе выполняется реальное соединение (ЛЕВОЕ СОЕДИНЕНИЕ) с физическими таблицами базы данных (например, для проверки существования элементов в базе данных).
Если нам необходимо сгруппировать данные табличной части по ключевому полю, но при этом гарантированно сохранить доступ к детальным строкам без обращения к СУБД, идеальным решением является использование объекта Соответствие. Поиск по ключу в объекте Соответствие реализован платформой на основе быстрых хэш-таблиц и работает со скоростью O(1).
Разберем данный алгоритм по шагам:
// Шаг 1. Создаем соответствие для группировки данных в ОЗУ
ГруппировкаПоЭтапам = Новый Соответствие;
// Шаг 2. Заполняем соответствие
Для Каждого СтрокаТЧ Из СсылкаНаОбъект.ТабличнаяЧасть Цикл
КлючГруппировки = СтрокаТЧ.НомерЭтапа;
// Пытаемся получить массив строк для текущего ключа
МассивСтрокЭтапа = ГруппировкаПоЭтапам.Получить(КлючГруппировки);
Если МассивСтрокЭтапа = Неопределено Тогда
// Если такого этапа еще не было в коллекции, инициализируем новый массив
МассивСтрокЭтапа = Новый Массив;
ГруппировкаПоЭтапам.Вставить(КлючГруппировки, МассивСтрокЭтапа);
КонецЕсли;
// Добавляем ссылку на детальную строку в массив текущей группы
МассивСтрокЭтапа.Добавить(СтрокаТЧ);
КонецЦикла;
// Шаг 3. Обходим сгруппированные данные без участия СУБД
Для Каждого ЭлементГруппировки Из ГруппировкаПоЭтапам Цикл
ТекущийЭтап = ЭлементГруппировки.Ключ;
ДетальныеСтроки = ЭлементГруппировки.Значение; // Это массив строк ТЧ
// Создаем новый элемент справочника
НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
НовыйЭлемент.Наименование = "Этап № " + ТекущийЭтап;
// Наполняем его детальными данными из массива
Для Каждого СтрокаДеталей Из ДетальныеСтроки Цикл
НоваяСтрока = НовыйЭлемент.Операции.Добавить();
НоваяСтрока.Операция = СтрокаДеталей.НомерОперации;
КонецЦикла;
НовыйЭлемент.Записать();
КонецЦикла;
Преимущества: Этот способ работает молниеносно в оперативной памяти сервера приложений. Он полностью исключает ненужные обращения к СУБД, бережно расходует ресурсы сервера, сохраняет полную детализацию данных и не требует предварительной сортировки исходной таблицы.
Еще один профессиональный способ группировки средних объемов данных в оперативной памяти — использование механизма индексирования таблиц значений совместно с методом НайтиСтроки().
Проанализируем пример кода:
// Шаг 1. Выгружаем табличную часть и создаем индекс по группировочной колонке
ТЗ = СсылкаНаОбъект.ТабличнаяЧасть.Выгрузить();
ТЗ.Индексы.Добавить("НомерЭтапа");
// Шаг 2. Создаем копию таблицы и сворачиваем ее для получения списка уникальных ключей
ТЗЭтапы = ТЗ.Скопировать();
ТЗЭтапы.Свернуть("НомерЭтапа");
// Шаг 3. Обходим уникальные ключи и быстро извлекаем детальные записи по индексу
Для Каждого СтрокаЭтапа Из ТЗЭтапы Цикл
НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
НовыйЭлемент.Наименование = "Этап № " + СтрокаЭтапа.НомерЭтапа;
// Быстрый поиск строк по индексу (работает практически мгновенно)
ПараметрыОтбора = Новый Структура("НомерЭтапа", СтрокаЭтапа.НомерЭтапа);
ДетальныеСтроки = ТЗ.НайтиСтроки(ПараметрыОтбора); // Возвращает массив строк
Для Каждого СтрокаДеталей Из ДетальныеСтроки Цикл
НоваяСтрока = НовыйЭлемент.Операции.Добавить();
НоваяСтрока.Операция = СтрокаДеталей.НомерОперации;
КонецЦикла;
НовыйЭлемент.Записать();
КонецЦикла;
Преимущества: Простой и понятный код. За счет явного добавления индекса в ТЗ.Индексы поиск строк методом НайтиСтроки() выполняется по хэш-таблице, минуя линейный перебор всей коллекции, что обеспечивает высокую скорость работы даже на таблицах в несколько тысяч строк.
Подводя итоги, сформулируем практические рекомендации по выбору правильного метода обхода коллекций в вашей повседневной практике программирования в 1С:
СОЕДИНЕНИЕ или ОБЪЕДИНИТЬ, либо когда объем обрабатываемых данных исчисляется десятками тысяч строк. Во всех остальных случаях помните об избыточной нагрузке на СУБД и транзакционный лог.