Как хранить и редактировать вложенные таблицы значений в ХранилищеЗначения на форме 1С:Предприятия?

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

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

Представим ситуацию: у нас есть «форма-1» элемента справочника, на которой расположена «таблица-1». Одна из колонок этой «таблицы-1» имеет тип «ХранилищеЗначения», а внутри каждого такого хранилища лежит своя `ТаблицаЗначений` — назовем ее «ТаблицаИзХранилища». Таким образом, каждая строка «таблицы-1» содержит собственную вложенную `ТаблицуЗначений`, упакованную в `ХранилищеЗначения`.

Наша цель — при выборе определенной строки в «таблице-1» и нажатии кнопки открыть «форму-2» с таблицей («ТаблицаФормы-2»), заполнить ее данными из соответствующего хранилища. Затем, после редактирования данных на «форме-2», при нажатии кнопки «Записать» мы должны передать измененные данные обратно в хранилище строки на «форме-1». И, наконец, при записи самого элемента справочника, необходимо проверить все «ТаблицыИзХранилища» из каждой строки «таблицы-1» и, если все проверки пройдены, записать элемент.

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

Обсудим недостатки использования ХранилищаЗначения для структурированных данных

Использование `ХранилищаЗначения` для хранения структурированных данных, таких как `ТаблицаЗначений`, в каждой строке другой таблицы, не является оптимальным решением и влечет за собой ряд существенных недостатков, о которых нам следует знать:

  1. Производительность: Данные в `ХранилищеЗначения` хранятся в сериализованном виде, обычно в бинарном формате. Каждый раз при чтении или записи их приходится десериализовывать и сериализовывать. Это ресурсоемкая операция, особенно при большом объеме данных или частом обращении к ним. Мы можем столкнуться с заметными задержками при открытии форм, сохранении данных и выполнении проверок.
  2. Доступ к данным: Содержимое `ХранилищаЗначения` невозможно получить напрямую с помощью запросов. Это значительно усложняет формирование отчетов, поиск и фильтрацию данных по полям вложенной `ТаблицыЗначений`. Для доступа к данным нам всегда придется сначала выгрузить все хранилище, десериализовать его и только затем обрабатывать программно. Такой подход ограничивает гибкость и скорость работы с данными.
  3. Размер базы данных: Неэффективное использование `ХранилищаЗначения` может привести к быстрому росту размера базы данных. Каждый раз при изменении даже небольшой части вложенной таблицы, перезаписывается все хранилище целиком. Это создает избыточные копии данных и увеличивает объем хранимой информации.
  4. Сопровождение: Отсутствие прямой структуры данных делает код, работающий с такими хранилищами, более сложным для понимания, отладки и последующего сопровождения. Разработчикам будет труднее разобраться в логике работы, что увеличивает время на разработку и поддержку.

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

Стандартным и наиболее гибким подходом в 1С для хранения и редактирования структуры «таблица в таблице» является использование нескольких табличных частей, связанных по ключу. Если основная таблица находится в объекте справочника (как в нашем случае), то для каждой строки этой основной табличной части можно создать записи во второй табличной части того же справочника. Эти две структуры связываются с помощью уникального идентификатора строки основной таблицы. Такой подход позволяет эффективно работать с данными запросами, индексировать поля и значительно упрощает разработку форм и отчетов.

Как связать табличные части?

  1. Создаем в справочнике две табличные части. Допустим, ТабличнаяЧасть1 (соответствует вашей «таблице-1») и ТабличнаяЧасть2 (которая будет хранить детализацию для каждой строки из ТабличнаяЧасть1).
  2. В ТабличнуюЧасть1 добавляем реквизит, который будет служить уникальным идентификатором для каждой строки, например, УникальныйИдентификаторСтроки с типом Строка или Число. Этот реквизит можно заполнять при добавлении новой строки, используя, например, функцию НовыйУникальныйИдентификатор() или просто порядковый номер.
  3. В ТабличнуюЧасть2 также добавляем реквизит, например, ИдентификаторРодительскойСтроки, с тем же типом, что и УникальныйИдентификаторСтроки в ТабличнойЧасти1. Этот реквизит будет связующим звеном.
  4. При создании строк в ТабличнойЧасти2 мы будем заполнять ИдентификаторРодительскойСтроки значением УникальныйИдентификаторСтроки из родительской строки ТабличнойЧасти1.

На форме работаем с этими двумя табличными частями. При выборе строки в ТабличнойЧасти1, мы отфильтровываем или загружаем в подчиненную таблицу на форме (соответствующую ТабличнойЧасти2) только те строки, у которых ИдентификаторРодительскойСтроки совпадает с УникальныйИдентификаторСтроки текущей строки ТабличнойЧасти1.

Преимущества этого подхода:

Альтернативные архитектурные решения

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

Решение при сохранении текущей архитектуры (с ХранилищемЗначения)

Однако, как часто бывает в реальной практике, изменение существующей архитектуры может быть невозможным из-за уже написанного прикладного решения или других ограничений. В таком случае, нам необходимо найти способ реализовать требуемую функциональность, используя имеющуюся структуру с `ХранилищемЗначения`. Рассмотрим пошагово, как это можно сделать, учитывая ваш опыт и предложенные варианты.

Шаг 1: Получение ТаблицыЗначений из ХранилищаЗначения и передача на форму 2

