ТаблицаЗначений внутри ХранилищеЗначения: запись, чтение, файлы и большие объёмы
Содержание
Чтобы записать таблицу значений в хранилище 1С, создайте объект на сервере: Новый ХранилищеЗначения(ТЗ, Новый СжатиеДанных(9)). Присвойте его реквизиту типа ХранилищеЗначения и запишите объект. Обратно таблица достаётся одним вызовом: Объект.Данные.Получить().
Частое заблуждение: хранилище можно заполнить прямо в клиентском обработчике управляемой формы. Не получится. ТаблицаЗначений на тонком клиенте не существует, а реквизит типа ХранилищеЗначения в данные формы не попадает. Вся работа с таблицей и хранилищем идёт в серверных процедурах. Ниже код для платформы 8.3, он одинаков для любой конфигурации: БП 3.0, ЗУП 3.1, УТ 11 или своей разработки.
Что такое хранилище значений и когда в него кладут таблицу
ХранилищеЗначения — это контейнер. Он сериализует любое значение, которое поддерживает внутреннюю сериализацию 1С, и хранит его как двоичные данные. В базе такой реквизит лежит одним полем, без разбивки на строки и колонки.
Таблицу значений в хранилище кладут, когда:
- 📦 нужно сохранить «снимок» результата запроса или отчёта на дату;
- ⚙️ у обработки или регламентного задания есть настройки табличного вида, а заводить под них табличную часть не хочется;
- 🔄 таблицу надо передать между сеансами, например из фонового задания в форму;
- 🧾 структура колонок меняется от случая к случаю, и жёсткие реквизиты не подходят.
Чем хранилище отличается от похожих механизмов:
| Механизм | Где живёт | Срок жизни | Когда брать |
|---|---|---|---|
Реквизит ХранилищеЗначения | В таблице базы | Пока не перезапишете | Снимки, настройки, произвольные данные объекта |
Временное хранилище (ПоместитьВоВременноеХранилище) | В памяти сервера | Сеанс или время жизни формы | Передача таблицы между клиентом, сервером и фоновым заданием |
| Хранилище общих настроек | Служебная таблица настроек | Постоянно, по пользователю | Пользовательские настройки по ключу |
| Табличная часть или регистр сведений | Отдельные строки в базе | Постоянно | Когда по данным нужны отборы, запросы и отчёты |
Главное ограничение: содержимое хранилища не видно запросу. Нельзя отобрать строки, посчитать сумму или соединить с другой таблицей. Если такие операции нужны — храните строки в регистре, а не в хранилище.
Как записать таблицу значений в хранилище: базовый код
Сценарий: в справочнике СохраненныеДанные есть реквизит Данные типа ХранилищеЗначения. Нужно положить туда таблицу из запроса.
Шаг 1. Добавьте реквизит в конфигураторе
- Откройте объект метаданных (справочник, документ или регистр сведений ).
- На вкладке «Данные» добавьте реквизит, например
Данные. - Выберите тип
ХранилищеЗначения. Длина и точность у этого типа не задаются. - Обновите конфигурацию базы данных (
F7).
У регистра сведений хранилище может быть ресурсом или реквизитом, но не измерением.
Шаг 2. Заполните и запишите на сервере
&НаСервере
Процедура СохранитьТаблицуНаСервере()
Запрос = Новый Запрос(
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Номенклатура,
| Номенклатура.Артикул КАК Артикул
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа");
ТЗ = Запрос.Выполнить().Выгрузить();
Ссылка = Справочники.СохраненныеДанные.НайтиПоНаименованию("Список номенклатуры", Истина);
Если Ссылка.Пустая() Тогда
ОбъектДанных = Справочники.СохраненныеДанные.СоздатьЭлемент();
ОбъектДанных.Наименование = "Список номенклатуры";
Иначе
ОбъектДанных = Ссылка.ПолучитьОбъект();
КонецЕсли;
ОбъектДанных.Данные = Новый ХранилищеЗначения(ТЗ, Новый СжатиеДанных(9));
ОбъектДанных.Записать();
КонецПроцедуры
Второй параметр конструктора — степень сжатия от 0 до 9. Без него данные не сжимаются. Для таблиц с повторяющимися строками и ссылками сжатие заметно уменьшает размер поля, а цена — немного процессорного времени при записи и чтении.
Присвоение реквизиту ничего не сохраняет в базу. Без Записать() таблица пропадёт вместе с объектом в памяти.
Чтение из хранилища: как вернуть таблицу значений
Метод Получить() разворачивает хранилище обратно в исходное значение. Типы колонок, их имена и порядок строк сохраняются.
&НаСервере
Функция ПрочитатьТаблицу(Ссылка)
Значение = Ссылка.Данные.Получить();
Если ТипЗнч(Значение) <> Тип("ТаблицаЗначений") Тогда
Возврат Неопределено;
КонецЕсли;
Возврат Значение;
КонецФункции
Проверка типа обязательна. У нового объекта реквизит пустой, и Получить() вернёт Неопределено. Дальнейшее обращение к Значение.Колонки упадёт с ошибкой.
Чтение через запрос
Поле типа ХранилищеЗначения можно выбрать в запросе. В выборке оно придёт как объект хранилища:
Запрос = Новый Запрос(
"ВЫБРАТЬ
| СохраненныеДанные.Наименование КАК Наименование,
| СохраненныеДанные.Данные КАК Данные
|ИЗ
| Справочник.СохраненныеДанные КАК СохраненныеДанные");
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
ТЗ = Выборка.Данные.Получить();
Если ТипЗнч(ТЗ) = Тип("ТаблицаЗначений") Тогда
Сообщить(СтрШаблон("%1: строк %2", Выборка.Наименование, Формат(ТЗ.Количество(), "ЧГ=0")));
КонецЕсли;
КонецЦикла;
В условиях ГДЕ, в группировках и соединениях поле хранилища использовать нельзя.
Хранилище в форме объекта
Это тот самый приём, без которого таблица «не сохраняется» в управляемой форме. Создайте реквизит формы типа ТаблицаЗначений (например, ТаблицаДанных) и опишите в нём колонки. Связь с реквизитом объекта настраивается в двух серверных событиях формы:
&НаСервере
Процедура ПриЧтенииНаСервере(ТекущийОбъект)
Таблица = ТекущийОбъект.Данные.Получить();
Если ТипЗнч(Таблица) = Тип("ТаблицаЗначений") Тогда
ЗначениеВРеквизитФормы(Таблица, "ТаблицаДанных");
КонецЕсли;
КонецПроцедуры
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
Таблица = РеквизитФормыВЗначение("ТаблицаДанных");
ТекущийОбъект.Данные = Новый ХранилищеЗначения(Таблица, Новый СжатиеДанных(9));
КонецПроцедуры
В ПередЗаписьюНаСервере объект ещё не записан, поэтому Записать() вызывать не нужно — платформа сделает это сама.
Большие таблицы на 10 000 строк и больше
Хранилище справляется и с сотнями тысяч строк, но каждое чтение разворачивает таблицу целиком в памяти рабочего процесса сервера. Частичного чтения нет. Отсюда правила.
- Убирайте лишние колонки до записи.
ТЗ.Скопировать(, "Номенклатура,Количество")вернёт копию только с нужными колонками. - Включайте сжатие
Новый СжатиеДанных(9). На текстовых и ссылочных данных выигрыш обычно кратный. - Вынесите хранилище в отдельный объект.
ПолучитьОбъект()читает все реквизиты, включая тяжёлое поле. Если хранилище лежит в карточке часто открываемого справочника, каждое открытие тянет его из базы. Отдельный регистр сведений с ключом-ссылкой решает проблему. - Не читайте хранилище в цикле по сотням объектов, если нужна одна цифра. Запишите её рядом обычным реквизитом (например,
КоличествоСтрок) и выбирайте запросом. - Делите очень большие снимки. Таблицу на миллионы строк лучше разложить по периодам или сразу хранить строками в регистре.
Как прикинуть размер до записи, на сервере:
Строка = ЗначениеВСтрокуВнутр(ТЗ);
Сообщить("Символов во внутреннем представлении: " + Формат(СтрДлина(Строка), "ЧГ=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
| Снимки.Данные КАК Данные
|ИЗ
| РегистрСведений.СнимкиОтчетов КАК Снимки
|ГДЕ
| Снимки.ИмяОтчета = &ИмяОтчета
|УПОРЯДОЧИТЬ ПО
| Снимки.ДатаСнимка УБЫВ");
Запрос.УстановитьПараметр("ИмяОтчета", ИмяОтчета);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Значение = Выборка.Данные.Получить();
Если ТипЗнч(Значение) = Тип("ТаблицаЗначений") Тогда
Возврат Значение;
КонецЕсли;
КонецЕсли;
Возврат Неопределено;
КонецФункции
Что даёт такая схема:
- 📊 список снимков и число строк видны в обычном списке регистра, без чтения хранилищ;
- 🔍 отбор по отчёту и дате работает запросом по измерениям;
- 🧹 старые снимки чистятся запросом по
ДатаСнимка, без перебора объектов.
Чтобы показать снимок в форме, создайте реквизит формы типа ТаблицаЗначений с колонками Номенклатура, Склад, Количество и на сервере вызовите ЗначениеВРеквизитФормы(ПоследнийСнимок("ОстаткиТоваров"), "ТаблицаСнимка"). Перед этим проверьте результат на Неопределено.