ТаблицаЗначений внутри ХранилищеЗначения: запись, чтение, файлы и большие объёмы

· 9 мин

Содержание

Чтобы записать таблицу значений в хранилище 1С, создайте объект на сервере: Новый ХранилищеЗначения(ТЗ, Новый СжатиеДанных(9)). Присвойте его реквизиту типа ХранилищеЗначения и запишите объект. Обратно таблица достаётся одним вызовом: Объект.Данные.Получить().

Частое заблуждение: хранилище можно заполнить прямо в клиентском обработчике управляемой формы. Не получится. ТаблицаЗначений на тонком клиенте не существует, а реквизит типа ХранилищеЗначения в данные формы не попадает. Вся работа с таблицей и хранилищем идёт в серверных процедурах. Ниже код для платформы 8.3, он одинаков для любой конфигурации: БП 3.0, ЗУП 3.1, УТ 11 или своей разработки.

Что такое хранилище значений и когда в него кладут таблицу

ХранилищеЗначения — это контейнер. Он сериализует любое значение, которое поддерживает внутреннюю сериализацию 1С, и хранит его как двоичные данные. В базе такой реквизит лежит одним полем, без разбивки на строки и колонки.

Таблицу значений в хранилище кладут, когда:

  • 📦 нужно сохранить «снимок» результата запроса или отчёта на дату;
  • ⚙️ у обработки или регламентного задания есть настройки табличного вида, а заводить под них табличную часть не хочется;
  • 🔄 таблицу надо передать между сеансами, например из фонового задания в форму;
  • 🧾 структура колонок меняется от случая к случаю, и жёсткие реквизиты не подходят.

Чем хранилище отличается от похожих механизмов:

МеханизмГде живётСрок жизниКогда брать
Реквизит ХранилищеЗначенияВ таблице базыПока не перезапишетеСнимки, настройки, произвольные данные объекта
Временное хранилище (ПоместитьВоВременноеХранилище)В памяти сервераСеанс или время жизни формыПередача таблицы между клиентом, сервером и фоновым заданием
Хранилище общих настроекСлужебная таблица настроекПостоянно, по пользователюПользовательские настройки по ключу
Табличная часть или регистр сведенийОтдельные строки в базеПостоянноКогда по данным нужны отборы, запросы и отчёты

Главное ограничение: содержимое хранилища не видно запросу. Нельзя отобрать строки, посчитать сумму или соединить с другой таблицей. Если такие операции нужны — храните строки в регистре, а не в хранилище.

Как записать таблицу значений в хранилище: базовый код

Сценарий: в справочнике СохраненныеДанные есть реквизит Данные типа ХранилищеЗначения. Нужно положить туда таблицу из запроса.

Шаг 1. Добавьте реквизит в конфигураторе

  1. Откройте объект метаданных (справочник, документ или регистр сведений ).
  2. На вкладке «Данные» добавьте реквизит, например Данные.
  3. Выберите тип ХранилищеЗначения. Длина и точность у этого типа не задаются.
  4. Обновите конфигурацию базы данных (F7).

У регистра сведений хранилище может быть ресурсом или реквизитом, но не измерением.

Шаг 2. Заполните и запишите на сервере

