В практике разработки на платформе 1С:Предприятие 8 часто возникает задача организации связи «один-ко-многим» для строк табличной части документа. Типичный пример — когда строка табличной части представляет собой отдельную задачу, проект или позицию, для которой необходимо указать не одного, а сразу несколько ответственных исполнителей. При этом система должна не просто визуально отображать список выбранных сотрудников, но и обеспечивать возможность полноценного поиска, наложения отборов и построения детальных отчетов по каждому конкретному лицу.
В рамках этой статьи мы подробно разберем, почему попытки записать несколько значений в одну ячейку стандартными методами приводят к архитектурному тупику, и по шагам реализуем классическое, стабильное и производительное решение на основе подчиненной табличной части и уникального ключа связи.
Когда разработчик впервые сталкивается с подобной задачей, у него часто возникает соблазн пойти по пути наименьшего сопротивления. Рассмотрим три популярных, но в корне неверных подхода, и выясним причины, почему от них необходимо отказаться:
ХранилищеЗначений полностью закрывает эти данные для СУБД. Платформа физически сохраняет этот объект в виде бинарного файла (BLOB). Написать стандартный запрос к СУБД для поиска строк, где фигурирует «Петров», будет невозможно. Придется программно перебирать каждый документ базы данных и распаковывать хранилище, что приведет к катастрофическому падению производительности.Массив или СписокЗначений, поскольку СУБД требует нормализации данных и строго определенных типов полей.Единственным правильным, с точки зрения архитектуры баз данных, решением является нормализация структуры данных. Мы должны создать классическое отношение «один-ко-многим» с использованием подчиненных таблиц.
Для реализации задачи нам понадобятся две табличные части внутри одного документа. Пусть первая будет основной ТЧ (назовем ее Задачи), а вторая — подчиненной вспомогательной ТЧ (назовем ее Исполнители).
В обсуждениях часто предлагают связывать подчиненную таблицу по стандартному реквизиту НомерСтроки. Внимание: делать это категорически не рекомендуется! Если пользователь решит переместить строки в основной табличной части вверх или вниз, отсортировать таблицу или удалить строку из середины списка, номера строк автоматически пересчитаются. Это приведет к тому, что связи нарушатся, данные «съедут», и исполнители первой задачи окажутся привязанными ко второй или третьей.
Чтобы избежать этой проблемы, мы применим связь по уникальному идентификатору (ключу связи):
Задачи создадим реквизит КлючСвязи с типом Строка (длина 36 символов, для записи UUID). Данное поле будет скрыто от пользователя.ОтветственныеСтрокой (тип Строка, неограниченная длина или длина 150-200 символов).Исполнители.Исполнители создадим два реквизита: КлючСвязи (тип Строка, 36 символов) и Исполнитель (тип СправочникСсылка.ФизическиеЛица).Перейдем в модуль формы документа. Наша цель — автоматизировать генерацию уникальных ключей при создании новых строк, копировании существующих и обеспечить своевременную очистку подчиненных данных при удалении строки основной таблицы.
Рассмотрим процедуру добавления строки в основную ТЧ. Нам необходимо перехватить событие создания и присвоить уникальный идентификатор:
&НаКлиенте
Процедура ЗадачиПриОкончанииРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования)
Если НоваяСтрока И Не ОтменаРедактирования Тогда
ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
Если ТекущиеДанные <> Неопределено Тогда
// Генерируем уникальный ключ связи для новой строки
ТекущиеДанные.КлючСвязи = Строка(Новый УникальныйИдентификатор());
КонецЕсли;
КонецЕсли;
КонецПроцедуры
При копировании строки основной ТЧ стандартными средствами платформы скопируется и значение реквизита КлючСвязи. Если оставить все как есть, две разные строки основной таблицы будут ссылаться на одни и те же строки подчиненной таблицы. Нам необходимо принудительно перегенерировать ключ связи для новой скопированной строки:
&НаКлиенте
Процедура ЗадачиПриКопировании(Элемент, КопируемыйОбъект)
ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
Если ТекущиеДанные <> Неопределено Тогда
// Заменяем старый скопированный ключ на новый уникальный
ТекущиеДанные.КлючСвязи = Строка(Новый УникальныйИдентификатор());
// Очищаем текстовое представление, так как исполнителей у новой строки пока нет
ТекущиеДанные.ОтветственныеСтрокой = "";
КонецЕсли;
КонецПроцедуры
Если пользователь решает удалить задачу из списка, мы обязаны очистить все связанные записи из подчиненной таблицы исполнителей, чтобы база данных не засорялась «сиротскими» строками:
&НаКлиенте
Процедура ЗадачиПередУдалением(Элемент, Отказ)
ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
Если ТекущиеДанные <> Неопределено И ЗначениеЗаполнено(ТекущиеДанные.КлючСвязи) Тогда
// Передаем управление на сервер для удаления связанных строк из ТЧ документа
УдалитьСвязанныхИсполнителейНаСервере(ТекущиеДанные.КлючСвязи);
КонецЕсли;
КонецПроцедуры
&НаСервере
Процедура УдалитьСвязанныхИсполнителейНаСервере(КлючСвязи)
// Осуществляем поиск строк в подчиненной ТЧ по КлючуСвязи
ПараметрыОтбора = Новый Структура("КлючСвязи", КлючСвязи);
НайденныеСтроки = Объект.Исполнители.НайтиСтроки(ПараметрыОтбора);
Для Каждого СтрокаИсполнителя Из НайденныеСтроки Цикл
Объект.Исполнители.Удалить(СтрокаИсполнителя);
КонецЦикла;
КонецПроцедуры
Показывать пользователю две связанные таблицы на одной форме — неудачное интерфейсное решение, которое перегружает форму и запутывает работу. Намного удобнее оставить на форме только основную ТЧ Задачи, скрыв подчиненную Исполнители, а в колонке ОтветственныеСтрокой реализовать открытие формы подбора или диалога отметки элементов.
Для этого настроим для поля ввода ЗадачиОтветственныеСтрокой событие НачалоВыбора:
&НаКлиенте
Процедура ЗадачиОтветственныеСтрокойНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
СтандартнаяОбработка = Ложь;
ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
Если ТекущиеДанные = Неопределено Тогда
Возврат;
КонецЕсли;
// В случае отсутствия ключа связи (например, в старых документах) генерируем его на лету
Если Не ЗначениеЗаполнено(ТекущиеДанные.КлючСвязи) Тогда
ТекущиеДанные.КлючСвязи = Строка(Новый УникальныйИдентификатор());
КонецЕсли;
// Подготавливаем список выбора на сервере и заполняем его текущими отметками
СписокВыбора = ПолучитьСписокВыбораСОтметками(ТекущиеДанные.КлючСвязи);
// Открываем модальное окно для выбора элементов списка с галочками
ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВыбораИсполнителей", ЭтотОбъект, ТекущиеДанные.КлючСвязи);
СписокВыбора.ПоказатьОтметкуЭлементов(ОписаниеОповещения, "Выберите ответственных исполнителей");
КонецПроцедуры
Теперь проанализируем серверную процедуру, которая строит список всех доступных физических лиц и проставляет флажки (свойство Пометка) у тех, кто уже сохранен в подчиненной ТЧ для данной строки:
&НаСервере
Функция ПолучитьСписокВыбораСОтметками(КлючСвязи)
Список = Новый СписокЗначений;
// 1. Получаем массив уже выбранных исполнителей для данной строки
ПараметрыОтбора = Новый Структура("КлючСвязи", КлючСвязи);
ВыбранныеСтроки = Объект.Исполнители.НайтиСтроки(ПараметрыОтбора);
СоответствиеВыбранных = Новый Соответствие;
Для Каждого СтрокаИзТЧ Из ВыбранныеСтроки Цикл
СоответствиеВыбранных.Вставить(СтрокаИзТЧ.Иполнитель, Истина);
КонецЦикла;
// 2. Получаем весь список физлиц из справочника
Выборка = Справочники.ФизическиеЛица.Выбрать();
Пока Выборка.Следующий() Цикл
// Исключаем группы физлиц
Если Выборка.ЭтоГруппа Тогда
Продолжить;
КонецЕсли;
// Добавляем элемент в список выбора
Пометка = (СоответствиеВыбранных[Выборка.Ссылка] <> Неопределено);
Список.Добавить(Выборка.Ссылка, Строка(Выборка.Ссылка), Пометка);
КонецЦикла;
Возврат Список;
КонецФункции
После закрытия диалогового окна выбора нам необходимо обработать результат: перезаписать данные во вспомогательной ТЧ Исполнители и собрать красивую текстовую строку для отображения пользователю:
&НаКлиенте
Процедура ПослеВыбораИсполнителей(РезультатВыбора, Параметры_КлючСвязи) Экспорт
Если РезультатВыбора = Неопределено Тогда
Возврат; // Пользователь нажал Отмена
КонецЕсли;
// Передаем список серверному методу для обновления табличной части
СтрокаОтветственных = ОбновитьИсполнителейНаСервере(Параметры_КлючСвязи, РезультатВыбора);
// Присваиваем текстовому полю на клиенте полученный результат
ТекущиеДанные = Элементы.Задачи.ТекущиеДанные;
Если ТекущиеДанные <> Неопределено Тогда
ТекущиеДанные.ОтветственныеСтрокой = СтрокаОтветственных;
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция ОбновитьИсполнителейНаСервере(КлючСвязи, СписокВыбора)
// Шаг 1. Очищаем старых исполнителей по текущему ключу связи
ПараметрыОтбора = Новый Структура("КлючСвязи", КлючСвязи);
СтрокиДляУдаления = Объект.Исполнители.НайтиСтроки(ПараметрыОтбора);
Для Каждого СтрокаИзТЧ Из СтрокиДляУдаления Цикл
Объект.Исполнители.Удалить(СтрокаИзТЧ);
КонецЦикла;
// Шаг 2. Записываем новые отмеченные элементы и формируем строку
МассивИмен = Новый Массив;
Для Каждого ЭлементСписка Из СписокВыбора Цикл
Если ЭлементСписка.Пометка Тогда
НоваяСтрока = Объект.Исполнители.Добавить();
НоваяСтрока.КлючСвязи = КлючСвязи;
НоваяСтрока.Иполнитель = ЭлементСписка.Значение;
МассивИмен.Добавить(ЭлементСписка.Представление);
КонецЕсли;
КонецЦикла;
// Соединяем имена через запятую для отображения в ячейке ТЧ
Возврат СтрСоединить(МассивИмен, ", ");
КонецФункции
При использовании классической связи по дополнительной табличной части СУБД с легкостью выполняет выборки данных. Посмотрим, как составить текст запроса для отчета, выводящего список задач и закрепленных за ними исполнителей:
ВЫБРАТЬ
ДокЗадачи.Ссылка КАК ДокументСсылка,
ДокЗадачи.Задача КАК ТекстЗадачи,
ДокИсполнители.Иполнитель КАК Исполнитель
ИЗ
Документ.ЗадачиПользователя.Задачи КАК ДокЗадачи
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗадачиПользователя.Иполнители КАК ДокИсполнители
ПО ДокЗадачи.КлючСвязи = ДокИсполнители.КлючСвязи
И ДокЗадачи.Ссылка = ДокИсполнители.Ссылка
Обратите внимание, что левое соединение необходимо выполнять не только по КлючСвязи, но и по полю Ссылка (владельцу табличной части). Это гарантирует корректность выборки и задействует оптимальные индексы СУБД.
При выводе детальных записей в Системе компоновки данных (СКД) такой запрос неизбежно приведет к дублированию строк основной таблицы: если на одну задачу назначено 3 человека, в отчете отобразятся 3 одинаковые строки задачи, у каждой из которых будет своя фамилия исполнителя. Чтобы представить данные в красивом и нераздутом виде (одна строка на одну задачу, где исполнители перечислены в одной ячейке), задействуем мощный встроенный механизм СКД:
Исполнитель в список ресурсов.СоединитьСтроки(Массив(Исполнитель), ", ").ДокументСсылка и ТекстЗадачи, а ресурс Исполнитель добавим в выбранные поля.Данный подход является отраслевым стандартом. СКД самостоятельно сгруппирует данные и свернет дублирующиеся строки на уровне вывода, при этом у пользователей сохранится штатная возможность делать точные отборы (например, «Исполнитель Равно Петров»), и система отработает запрос быстро.
Описанный выше подход с использованием подчиненной ТЧ идеален для стандартных сценариев. Однако, если база данных является высоконагруженной, а в одном документе могут содержаться тысячи строк ТЧ со множеством ответственных лиц, запись тяжелых объектов документов может замедлиться. В таком случае оптимальной альтернативой выступает вынос связей в независимый или подчиненный **Регистр сведений**.
Выделим ключевые особенности такой схемы:
ОтветственныеПоЗадачам) с режимом записи «Подчинение регистратору».КлючСвязи (тип: Строка или УникальныйИдентификатор), Ответственный (тип: СправочникСсылка.ФизическиеЛица).ОбработкаПроведения.Этот подход гарантирует высокую скорость выполнения транзакций и снижает размер самого файла таблицы документов в базе данных SQL, обеспечивая отличную производительность решения в долгосрочной перспективе.