К статье
Практический материал

Разработка и Конфигурация · 11 мин

Как сохранить дерево значений в документе 1С

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

Дерево значений в 1С — объект встроенного языка, который существует только в оперативной памяти: после закрытия формы или завершения сеанса он теряется. Чтобы данные пережили перезапуск, их нужно перенести в объект конфигурации — чаще всего в документ. Существует три рабочих подхода: табличная часть с самоссылкой на родителя, табличная часть с ключами строк и хранилище значений с сериализованным деревом. Выбор зависит от того, нужно ли искать по строкам дерева запросами и редактировать отдельные ветки.

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

Почему дерево значений нельзя записать «как есть»

Дерево значений — это несериализуемый объект платформы, привязанный к сеансу. У документа нет типа реквизита «ДеревоЗначений», и попытка присвоить дерево реквизиту приведёт к ошибке несоответствия типов. Платформа хранит в базе только те данные, которые описаны в метаданных: реквизиты, табличные части, движения.

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

Способ 1: табличная часть с реквизитом «Родитель»

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

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

Процедура ЗаписатьДеревоВТЧ(Дерево, ТабЧасть)

ТабЧасть.Очистить();

ЗаписатьСтрокиРекурсивно(Дерево.Строки, ТабЧасть, "");

КонецПроцедуры

Процедура ЗаписатьСтрокиРекурсивно(Строки, ТабЧасть, КлючРодителя)

Для Каждого СтрокаДерева Из Строки Цикл

НоваяСтрока = ТабЧасть.Добавить();

НоваяСтрока.КлючСтроки = Строка(Новый УникальныйИдентификатор);

НоваяСтрока.КлючРодителя = КлючРодителя;

ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева);

ЗаписатьСтрокиРекурсивно(СтрокаДерева.Строки, ТабЧасть, НоваяСтрока.КлючСтроки);

КонецЦикла;

КонецПроцедуры

Чтение строится аналогично: сначала выбираются строки с пустым ключом родителя, затем для каждой ищутся потомки через НайтиСтроки с отбором по ключу родителя. Для ускорения на больших объёмах табличную часть выгружают в таблицу значений и строят индекс по ключу родителя.

Способ 2: табличная часть с уровнем и порядком

Более простой в реализации вариант — хранить в каждой строке числовой уровень вложенности и порядковый номер ветки. При записи дерево обходится сверху вниз, каждой строке присваивается уровень (0, 1, 2…) и сквозной номер. При чтении строки сортируются по номеру, а родитель определяется как ближайшая предыдущая строка с уровнем на единицу меньше.

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

Сравнение вариантов

Табличная часть с ключами строк

Подходит, когда по строкам нужны запросы, отчёты и отборы. Требует рекурсивного обхода и генерации ключей.

Табличная часть с уровнем и порядком

Проще в коде, но не выдерживает ручного редактирования табличной части. Только для программно управляемых деревьев.

Хранилище значений

Дерево недоступно для запросов и отчётов, зато сохраняется и восстанавливается одной строкой кода. Для служебных настроек.

Способ 3: хранилище значений с сериализацией

Если по строкам дерева не нужно строить отчёты и искать запросами, дерево можно сохранить целиком. Для этого в документ добавляется реквизит с типом «ХранилищеЗначения», а дерево преобразуется во внутреннее строковое представление функцией ЗначениеВСтрокуВнутр. Полученная строка помещается в хранилище через конструктор Новый ХранилищеЗначения.

// Запись

СтрокаВнутр = ЗначениеВСтрокуВнутр(ДеревоНастроек);

Объект.ДанныеДерева = Новый ХранилищеЗначения(СтрокаВнутр, Новый СжатиеДанных(9));

// Чтение

СтрокаВнутр = Объект.ДанныеДерева.Получить();

Если СтрокаВнутр <> Неопределено Тогда

ДеревоНастроек = ЗначениеИзСтрокиВнутр(СтрокаВнутр);

КонецЕсли;

Сжатие данных в конструкторе хранилища уменьшает объём при больших деревьях. У этого способа есть жёсткое ограничение: ЗначениеВСтрокуВнутр не работает в безопасном режиме и на веб-клиенте с ограничениями, а содержимое хранилища невозможно проанализировать запросом — для базы это непрозрачный бинарный блок.

Как восстановить дерево при открытии документа

Восстановление выполняется в обработчике ПриЧтенииНаСервере управляемой формы или ПриОткрытии после чтения объекта. Для табличной части с ключами сначала создаются строки верхнего уровня, затем рекурсивно подцепляются потомки по ключу родителя. Важно очищать дерево на форме перед заполнением, иначе при повторном чтении строки задвоятся.

Процедура ЗаполнитьДеревоИзТЧ(Дерево, ТабЧасть, КлючРодителя = "")

