Почему Табличное поле 1С++ выдает ошибку при выполнении сложного пакетного SQL-запроса с временными таблицами?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

При разработке интерфейсов на платформе 1С:Предприятие 7.7 с использованием внешней компоненты 1С++ разработчики часто применяют связку из элемента управления ТабличноеПоле и объекта ПоставщикДанныхODBC.MSSQL (ODBCDataProvider.MSSQL). Этот механизм обеспечивает быстрое отображение больших объемов информации за счет динамической постраничной подкачки строк напрямую с сервера MS SQL Server.

Однако при попытке передать в метод УстТекстЗапроса сложный пакетный SQL-скрипт — содержащий создание временных таблиц (SELECT ... INTO #temp), их обновление через UPDATE и финальную выборку — разработчики сталкиваются с непредвиденным поведением: свойство Поля оказывается неинициализированным (ошибка «Поле агрегатного объекта не обнаружено» или обращение к неагрегатному объекту), а вызовы методов УстКлючПорядка или УстИДПоле завершаются системными ошибками.

Проанализируем архитектурные причины такого поведения компоненты и подробно разберем рабочие варианты обхода этого ограничения.

Анализ проблемы: как работает парсер SQL в ПоставщикеДанных

Разберем типичный фрагмент кода, приводящий к сбою:


оПоставщикДанных = СоздатьОбъект("ПоставщикДанныхODBC.MSSQL");
оПоставщикДанных.БазаДанных = СоздатьОбъект("ODBCDataBase");
// ... формирование ТекстЗапроса с DROP, SELECT INTO #temp, UPDATE и финальным SELECT ...
оПоставщикДанных.УстТекстЗапроса(ТекстЗапроса);
оПоставщикДанных.Поля.Добавить("Элемент").Автоудаление = 0; // Ошибка: Поля не инициализированы
оПоставщикДанных.УстКлючПорядка("Аналог"); // Ошибка: поле не найдено

Выясним причину сбоя. Объект ПоставщикДанныхODBC.MSSQL не просто отправляет текст в СУБД на прямое выполнение, как это делает, например, ODBCRecordset. Его основная задача — построение виртуального списка с курсорной постраничной загрузкой данных.

Для реализации этого механизма встроенный парсер 1С++ берет переданный SQL-запрос и динамически модифицирует его в зависимости от действий пользователя в табличном поле:

  1. Добавляет конструкции ограничения порции (TOP N) для подгрузки только видимых строк.
  2. Подставляет дополнительные условия фильтрации в секцию WHERE (для пагинации по ключу порядка при скроллинге).
  3. Динамически перестраивает предложение ORDER BY при щелчках по заголовкам колонок.
  4. Оптимизирует выборку полей, удаляя те столбцы, которые не отображаются в табличной части (если для них не сброшен флаг Автоудаление).

Собственный синтаксический анализатор поставщика данных рассчитан строго на одиночную инструкцию SELECT. Когда парсер встречает конструкции IF EXISTS, DROP TABLE, цепочку создания временных таблиц и операторы UPDATE, он не может выделить результирующую структуру колонок. В результате парсинг завершается неудачей, коллекция Поля не заполняется, а табличное поле теряет метаданные выборки.

Способ 1. Сворачивание логики в единый запрос SELECT

Если требуется сохранить динамическую серверную пагинацию и возможность быстрой сортировки по колонкам, наилучшим подходом является отказ от временных таблиц и операторов UPDATE в пользу единого запроса с подзапросами или соединениями (LEFT JOIN).

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


ТекстЗапроса = "
|SELECT 
|    тов.CODE AS КодСырья,
|    спр.Номенклатура AS [Аналог $Справочник.Товары],
|    спр.Коэффициент AS Коэффициент,
|    'Х' AS Исключить,
|    ISNULL((
|        SELECT TOP 1 t.ВвестиВДействиеС 
|        FROM Справочник_ОсновнойАналог AS t 
|        WHERE t.ParentExt = спр.ID 
|          AND t.IsMark = 0 
|          AND t.ВвестиВДействиеС <= " + стрДатаЗапроса + "
|          AND t.ФлагРаботы = 1
|        ORDER BY t.ВвестиВДействиеС DESC
|    ), CAST('1753-01-01' AS datetime)) AS Основной,
|    спр.ID AS [Элемент $Справочник.НоменклатураАналога]
|FROM Справочник_НоменклатураАналога AS спр
|INNER JOIN Справочник_Товары AS тов ON тов.ID = спр.Номенклатура
|WHERE спр.ParentExt = ?текВладелец(14,9) 
|  AND спр.IsMark = 0";

оПоставщикДанных.УстТекстЗапроса(ТекстЗапроса);
оПоставщикДанных.УстИДПоле("Элемент");
оПоставщикДанных.УстКлючПорядка("Аналог");
оПоставщикДанных.Поля.Добавить("Элемент").Автоудаление = 0;

Такой запрос парсер 1С++ разбирает без затруднений, корректно инициализирует метаданные полей и применяет внутренние механизмы прокрутки.

Способ 2. Разделение этапа подготовки данных и отображения

Когда вычисления настолько громоздки, что переписать их в один оператор SELECT без потери быстродействия невозможно, процесс разделяют на два последовательных шага:

  1. Формирование и заполнение временной таблицы через отдельный вызов SQL-скрипта.
  2. Передача поставщику данных простого запроса на чтение из уже готовой таблицы.

Разберем реализацию этого метода по шагам:


// 1. Подготавливаем временную таблицу через объект соединения базы
оБД = СоздатьОбъект("ODBCDataBase");
оБД.Соединение = "ТвояСтрокаПодключения"; // Либо использовать текущее соединение

ТекстПодготовки = "
|IF OBJECT_ID('tempdb..#ВременноСырье') IS NOT NULL DROP TABLE #ВременноСырье;
|
|SELECT тов.CODE as КодСырья, спр.Номенклатура, спр.ID
|INTO #ВременноСырье
|FROM Справочник_НоменклатураАналога as спр
|JOIN Справочник_Товары as тов ON тов.ID = спр.Номенклатура
|WHERE спр.IsMark = 0;
|-- Выполнение других расчетов и UPDATE...";

оБД.Выполнить(ТекстПодготовки);

// 2. В ПоставщикДанных передаем только простой SELECT
оПоставщикДанных = СоздатьОбъект("ПоставщикДанныхODBC.MSSQL");
оПоставщикДанных.БазаДанных = оБД;

ТекстЗапросаТП = "
|SELECT 
|    спр.КодСырья AS КодСырья,
|    спр.Номенклатура AS [Аналог $Справочник.Товары],
|    спр.ID AS [Элемент $Справочник.НоменклатураАналога]
|FROM #ВременноСырье AS спр";

оПоставщикДанных.УстТекстЗапроса(ТекстЗапросаТП);
оПоставщикДанных.УстИДПоле("Элемент");
оПоставщикДанных.УстКлючПорядка("Аналог");

Важные нюансы реализации:

  1. Единое подключение к СУБД: Локальные временные таблицы (с одним символом #) существуют только в контексте текущей сессии (SPID). Объект оБД и оПоставщикДанных.БазаДанных должны физически делить одно и то же соединение с базой данных, иначе временная таблица не будет найдена при обращении табличного поля.
  2. Обязательные алиасы: При указании временной таблицы во фразе FROM #ВременноСырье AS спр обязательно задавайте псевдоним (алиас), чтобы внутренний транслятор 1С++ не конфликтовал со спецсимволом решетки в имени таблицы.
  3. Повторное заполнение при обновлении: При вызове метода ТабПоле.ПоставщикДанных.Обновить() необходимо заново запускать скрипт наполнения временной таблицы, если исходные данные изменились.

Способ 3. Переход на ПоставщикДанныхТЗ или ПоставщикДанныхИТЗ

Если результирующий набор данных не превышает нескольких тысяч строк, использование динамической серверной подкачки ПоставщикДанныхODBC.MSSQL избыточно. В таких случаях проще полностью выполнить сложный пакетный запрос стандартным объектом ODBCRecordset, выгрузить результат в таблицу значений и передать ее табличному полю:


Запрос = СоздатьОбъект("ODBCRecordset");
// Выполняем исходный сложный пакетный запрос целиком со всеми DROP, INTO и UPDATE
ТЗ = Запрос.ВыполнитьИнструкцию(ТекстСложногоЗапроса);

// Подключаем к ТабличномуПолю через табличный поставщик данных
ТабПоле.ПоставщикДанных = СоздатьОбъект("ПоставщикДанныхТЗ");
ТабПоле.ПоставщикДанных.УстДанные(ТЗ);
ТабПоле.Обновить();

Этот подход снимает любые ограничения на структуру и сложность SQL-запроса, исключает ошибки парсинга метаданных и гарантирует стабильную работу интерфейса формы.

← На главную