Как сохранить и восстановить дерево значений на управляемой форме в 1С 8.3?

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

Разработка интерфейсов на управляемых формах в платформе 1С:Предприятие 8.3 часто требует вывода сложных иерархических структур. Для визуализации таких данных идеально подходит элемент формы, связанный с реквизитом типа ДеревоЗначений. Однако разработчики сталкиваются с серьезной архитектурной особенностью: ДеревоЗначений и ТаблицаЗначений являются чисто интерфейсными («мутабельными») коллекциями. Они существуют только в оперативной памяти и не могут быть напрямую записаны в базу данных как физическая таблица.

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

Выясняем причину: почему данные «исчезают» при записи?

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

Причина кроется в непонимании жизненного цикла управляемой формы и объекта базы данных:

Когда пользователь нажимает кнопку «Записать», платформа запускает следующий процесс:

  1. Данные реквизита формы Объект автоматически конвертируются в прикладной объект (например, ДокументОбъект.ИмяДокумента) с помощью встроенного механизма ДанныеФормыВЗначение.
  2. Этот полученный прикладной объект передается в обработчик события записи на сервере как параметр ТекущийОбъект.
  3. Если внутри обработчика ПередЗаписьюНаСервере вы изменяете коллекцию Объект.ТабличнаяЧасть, вы модифицируете данные буфера формы, которые уже прошли конвертацию. Платформа же физически записывает в СУБД именно ТекущийОбъект. Изменения, внесенные в Объект напрямую, просто игнорируются и не попадают в базу данных.

Правило разработки: Любые манипуляции с данными документа перед транзакцией записи на сервере необходимо проводить строго с параметром ТекущийОбъект!


Метод 1. Сохранение в плоскую Табличную часть с помощью уникальных идентификаторов (GUID)

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

Для реализации этого метода добавим в нашу табличную часть (например, ФинБюджет) две служебные колонки:

Лайфхак: Выгрузка дерева без рекурсии

Вместо написания сложных рекурсивных функций для обхода веток дерева, мы применим эффективный встроенный метод платформы НайтиСтроки(). Если передать в него пустую структуру поиска и установить флаг поиска по подчиненным в значение Истина, платформа мгновенно вернет нам линейный массив абсолютно всех строк дерева любого уровня вложенности.

Рассмотрим код выгрузки дерева в табличную часть документа при записи:


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

Восстановление дерева из Табличной части в один проход без рекурсии

Для обратного преобразования плоской таблицы в иерархическое дерево обычно используют рекурсию, совершая многократные поиски по таблице. Но при большом объеме данных это может замедлить открытие формы. Мы применим оптимизированный алгоритм сборки дерева за один проход цикла с использованием объекта Соответствие (Map):


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

Метод 2. Рекурсивное заполнение дерева на основе иерархического запроса

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

В данном случае при записи мы «схлопываем» дерево и записываем в табличную часть только конечные («листовые») элементы, у которых колонка ЭтоГруппа равна Ложь. Группировочные строки в базу данных не пишутся — они будут автоматически рассчитаны при открытии формы с помощью иерархического запроса.

Рекурсивное сохранение конечных веток дерева:


&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
    
    ДеревоБДРФорма = ДанныеФормыВЗначение(ДеревоБДР, Тип("ДеревоЗначений"));
    ТекущийОбъект.БюджетПоМесяцам.Очистить();
    
    ГодДокумента = Год(Объект.Дата);
    
    // Запускаем рекурсивный обход дерева с верхнего уровня
    СохранитьДеревоРекурсивно(ДеревоБДРФорма.Строки, ТекущийОбъект.БюджетПоМесяцам, ГодДокумента);
    
КонецПроцедуры

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

Восстановление дерева при открытии через запрос с обходом по иерархии:

Для восстановления дерева при создании формы мы используем возможности языка запросов 1С, а именно предложение ИТОГИ... ПО... ИЕРАРХИЯ. Платформа самостоятельно выстроит древовидную структуру результатов, которую мы обойдем рекурсивно.


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

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

Метод 3. Сериализация Дерева значений в формат JSON (современный гибкий подход)

Иногда структура колонок дерева на форме динамически меняется, либо данные дерева не предназначены для построения аналитических отчетов и нужны только для отображения на форме конкретного документа. В таких случаях создавать сложные ТЧ нецелесообразно.

Современным стандартом разработки в среде 1С является сериализация дерева значений в строку формата JSON. Мы преобразуем дерево в массив структур, сериализуем его в строку и сохраняем эту строку в реквизит документа с типом СтрокаНеограниченнойДлины.

Преимущества 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. Быстрое сохранение через Хранилище значения (ХранилищеЗначения)

Этот метод был предложен в самом начале обсуждения на форуме (сообщение 4). Он заключается в упаковке всего объекта ДеревоЗначений в бинарный формат с помощью объекта ХранилищеЗначения. Данные записываются в реквизит документа с типом ХранилищеЗначения.

Важное предупреждение: Использование бинарного ХранилищаЗначения имеет серьезные минусы. СУБД не видит эти данные, по ним невозможно построить отчеты, и они абсолютно нечитаемы извне. Кроме того, при изменении типов колонок дерева в будущих версиях вашей конфигурации сохраненные старые данные могут вызывать ошибки десериализации (ошибки чтения формата).

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


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

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

Резюме: Какой подход выбрать для вашей задачи?

Подведем краткий итог по выбору архитектуры для сохранения иерархических структур в 1С:

  1. Табличная часть документа с GUID (Метод 1) — выбирайте этот вариант в 90% случаев. Он обеспечивает ссылочную целостность базы данных, легкую интеграцию с СКД и превосходную скорость работы за счет однопроходного алгоритма заполнения через Соответствие.
  2. Рекурсивный запрос с иерархией (Метод 2) — оптимален, когда структура дерева дублирует иерархию уже существующего справочника (например, Статьи оборотов или Подразделения), а в табличной части хранятся только числовые транзакции.
  3. Сериализация в JSON (Метод 3) — идеальный выбор для хранения динамических настроек, форм-опросников или интерфейсных деревьев, состав колонок которых может непредсказуемо меняться в будущем.
  4. Хранилище значения (Метод 4) — используйте с осторожностью и только для второстепенных, не связанных с бизнес-аналитикой данных, когда критически важна скорость разработки.
← На главную