Как программно добавить колонку Уникального Идентификатора (УИД) в динамический список 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке универсальных инструментов, таких как консоли просмотра объектов базы данных, универсальные отчеты или специализированные рабочие места, часто возникает необходимость программного создания динамических списков. Одной из частых задач при этом является вывод системного поля — Уникального Идентификатора ссылки (UUID / УИД).

Однако при попытке добавить такую колонку «в лоб» разработчики сталкиваются с аппаратными ограничениями платформы и ошибками выполнения кода, например: Поле объекта не обнаружено (Данные). В рамках данной статьи мы подробно разберем причины этой проблемы, исправим ошибки в коде и рассмотрим несколько альтернативных архитектурных решений, включая современные возможности платформы 1С:Предприятие.

Анализ проблемы: почему возникает ошибка «Поле объекта не обнаружено»?

Рассмотрим ситуацию, с которой столкнулся автор темы. При инициализации таблицы на форме динамическому списку программно назначается основная таблица метаданных:

РеквизитСписок.ОсновнаяТаблица = "Справочник." + ИмяСправочника;

Далее разработчик пытается добавить на форму колонку, которая не привязана к какому-либо физическому реквизиту справочника в базе данных:


НоваяКолонкаТаблицы = Элементы.Добавить("КолонкаУИД", Тип("ПолеФормы"), НовыйЭлемент);

При этом свойство ПутьКДанным для этой колонки либо остается пустым, либо заполняется некорректно. В обработчике события ПриПолученииДанныхНаСервере выполняется попытка присвоить значение напрямую в структуру данных строки:


СтрокаДинСписка.Данные["КолонкаУИД"] = СтрокаДинСписка.Данные["Ссылка"].УникальныйИдентификатор();

Проанализируем причины падения платформы в данном сценарии:

  1. Отсутствие свойства в схеме данных: Динамический список строится на основе метаданных основной таблицы. Если в справочнике нет реального реквизита с именем КолонкаУИД, то и структура данных строки динамического списка на клиенте и сервере не содержит этого свойства. Платформа попросту не выделяет под него память в кэше строк.
  2. Неверная адресация в коллекции строк: В современных версиях платформы 1С элемент коллекции Строки в событии ПриПолученииДанныхНаСервере имеет другую структуру обращения. Правильный путь доступа к внутренним данным строки выглядит как СтрокаДинСписка.Значение.Данные, а не прямой вызов свойства Данные у элемента коллекции.
  3. Отсутствие связи с элементом формы: Чтобы колонка отображала программно рассчитанные значения, элемент формы (поле ввода или поле надписи) должен иметь четко заданный ПутьКДанным, который ссылается на существующее поле источника данных динамического списка.

Ниже мы подробно разберем три способа решения этой задачи, начиная от классического обхода ограничений и заканчивая современными стандартами разработки 1С.

Решение 1. Классический метод с использованием «колонки-заглушки» в запросе

Если мы работаем на версиях платформы ниже 8.3.22, напрямую получить УИД в запросе стандартными функциями невозможно. В этом случае мы должны использовать комбинацию произвольного запроса динамического списка и обработчика события ПриПолученииДанныхНаСервере.

Разберем по шагам, как заставить платформу выделить память под виртуальную колонку:

Шаг 1. Инициализация списка через произвольный запрос. Мы должны отказаться от простой привязки к основной таблице и задать явный текст запроса. В этом запросе мы создаем виртуальное пустое поле нужного типа (строка фиксированной длины 36 символов, соответствующая формату UUID):


// Указываем, что список строится на произвольном запросе
РеквизитСписок.ПроизвольныйЗапрос = Истина;

// В тексте запроса принудительно типизируем пустую колонку под будущий УИД
РеквизитСписок.ТекстЗапроса = 
    "ВЫБРАТЬ
    |    Товары.Ссылка КАК Ссылка,
    |    Товары.Код КАК Код,
    |    Товары.Наименование КАК Наименование,
    |    ВЫРАЗИТЬ(NULL КАК СТРОКА(36)) КАК КолонкаУИД
    |ИЗ
    |    Справочник.Товары КАК Товары";

