В процессе разработки в 1С мы часто сталкиваемся с необходимостью получить не только агрегированные числовые данные, но и сгруппированные строковые значения. Представьте себе ситуацию: у нас есть список контрагентов, и каждый контрагент пользуется несколькими услугами. Нам требуется вывести информацию о каждом контрагенте в одной строке, а все связанные с ним услуги объединить в одну строку через запятую или пробел, например: «Контрагент А — Услуга1, Услуга2, Услуга3».
На первый взгляд, задача кажется простой, если исходить из логики работы с числовыми данными в запросах. Если бы мы хотели просуммировать значения, мы бы просто использовали агрегатную функцию СУММА() в группирующем запросе. Однако, язык запросов 1С не предоставляет прямой агрегатной функции для объединения строк, аналогичной GROUP_CONCAT в MySQL или специализированным функциям в других СУБД. Попытка «просуммировать» строковое поле при группировке в запросе 1С приведет к ошибке, так как строковый тип не поддерживает операцию суммирования.
Возникает вопрос: как же тогда достичь желаемого результата, когда требуется получить агрегированную строку по группе? Рассмотрим два основных подхода к решению этой задачи, которые эффективно применяются в разработке на платформе 1С Предприятие.
Система Компоновки Данных (СКД) — это мощный инструмент, встроенный в платформу 1С, предназначенный для создания отчетов, но также широко используемый для гибкой выборки и обработки данных. СКД предоставляет более широкие возможности для работы с данными, включая агрегацию строк, пусть и не всегда самым очевидным способом. Для упрощения программной настройки схемы компоновки подойдёт модуль-обёртка для программного создания СКД.
Разберем по шагам, как мы можем использовать СКД для решения нашей задачи:
Создание схемы компоновки данных. Первым делом мы создадим схему компоновки данных. Это можно сделать в конфигураторе, добавив новый объект «Отчет» или «Обработка» с формой, содержащей поле ПолеКомпоновкиДанных. Либо, что чаще встречается в программной реализации, создадим схему динамически в коде или загрузим из макета.
Определение набора данных. В нашей схеме компоновки данных мы определим набор данных — запрос, который выбирает нужные нам поля: Контрагент и НаименованиеУслуги. Обратите внимание, что на этом этапе мы пока не выполняем агрегацию строк:
ВЫБРАТЬ
ТЧУслуги.Контрагент КАК Контрагент,
ТЧУслуги.Услуга.Наименование КАК НаименованиеУслуги
ИЗ
Документ.РеализацияТоваровУслуг.Услуги КАК ТЧУслуги
// Или любой другой источник данных, содержащий контрагентов и их услуги
Здесь мы предполагаем, что у нас есть некая табличная часть документа (например, Документ.РеализацияТоваровУслуг.Услуги) или регистр сведений, где хранятся данные о связи контрагентов и услуг. Мы выбираем сам контрагент и наименование услуги.
Настройка группировки в СКД. Далее, в настройках отчета (или программно формируя настройки компоновки), мы укажем группировку по полю Контрагент. Это означает, что все записи, относящиеся к одному контрагенту, будут сгруппированы.
Агрегация списка услуг с помощью функции Массив(). Для поля НаименованиеУслуги мы можем задать агрегатную функцию Массив. Эта функция СКД собирает все значения поля для каждой группы в объект типа Массив. В результате, после выгрузки данных, в колонке для услуг у нас будет не одна строка, а массив строк.
Выгрузка результата СКД в ТаблицуЗначений. СКД может не только выводить данные в макет отчета, но и выгружать их в коллекции значений, такие как ТаблицаЗначений или ДеревоЗначений. Это крайне полезно, когда нам нужны обработанные данные для дальнейшего использования в коде.
// Предположим, что СхемаКомпоновкиДанных и НастройкиКомпоновки уже определены
// и в настройках СКД для поля 'НаименованиеУслуги' установлена агрегатная функция 'Массив'.
// Создаем компоновщик макета
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, НастройкиКомпоновки);
// Создаем процессор компоновки данных
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки);
// Создаем таблицу значений, куда будем выгружать результат
ТаблицаРезультатаСКД = Новый ТаблицаЗначений;
// Создаем процессор вывода результата в коллекцию значений
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ПроцессорВывода.УстановитьОбъект(ТаблицаРезультатаСКД); // Указываем, куда выводить
ПроцессорВывода.Вывести(ПроцессорКомпоновки); // Выполняем вывод
// Теперь ТаблицаРезультатаСКД содержит колонки 'Контрагент' и 'НаименованиеУслуги'.
// В колонке 'НаименованиеУслуги' будут содержаться объекты типа 'Массив' для каждого контрагента.
Программная обработка массива услуг. После того как мы получили ТаблицуЗначений из СКД, нам потребуется пройтись по ней в цикле. Для каждой строки этой таблицы мы получим контрагента и массив его услуг. Затем мы программно объединим элементы этого массива в одну строку с нужным разделителем.
// Создадим новую таблицу, куда будем собирать окончательный результат
НоваяТаблицаСФорматированнымСписком = Новый ТаблицаЗначений;
НоваяТаблицаСФорматированнымСписком.Колонки.Добавить("Контрагент");
НоваяТаблицаСФорматированнымСписком.Колонки.Добавить("СписокУслуг");
Для Каждого СтрокаРезультата Из ТаблицаРезультатаСКД Цикл
НоваяСтрока = НоваяТаблицаСФорматированнымСписком.Добавить();
НоваяСтрока.Контрагент = СтрокаРезультата.Контрагент;
// Проверяем, что в поле 'НаименованиеУслуги' действительно массив
Если ТипЗнч(СтрокаРезультата.НаименованиеУслуги) = Тип("Массив") Тогда
МассивУслуг = СтрокаРезультата.НаименованиеУслуги;
СтрокаУслуг = "";
// Объединяем элементы массива в одну строку
Для Каждого ЭлементМассива Из МассивУслуг Цикл
Если Не ПустаяСтрока(СтрокаУслуг) Тогда
СтрокаУслуг = СтрокаУслуг + ", "; // Используем запятую и пробел как разделитель
КонецЕсли;
СтрокаУслуг = СтрокаУслуг + Строка(ЭлементМассива);
КонецЦикла;
НоваяСтрока.СписокУслуг = СтрокаУслуг;
Иначе
// Если по какой-то причине это не массив (например, одна услуга или ошибка)
НоваяСтрока.СписокУслуг = Строка(СтрокаРезультата.НаименованиеУслуги);
КонецЕсли;
КонецЦикла;
// Теперь НоваяТаблицаСФорматированнымСписком содержит контрагентов и их услуги в виде одной строки.
// Мы можем использовать эту таблицу для вывода в отчет, на форму или для дальнейшей обработки.
Этот подход достаточно гибок, особенно если нам нужно интегрировать результат в отчет, созданный средствами СКД. Однако, он требует некоторой дополнительной программной обработки после получения данных из СКД. Для реализации подобного объединения можно использовать готовый способ сбора значений колонки из строк в СКД.
Наиболее распространенным, простым для понимания и зачастую самым производительным для подобных задач является подход, при котором мы выполняем запрос для получения всех необходимых данных, а затем обрабатываем их программно на стороне 1С. Этот метод дает нам полный контроль над процессом формирования строки.
Разберем этот подход по шагам:
Формирование запроса с группировкой данных. Мы создаем запрос, который выбирает контрагента и наименование услуги. Ключевым моментом здесь является правильная сортировка, чтобы мы могли эффективно обходить результат запроса по группам.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КонтрагентУслуги.Контрагент КАК Контрагент,
| КонтрагентУслуги.Услуга.Наименование КАК НаименованиеУслуги
|ИЗ
| РегистрСведений.НашиУслугиКонтрагентам КАК КонтрагентУслуги // Пример: Регистр сведений или ТЧ документа
|
|УПОРЯДОЧИТЬ ПО
| Контрагент, НаименованиеУслуги"; // Важно для обхода по группировкам
В этом запросе мы выбираем Контрагента и НаименованиеУслуги. Критически важно установить упорядочивание по Контрагент. Это позволит нам использовать метод Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Контрагент") для эффективного перебора уникальных контрагентов, а внутри каждой группы — их услуг.
Выполнение запроса и получение выборки. После определения текста запроса мы выполняем его и получаем результат запроса, который мы будем обходить.
// ... (текст запроса)
ВыборкаПоКонтрагенту = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Контрагент");
Метод Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Контрагент") создает выборку, которая позволяет нам последовательно перебирать уникальные значения поля Контрагент. Внутри каждого элемента этой выборки будут доступны детальные записи для текущего контрагента.
Два цикла для формирования строки. Теперь мы организуем два вложенных цикла. Внешний цикл будет перебирать контрагентов, а внутренний — услуги для текущего контрагента, формируя нужную строку.
// Создадим новую таблицу значений для хранения окончательного результата
НоваяТаблица = Новый ТаблицаЗначений;
НоваяТаблица.Колонки.Добавить("Контрагент", Метаданные.Справочники.Контрагенты.ТипСсылки()); // Предполагаем, что контрагенты - это справочник
НоваяТаблица.Колонки.Добавить("СписокУслуг", Новый ОписаниеТипов("Строка"));
Пока ВыборкаПоКонтрагенту.Следующий() Цикл
ТекущийКонтрагент = ВыборкаПоКонтрагенту.Контрагент;
СписокУслуг = ""; // Переменная для накопления списка услуг
// Внутренний цикл для выборки детальных записей (услуг) по текущему контрагенту
ВыборкаПоУслуге = ВыборкаПоКонтрагенту.Выбрать();
Пока ВыборкаПоУслуге.Следующий() Цикл
// Добавляем разделитель, если это не первая услуга в списке
Если Не ПустаяСтрока(СписокУслуг) Тогда
СписокУслуг = СписокУслуг + ", "; // Разделитель: запятая и пробел
КонецЕсли;
СписокУслуг = СписокУслуг + ВыборкаПоУслуге.НаименованиеУслуги;
КонецЦикла;
// Добавляем сформированную строку в новую таблицу
НоваяСтрока = НоваяТаблица.Добавить();
НоваяСтрока.Контрагент = ТекущийКонтрагент;
НоваяСтрока.СписокУслуг = СписокУслуг;
КонецЦикла;
// Теперь переменная "НоваяТаблица" содержит Контрагентов и сформированные списки их услуг.
// Мы можем использовать эту таблицу для вывода на форму, в отчет или для дальнейших операций.
// Пример вывода для проверки:
// Для Каждого СтрокаТЗ Из НоваяТаблица Цикл
// Сообщить("Контрагент: " + СтрокаТЗ.Контрагент.Наименование + ", Услуги: " + СтрокаТЗ.СписокУслуг);
// КонецЦикла;
В этом коде мы последовательно перебираем контрагентов. Для каждого контрагента мы инициализируем пустую строку СписокУслуг. Затем, с помощью внутреннего цикла ВыборкаПоКонтрагенту.Выбрать(), мы получаем все услуги, относящиеся к текущему контрагенту. Мы используем условное добавление разделителя (", "), чтобы он не появлялся перед первой услугой.
Оба рассмотренных подхода эффективны и имеют свои преимущества:
СКД:
Массив() и последующая программная обработка добавляет несколько шагов.Программная обработка выборки после запроса:
Выбор конкретного метода зависит от специфики задачи, требований к гибкости и производительности, а также от того, где будет использоваться полученный результат.
Отсутствие прямой агрегатной функции для строк в языке запросов 1С является фундаментальным отличием от некоторых других SQL-подобных языков. Например, в MS SQL Server существует оператор PIVOT, позволяющий транспонировать строки в столбцы, а также функции, подобные STRING_AGG (начиная с SQL Server 2017) или использование FOR XML PATH('') для объединения строк. В MySQL мы знаем функцию GROUP_CONCAT().
Язык запросов 1С, будучи специфичным для платформы, ориентирован на высокоуровневую работу с объектами метаданных и не всегда предоставляет низкоуровневые возможности манипуляции данными, характерные для чистых SQL-серверов. Тем не менее, платформа 1С предоставляет достаточно инструментов (таких как СКД и мощный встроенный язык) для обхода этих ограничений, позволяя нам достигать желаемых результатов, пусть и другими средствами.
Таким образом, хотя мы не можем "просуммировать" строки напрямую в запросе 1С, у нас есть надежные и проверенные способы решения этой задачи, которые программисты 1С активно используют в своей работе.