Почему не работает метод УложитьСписокОбъектов13 в 1С++ и как правильно передавать списки объектов в SQL-запрос?

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

При написании прямых SQL-запросов к базе данных 1С:Предприятия 7.7 с использованием внешней компоненты 1С++ и класса ODBCRecordSet часто возникает задача передать список значений или объектов во временную таблицу для последующей фильтрации. Разработчики нередко сталкиваются с ситуацией, когда метод УложитьСписокОбъектов13 отрабатывает без явных ошибок, но результирующий SQL-запрос возвращает пустой набор данных или условия сравнения вида IN (SELECT VAL FROM #Таблица) всегда вычисляются как ложные.

Разберем детально, почему это происходит, в чем заключается разница форматов хранения ссылок в 1С 7.7 и как правильно организовать передачу списков объектов с сохранением иерархии и типизации.

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

Чтобы понять, почему условие фильтрации дает сбой, проанализируем внутреннее устройство хранения ссылочных данных в MS SQL Server для платформы 1С 7.7:

  1. Типизированные реквизиты и системные поля (CHAR(9)): если реквизит имеет строго определенный тип (например, конкретный справочник или документ), а также для системных полей вроде ID и PARENTID в таблицах справочников, идентификатор хранится в виде строки фиксированной длины CHAR(9).
  2. Реквизиты неопределенного типа (CHAR(13)): для реквизитов составного типа используется формат CHAR(13). Первые 4 символа содержат код вида объекта метаданных в системе счисления Base36, а оставшиеся 9 символов — уникальный идентификатор самого объекта.
  3. Поведение метода УложитьСписокОбъектов13: данный метод всегда создает временную таблицу с единственной колонкой val char(13) и заполняет ее полными 13-символьными идентификаторами. Если в запросе сравнивать Товары.PARENTID (тип CHAR(9)) со значением из #SpisRod (тип CHAR(13)), сравнение строк разной длины никогда не будет истинным.
  4. Отсутствие разворачивания групп: в отличие от стандартного метода УложитьСписокОбъектов, метод УложитьСписокОбъектов13 не имеет встроенного механизма рекурсивного разворота иерархии групп справочника. В таблицу попадают строго те элементы, которые были непосредственно переданы во входном списке.

Решение 1. Использование УложитьСписокОбъектов для типизированных полей

Если цель заключается в фильтрации по конкретному справочнику или по полю PARENTID, нет необходимости использовать 13-значные идентификаторы. Рассмотрим штатный метод УложитьСписокОбъектов, который создает поле val char(9) и умеет автоматически разворачивать иерархию папок:


// Создаем объект Recordset
Запрос = СоздатьОбъект("ODBCRecordSet");

// Третий параметр "1" включает автоматический разворот иерархии переданных групп
Запрос.УложитьСписокОбъектов(СписокГруппИЭлементов, "#SpisRod", 1);

// Выполняем SQL-запрос с фильтром по 9-значному идентификатору
ТекстЗапроса = "
|SELECT 
|    Спр.ID as [Элемент $Справочник.Номенклатура],
|    Спр.DESCR as [Наименование $Строка]
|FROM 
|    $Справочник.Номенклатура as Спр
|WHERE 
|    Спр.PARENTID IN (SELECT val FROM #SpisRod)
|";

ТЗ = Запрос.ВыполнитьИнструкцию(ТекстЗапроса);

Решение 2. Преобразование 9-значных ID в 13-значные на уровне SQL

Если требуется получить список 13-значных идентификаторов с учетом иерархии (например, для последующего сравнения с реквизитом неопределенного вида в журнале документов), разберем комбинированный подход: укладываем объекты с иерархией через УложитьСписокОбъектов, а затем преобразуем их во временную таблицу формата CHAR(13) средствами SQL и метапарсера 1С++.


Запрос = СоздатьОбъект("ODBCRecordSet");

// 1. Укладываем 9-символьные ID с разворачиванием иерархии
Запрос.УложитьСписокОбъектов(СписокЭлементов, "#Temp9", 1);

// 2. Формируем 13-значную таблицу с префиксом вида метаданных
ТекстПодготовки = "
|SELECT 
|    $ВидСправочника36.Номенклатура + val AS val
|INTO #Spis13
|FROM #Temp9;
|";
Запрос.ВыполнитьИнструкцию(ТекстПодготовки);

// 3. Используем полученную таблицу для фильтрации неопределенного реквизита
ТекстЗапроса = "
|SELECT 
|    Жур.IDDOC as [Док $Документ]
|FROM 
|    _1SJOURN as Жур
|WHERE 
|    Жур.$ОбщийРеквизит.ТекущийЭлемент IN (SELECT val FROM #Spis13)
|";
ТЗ = Запрос.ВыполнитьИнструкцию(ТекстЗапроса);

Решение 3. Универсальная процедура ручной укладки через Таблицу Значений

В ситуациях, когда требуется полностью контролировать типы данных, структуру создаваемой временной таблицы и передавать произвольные наборы колонок (включая принудительное приведение к CHAR(9) или CHAR(13)), посмотрим на программную реализацию универсальной выгрузки:


Процедура УложитьТаблицу_л(пТЗ, ИмяТаб, Колонки="") 
    Перем ТЗ, Запрос, рс_;
    
    Запрос = СоздатьОбъект("ODBCRecordSet");
    
    Если ПустаяСтрока(Колонки) = 1 Тогда
        типВхода = ТипЗначенияСтр(пТЗ);
        Попытка
            Если типВхода = "ТаблицаЗначений" Тогда
                ТЗ = пТЗ;
            ИначеЕсли типВхода = "СписокЗначений" Тогда 
                ТЗ = СоздатьОбъект("ТаблицаЗначений");
                пТЗ.Выгрузить(ТЗ);    
            КонецЕсли;    
        Исключение
            Возврат;
        КонецПопытки;    
    Иначе
        Попытка
            пТЗ.Выгрузить(ТЗ,,, Колонки);
        Исключение
            ТЗ = СоздатьОбъект("ТаблицаЗначений");
            пТЗ.Выгрузить(ТЗ);
        КонецПопытки;    
    КонецЕсли;

    СтрCreate = "create table %Имя% (/*column_definition*/)";
    СтрInsert = "insert into %Имя% values(/*?*/)";
    Тип = ""; Длина = ""; Точность = ""; ФорматнаяСтрока = "";
    КолонкиЕсть = 0;

    Для Тек = 1 По ТЗ.КоличествоКолонок() Цикл
        Идентификатор = ТЗ.ПолучитьПараметрыКолонки(Тек, Тип, Длина, Точность,,, ФорматнаяСтрока); 
        Если (ПустаяСтрока(Колонки) = 0) И (Найти(Колонки, Идентификатор) = 0) Тогда 
            Продолжить; 
        КонецЕсли;
        
        КолонкиЕсть = 1;
        Если ПустаяСтрока(ФорматнаяСтрока) = 0 Тогда
            SQL_тип = ФорматнаяСтрока;
            Запрос.ДобПараметр(1,);
        ИначеЕсли ПустаяСтрока(Тип) = 1 Тогда
            SQL_тип = "char(9)";
            Запрос.ДобПараметр(1, 14, 9, 0);
        ИначеЕсли Тип = "Число" Тогда
            Если Длина = 0 Тогда Длина = 38; Точность = 10; КонецЕсли;
            SQL_тип = "numeric(" + Длина + "," + Точность + ")";
            Запрос.ДобПараметр(1, 11, Длина, Точность);
        ИначеЕсли Тип = "Строка" Тогда
            Если Длина = 0 Тогда Длина = 255; КонецЕсли;
            SQL_тип = "varchar(" + Длина + ")";
            Запрос.ДобПараметр(1, 15, Длина, 0);
        ИначеЕсли Тип = "Дата" Тогда
            SQL_тип = "datetime";
            Запрос.ДобПараметр(1, 8, 0, 0);
        ИначеЕсли Найти(Тип, ".") > 0 Тогда
            SQL_тип = "char(9)";
            Запрос.ДобПараметр(1, 14, 9, 0);
        Иначе
            SQL_тип = "char(13)";
            Запрос.ДобПараметр(1, 14, 13, 0);
            ТЗ.УстановитьПараметрыКолонки(Идентификатор,,,, "?2");
        КонецЕсли;

        СтрCreate = СтрЗаменить(СтрCreate, "/*,*/", ", ");
        Если Тек = ТЗ.КоличествоКолонок() Тогда  
            СтрCreate = СтрЗаменить(СтрCreate, "/*column_definition*/", Идентификатор + " " + SQL_тип + "/*,*/");
        Иначе    
            СтрCreate = СтрЗаменить(СтрCreate, "/*column_definition*/", Идентификатор + " " + SQL_тип + "/*,*//*column_definition*/");
        КонецЕсли;    
          
        СтрInsert = СтрЗаменить(СтрInsert, "/*,*/", ", ");
        СтрInsert = СтрЗаменить(СтрInsert, "/*?*/", "?/*,*//*?*/");
    КонецЦикла;
    
    Если КолонкиЕсть = 1 Тогда
        рс_ = СоздатьОбъект("ODBCRecordset");
        ЗапросУдаления = "set nocount on if exists (select * from tempdb..sysobjects where id=object_id('tempdb..%Имя%') and sysstat & 0xf = 3 ) drop table %Имя%;";
        рс_.ВыполнитьИнструкцию(СтрЗаменить(ЗапросУдаления, "%Имя%", ИмяТаб));
        
        ЗапросСоздания = СтрCreate + " set nocount off";
        рс_.ВыполнитьИнструкцию(СтрЗаменить(ЗапросСоздания, "%Имя%", ИмяТаб));
        
        Запрос.Подготовить(СтрЗаменить(СтрInsert, "%Имя%", ИмяТаб));
        Запрос.ВыполнитьSQL_ИзТЗ(ТЗ, 100);
    КонецЕсли;
КонецПроцедуры

Рекомендации по оптимизации запросов

Для обеспечения максимального быстродействия при работе с временными таблицами в MS SQL Server обратим внимание на следующие правила:

  1. Создание индексов на временные таблицы: если список содержит сотни или тысячи записей, обязательно создавайте кластерный индекс после заполнения таблицы. Это переведет выполнение соединения с полного сканирования (Table Scan) на быстрый поиск (Index Seek):
    
    Запрос.ВыполнитьИнструкцию("CREATE CLUSTERED INDEX IX_TMP_VAL ON #SpisRod (VAL);");
    
  2. Выбор формата поля: для фильтрации реквизитов конкретного типа всегда используйте 9-символьный формат (CHAR(9)), а 13-символьный (CHAR(13)) оставляйте исключительно для граф журналов и реквизитов неопределенного вида.
  3. Использование RIGHT(): если в существующей временной таблице уже находятся 13-значные строки, а условие требует сравнения с 9-значным полем, можно отсечь первые 4 символа функции SQL прямо в выражении: RIGHT(VAL, 9).
← На главную