Отбор = Новый Структура("КлючРодителя", КлючРодителя);

Найденные = ТабЧасть.НайтиСтроки(Отбор);

Для Каждого СтрокаТЧ Из Найденные Цикл

СтрокаДерева = Дерево.Строки.Добавить();

ЗаполнитьЗначенияСвойств(СтрокаДерева, СтрокаТЧ);

ЗаполнитьДеревоИзТЧ(СтрокаДерева, ТабЧасть, СтрокаТЧ.КлючСтроки);

КонецЦикла;

КонецПроцедуры

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

Порядок действий
  1. 1
    Добавьте в документ табличную часть с реквизитами КлючСтроки и КлючРодителя либо реквизит типа ХранилищеЗначения
  2. 2
    Реализуйте запись дерева: рекурсивный обход с присвоением ключей или сериализацию через ЗначениеВСтрокуВнутр
  3. 3
    Реализуйте чтение: построение веток по ключу родителя или десериализацию из хранилища
  4. 4
    Подключите запись в ПередЗаписью или ПриЗаписи формы, чтение — в ПриЧтенииНаСервере
  5. 5
    Проверьте цикл: заполнить дерево, записать, закрыть, открыть документ заново

Проверка результата и контроль целостности

Минимальная проверка — записать документ с деревом из трёх уровней, закрыть форму, открыть заново и сравнить количество строк и вложенность. Дополнительно стоит проверить крайние случаи: пустое дерево, дерево из одной корневой строки, глубокую ветку и строки с одинаковыми значениями в разных ветках — именно на них всплывают ошибки с ключами.

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

Контрольные точки

Как проверить результат

Открытие записанного документа
Норма

Дерево восстанавливается с той же вложенностью и порядком строк

Тревожный признак

Строки выстроены плоско, ветки потеряны или задвоены

Ключи строк в табличной части
Норма

У каждой непустой строки-родителя есть строка с совпадающим КлючСтроки

Тревожный признак

Есть строки, ссылающиеся на несуществующий ключ родителя

Повторная запись без изменений
Норма

Количество строк табличной части не меняется

Тревожный признак

При каждой записи строки добавляются заново

Типичные ошибки и ограничения

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

Третья группа ошибок связана с сериализацией: попытка сохранить в дерево значений ссылочные объекты, которые потом удаляются из базы, или использование ЗначениеВСтрокуВнутр в безопасном режиме расширения. Также учитывайте, что табличная часть документа имеет ограничение на количество строк — для очень больших деревьев (десятки тысяч строк) лучше использовать регистр сведений или отдельный справочник с иерархией.

Типичные ошибки
01
Использовать НомерСтроки как связь с родителем — при удалении или сортировке строк иерархия разрушается
02
Не очищать табличную часть перед записью дерева — строки дублируются при каждой перезаписи документа
03
Сохранять дерево в хранилище значений, когда по строкам потом требуются отчёты — данные недоступны для запросов
04
Применять ЗначениеВСтрокуВнутр в расширении с безопасным режимом — метод вызывает исключение

FAQ

Можно ли сохранить дерево значений в реквизит документа без табличной части?

Напрямую — нет, типа «ДеревоЗначений» среди типов реквизитов нет. Единственный способ обойтись без табличной части — реквизит типа ХранилищеЗначения с сериализацией через ЗначениеВСтрокуВнутр. При этом строки дерева не будут доступны для запросов и отчётов.

Что лучше использовать как ключ строки: число или GUID?

Строковое представление GUID надёжнее: исключены коллизии при копировании документа и слиянии данных. Числовой ключ компактнее, но требует контроля уникальности внутри документа — например, хранения счётчика в реквизите.

Как сохранить дерево, если документ редактируют несколько пользователей?

Механизм хранения тот же, но запись выполняйте в транзакции с объектной блокировкой документа. При конфликте платформа выдаст ошибку блокировки, и пользователю нужно будет перечитать объект. Дерево на форме при этом придётся перезаполнить из базы.

Перенесётся ли дерево при копировании документа?

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

Подходит ли хранилище значений для обмена деревом между базами?

Нет, внутренний формат ЗначениеВСтрокуВнутр не предназначен для обмена. Для передачи дерева между базами сериализуйте его в JSON через ЗаписьJSON с рекурсивным обходом или используйте XDTO-пакеты.

Как сохранить дерево на форме, если документ ещё не записан?

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

Итог

Что важно запомнить

Дерево значений существует только в памяти, поэтому его переносят в документ одним из трёх способов: табличная часть с ключами строк и родителя — когда нужны запросы и отчёты; табличная часть с уровнем и порядком — для простых программно управляемых деревьев; хранилище значений с сериализацией — для служебных структур, которые читаются целиком. Не используйте номера строк как связи и всегда проверяйте полный цикл «записать — открыть — сравнить».