Множественные значения в реквизите 1С: от массива до таблицы значений

· 9 мин

Содержание

Сам реквизит хранит ровно одно значение своего типа — «список» или «массив» в типе реквизита выбрать нельзя. Чтобы держать в нём несколько значений, есть три пути: работать с коллекцией в памяти (Массив, СписокЗначений, ТаблицаЗначений), упаковать коллекцию в реквизит типа ХранилищеЗначения или вынести данные в табличную часть либо регистр.

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

Что выбрать под набор значений?

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

Когда вы создаёте реквизит справочника, документа или регистра, платформа просит указать его тип: число, строка, ссылка, дата. Все эти типы хранят одно значение. Типов «массив» или «список» в этом окне нет — и это не ограничение конкретной конфигурации, а устройство модели данных 1С.

Коллекции (Массив, СписокЗначений, ТаблицаЗначений) — это объекты, которые живут в оперативной памяти во время выполнения кода. Напрямую положить такой объект в поле таблицы базы нельзя. Поэтому задача распадается на две:

  • 📦 собрать и обработать значения — в памяти, подходящей коллекцией;
  • 💾 сохранить набор — через ХранилищеЗначения или через отдельную таблицу (табличную часть, регистр).

Дальше идём от простого к сложному.

Массив — простой перечень однотипных значений

Массив — самая лёгкая коллекция. Создаёте, добавляете элементы, обращаетесь по индексу:

СписокТоваров = Новый Массив;
СписокТоваров.Добавить(Товар1);
СписокТоваров.Добавить(Товар2);

Количество = СписокТоваров.Количество();   // 2
Первый = СписокТоваров[0];
Последний = СписокТоваров[СписокТоваров.ВГраница()];

Полезные методы: Добавить, Вставить(Индекс, Значение), Удалить(Индекс), Количество(), ВГраница() (индекс последнего элемента), Найти(Значение) — вернёт индекс или Неопределено.

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

Когда в массиве нужно часто искать значение, Найти работает перебором — это медленно на больших объёмах. Для поиска по ключу берите Соответствие: Соответствие.Получить(Ключ) находит значение сразу, без перебора.

Рядом с массивом стоит упомянуть ещё две коллекции, которые закрывают соседние задачи:

  • Структура — набор именованных полей фиксированного состава: Новый Структура("Код, Наименование", 1, "Товар"), доступ Стр.Код. Удобна, когда «несколько значений» — это один объект с понятными свойствами, а не однородный список.
  • Соответствие — пары ключ-значение с мгновенным поиском по ключу: Соотв.Вставить(Ключ, Значение), Соотв.Получить(Ключ). Идеальна для справочника соответствий «вход → результат».

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

СписокЗначений — когда нужны названия и пометки

СписокЗначений отличается от массива тем, что у каждого элемента есть не только значение, но и представление (текст для пользователя), пометка (галочка) и картинка. Это коллекция «с человеческим лицом» — её удобно показывать в диалоге.

Варианты = Новый СписокЗначений;
Варианты.Добавить(Контрагент1, "Поставщик", Истина);
Варианты.Добавить(Контрагент2, "Покупатель", Ложь);

Для Каждого Элемент Из Варианты Цикл
    Сообщить(Элемент.Представление + ": " + Строка(Элемент.Пометка));
КонецЦикла;

У элемента доступны Элемент.Значение, Элемент.Представление, Элемент.Пометка. У самого списка — НайтиПоЗначению, СортироватьПоПредставлению(), ВыгрузитьЗначения() (вернёт массив только значений) и обратный ЗагрузитьЗначения(Массив).

Отдельный плюс — готовый интерактивный выбор. На толстом клиенте Варианты.ВыбратьЭлемент(Заголовок) сам покажет окно; в управляемом приложении список передают в ПоказатьВыборИзСписка. Берите СписокЗначений, когда пользователь должен отметить несколько пунктов или выбрать один из набора.

ТаблицаЗначений — несколько полей в каждой строке

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

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

НоваяСтрока = ТЗ.Добавить();
НоваяСтрока.Номенклатура = Товар;
НоваяСтрока.Количество = 5;

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

