Как вывести список сегментов партнера через запятую в динамическом списке 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке интерфейсов на платформе 1С программисты часто сталкиваются с задачей вывода агрегированных текстовых данных в динамических списках. Типичный пример такой задачи — необходимость показать в одной строке списка задач все сегменты, к которым относится партнер (клиент), через запятую: "Сегмент 1, Сегмент 2, Сегмент 3".

Сложность заключается в том, что в стандартном языке запросов 1С отсутствует прямой аналог функции СУБД STRING_AGG или GROUP_CONCAT. Попытка решить задачу "в лоб" через обычный запрос динамического списка приведет к дублированию строк в интерфейсе — для каждого сегмента партнера будет создана отдельная строка с задачей. Выясним, как элегантно обойти это ограничение платформы, подробно разобрав рабочие методики решения этой задачи.

Почему задачу нельзя решить простым запросом?

В реляционных базах данных связь между партнерами и их сегментами обычно реализуется по схеме "многие-ко-многим" через промежуточную таблицу (в 1С это чаще всего регистр сведений ПартнерыСегмента). Если мы попытаемся связать таблицу задач с регистром сегментов с помощью левого соединения в запросе динамического списка, СУБД вернет декартово произведение строк. Для одной задачи, связанной с партнером из трех сегментов, в списке отобразятся три одинаковые строки, отличающиеся только названием сегмента. Нам же требуется строго одна строка на одну задачу.

Разберем три основных способа реализации этой задачи, начиная с самого популярного программного метода и заканчивая наиболее производительным архитектурным паттерном.


Решение 1: Использование события ПриПолученииДанныхНаСервере

Этот метод является оптимальным, если состав сегментов меняется динамически, а доработка структуры хранения данных нежелательна. Событие ПриПолученииДанныхНаСервере срабатывает на сервере непосредственно перед отправкой порции данных динамического списка клиенту. Платформа передает в этот обработчик только те строки, которые реально видны на экране пользователя в данный момент (обычно около 30-45 строк), что позволяет существенно снизить нагрузку на базу данных.

Критическая ошибка новичков: Выполнение запроса в цикле по каждой строке динамического списка. Это порождает классическую проблему производительности "N+1 запрос" и моментально снижает скорость работы интерфейса до неприемлемых значений.

Проанализируем правильный пакетный алгоритм реализации этой задачи по шагам.

Шаг 1. Модификация запроса динамического списка

Для начала нам необходимо подготовить "место" (колонку) в динамическом списке, куда мы будем записывать сформированную строку сегментов, а также вытащить ссылку на самого партнера для последующей связи. В запросе динамического списка (в нашем примере это список задач исполнителя) добавим пустое поле-пустышку и поле партнера:


ВЫБРАТЬ
    ЗадачиПоИсполнителю.Ссылка КАК Ссылка,
    ЗадачиПоИсполнителю.Наименование КАК Наименование,
    ЗадачиПоИсполнителю.Предмет КАК Предмет,
    // Вытаскиваем партнера из предмета (например, Заказа клиента)
    ВЫРАЗИТЬ(ЗадачиПоИсполнителю.Предмет КАК Документ.ЗаказКлиента).Партнер КАК Партнер,
    // Создаем пустую колонку-заглушку для списка сегментов
    "" КАК СегментыСписок
ИЗ
    Задача.ЗадачаИсполнителя.ЗадачиПоИсполнителю КАК ЗадачиПоИсполнителю
ГДЕ
    ЗадачиПоИсполнителю.СостояниеБизнесПроцесса = ЗНАЧЕНИЕ(Перечисление.СостоянияБизнесПроцессов.Активен)

Шаг 2. Размещение элемента на форме и настройка свойств

Добавим новое поле формы в таблицу динамического списка и свяжем его с путем к данным Список.СегментыСписок. Чтобы платформа гарантированно считывала данные партнера из БД, даже если пользователь скроет эту колонку в интерфейсе, найдем в настройках динамического списка поле Партнер и установим для него флаг Использовать всегда.

Шаг 3. Программный код обработчика события

Создадим обработчик события ПриПолученииДанныхНаСервере для таблицы формы динамического списка. Обратите внимание, что данный метод должен выполняться с директивой &НаСервереБезКонтекста для обеспечения максимального быстродействия.