&НаСервере
Процедура СохранитьТаблицуНаСервере()

    Запрос = Новый Запрос(
    "ВЫБРАТЬ
    |    Номенклатура.Ссылка КАК Номенклатура,
    |    Номенклатура.Артикул КАК Артикул
    |ИЗ
    |    Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |    НЕ Номенклатура.ЭтоГруппа");
    ТЗ = Запрос.Выполнить().Выгрузить();

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

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

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

Второй параметр конструктора — степень сжатия от 0 до 9. Без него данные не сжимаются. Для таблиц с повторяющимися строками и ссылками сжатие заметно уменьшает размер поля, а цена — немного процессорного времени при записи и чтении.

Присвоение реквизиту ничего не сохраняет в базу. Без Записать() таблица пропадёт вместе с объектом в памяти.

Чтение из хранилища: как вернуть таблицу значений

Метод Получить() разворачивает хранилище обратно в исходное значение. Типы колонок, их имена и порядок строк сохраняются.

&НаСервере
Функция ПрочитатьТаблицу(Ссылка)

    Значение = Ссылка.Данные.Получить();

    Если ТипЗнч(Значение) <> Тип("ТаблицаЗначений") Тогда
        Возврат Неопределено;
    КонецЕсли;

    Возврат Значение;

КонецФункции

Проверка типа обязательна. У нового объекта реквизит пустой, и Получить() вернёт Неопределено. Дальнейшее обращение к Значение.Колонки упадёт с ошибкой.

Чтение через запрос

Поле типа ХранилищеЗначения можно выбрать в запросе. В выборке оно придёт как объект хранилища:

Запрос = Новый Запрос(
"ВЫБРАТЬ
|    СохраненныеДанные.Наименование КАК Наименование,
|    СохраненныеДанные.Данные КАК Данные
|ИЗ
|    Справочник.СохраненныеДанные КАК СохраненныеДанные");
Выборка = Запрос.Выполнить().Выбрать();

Пока Выборка.Следующий() Цикл
    ТЗ = Выборка.Данные.Получить();
    Если ТипЗнч(ТЗ) = Тип("ТаблицаЗначений") Тогда
        Сообщить(СтрШаблон("%1: строк %2", Выборка.Наименование, Формат(ТЗ.Количество(), "ЧГ=0")));
    КонецЕсли;
КонецЦикла;

В условиях ГДЕ, в группировках и соединениях поле хранилища использовать нельзя.

Хранилище в форме объекта

Это тот самый приём, без которого таблица «не сохраняется» в управляемой форме. Создайте реквизит формы типа ТаблицаЗначений (например, ТаблицаДанных) и опишите в нём колонки. Связь с реквизитом объекта настраивается в двух серверных событиях формы:

&НаСервере
Процедура ПриЧтенииНаСервере(ТекущийОбъект)

    Таблица = ТекущийОбъект.Данные.Получить();
    Если ТипЗнч(Таблица) = Тип("ТаблицаЗначений") Тогда
        ЗначениеВРеквизитФормы(Таблица, "ТаблицаДанных");
    КонецЕсли;

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

&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)

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

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

В ПередЗаписьюНаСервере объект ещё не записан, поэтому Записать() вызывать не нужно — платформа сделает это сама.

Большие таблицы на 10 000 строк и больше

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

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

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

Строка = ЗначениеВСтрокуВнутр(ТЗ);
Сообщить("Символов во внутреннем представлении: " + Формат(СтрДлина(Строка), "ЧГ=0"));

Цифра без учёта сжатия, но для сравнения вариантов (до и после удаления колонок) её хватает.

Где держать вашу таблицу?

Хранилище и файлы: выгрузка таблицы на диск и загрузка обратно

Сам объект ХранилищеЗначения в файл напрямую не пишется. Пишут его содержимое — таблицу. Есть два пути.

Внутренний формат 1С

&НаСервере
Процедура ВыгрузитьВФайл(ТЗ, ПутьКФайлу)

    Если НЕ ЗначениеВФайл(ПутьКФайлу, ТЗ) Тогда
        ВызватьИсключение "Не удалось записать файл " + ПутьКФайлу;
    КонецЕсли;

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

&НаСервере
Функция ЗагрузитьИзФайла(ПутьКФайлу)

    Возврат ЗначениеИзФайла(ПутьКФайлу);

КонецФункции

Формат компактный, но читает его только 1С. Ссылки при загрузке в другую базу превратятся в «битые», если там нет объектов с такими же уникальными идентификаторами.

XML через СериализаторXDTO

// Запись
Запись = Новый ЗаписьXML;
Запись.ОткрытьФайл(ПутьКФайлу);
СериализаторXDTO.ЗаписатьXML(Запись, ТЗ, НазначениеТипаXML.Явное);
Запись.Закрыть();

// Чтение
Чтение = Новый ЧтениеXML;
Чтение.ОткрытьФайл(ПутьКФайлу);
ТЗ = СериализаторXDTO.ПрочитатьXML(Чтение);
Чтение.Закрыть();

XML можно открыть в редакторе и проверить глазами. Файл больше, чем во внутреннем формате, и запись медленнее.

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

Альтернативы: XDTO, строка и УниверсальныйОбменДанными

Хранилище — не единственный способ сохранить таблицу. Сравнение:

СпособКогдаРиски
ХранилищеЗначения в реквизитеХранение внутри той же базыНет запросов по содержимому
ЗначениеВСтрокуВнутр в реквизите типа «Строка неограниченной длины»Нужна строка, например для обмена по HTTPНет сжатия, поле больше
XML через СериализаторXDTOПередача в другую базу или внешнюю систему, ручная проверкаОбъём, ссылки сопоставляются по идентификатору
Обработка «Универсальный обмен данными в формате XML»Перенос объектов вместе с их реквизитами-хранилищами между базамиИзбыточна для одной таблицы, нужны правила обмена

Универсальный обмен переносит объекты целиком. Если в справочнике есть реквизит-хранилище, он уедет вместе с элементом. Писать правила ради одной таблицы смысла нет: проще выгрузить её через СериализаторXDTO и прочитать в базе-приёмнике тем же кодом.

Частые ошибки при работе с хранилищем значений

Диагностика: выберите симптом

Ещё одна ловушка — изменение таблицы «на месте». Получить() возвращает копию. Если добавить строки в полученную таблицу и ничего не записать, хранилище в базе не изменится. Каждый раз создавайте новое ХранилищеЗначения и записывайте объект.

Пример: снимок отчёта по остаткам в регистре сведений

Задача: раз в день сохранять таблицу остатков, чтобы потом сравнить «как было» с «как стало».

Создайте регистр сведений СнимкиОтчетов, непериодический:

  • 🔑 измерение ИмяОтчета — Строка(100);
  • 🔑 измерение ДатаСнимка — Дата (состав «Дата и время»);
  • 📦 ресурс Данные — ХранилищеЗначения;
  • 🔢 ресурс КоличествоСтрок — Число(10, 0).

Код записи, например для регламентного задания:

Процедура СохранитьСнимокОстатков(ТаблицаОстатков) Экспорт

    ТЗ = ТаблицаОстатков.Скопировать(, "Номенклатура,Склад,Количество");

    МенеджерЗаписи = РегистрыСведений.СнимкиОтчетов.СоздатьМенеджерЗаписи();
    МенеджерЗаписи.ИмяОтчета = "ОстаткиТоваров";
    МенеджерЗаписи.ДатаСнимка = ТекущаяДатаСеанса();
    МенеджерЗаписи.Данные = Новый ХранилищеЗначения(ТЗ, Новый СжатиеДанных(9));
    МенеджерЗаписи.КоличествоСтрок = ТЗ.Количество();
    МенеджерЗаписи.Записать();

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

Чтение последнего снимка:

Функция ПоследнийСнимок(ИмяОтчета) Экспорт

    Запрос = Новый Запрос(
    "ВЫБРАТЬ ПЕРВЫЕ 1
    |    Снимки.Данные КАК Данные
    |ИЗ
    |    РегистрСведений.СнимкиОтчетов КАК Снимки
    |ГДЕ
    |    Снимки.ИмяОтчета = &ИмяОтчета
    |УПОРЯДОЧИТЬ ПО
    |    Снимки.ДатаСнимка УБЫВ");
    Запрос.УстановитьПараметр("ИмяОтчета", ИмяОтчета);

    Выборка = Запрос.Выполнить().Выбрать();
    Если Выборка.Следующий() Тогда
        Значение = Выборка.Данные.Получить();
        Если ТипЗнч(Значение) = Тип("ТаблицаЗначений") Тогда
            Возврат Значение;
        КонецЕсли;
    КонецЕсли;

    Возврат Неопределено;

КонецФункции

Что даёт такая схема:

  • 📊 список снимков и число строк видны в обычном списке регистра, без чтения хранилищ;
  • 🔍 отбор по отчёту и дате работает запросом по измерениям;
  • 🧹 старые снимки чистятся запросом по ДатаСнимка, без перебора объектов.

Чтобы показать снимок в форме, создайте реквизит формы типа ТаблицаЗначений с колонками Номенклатура, Склад, Количество и на сервере вызовите ЗначениеВРеквизитФормы(ПоследнийСнимок("ОстаткиТоваров"), "ТаблицаСнимка"). Перед этим проверьте результат на Неопределено.

Частые вопросы

Можно ли положить в хранилище дерево значений или структуру с таблицей внутри?
Да. ДеревоЗначений, Структура, Соответствие и Массив сериализуются, в том числе вложенные друг в друга. Удобно хранить Структуру вида «настройки + таблица» в одном реквизите.
Сохраняются ли типы колонок после чтения?
Да. Получить() возвращает таблицу с теми же именами колонок, описаниями типов и порядком строк, что были при записи.
Видно ли содержимое хранилища в консоли запросов?
Нет. Консоль покажет только факт наличия значения. Чтобы посмотреть таблицу, вызовите Получить() в коде или в отладчике.
Что будет со ссылками, если объект из таблицы удалили?
В хранилище останется ссылка с прежним идентификатором. После чтения она будет битой: представление вида «Объект не найден». Проверяйте такие ссылки перед использованием.
Можно ли сравнить два хранилища на равенство?
Напрямую нет. Получите обе таблицы и сравните их содержимое, либо сравните строки ЗначениеВСтрокуВнутр от исходных таблиц.