Сильные стороны таблицы: отбор строк НайтиСтроки(Новый Структура("Номенклатура", Товар)), итоги Итог, группировка Свернуть, сортировка Сортировать("Количество Убыв"). Для частого поиска добавьте индекс: ТЗ.Индексы.Добавить("Номенклатура") — тогда НайтиСтроки отработает заметно быстрее.

Чаще всего таблицу не строят строка за строкой, а получают готовой из запроса: Результат.Выгрузить() возвращает ТаблицаЗначений с колонками по полям выборки. Копию с отбором делают через Скопировать(Отбор, Колонки) — удобно, когда из большой таблицы нужен фрагмент по условию. Это разом снимает вопрос «как собрать несколько значений» — платформа собирает их за вас.

ТаблицаЗначений доступна на сервере и в толстом клиенте, но не доступна напрямую на тонком и веб-клиенте и не передаётся параметром между клиентом и сервером. На форме с таблицей работают через реквизит формы (данные формы), а между клиентом и сервером гоняют не саму таблицу, а её адрес во временном хранилище либо массив структур.

Как сохранить коллекцию в базе

Всё перечисленное живёт в памяти. Чтобы набор пережил закрытие сеанса, его нужно записать. Два рабочих варианта.

Реквизит типа ХранилищеЗначения. В него можно упаковать любое сериализуемое значение — массив, список, таблицу — и получить обратно:

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

// распаковали при чтении
ТЗ = Объект.ДопДанные.Получить();

Второй параметр Новый СжатиеДанных(9) сжимает данные (уровень 0–9). Минус способа принципиальный: содержимое ХранилищеЗначения нельзя прочитать запросом — для СУБД это непрозрачный двоичный блок. Годится для служебных данных, настроек, снимков, которые читаются только кодом целиком.

Табличная часть или регистр. Когда по значениям нужны запросы, отборы, отчёты и соединения — не коллекция в реквизите, а нормальная таблица: табличная часть объекта или регистр сведений . Это чуть больше работы на старте, зато данные остаются «видимыми» для языка запросов.

Сериализация для внешних систем. Если набор значений нужно не хранить, а передать наружу — в веб-сервис, файл обмена, стороннюю программу, — коллекцию сериализуют в текст. Массив, Структура и Соответствие штатно пишутся в JSON через ЗаписатьJSON, читаются обратно через ПрочитатьJSON. Для XML-обмена применяют СериализаторXDTO. ТаблицаЗначений напрямую в JSON не укладывается — её сначала перегоняют в массив структур (строка → структура с полями-колонками), и уже массив сериализуют. Такой текст кладут в строковый реквизит или отдают во внешнюю систему без всякого хранилища.

Короткая развилка:

НужноЧем хранить
Набор читается только кодом, целикомХранилищеЗначения
По значениям будут запросы и отчётыТабличная часть / регистр
Строки с несколькими полями и связямиТабличная часть / регистр сведений
Временный набор на время расчётаКоллекция в памяти, в базу не пишем

Производительность: где незаметно теряется скорость

Коллекции прощают мелкие объёмы и жестоко наказывают за большие. Несколько мест, где чаще всего проседает скорость.

  • 🔍 Поиск перебором. Массив.Найти и ТаблицаЗначений.НайтиСтроки без индекса — это проход по всем элементам. На сотнях тысяч строк в цикле получаете квадрат. Решение: Соответствие для поиска по ключу, Индексы.Добавить для таблицы.
  • ➕ Поэлементное добавление в большом цикле. Само по себе Добавить дёшево, дорого — то, что внутри: обращение к базе на каждой итерации. Собирайте данные одним запросом и выгружайте результат в таблицу через Выгрузить().
  • 🗜️ Крупные ХранилищеЗначения. Большой упакованный объём читается и распаковывается целиком при каждом обращении к реквизиту. Если набор растёт — переводите в регистр.
  • 🔁 Изменение коллекции внутри Для Каждого. Удаление элементов прямо в цикле обхода ломает перебор (см. ниже).

