Как правильно реализовать выбор и хранение нескольких значений в одной строке табличной части 1С

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

В практике разработки на платформе 1С:Предприятие 8 часто возникает задача организации связи «один-ко-многим» для строк табличной части документа. Типичный пример — когда строка табличной части представляет собой отдельную задачу, проект или позицию, для которой необходимо указать не одного, а сразу несколько ответственных исполнителей. При этом система должна не просто визуально отображать список выбранных сотрудников, но и обеспечивать возможность полноценного поиска, наложения отборов и построения детальных отчетов по каждому конкретному лицу.

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

Почему популярные «простые» варианты хранения не работают?

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

  1. Хранение списка в виде строки через запятую. Нам кажется заманчивым хранить строку вида «Петров, Сидоров, Иванов». Однако при таком подходе полностью теряется контроль ссылочной целостности. Если переименовать или удалить физическое лицо в справочнике, текстовая строка в документе останется неизменной и превратится в «битые» данные. Более того, построить производительный отчет с отбором по конкретному сотруднику с помощью запросов станет практически невозможно.
  2. Использование Хранилища значений в ячейке. Помещение массива или списка значений в реквизит с типом ХранилищеЗначений полностью закрывает эти данные для СУБД. Платформа физически сохраняет этот объект в виде бинарного файла (BLOB). Написать стандартный запрос к СУБД для поиска строк, где фигурирует «Петров», будет невозможно. Придется программно перебирать каждый документ базы данных и распаковывать хранилище, что приведет к катастрофическому падению производительности.
  3. Использование встроенных коллекций (Массив, СписокЗначений) напрямую. Реквизит табличной части физически не может иметь тип Массив или СписокЗначений, поскольку СУБД требует нормализации данных и строго определенных типов полей.

Единственным правильным, с точки зрения архитектуры баз данных, решением является нормализация структуры данных. Мы должны создать классическое отношение «один-ко-многим» с использованием подчиненных таблиц.

Шаг 1. Проектирование метаданных и создание надежной связи

Для реализации задачи нам понадобятся две табличные части внутри одного документа. Пусть первая будет основной ТЧ (назовем ее Задачи), а вторая — подчиненной вспомогательной ТЧ (назовем ее Исполнители).

В обсуждениях часто предлагают связывать подчиненную таблицу по стандартному реквизиту НомерСтроки. Внимание: делать это категорически не рекомендуется! Если пользователь решит переместить строки в основной табличной части вверх или вниз, отсортировать таблицу или удалить строку из середины списка, номера строк автоматически пересчитаются. Это приведет к тому, что связи нарушатся, данные «съедут», и исполнители первой задачи окажутся привязанными ко второй или третьей.

Чтобы избежать этой проблемы, мы применим связь по уникальному идентификатору (ключу связи):

  1. В основной табличной части Задачи создадим реквизит КлючСвязи с типом Строка (длина 36 символов, для записи UUID). Данное поле будет скрыто от пользователя.
  2. Для удобства отображения в основной ТЧ создадим текстовый реквизит ОтветственныеСтрокой (тип Строка, неограниченная длина или длина 150-200 символов).
  3. Создадим вторую (вспомогательную) табличную часть документа и назовем ее Исполнители.
  4. В табличной части Исполнители создадим два реквизита: КлючСвязи (тип Строка, 36 символов) и Исполнитель (тип СправочникСсылка.ФизическиеЛица).

Шаг 2. Программное управление ключами связи на форме

Перейдем в модуль формы документа. Наша цель — автоматизировать генерацию уникальных ключей при создании новых строк, копировании существующих и обеспечить своевременную очистку подчиненных данных при удалении строки основной таблицы.

Рассмотрим процедуру добавления строки в основную ТЧ. Нам необходимо перехватить событие создания и присвоить уникальный идентификатор:


&НаКлиенте
Процедура ЗадачиПриОкончанииРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования)
    Если НоваяСтрока И Не ОтменаРедактирования Тогда
        ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
        Если ТекущиеДанные <> Неопределено Тогда
            // Генерируем уникальный ключ связи для новой строки
            ТекущиеДанные.КлючСвязи = Строка(Новый УникальныйИдентификатор());
        КонецЕсли;
    КонецЕсли;
КонецПроцедуры

При копировании строки основной ТЧ стандартными средствами платформы скопируется и значение реквизита КлючСвязи. Если оставить все как есть, две разные строки основной таблицы будут ссылаться на одни и те же строки подчиненной таблицы. Нам необходимо принудительно перегенерировать ключ связи для новой скопированной строки:


&НаКлиенте
Процедура ЗадачиПриКопировании(Элемент, КопируемыйОбъект)
    ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
    Если ТекущиеДанные <> Неопределено Тогда
        // Заменяем старый скопированный ключ на новый уникальный
        ТекущиеДанные.КлючСвязи = Строка(Новый УникальныйИдентификатор());
        // Очищаем текстовое представление, так как исполнителей у новой строки пока нет
        ТекущиеДанные.ОтветственныеСтрокой = "";
    КонецЕсли;
КонецПроцедуры

Если пользователь решает удалить задачу из списка, мы обязаны очистить все связанные записи из подчиненной таблицы исполнителей, чтобы база данных не засорялась «сиротскими» строками:


&НаКлиенте
Процедура ЗадачиПередУдалением(Элемент, Отказ)
    ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
    Если ТекущиеДанные <> Неопределено И ЗначениеЗаполнено(ТекущиеДанные.КлючСвязи) Тогда
        // Передаем управление на сервер для удаления связанных строк из ТЧ документа
        УдалитьСвязанныхИсполнителейНаСервере(ТекущиеДанные.КлючСвязи);
    КонецЕсли;
КонецПроцедуры

&НаСервере
Процедура УдалитьСвязанныхИсполнителейНаСервере(КлючСвязи)
    // Осуществляем поиск строк в подчиненной ТЧ по КлючуСвязи
    ПараметрыОтбора = Новый Структура("КлючСвязи", КлючСвязи);
    НайденныеСтроки = Объект.Исполнители.НайтиСтроки(ПараметрыОтбора);
    
    Для Каждого СтрокаИсполнителя Из НайденныеСтроки Цикл
        Объект.Исполнители.Удалить(СтрокаИсполнителя);
    КонецЦикла;
КонецПроцедуры

Шаг 3. Реализация эргономичного интерфейса выбора значений

Показывать пользователю две связанные таблицы на одной форме — неудачное интерфейсное решение, которое перегружает форму и запутывает работу. Намного удобнее оставить на форме только основную ТЧ Задачи, скрыв подчиненную Исполнители, а в колонке ОтветственныеСтрокой реализовать открытие формы подбора или диалога отметки элементов.

Для этого настроим для поля ввода ЗадачиОтветственныеСтрокой событие НачалоВыбора:


&НаКлиенте
Процедура ЗадачиОтветственныеСтрокойНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
    СтандартнаяОбработка = Ложь;
    ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
    
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // В случае отсутствия ключа связи (например, в старых документах) генерируем его на лету
    Если Не ЗначениеЗаполнено(ТекущиеДанные.КлючСвязи) Тогда
        ТекущиеДанные.КлючСвязи = Строка(Новый УникальныйИдентификатор());
    КонецЕсли;
    
    // Подготавливаем список выбора на сервере и заполняем его текущими отметками
    СписокВыбора = ПолучитьСписокВыбораСОтметками(ТекущиеДанные.КлючСвязи);
    
    // Открываем модальное окно для выбора элементов списка с галочками
    ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВыбораИсполнителей", ЭтотОбъект, ТекущиеДанные.КлючСвязи);
    СписокВыбора.ПоказатьОтметкуЭлементов(ОписаниеОповещения, "Выберите ответственных исполнителей");
КонецПроцедуры

Теперь проанализируем серверную процедуру, которая строит список всех доступных физических лиц и проставляет флажки (свойство Пометка) у тех, кто уже сохранен в подчиненной ТЧ для данной строки:


&НаСервере
Функция ПолучитьСписокВыбораСОтметками(КлючСвязи)
    Список = Новый СписокЗначений;
    
    // 1. Получаем массив уже выбранных исполнителей для данной строки
    ПараметрыОтбора = Новый Структура("КлючСвязи", КлючСвязи);
    ВыбранныеСтроки = Объект.Исполнители.НайтиСтроки(ПараметрыОтбора);
    СоответствиеВыбранных = Новый Соответствие;
    Для Каждого СтрокаИзТЧ Из ВыбранныеСтроки Цикл
        СоответствиеВыбранных.Вставить(СтрокаИзТЧ.Иполнитель, Истина);
    КонецЦикла;
    
    // 2. Получаем весь список физлиц из справочника
    Выборка = Справочники.ФизическиеЛица.Выбрать();
    Пока Выборка.Следующий() Цикл
        // Исключаем группы физлиц
        Если Выборка.ЭтоГруппа Тогда
            Продолжить;
        КонецЕсли;
        
        // Добавляем элемент в список выбора
        Пометка = (СоответствиеВыбранных[Выборка.Ссылка] <> Неопределено);
        Список.Добавить(Выборка.Ссылка, Строка(Выборка.Ссылка), Пометка);
    КонецЦикла;
    
    Возврат Список;
КонецФункции

После закрытия диалогового окна выбора нам необходимо обработать результат: перезаписать данные во вспомогательной ТЧ Исполнители и собрать красивую текстовую строку для отображения пользователю:


&НаКлиенте
Процедура ПослеВыбораИсполнителей(РезультатВыбора, Параметры_КлючСвязи) Экспорт
    Если РезультатВыбора = Неопределено Тогда
        Возврат; // Пользователь нажал Отмена
    КонецЕсли;
    
    // Передаем список серверному методу для обновления табличной части
    СтрокаОтветственных = ОбновитьИсполнителейНаСервере(Параметры_КлючСвязи, РезультатВыбора);
    
    // Присваиваем текстовому полю на клиенте полученный результат
    ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
    Если ТекущиеДанные <> Неопределено Тогда
        ТекущиеДанные.ОтветственныеСтрокой = СтрокаОтветственных;
    КонецЕсли;
КонецПроцедуры

&НаСервере
Функция ОбновитьИсполнителейНаСервере(КлючСвязи, СписокВыбора)
    // Шаг 1. Очищаем старых исполнителей по текущему ключу связи
    ПараметрыОтбора = Новый Структура("КлючСвязи", КлючСвязи);
    СтрокиДляУдаления = Объект.Исполнители.НайтиСтроки(ПараметрыОтбора);
    Для Каждого СтрокаИзТЧ Из СтрокиДляУдаления Цикл
        Объект.Исполнители.Удалить(СтрокаИзТЧ);
    КонецЦикла;
    
    // Шаг 2. Записываем новые отмеченные элементы и формируем строку
    МассивИмен = Новый Массив;
    Для Каждого ЭлементСписка Из СписокВыбора Цикл
        Если ЭлементСписка.Пометка Тогда
            НоваяСтрока = Объект.Исполнители.Добавить();
            НоваяСтрока.КлючСвязи = КлючСвязи;
            НоваяСтрока.Иполнитель = ЭлементСписка.Значение;
            
            МассивИмен.Добавить(ЭлементСписка.Представление);
        КонецЕсли;
    КонецЦикла;
    
    // Соединяем имена через запятую для отображения в ячейке ТЧ
    Возврат СтрСоединить(МассивИмен, ", ");
КонецФункции

Шаг 4. Построение отчетов по исполнителям в СКД

При использовании классической связи по дополнительной табличной части СУБД с легкостью выполняет выборки данных. Посмотрим, как составить текст запроса для отчета, выводящего список задач и закрепленных за ними исполнителей:


ВЫБРАТЬ
    ДокЗадачи.Ссылка КАК ДокументСсылка,
    ДокЗадачи.Задача КАК ТекстЗадачи,
    ДокИсполнители.Иполнитель КАК Исполнитель
ИЗ
    Документ.ЗадачиПользователя.Задачи КАК ДокЗадачи
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗадачиПользователя.Иполнители КАК ДокИсполнители
        ПО ДокЗадачи.КлючСвязи = ДокИсполнители.КлючСвязи 
        И ДокЗадачи.Ссылка = ДокИсполнители.Ссылка

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

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

  1. В схеме компоновки данных перейдем на вкладку «Ресурсы».
  2. Перенесем поле Исполнитель в список ресурсов.
  3. В колонке «Выражение» напишем следующую агрегатную функцию СКД: СоединитьСтроки(Массив(Исполнитель), ", ").
  4. При формировании структуры отчета выполним группировку по полям ДокументСсылка и ТекстЗадачи, а ресурс Исполнитель добавим в выбранные поля.

Данный подход является отраслевым стандартом. СКД самостоятельно сгруппирует данные и свернет дублирующиеся строки на уровне вывода, при этом у пользователей сохранится штатная возможность делать точные отборы (например, «Исполнитель Равно Петров»), и система отработает запрос быстро.

Шаг 5. Альтернатива для высоконагруженных баз: Регистр сведений

Описанный выше подход с использованием подчиненной ТЧ идеален для стандартных сценариев. Однако, если база данных является высоконагруженной, а в одном документе могут содержаться тысячи строк ТЧ со множеством ответственных лиц, запись тяжелых объектов документов может замедлиться. В таком случае оптимальной альтернативой выступает вынос связей в независимый или подчиненный **Регистр сведений**.

Выделим ключевые особенности такой схемы:

  1. Создается регистр сведений (например, ОтветственныеПоЗадачам) с режимом записи «Подчинение регистратору».
  2. Измерения регистра: КлючСвязи (тип: Строка или УникальныйИдентификатор), Ответственный (тип: СправочникСсылка.ФизическиеЛица).
  3. Заполнение данных на форме происходит по той же схеме (с использованием виртуальной текстовой строки и диалога), но при проведении документа движения по регистру записываются автоматически через модуль объекта в обработчике ОбработкаПроведения.

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

← На главную