При выборе конкретной строки в «таблица-1» на «форме-1» и нажатии кнопки для открытия «формы-2», нам необходимо получить `ТаблицуЗначений` из `ХранилищаЗначения` и передать ее в новую форму.

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

    Обратите внимание: `ИдентификаторСтроки` должен быть реквизитом нашей «таблицы-1» на форме. Если его нет, можем создать временный, либо использовать `УникальныйИдентификатор` самой строки, если она уже существует в базе.

  3. На «форме-2» при создании на сервере:
    
    &НаСервере
    Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
        Если Параметры.Свойство("ДанныеДляРедактирования") Тогда
            ТаблицаФормы2 = Параметры.ДанныеДляРедактирования;
            // ИдентификаторРодительскойСтроки сохраняем, чтобы потом вернуть обратно
            // Например, в реквизит формы Формы2
            ИдентификаторРодительскойСтроки = Параметры.ИдентификаторРодительскойСтроки; 
        КонецЕсли;
    КонецПроцедуры
    

    Таким образом, мы заполняем «ТаблицуФормы-2» данными из хранилища.

Шаг 2: Редактирование данных на форме 2

Пользователь работает с «ТаблицейФормы-2» как с обычной таблицей значений на управляемой форме. Добавляет, удаляет, редактирует строки и ячейки.

Шаг 3: Возврат измененных данных на исходную форму 1

После завершения редактирования на «форме-2» и нажатия кнопки «Записать» или «ОК», нам нужно вернуть измененные данные обратно на «форму-1».

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

    Здесь мы используем дополнительный реквизит формы «МодифицированныеТаблицыЗначений» типа `Соответствие`, где ключом является `ИдентификаторРодительскойСтроки`, а значением – измененная `ТаблицаЗначений`. Это позволит нам хранить измененные данные для нескольких строк до момента записи всего объекта.

Шаг 4: Запись данных обратно в ХранилищеЗначения строки

Это самый критичный момент. Проблема, с которой мы сталкиваемся, заключается в том, что `ХранилищеЗначения` в новой строке объекта (то есть в строке, добавленной после открытия «формы-1», но еще не записанной в базу) еще не существует в постоянном хранилище. Оно будет создано только после записи объекта.

Решение 1: Немедленная запись справочника при добавлении каждой строки

Один из предложенных вариантов — при добавлении каждой новой строки в «таблицу-1» сразу же перезаписывать элемент справочника. Это гарантирует, что для новой строки будет создан физический объект в базе данных, и ее `ХранилищеЗначения` станет доступным для записи.

Решение 2 (предпочтительнее в рамках текущей архитектуры): Временное хранение ТаблицыЗначений на Форме-1 для новых строк

Более гибкий подход, который избегает частых обращений к базе данных, заключается во временном хранении `ТаблицыЗначений` для новых (или модифицированных) строк непосредственно на «форме-1» до момента записи всего объекта. Для этого мы можем использовать дополнительный реквизит формы.

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

    Таким образом, мы собираем все измененные и новые `ТаблицыЗначений` непосредственно перед записью объекта, минимизируя обращения к базе данных во время интерактивной работы пользователя.

Шаг 5: Проверка данных при записи элемента справочника

При записи элемента справочника нам необходимо циклично проверить все «ТаблицыИзХранилища» из каждой строки «таблицы-1». Это также лучше выполнять на сервере в обработчике `ПередЗаписьюНаСервере`.


&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ПараметрыЗаписи)
    // ... (код для обновления ХранилищаЗначения из Шага 4)

    // Проверка всех ТаблицИзХранилища
    Для Каждого СтрокаТ1ИзОбъекта Из Объект.Таблица1 Цикл
        Если СтрокаТ1ИзОбъекта.ХранилищеКолонки <> Неопределено Тогда
            Попытка
                ТекущаяТаблицаИзХранилища = СтрокаТ1ИзОбъекта.ХранилищеКолонки.Получить();
            Исключение
                Сообщить("Ошибка при получении ТаблицыЗначений для проверки из строки: " + ОписаниеОшибки());
                Отказ = Истина;
                Возврат;
            КонецПопытки;

            Если ТекущаяТаблицаИзХранилища <> Неопределено Тогда
                // Вложенный цикл для проверки каждой строки в ТаблицеИзХранилища
                Для Каждого ВложеннаяСтрокаТЗ Из ТекущаяТаблицаИзХранилища Цикл
                    // Выполняем необходимые проверки
                    Если ВложеннаяСтрокаТЗ.ПолеДляПроверки = "" Тогда // Пример условия
                        Сообщить("Поле 'ПолеДляПроверки' в одной из вложенных таблиц не заполнено!");
                        Отказ = Истина;
                        Возврат; // Прерываем проверку и запись
                    КонецЕсли;
                    // ... другие проверки
                КонецЦикла;
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;

    Если Отказ Тогда
        Возврат;
    КонецЕсли;
    
    // Если все проверки прошли, можно продолжить запись
КонецПроцедуры

Если хоть одна строка не проходит по условию, мы устанавливаем флаг `Отказ = Истина` и прерываем циклы проверки, а также саму запись элемента справочника.

Заключительные рекомендации

Мы рассмотрели два основных подхода к решению вашей задачи: оптимальный архитектурный вариант с двумя связанными табличными частями и решение, адаптированное под текущую архитектуру с `ХранилищемЗначения`. Несмотря на то, что второй вариант позволяет решить поставленную задачу в рамках существующих ограничений, мы настоятельно рекомендуем, если появится такая возможность, пересмотреть и оптимизировать структуру данных с использованием связанных табличных частей или регистров сведений. Такой подход обеспечит лучшую производительность, упростит разработку и поддержку в долгосрочной перспективе, а также сделает вашу систему более масштабируемой и удобной для анализа данных.

← На главную