РеквизитСписок.ОсновнаяТаблица = "Справочник.Товары";

Благодаря конструкции ВЫРАЗИТЬ(NULL КАК СТРОКА(36)), платформа 1С добавляет поле КолонкаУИД в схему компоновки данных динамического списка. Теперь это поле официально существует в схеме данных.

Шаг 2. Программное создание элемента формы. Теперь мы можем связать элемент формы с созданным полем:


НовыйЭлементФормы = Элементы.Добавить("КолонкаУИД", Тип("ПолеФормы"), Элементы.ТаблицаДанных);
НовыйЭлементФормы.Вид = ВидПоляФормы.ПолеВвода;
НовыйЭлементФормы.ПутьКДанным = "ТаблицаДанных.КолонкаУИД"; // Путь к нашей виртуальной колонке
НовыйЭлементФормы.Заголовок = "УИД (GUID)";

Шаг 3. Заполнение данных на сервере. Теперь пишем корректный обработчик события ПриПолученииДанныхНаСервере. Обратите внимание на правильную адресацию свойств через Значение.Данные:


&НаСервереБезКонтекста
Процедура ТаблицаДанныхПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
    
    Для Каждого СтрокаДинСписка Из Строки Цикл
        
        // Получаем ссылку на текущий объект
        ТекущаяСсылка = СтрокаДинСписка.Ключ;
        
        // Записываем строковое представление УИД в подготовленную колонку-заглушку
        СтрокаДинСписка.Ветка.Значение.Данные.КолонкаУИД = Строка(ТекущаяСсылка.УникальныйИдентификатор());
        
    КонецЦикла;
    
КонецПроцедуры

Этот метод полностью рабочий и гарантирует отсутствие ошибок на любых версиях платформы 1С v8.3.

Решение 2. Современный и самый производительный метод (Платформа 8.3.22 и выше)

Начиная с версии платформы 8.3.22, фирма «1С» добавила в язык запросов и язык выражений СКД долгожданную встроенную функцию — УникальныйИдентификатор(). Это полностью меняет подход к решению нашей задачи.

Нам больше не нужно задействовать серверные обработчики событий формы, перебирать строки в цикле и нагружать память сервера приложений. Платформа выполнит эту операцию на уровне СУБД чрезвычайно быстро.

Посмотрим на пример реализации данного подхода:


// Настройка динамического списка с использованием встроенной функции запроса
РеквизитСписок = ЭтаФорма["ТаблицаДанных"];
РеквизитСписок.ПроизвольныйЗапрос = Истина;

// Вызываем функцию УникальныйИдентификатор напрямую в запросе к БД
РеквизитСписок.ТекстЗапроса = 
    "ВЫБРАТЬ
    |    Товары.Ссылка КАК Ссылка,
    |    Товары.Код КАК Код,
    |    Товары.Наименование КАК Наименование,
    |    УникальныйИдентификатор(Товары.Ссылка) КАК КолонкаУИД
    |ИЗ
    |    Справочник.Товары КАК Товары";

РеквизитСписок.ОсновнаяТаблица = "Справочник.Товары";

// Программное добавление элемента формы
НовыйЭлементФормы = Элементы.Добавить("КолонкаУИД", Тип("ПолеФормы"), Элементы.ТаблицаДанных);
НовыйЭлементФормы.Вид = ВидПоляФормы.ПолеНадписи;
НовыйЭлементФормы.ПутьКДанным = "ТаблицаДанных.КолонкаУИД";
НовыйЭлементФормы.Заголовок = "УИД (СУБД)";

Использование этого метода является приоритетным. Оно исключает человеческий фактор при написании серверных обработчиков оформления строк, снижает объем передаваемых данных между сервером и клиентом и работает в разы быстрее на больших объемах данных.

Обязательные требования стандартов разработки к рассчитываемым полям