&НаСервереБезКонтекста
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
    
    // Шаг 3.1: Собираем уникальные ссылки на партнеров из отображаемых на экране строк
    МассивПартнеров = Новый Массив;
    Для Каждого КлючИЗначение Из Строки Цикл
        СтрокаДанных = КлючИЗначение.Значение;
        
        // Получаем значение партнера из считанных платформой данных строки
        Партнер = Неопределено;
        СтрокаДанных.Данные.Свойства().Вставить("Партнер", Партнер);
        
        Если ЗначениеЗаполнено(Партнер) И МассивПартнеров.Найти(Партнер) = Неопределено Тогда
            МассивПартнеров.Добавить(Партнер);
        КонецЕсли;
    КонецЦикла;
    
    Если МассивПартнеров.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // Шаг 3.2: Выполняем ОДИН пакетный запрос к базе данных по всем партнерам пачки
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ПартнерыСегмента.Партнер КАК Партнер,
        |	ПартнерыСегмента.Сегмент.Наименование КАК СегментНаименование
        |ИЗ
        |	РегистрСведений.ПартнерыСегмента КАК ПартнерыСегмента
        |ГДЕ
        |	ПартнерыСегмента.Партнер В(&МассивПартнеров)
        |УПОРЯДОЧИТЬ ПО
        |	ПартнерыСегмента.Партнер,
        |	СегментНаименование";
    
    Запрос.УстановитьПараметр("МассивПартнеров", МассивПартнеров);
    Выборка = Запрос.Выполнить().Выбрать();
    
    // Шаг 3.3: Группируем сегменты по партнерам в оперативной памяти сервера
    СоответствиеСегментов = Новый Соответствие; // Ключ: Партнер, Значение: Строка сегментов
    Пока Выборка.Следующий() Цикл
        ТекСтрока = СоответствиеСегментов[Выборка.Партнер];
        Если ТекСтрока = Неопределено Тогда
            ТекСтрока = Выборка.СегментНаименование;
        Иначе
            ТекСтрока = ТекСтрока + ", " + Выборка.СегментНаименование;
        КонецЕсли;
        СоответствиеСегментов.Вставить(Выборка.Партнер, ТекСтрока);
    КонецЦикла;
    
    // Шаг 3.4: Заполняем поле оформления для вывода строки в интерфейс пользователя
    Для Каждого КлючИЗначение Из Строки Цикл
        СтрокаДанных = КлючИЗначение.Значение;
        
        Партнер = Неопределено;
        СтрокаДанных.Данные.Свойства().Вставить("Партнер", Партнер);
        
        Если ЗначениеЗаполнено(Партнер) Тогда
            СтрокаСегментов = СоответствиеСегментов[Партнер];
            Если СтрокаСегментов <> Неопределено Тогда
                // Записываем результат непосредственно в оформление ячейки строки
                СтрокаДанных.Оформление["СегментыСписок"].УстановитьЗначениеПараметра("Текст", СтрокаСегментов);
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Важное преимущество данного метода — высокая скорость рендеринга формы при прокрутке, поскольку СУБД делает выборку сегментов только по идентификаторам видимых на экране партнеров.

Однако у метода динамического заполнения есть существенный недостаток: по колонке СегментыСписок невозможно осуществлять стандартный поиск, сортировку и отборы в динамическом списке, так как эти данные формируются "на лету" в памяти сервера приложений и отсутствуют в базе данных в виде готового поля.


Решение 2: Денормализация данных (Рекомендуемый высокопроизводительный подход)

Если пользователям критически важна возможность быстрого текстового поиска задач по сегменту (например, ввести в строку поиска "VIP" и увидеть все задачи, связанные с партнерами данного сегмента), первый способ не подойдет.

В этом случае лучшей практикой разработки является денормализация данных — хранение уже сформированной текстовой строки сегментов непосредственно внутри объекта базы данных. Посмотрим, как это реализовать.

  1. Добавим в справочник Партнеры дополнительный реквизит СегментыСтрокой с типом Строка (длина 150-250 символов, неограниченную длину делать не рекомендуется для сохранения возможности индексации).
  2. Создадим подписку на события ПриЗаписи для регистра сведений ПартнерыСегмента.
  3. В обработчике подписки на событие напишем процедуру, которая при любом изменении состава сегментов конкретного партнера будет собирать их названия в строку и перезаписывать реквизит СегментыСтрокой в справочнике Партнеры:

Процедура ОбновитьТекстовоеПредставлениеСегментовПартнера(ПартнерСсылка) Экспорт
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ПартнерыСегмента.Сегмент.Наименование КАК СегментНаименование
        |ИЗ
        |	РегистрСведений.ПартнерыСегмента КАК ПартнерыСегмента
        |ГДЕ
        |	ПартнерыСегмента.Партнер = &Партнер";
    
    Запрос.УстановитьПараметр("Партнер", ПартнерСсылка);
    Выборка = Запрос.Выполнить().Выбрать();
    
    МассивСегментов = Новый Массив;
    Пока Выборка.Следующий() Цикл
        МассивСегментов.Добавить(Выборка.СегментНаименование);
    КонецЦикла;
    
    СтрокаСегментов = СтрСоединить(МассивСегментов, ", ");
    
    // Записываем денормализованные данные в справочник Партнеры
    ПартнерОбъект = ПартнерСсылка.ПолучитьОбъект();
    Если ПартнерОбъект.СегментыСтрокой <> СтрокаСегментов Тогда
        ПартнерОбъект.СегментыСтрокой = СтрокаСегментов;
        ПартнерОбъект.ОбменДанными.Загрузка = Истина; // Избегаем лишних проверок при записи
        ПартнерОбъект.Записать();
    КонецЕсли;
    
КонецПроцедуры

После проведения этой настройки запрос нашего динамического списка станет предельно простым и быстрым. Нам больше не нужны сложные обработчики событий формы, достаточно просто вытащить готовый реквизит через точку:


ВЫБРАТЬ
    ЗадачиПоИсполнителю.Ссылка КАК Ссылка,
    ЗадачиПоИсполнителю.Наименование КАК Наименование,
    ВЫРАЗИТЬ(ЗадачиПоИсполнителю.Предмет КАК Документ.ЗаказКлиента).Партнер.СегментыСтрокой КАК СегментыСписок
ИЗ
    Задача.ЗадачаИсполнителя.ЗадачиПоИсполнителю КАК ЗадачиПоИсполнителю

Этот подход гарантирует максимальную производительность СУБД. Динамический список будет мгновенно считывать данные одной таблицей без соединений, а встроенный полнотекстовый поиск 1С без проблем найдет нужную задачу по названию сегмента.


Решение 3: Использование СКД СоединитьСтроки()

Если динамический список построен на базе Схемы компоновки данных (СКД), мы можем задействовать мощную агрегатную функцию СКД СоединитьСтроки(). Этот метод позволяет решить задачу без написания сложного программного кода в модуле формы.

Рассмотрим, как настроить этот механизм:

  1. Откроем основную схему компоновки данных нашего динамического списка.
  2. Вкладка Наборы данных: добавим в качестве дополнительного набора данных запрос к регистру сведений ПартнерыСегмента.
  3. Настроим связь наборов данных по полю Партнер.
  4. На вкладке Вычисляемые поля добавим новое поле с выражением: СоединитьСтроки(ВычислитьВыражениеСГруппировкойТаблицаЗначений("Сегмент.Наименование", "Сегмент.Наименование"), ", ").

Несмотря на кажущуюся простоту настройки в интерфейсе конфигуратора, использование сложных выражений СКД в динамических списках на больших объемах данных может приводить к неоптимальным планам запросов СУБД. Рекомендуется использовать этот метод с осторожностью на таблицах, содержащих более 100 000 записей.


Решение проблемы "невидимки": почему добавленная колонка не отображается?

Иногда разработчики сталкиваются с ситуацией, когда колонка добавлена в запрос и выведена на форму, но в режиме "1С:Предприятие" пользователь ее не видит. Проанализируем основные причины этой проблемы:

Мы подробно разобрали основные подходы к получению и выводу объединенных строк сегментов в интерфейсе 1С. Выбирайте оптимальный вариант исходя из требований к производительности системы и необходимости реализации поиска по выводимой колонке.

← На главную