Разработка интерфейсов на управляемых формах в платформе 1С:Предприятие 8.3 часто требует вывода сложных иерархических структур. Для визуализации таких данных идеально подходит элемент формы, связанный с реквизитом типа ДеревоЗначений. Однако разработчики сталкиваются с серьезной архитектурной особенностью: ДеревоЗначений и ТаблицаЗначений являются чисто интерфейсными («мутабельными») коллекциями. Они существуют только в оперативной памяти и не могут быть напрямую записаны в базу данных как физическая таблица.
В этой статье мы подробно разберем, почему у многих разработчиков «не взлетает» классическое сохранение дерева значений, проанализируем критические ошибки клиент-серверного взаимодействия и рассмотрим четыре практических метода сохранения и воссоздания иерархической структуры с готовыми примерами кода.
Сначала разберем типичную ошибку, которую совершил автор темы на форуме. При попытке перелить данные из дерева значений управляемой формы в табличную часть документа в обработчиках ПередЗаписьюНаСервере или ПриЗаписиНаСервере, после закрытия и повторного открытия документа табличная часть оказывалась пустой.
Причина кроется в непонимании жизненного цикла управляемой формы и объекта базы данных:
Когда пользователь нажимает кнопку «Записать», платформа запускает следующий процесс:
Объект автоматически конвертируются в прикладной объект (например, ДокументОбъект.ИмяДокумента) с помощью встроенного механизма ДанныеФормыВЗначение.ТекущийОбъект.ПередЗаписьюНаСервере вы изменяете коллекцию Объект.ТабличнаяЧасть, вы модифицируете данные буфера формы, которые уже прошли конвертацию. Платформа же физически записывает в СУБД именно ТекущийОбъект. Изменения, внесенные в Объект напрямую, просто игнорируются и не попадают в базу данных.Правило разработки: Любые манипуляции с данными документа перед транзакцией записи на сервере необходимо проводить строго с параметром ТекущийОбъект!
Это самый надежный, прозрачный и рекомендуемый метод. Он сохраняет ссылочную целостность базы данных (все аналитические разрезы остаются ссылками, а не строками) и позволяет строить по сохраненным данным любые отчеты с помощью механизма компоновки данных (СКД).
Для реализации этого метода добавим в нашу табличную часть (например, ФинБюджет) две служебные колонки:
ГУИД (тип: Строка, длина 36 или тип УникальныйИдентификатор) — уникальный маркер самой строки.РодительГУИД (тип: Строка или УникальныйИдентификатор) — маркер родительской строки. Для строк верхнего уровня это поле будет пустым.Вместо написания сложных рекурсивных функций для обхода веток дерева, мы применим эффективный встроенный метод платформы НайтиСтроки(). Если передать в него пустую структуру поиска и установить флаг поиска по подчиненным в значение Истина, платформа мгновенно вернет нам линейный массив абсолютно всех строк дерева любого уровня вложенности.
Рассмотрим код выгрузки дерева в табличную часть документа при записи:
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// Получаем объект ДеревоЗначений из реквизита управляемой формы
ДеревоОбъект = РеквизитФормыВЗначение("ДанныеФинБюджета");
// Очищаем табличную часть записываемого объекта
ТекущийОбъект.ФинБюджет.Очистить();
// Получаем плоский массив всех строк дерева (все уровни вложенности)
ВсеСтрокиДерева = ДеревоОбъект.Строки.НайтиСтроки(Новый Структура, Истина);
Для Каждого СтрДерева Из ВсеСтрокиДерева Цикл
НоваяСтрокаТЧ = ТекущийОбъект.ФинБюджет.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаТЧ, СтрДерева);
// Генерируем уникальный идентификатор для строки, если его еще нет
Если Не ЗначениеЗаполнено(СтрДерева.ГУИД) Тогда
СтрДерева.ГУИД = Строка(Новый УникальныйИдентификатор);
КонецЕсли;
НоваяСтрокаТЧ.ГУИД = СтрДерева.ГУИД;
// Определяем родителя для восстановления иерархии
Если СтрДерева.Родитель <> Неопределено Тогда
// Если родитель есть, записываем его ГУИД
НоваяСтрокаТЧ.РодительГУИД = СтрДерева.Родитель.ГУИД;
Иначе
// Для корневых элементов пишем пустую строку или пустой ГУИД
НоваяСтрокаТЧ.РодительГУИД = "";
КонецЕсли;
КонецЦикла;
// Возвращаем обновленное дерево (с заполненными ГУИД) обратно на форму
ЗначениеВРеквизитФормы(ДеревоОбъект, "ДанныеФинБюджета");
КонецПроцедуры
Для обратного преобразования плоской таблицы в иерархическое дерево обычно используют рекурсию, совершая многократные поиски по таблице. Но при большом объеме данных это может замедлить открытие формы. Мы применим оптимизированный алгоритм сборки дерева за один проход цикла с использованием объекта Соответствие (Map):
&НаСервере
Процедура ВосстановитьДеревоИзТабличнойЧасти()
ДеревоОбъект = РеквизитФормыВЗначение("ДанныеФинБюджета");
ДеревоОбъект.Строки.Очистить();
// Соответствие будет хранить связи: Ключ - ГУИД строки, Значение - СтрокаДереваЗначений
КартаСтрок = Новый Соответствие;
// Важно: Строки в ТЧ должны быть упорядочены так, чтобы родители шли раньше детей.
// Если порядок не гарантирован, предварительно отсортируйте ТЧ по уровню вложенности.
Для Каждого СтрокаТЧ Из Объект.ФинБюджет Цикл
Если ПустаяСтрока(СтрокаТЧ.РодительГУИД) Тогда
// Элемент верхнего уровня — добавляем напрямую в корень дерева
НоваяСтрокаДЗ = ДеревоОбъект.Строки.Добавить();
Иначе
// Элемент подчиненного уровня — ищем строку-родителя в нашей карте по ГУИД
РодительскаяСтрокаДЗ = КартаСтрок.Получить(СтрокаТЧ.РодительГУИД);
Если РодительскаяСтрокаДЗ <> Неопределено Тогда
НоваяСтрокаДЗ = РодительскаяСтрокаДЗ.Строки.Добавить();
Иначе
// Страховочный вариант: если родитель не найден, добавляем в корень
НоваяСтрокаДЗ = ДеревоОбъект.Строки.Добавить();
КонецЕсли;
КонецЕсли;
// Заполняем реквизиты строки дерева
ЗаполнитьЗначенияСвойств(НоваяСтрокаДЗ, СтрокаТЧ);
// Регистрируем созданную строку в соответствии, чтобы её дочерние элементы смогли её найти
КартаСтрок.Вставить(СтрокаТЧ.ГУИД, НоваяСтрокаДЗ);
КонецЦикла;
// Обновляем данные на форме
ЗначениеВРеквизитФормы(ДеревоОбъект, "ДанныеФинБюджета");
КонецПроцедуры
Проанализируем ситуацию, когда дерево строится на основе иерархического справочника (например, Статьи бюджетов), а числовые показатели берутся из плоской табличной части документа. Этот подход был предложен в сообщении 24 темы форума.
В данном случае при записи мы «схлопываем» дерево и записываем в табличную часть только конечные («листовые») элементы, у которых колонка ЭтоГруппа равна Ложь. Группировочные строки в базу данных не пишутся — они будут автоматически рассчитаны при открытии формы с помощью иерархического запроса.
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
ДеревоБДРФорма = ДанныеФормыВЗначение(ДеревоБДР, Тип("ДеревоЗначений"));
ТекущийОбъект.БюджетПоМесяцам.Очистить();
ГодДокумента = Год(Объект.Дата);
// Запускаем рекурсивный обход дерева с верхнего уровня
СохранитьДеревоРекурсивно(ДеревоБДРФорма.Строки, ТекущийОбъект.БюджетПоМесяцам, ГодДокумента);
КонецПроцедуры
&НаСервере
Процедура СохранитьДеревоРекурсивно(СтрокиДерева, ТабличнаяЧастьОбъекта, ГодДокумента)
Для Каждого Стр Из СтрокиДерева Цикл
Если Стр.ЭтоГруппа Тогда
// Если строка является группой, рекурсивно заходим внутрь этой ветки
СохранитьДеревоРекурсивно(Стр.Строки, ТабличнаяЧастьОбъекта, ГодДокумента);
Иначе
// Для конечных строк переносим помесячные данные в плоскую ТЧ
Для НомерМесяца = 1 По 12 Цикл
СуммаМесяца = Стр["План" + НомерМесяца];
Если СуммаМесяца = 0 Тогда
Продолжить; // Пропускаем пустые показатели для экономии места в базе
КонецЕсли;
НоваяЗапись = ТабличнаяЧастьОбъекта.Добавить();
НоваяЗапись.Период = Дата(ГодДокумента, НомерМесяца, 1);
НоваяЗапись.Статья = Стр.СтатьяДР;
НоваяЗапись.СуммаПлан = СуммаМесяца;
КонецЦикла;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Для восстановления дерева при создании формы мы используем возможности языка запросов 1С, а именно предложение ИТОГИ... ПО... ИЕРАРХИЯ. Платформа самостоятельно выстроит древовидную структуру результатов, которую мы обойдем рекурсивно.
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ДеревоБДРФорма = ДанныеФормыВЗначение(ДеревоБДР, Тип("ДеревоЗначений"));
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Статьи.Ссылка КАК СтатьяДР,
| Статьи.ЭтоГруппа КАК ЭтоГруппа,
| ЕСТЬNULL(ДанныеДокумента.СуммаПлан, 0) КАК Сумма
|ИЗ
| Справочник.СтатьиБюджетов КАК Статьи
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.Бюджет.БюджетПоМесяцам КАК ДанныеДокумента
| ПО ДанныеДокумента.Статья = Статьи.Ссылка И ДанныеДокумента.Ссылка = &Ссылка
|ИТОГИ
| СУММА(Сумма)
|ПО
| СтатьяДР ИЕРАРХИЯ";
Запрос.УстановитьПараметр("Ссылка", Объект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();
// Выбираем данные с обходом по группировкам и иерархии
Выборка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
ДеревоБДРФорма.Строки.Oчистить();
// Заполняем дерево рекурсивно на основании выборки запроса
ЗаполнитьДеревоРекурсивноИзВыборки(Выборка, ДеревоБДРФорма);
ЗначениеВДанныеФормы(ДеревоБДРФорма, ДеревоБДР);
КонецПроцедуры
&НаСервере
Процедура ЗаполнитьДеревоРекурсивноИзВыборки(Выборка, ТекущаяСтрокаДерева)
Пока Выборка.Следующий() Цикл
// Исключаем дублирование системных группировок для иерархических справочников
Если Выборка.ТипЗаписи() = ТипЗаписиЗапроса.ИтогПоГруппировке И Выборка.ЭтоГруппа Тогда
Продолжить;
КонецЕсли;
НоваяСтрока = ТекущаяСтрокаДерева.Строки.Добавить();
НоваяСтрока.СтатьяДР = Выборка.СтатьяДР;
НоваяСтрока.Сумма = Выборка.Сумма;
НоваяСтрока.ЭтоГруппа = Выборка.ЭтоГруппа;
// Получаем подчиненную выборку по текущему узлу иерархии
ПодчиненнаяВыборка = Выборка.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией, "СтатьяДР");
ЗаполнитьДеревоРекурсивноИзВыборки(ПодчиненнаяВыборка, НоваяСтрока);
КонецЦикла;
КонецПроцедуры
Иногда структура колонок дерева на форме динамически меняется, либо данные дерева не предназначены для построения аналитических отчетов и нужны только для отображения на форме конкретного документа. В таких случаях создавать сложные ТЧ нецелесообразно.
Современным стандартом разработки в среде 1С является сериализация дерева значений в строку формата JSON. Мы преобразуем дерево в массив структур, сериализуем его в строку и сохраняем эту строку в реквизит документа с типом СтрокаНеограниченнойДлины.
Рассмотрим код для выгрузки дерева значений в JSON-строку при сохранении документа:
&НаСервере
Функция ПолучитьМассивСтруктурИзДерева(СтрокиДерева)
МассивРезультат = Новый Массив;
Для Каждого СтрДерева Из СтрокиДерева Цикл
СтруктураСтроки = Новый Структура;
СтруктураСтроки.Вставить("Статья", СтрДерева.Статья);
СтруктураСтроки.Вставить("Сумма", СтрДерева.Сумма);
// Рекурсивно обрабатываем подчиненные строки (ветви дерева)
Если СтрДерева.Строки.Количество() > 0 Тогда
СтруктураСтроки.Вставить("Ветки", ПолучитьМассивСтруктурИзДерева(СтрДерева.Строки));
Иначе
СтруктураСтроки.Вставить("Ветки", Новый Массив);
КонецЕсли;
МассивРезультат.Добавить(СтруктураСтроки);
КонецЦикла;
Возврат МассивРезультат;
КонецФункции
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
ДеревоОбъект = РеквизитФормыВЗначение("ДанныеФинБюджета");
// Конвертируем иерархию дерева в массив структур
МассивДанных = ПолучитьМассивСтруктурИзДерева(ДеревоОбъект.Строки);
// Сериализуем полученный массив в JSON
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, МассивДанных);
СтрокаJSON = ЗаписьJSON.Закрыть();
// Записываем строку в реквизит документа
ТекущийОбъект.ДанныеБюджетаСтрокойJSON = СтрокаJSON;
КонецПроцедуры
А теперь посмотрим, как восстановить дерево значений обратно из строки JSON при открытии документа:
&НаСервере
Процедура ВосстановитьДеревоИзJSON(МассивДанных, СтрокиДерева)
Для Каждого ЭлементДанных Из МассивДанных Цикл
НоваяСтрокаДЗ = СтрокиДерева.Добавить();
НоваяСтрокаДЗ.Статья = ЭлементДанных.Статья;
НоваяСтрокаДЗ.Сумма = ЭлементДанных.Сумма;
// Если у ветки есть подчиненные элементы, восстанавливаем их рекурсивно
Если ЭлементДанных.Свойство("Ветки") И ЭлементДанных.Ветки.Количество() > 0 Тогда
ВосстановитьДеревоИзJSON(ЭлементДанных.Ветки, НоваяСтрокаДЗ.Строки);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
Если ЗначениеЗаполнено(Объект.ДанныеБюджетаСтрокойJSON) Тогда
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(Объект.ДанныеБюджетаСтрокойJSON);
МассивДанных = ПрочитатьJSON(ЧтениеJSON);
ЧтениеJSON.Закрыть();
ДеревоОбъект = РеквизитФормыВЗначение("ДанныеФинБюджета");
ДеревоОбъект.Строки.Очистить();
ВосстановитьДеревоИзJSON(МассивДанных, ДеревоОбъект.Строки);
ЗначениеВРеквизитФормы(ДеревоОбъект, "ДанныеФинБюджета");
КонецЕсли;
КонецПроцедуры
Этот метод был предложен в самом начале обсуждения на форуме (сообщение 4). Он заключается в упаковке всего объекта ДеревоЗначений в бинарный формат с помощью объекта ХранилищеЗначения. Данные записываются в реквизит документа с типом ХранилищеЗначения.
Важное предупреждение: Использование бинарного ХранилищаЗначения имеет серьезные минусы. СУБД не видит эти данные, по ним невозможно построить отчеты, и они абсолютно нечитаемы извне. Кроме того, при изменении типов колонок дерева в будущих версиях вашей конфигурации сохраненные старые данные могут вызывать ошибки десериализации (ошибки чтения формата).
Тем не менее, для быстрого сохранения несложных промежуточных настроек этот метод незаменим своей лаконичностью:
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// Переводим данные дерева формы в полноценный объект Дерева значений
ДеревоОбъект = РеквизитФормыВЗначение("ДанныеФинБюджета");
// Упаковываем объект дерева в бинарный буфер Хранилища Значения с максимальным сжатием
ТекущийОбъект.ХранилищеДерева = Новый ХранилищеЗначения(ДеревоОбъект, Новый СжатиеДанных(9));
КонецПроцедуры
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Если документ открывается повторно (не новый)
Если Не Объект.Ссылка.Пустая() Тогда
// Получаем объект Документа из ссылки для доступа к реквизиту ХранилищеЗначения
ДокументОбъект = Объект.Ссылка.ПолучитьОбъект();
Если ДокументОбъект.ХранилищеДерева <> Неопределено Тогда
// Распаковываем дерево из бинарного хранилища
ДеревоОбъект = ДокументОбъект.ХранилищеДерева.Получить();
Если ТипЗнч(ДеревоОбъект) = Тип("ДеревоЗначений") Тогда
// Помещаем извлеченные данные обратно на управляемую форму
ЗначениеВРеквизитФормы(ДеревоОбъект, "ДанныеФинБюджета");
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Подведем краткий итог по выбору архитектуры для сохранения иерархических структур в 1С:
Соответствие.