Перед оптимизацией замерьте. Включите замер производительности в конфигураторе или выведите ТекущаяУниверсальнаяДатаВМиллисекундах() до и после участка — часто «тормозит коллекция» оказывается запросом в цикле рядом с ней.

Что чаще всего ломается

Ошибки по этой теме повторяются из проекта в проект.

Попытка задать реквизиту тип «массив». Разработчик ищет такой тип в конструкторе и не находит, потому что его нет. Либо коллекция в памяти, либо ХранилищеЗначения, либо отдельная таблица.

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

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

Отфильтрованный = Новый Массив;
Для Каждого Элемент Из Исходный Цикл
    Если НЕ НужноУдалить(Элемент) Тогда
        Отфильтрованный.Добавить(Элемент);
    КонецЦикла;
КонецЦикла;
Исходный = Отфильтрованный;

Если удалять нужно именно на месте, идите с конца индексом в цикле Пока:

Инд = Массив.Количество() - 1;
Пока Инд >= 0 Цикл
    Если НужноУдалить(Массив[Инд]) Тогда
        Массив.Удалить(Инд);
    КонецЕсли;
    Инд = Инд - 1;
КонецЦикла;

Коллекция передаётся по ссылке. Если передать массив или таблицу в процедуру и там его изменить, изменится и исходная коллекция — это один и тот же объект, а не копия. Когда нужна независимая копия таблицы, явно вызывайте ТЗ.Скопировать().

Ссылки в ХранилищеЗначения, которые потом удалили. Если вы упаковали в хранилище ссылки на справочник, а объекты позже пометили на удаление, при распаковке получите «битые» ссылки. Для долгого хранения кладите не ссылки, а их строковые идентификаторы или значимые реквизиты.

Пример: храним историю изменений документа

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

// Реквизит документа ИсторияИзменений имеет тип ХранилищеЗначения

Процедура ДописатьВИсторию(ТекстИзменения) Экспорт

    // достаём накопленную историю или создаём пустую
    Если ТипЗнч(ИсторияИзменений) = Тип("ХранилищеЗначения") Тогда
        Журнал = ИсторияИзменений.Получить();
    Иначе
        Журнал = Неопределено;
    КонецЕсли;

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

    Строка = Журнал.Добавить();
    Строка.Дата = ТекущаяДата();
    Строка.Пользователь = Строка(ПользователиИнформационнойБазы.ТекущийПользователь());
    Строка.Изменение = ТекстИзменения;

    // упаковываем обратно со сжатием
    ИсторияИзменений = Новый ХранилищеЗначения(Журнал, Новый СжатиеДанных(9));

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

Код решает задачу целиком: читает прошлую историю, при первом вызове создаёт таблицу с колонками, добавляет строку и пакует обратно. Перед записью документа не забудьте, что реквизит меняется — объект нужно записать.

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

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

Можно ли выбрать для реквизита тип «массив» или «список»?
Нет. Реквизит хранит одно значение своего типа. Набор держат в коллекции в памяти, в реквизите типа ХранилищеЗначения или в отдельной таблице (табличная часть, регистр).
Чем `СписокЗначений` лучше массива?
У элементов есть представление, пометка и картинка, а у списка — готовый диалог выбора. Если значения показываются пользователю и их надо отмечать — берите список; если это внутренний перечень для кода — хватит массива.
Почему падает передача `ТаблицаЗначений` на сервер?
Таблица значений не передаётся параметром между клиентом и сервером и недоступна на тонком клиенте. Передавайте адрес во временном хранилище через ПоместитьВоВременноеХранилище или массив структур, а таблицу собирайте на сервере.
Можно ли искать запросом по данным в `ХранилищеЗначения`?
Нет, для СУБД это двоичный блок. Если нужны запросы, отборы и отчёты — храните данные в табличной части или регистре, а не в хранилище.
Как ускорить поиск в большой коллекции?
Для поиска по ключу используйте Соответствие вместо Массив.Найти. Для таблицы добавьте индекс ТЗ.Индексы.Добавить(ИмяКолонки) — тогда НайтиСтроки работает быстрее перебора.