Если мы вынуждены использовать Решение 1 (через ПриПолученииДанныхНаСервере), мы должны помнить о жестких стандартах разработки фирмы «1С».

Поскольку данные в колонку-заглушку попадают динамически уже после выполнения основного запроса к базе данных, СУБД ничего не знает о содержимом поля КолонкаУИД. Соответственно, если пользователь попытается отсортировать список по этой колонке, сгруппировать данные или установить отбор по значению УИД, система выдаст критическую ошибку или вернет непредсказуемый результат.

Чтобы избежать этого, мы обязаны программно запретить любые интерактивные действия с рассчитываемым полем в настройках компоновщика данных списка. Рассмотрим пример такой настройки:


// Отключение интерактивных операций для рассчитываемой колонки УИД
ДоступныеПоля = РеквизитСписок.КомпоновщикНастроек.Настройки.ДоступныеПоляВыбора;
ПолеНастройки = ДоступныеПоля.НайтиПоле(Новый ПолеКомпоновкиДанных("КолонкаУИД"));

Если ПолеНастройки <> Неопределено Тогда
    ПолеНастройки.ОграничениеОтбора = Истина;
    ПолеНастройки.ОграничениеПорядка = Истина;
    ПолеНастройки.ОграничениеГруппировки = Истина;
КонецЕсли;

При использовании Решения 2 (встроенная функция в платформе 8.3.22+) данное ограничение накладывать не требуется! Так как расчет идентификатора происходит на уровне СУБД, платформа может полноценно индексировать, группировать, сортировать и фильтровать данные по этой колонке.

Оптимизация производительности: когда нужен метод ПолучитьКлючи()

В обсуждении на форуме затрагивался вопрос использования метода ПолучитьКлючи(). Проанализируем, в каких ситуациях он критически необходим.

Для получения УИД ссылки обращаться к базе данных не требуется: значение UUID хранится в самой ссылке, которая уже находится в оперативной памяти сервера приложений 1С. Поэтому простой перебор коллекции Строки в цикле без дополнительных запросов к СУБД не вызывает проблем с производительностью.

Однако, если бы перед нами стояла задача вывести в динамический список сопутствующие данные из других таблиц (например, остатки товаров из регистров накопления или актуальные цены), обращение к базе данных внутри цикла привело бы к катастрофическому падению производительности информационной системы (проблема «N+1 запросов»).

Для предотвращения подобных проблем разберем правильный шаблон проектирования групповой выборки данных с использованием ПолучитьКлючи():


&НаСервереБезКонтекста
Процедура ТаблицаДанныхПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
    
    // Шаг 1. Собираем массив уникальных идентификаторов (ссылок) видимых на экране строк
    МассивСсылок = Строки.ПолучитьКлючи();
    
    Если МассивСсылок.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // Шаг 2. Выполняем ОДИН пакетный запрос к базе данных по массиву ссылок
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
        |    ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
        |ИЗ
        |    РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, Номенклатура В (&МассивСсылок)) КАК ЦеныНоменклатурыСрезПоследних";
        
    Запрос.УстановитьПараметр("МассивСсылок", МассивСсылок);
    ТаблицаЦен = Запрос.Выполнить().Выгрузить();
    
    // Шаг 3. Индексируем таблицу цен для быстрого поиска в памяти
    ТаблицаЦен.Индексы.Добавить("Номенклатура");
    
    // Шаг 4. Быстро заполняем данные строк динамического списка из кэшированной таблицы
    Для Каждого СтрокаЦены Из ТаблицаЦен Цикл
        ТекущаяСтрокаСписка = Строки[СтрокаЦены.Номенклатура];
        Если ТекущаяСтрокаСписка <> Неопределено Тогда
            ТекущаяСтрокаСписка.Данные.Цена = СтрокаЦены.Price;
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Применение этого шаблона гарантирует быстродействие интерфейса динамических списков даже при работе со сложными, расчетными и внешними данными.

← На главную