При разработке сложных интеграционных механизмов, например, при обмене данными с интернет-магазинами, парсинге многоуровневых XML/JSON-пакетов или выполнении тяжелых расчетов в регламентных заданиях, разработчики часто сталкиваются с необходимостью хранения иерархических структур в памяти. Одним из первых решений, приходящих на ум, является использование вложенных коллекций — когда одна ТаблицаЗначений хранится внутри ячейки другой ТаблицыЗначений.
Однако при попытке реализовать эту схему программно (в режиме выполнения / runtime) без использования визуального конструктора форм многие разработчики совершают классическую ошибку, путая описание типов колонок с инициализацией самих объектов. В этой статье мы подробно разберем, почему возникает популярная ошибка "Нет поля Колонки", как правильно создавать вложенные таблицы значений, с какими критическими проблемами производительности вы можете столкнуться и какие альтернативные подходы существуют в платформе 1С.
Рассмотрим типичный сценарий программного создания вложенной структуры, который часто приводит к ошибке. Разработчик пытается объявить колонку с типом ТаблицаЗначений и сразу же наполнить её собственной структурой колонок:
// Пример кода, вызывающий ошибку
ТипТЗ = Новый ОписаниеТипов("ТаблицаЗначений");
ЗаказыИМ.Колонки.Добавить("ПримечаниеЗаказа", ТипСтрока1024);
// Пытаемся добавить колонку для вложенной таблицы
ПозицииЗаказа = ЗаказыИМ.Колонки.Добавить("ПозицииЗаказа", ТипТЗ);
// На следующей строке платформа выдаст ошибку: "Поле объекта не обнаружено (Колонки)"
ПозицииЗаказа.Колонки.Добавить("ИдентификаторИМ", ТипЧисло);
Проанализируем ситуацию и разберемся, почему этот код принципиально неработоспособен. Метод Колонки.Добавить() возвращает объект метаданных колонки — КолонкаТаблицыЗначений, а не сам экземпляр таблицы значений, который будет храниться в ячейках. У объекта КолонкаТаблицыЗначений есть лишь несколько свойств: Имя, ТипЗначения, Заголовок и Ширина. Естественно, у него нет и не может быть свойства Колонки.
Кроме того, важно понимать ключевую особенность платформы: в 1С тип ТаблицаЗначений не может выступать в роли жестко типизированного объекта на уровне метаданных колонки. Когда вы пишите Новый ОписаниеТипов("ТаблицаЗначений"), платформа технически воспринимает это как тип Произвольный. Родительская колонка не хранит и не может хранить схему колонок вложенной таблицы. Схема колонок должна создаваться динамически для каждого конкретного экземпляра таблицы в конкретной строке.
Чтобы решить эту задачу, нам необходимо разделить процесс на два этапа: объявление колонки-контейнера в родительской таблице и динамическое создание новой таблицы значений для каждой добавляемой строки.
Разберем по шагам корректный алгоритм реализации:
ТаблицаЗначений (или вообще без указания типа, по умолчанию тип будет Произвольный).ТаблицаЗначений.Посмотрим на практический пример реализации этого алгоритма в коде:
// Шаг 1. Инициализация родительской таблицы значений
ЗаказыИМ = Новый ТаблицаЗначений;
ЗаказыИМ.Колонки.Добавить("НомерЗаказа", Новый ОписаниеТипов("Строка"));
ЗаказыИМ.Колонки.Добавить("ПозицииЗаказа"); // Колонка под вложенную ТЗ (тип Произвольный)
// Предположим, мы обрабатываем входящие данные в цикле
Для Каждого ТекущийЗаказ Из МассивВходящихДанных Цикл
// Добавляем строку в родительскую таблицу
НоваяСтрокаЗаказа = ЗаказыИМ.Добавить();
НоваяСтрокаЗаказа.НомерЗаказа = ТекущийЗаказ.Номер;
// Шаг 2. Создаем новый экземпляр вложенной таблицы для конкретной строки
ВложеннаяТЗ = Новый ТаблицаЗначений;
// Шаг 3. Формируем структуру колонок вложенной таблицы
ВложеннаяТЗ.Колонки.Добавить("Товар", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ВложеннаяТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
ВложеннаяТЗ.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));
// Наполняем вложенную таблицу данными по позициям заказа
Для Каждого Товар Из ТекущийЗаказ.Товары Цикл
НоваяСтрокаТовара = ВложеннаяТЗ.Добавить();
НоваяСтрокаТовара.Товар = Товар.Ссылка;
НоваяСтрокаТовара.Количество = Товар.Количество;
НоваяСтрокаТовара.Цена = Товар.Цена;
КонецЦикла;
// Шаг 4. Помещаем вложенную таблицу в ячейку родительской строки
НоваяСтрокаЗаказа.ПозицииЗаказа = ВложеннаяТЗ;
КонецЦикла;
Используя этот подход, мы получаем полностью рабочую структуру. Каждая строка таблицы ЗаказыИМ теперь содержит в ячейке ПозицииЗаказа свою собственную, уникальную таблицу значений со своим набором строк.
При работе с вложенными таблицами значений разработчики часто наступают на скрытые "грабли" платформы 1С, связанные с копированием данных. Нам необходимо очень подробно разобрать этот нюанс, чтобы избежать трудноотслеживаемых багов в логике приложения.
Допустим, у вас есть заполненная родительская таблица с вложенными таблицами, и вы решаете сделать её копию с помощью стандартного метода Скопировать():
КопияЗаказов = ЗаказыИМ.Скопировать();
Внимание! Платформа 1С при вызове метода Скопировать() для таблиц значений выполняет поверхностное копирование. Это означает следующее:
КопияЗаказов и её простые колонки (строки, числа, даты) будут успешно продублированы в новые области памяти.ПозицииЗаказа скопированы не будут. В новой таблице ячейки будут содержать ссылки на те же самые объекты вложенных таблиц, что и в исходной таблице ЗаказыИМ.Если вы измените количество товара во вложенной таблице внутри КопияЗаказов, эти изменения автоматически отразятся и в исходной таблице ЗаказыИМ, так как оба родительских объекта ссылаются на один и тот же физический экземпляр вложенной таблицы в памяти компьютера.
Чтобы избежать этой проблемы и получить полную (глубокую) копию структуры, необходимо вручную обойти скопированную таблицу в цикле и принудительно пересоздать каждую вложенную таблицу:
КопияЗаказов = ЗаказыИМ.Скопировать();
Для Каждого СтрокаЗаказа Из КопияЗаказов Цикл
// Проверяем на заполненность и принудительно копируем вложенный объект
Если ЗначениеЗаполнено(СтрокаЗаказа.ПозицииЗаказа) Тогда
СтрокаЗаказа.ПозицииЗаказа = СтрокаЗаказа.ПозицииЗаказа.Скопировать();
КонецЕсли;
КонецЦикла;
Вложенные таблицы значений крайне неэффективны с точки зрения расхода памяти при больших объемах данных. Каждая созданная ТаблицаЗначений — это "тяжелый" системный объект со своей служебной информацией (метаданные колонок, внутренние индексы, менеджеры памяти).
Если вы обрабатываете большой объем данных (например, загружаете 10 000 заказов за сессию, в каждом из которых в среднем по 5-10 позиций), платформа создаст 10 000 независимых объектов Таблиц Значений в оперативной памяти сервера 1С. Это неизбежно приведет к следующим негативным последствиям:
rphost.Именно поэтому использование вложенных таблиц значений считается в среде профессиональных разработчиков 1С не самым лучшим архитектурным решением (или, как выразился автор темы, "дурно пахнущим").
Наиболее производительным и соответствующим стандартам разработки 1С (включая стандарты БСП) подходом является использование двух независимых "плоских" таблиц значений, связанных между собой уникальным ключом связи. Этот метод работает на порядок быстрее и потребляет в разы меньше оперативной памяти.
Рассмотрим пример реализации такой архитектуры:
// Создаем основную таблицу заказов
ТаблицаЗаказов = Новый ТаблицаЗначений;
ТаблицаЗаказов.Колонки.Добавить("КлючСвязи", Новый ОписаниеТипов("УникальныйИдентификатор"));
ТаблицаЗаказов.Колонки.Добавить("НомерЗаказа", Новый ОписаниеТипов("Строка"));
// Создаем плоскую таблицу позиций (для ВСЕХ заказов)
ТаблицаПозиций = Новый ТаблицаЗначений;
ТаблицаПозиций.Колонки.Добавить("КлючСвязи", Новый ОписаниеТипов("УникальныйИдентификатор"));
ТаблицаПозиций.Колонки.Добавить("Товар", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаПозиций.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
// КРИТИЧЕСКИ ВАЖНО: Добавляем индекс по ключу связи для мгновенного поиска
ТаблицаПозиций.Индексы.Добавить("КлючСвязи");
// Заполняем данными
Для Каждого ТекущийЗаказ Из МассивВходящихДанных Цикл
// Генерируем уникальный ключ для связи
НовыйКлюч = Новый УникальныйИдентификатор;
// Добавляем заказ
НоваяСтрокаЗаказа = ТаблицаЗаказов.Добавить();
НоваяСтрокаЗаказа.КлючСвязи = НовыйКлюч;
НоваяСтрокаЗаказа.НомерЗаказа = ТекущийЗаказ.Номер;
// Добавляем позиции этого заказа в общую плоскую таблицу
Для Каждого Товар Из ТекущийЗаказ.Товары Цикл
НоваяСтрокаТовара = ТаблицаПозиций.Добавить();
НоваяСтрокаТовара.КлючСвязи = НовыйКлюч;
НоваяСтрокаТовара.Товар = Товар.Ссылка;
НоваяСтрокаТовара.Количество = Товар.Количество;
КонецЦикла;
КонецЦикла;
Для того чтобы быстро получить позиции конкретного заказа из такой структуры, мы используем высокопроизводительный метод НайтиСтроки(), который благодаря предварительно созданному индексу отработает практически мгновенно:
// Пример выборки позиций по заказу
Отбор = Новый Структура("КлючСвязи", ТекущийЗаказ.КлючСвязи);
НайденныеПозиции = ТаблицаПозиций.НайтиСтроки(Отбор);
Для Каждого СтрокаТовара Из НайденныеПозиции Цикл
// Обрабатываем позиции заказа
КонецЦикла;
В данном сценарии в памяти сервера создаются всего два объекта ТаблицаЗначений. Это обеспечивает колоссальный выигрыш в скорости работы и экономии системных ресурсов.
Если задача заключается исключительно в парсинге данных из внешнего источника (например, получение данных по HTTP в формате JSON или XML) и последующей обработке в фоновом задании без вывода на форму, то использование таблиц значений вообще не имеет практического смысла.
Таблицы значений — это тяжелые интерфейсные объекты. Для чистой обработки данных гораздо эффективнее использовать связку легких коллекций: Массивы и Структуры. Платформа 1С идеально адаптирована под сериализацию таких структур.
Разберем пример обработки структуры "Заказ - Позиции" через легкие коллекции:
// Создаем общий массив для хранения заказов
ПакетЗаказов = Новый Массив;
Для Каждого ТекущийЗаказ Из ВходящийПакетДанных Цикл
// Каждый заказ представляем в виде структуры
ЗаказСтруктура = Новый Структура;
ЗаказСтруктура.Вставить("Номер", ТекущийЗаказ.Номер);
ЗаказСтруктура.Вставить("Дата", ТекущийЗаказ.Дата);
// Позиции заказа храним в виде массива структур
МассивПозиций = Новый Массив;
Для Каждого Товар Из ТекущийЗаказ.Товары Цикл
ПозицияСтруктура = Новый Структура;
ПозицияСтруктура.Вставить("Номенклатура", Товар.Ссылка);
ПозицияСтруктура.Вставить("Количество", Товар.Количество);
МассивПозиций.Добавить(ПозицияСтруктура);
КонецЦикла;
// Вкладываем массив позиций в структуру заказа
ЗаказСтруктура.Вставить("Позиции", МассивПозиций);
// Добавляем заказ в общий пакет
ПакетЗаказов.Добавить(ЗаказСтруктура);
КонецЦикла;
Такой подход обладает рядом неоспоримых преимуществ:
ЗаписатьJSON() без написания сложных велосипедов обхода таблиц.Мы подробно разобрали особенности работы с вложенными таблицами значений в 1С. Подводя итог, можно сделать следующие выводы:
ТаблицаЗначений для каждой строки отдельно. Настроить структуру вложенных колонок через метод родительской таблицы Колонки.Добавить() нельзя.Скопировать(), требуя глубокого ручного копирования вложенных объектов в цикле.