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