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