Статьи‎ > ‎

Прямые запросы с типизацией

Цели


    Возможность формирования прямых запросов к БД 1с вечно мучила умы разработчиков. У каждого свои цели и задачи, но все эти попытки были направлены на то  чтобы снять тот уровень абстракции, который предлагает 1С и получить данные без ограничения. Со временем некоторые, талантливые головы открыли нам описания таблиц SQL, и формировать запросы осталось только делом техники. Однако построенные таким образом запросы могли быть более менее пригодны из сторонних  систем. Потому как они они не могли работать с прикладными типами объектов (то есть  запрос не мог вернуть ссылку на элемент справочника,документа .. и т.д.) в чем он и проигрывал своему оригинальному собрату. Таким образом прямые запросы  на 8 так и не прижились в повседневной разработке конфигураций на 1с, из за следующих своих недостатков:
  • невозможность работы с прикладными типами объектов;
  • длительное возвращение результатов запроса по средством перебора всех записей RecordSet;  (можно обойти используя ВК GameWithFire)
  • отсутствие вменяемого визуального конструктора запросов;
однако для определенных специфических задач, связанных с интеграцией из различных источников данных, прямые запросы в 8 используются. Ситуация у младшего брата 8 -7.7 сложилось более благополучно, как говорится "Не было счастья, да не счастье помогло". Дак вот тем самым не счастьем в 7.7 был ее так называемый (у кого язык повернулся) "язык запросов". Те кто работал на 7.7 и 8 меня поймут. Это  "язык запросов" похож на все что угодно, кроме запроса.
Наигравшись с такими "запросами" вдоволь была создана ВК 1С++ которая позволяет выполнять прямые запросы. Все полюсы этих запросов очевидны из текста запроса, кроме типизирующих псевдонимов это чистый SQL. К тому же все недостатки прямых запросов заявленные выше, в 1с++ обошли, в том числе и конструктор запросов.

 
Период с ДатаПрошлогоПериода по ДатаПрошлогоПериода;
ВидРасч = ЖурналРасчетов.Зарплата.ВидРасч;
Объект = ЖурналРасчетов.Зарплата.Объект;
Результат = ЖурналРасчетов.Зарплата.Результат;
ПериодДействия = ЖурналРасчетов.Зарплата.ПериодДействия;
ДатаОкончания  = ЖурналРасчетов.Зарплата.ДатаОкончания;
Группировка Объект Без Групп;
Условие(ВидРасч = ВидРасчета.НДФЛ);
Условие(Результат <> 0);
Условие(ДатаОкончания <> ДатаПрошлогоПериода)
SELECT Контрагенты.ID [Ссылка $Справочник.Контрагенты]
, Контрагенты.ISMARK ПометкаУдаления
, Контрагенты.PARENTID [Родитель $Справочник.Контрагенты]
, Контрагенты.ISFOLDER ЭтоГруппа
, Контрагенты.CODE Код
, Контрагенты.DESCR Наименование
, $Контрагенты.ВидКонтрагента [ВидКонтрагента $Перечисление.ВидыКонтрагентов]
, $Контрагенты.ПолнНаименование ПолнНаименование
, $Контрагенты.ОсновнойДоговор [ОсновнойДоговор $Справочник.Договоры]
, $Контрагенты.ОсновнойСчет [ОсновнойСчет $Справочник.РасчетныеСчета]
, $Контрагенты.ОКПО ОКПО
FROM $Справочник.Контрагенты AS Контрагенты
INNER JOIN $Справочник.Контрагенты AS Контрагенты1
ON Контрагенты.DESCR = Контрагенты1.DESCR
Where (Контрагенты.PARENTID=' K0' or Контрагенты.PARENTID=' jj')
and (Контрагенты1.PARENTID=' K0' or Контрагенты1.PARENTID=' jj')
Нашей задачей в этой статье будет получение аналогичного  результат прямых запросов на 1с++, только в контексте 8.1 и старше.

Общий подход


    В изложенном подходе не будет использоваться внешних компонент. Пока работа данного подхода проверена на MSSQL 2000  возможность работы в других  СУБД  пока  не изучались . Достоверно известно  что описанный здесь подход, ПОКА не будет работать в  MSSQL 2005.
Основная идея реализации будет крутится вокруг временных таблиц, и возможности 1с создавать и возвращать данные из них (Временных таблиц). Обобщенно подход можно представить в виде следующей последовательности.

Средствами 1С создается временная таблица. Временная таблица должна иметь такую структуру колонок и их типов, которую мы хотим получить по результатам выполнения запроса.
Запрос = Новый Запрос("ВЫБРАТЬ
| ТЗ_Null.*
|ПОМЕСТИТЬ ТЗ
|ИЗ
| &ТЗ КАК ТЗ_Null");
МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
ТЗ=Новый ТаблицаЗначений;
Запрос.УстановитьПараметр("ТЗ", ТЗ);
РезультатЗапроса = Запрос.Выполнить();
Созданная временная таблица будет локальной (#tt1), и соответственно будет доступна только из той сесии, в которой она была создана. На этом этапе необходимо будет сделать из это локальной временной таблицы глобальную (##tt). Подробное описание этого процесса  приведено в статье Смирнова Андрея. В  нашем случае задача еще усложняется тем что мы не знаем имени временной таблицы.  Поэтому при  изменении имени используется следующий запрос (пример):
DECLARE @table_name varchar(100)
--Перименовывем таблицу
set @table_name = 'temp_'+Left(cast(newid() as varchar(36)),8)
Select @table_name as table_name
update tempdb..sysobjects
set name = @table_name
where id in (SELECT top 1
id
FROM
tempdb..
sysobjects
WHERE
Len(Name)=128 and info >= 8 and
CRDate >=
29.04.2008 15:15:15)
Теперь когда у нас есть ~глобальная временная таблица (доступна из сеанса 1с и из любого подключения к MSSQL) остается только делом техники наполнить  ее необходимыми данными.  
INSERT Into temp..##tt Select * From dbo._Reference32 
Здесь и далее имя временной таблицы будет ##tt, однако оно отличается от реального
set @table_name = 'temp_'+Left(cast(newid() as varchar(36)),8)
преобразование в реальное имя осуществляется непосредственно перед выполнением запроса. Такая "конспирация" нужна, потому что мы текст запроса  задаем раньше чем узнаем имя временной таблицы
Теперь когда у нас временная таблица наполнена необходимыми данными вернем результат запроса в текущее соединение 1С. Обращаю внимание что в данном случае нужно использовать именно тот менеджер временных таблиц в котором мы и создавали временную таблицу с заданной структурой и типами колонок.
Запрос.Текст = "ВЫБРАТЬ
                           |    ТЗ.*
                           |ИЗ
                           |    ТЗ КАК ТЗ";
            Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
            мРезЗапроса = Запрос.Выполнить();

Реализация


Основными сложностями в реализации будет правильно продекларировать типы  колонок временной таблиц. Сразу предупрежу это занятие довольно необычное, обычно мы привыкли просто выполнять запрос и  на основе возвращаемых  данных происходит автоматическое преобразование типов. Но к сожалению данная процедура необходима для текущей конструкции менеджера временных таблиц. О нем сейчас и поговорим.

Для начала  попробуем создать простую временную таблицу с  2 колонками  следующих типов.
В результате в tempdb будет создана следующая таблица.
Имя temp_71650AC7 получено при выполнении 2 этапа "Общего подхода". В данном случае только  менеджер временных таблиц знает что в Колонкt _Q_000_F
000RRef соответсвует  (1) колонка "Ссылка" со ссылочным типом  и так далее. Заполнить такую таблицу достаточно просто - 2 колонки в 1С и 2 колонки в MSSQL
INSERT Into temp..##tt 
Select _idRRef,
        _Description
From dbo._Reference32
Интерфейсно это можно сделать используя Конструктор запросов   Enterprise Integrator начиная с версии 0.54 (на закладке SQL включить режим временных таблиц)
Теперь сделаем первую колонку "ссылка" составного типа со следующими ссылочными типами "СправочникСсылка.Валюты, СправочникСсылка.Контрагенты"
В соответствии с правилами хранения составных типов данных для первого составного поля в 1с теперь предназначено 3 поля в MSSQL. Заполнить набор таких полей достаточно трудоемко  поскольку в  поле *_RTRef  нужно вставлять 16-ричный номер таблицы из которой выбираются данные. Поэтому при построении прямых запросов рекомендуется  использовать трассировку родных запросов, то есть строятся аналогичный запрос, преобразовывается в SQL после чего дорабатывается. При таком подходе мы преодолеваем множество проблем связанных с построеним прямых запросов  и решаем основную(озвученную в целях), а именно "отсутствие визуального конструктора запросов"
  
ВЫБРАТЬ
    Валюты.Ссылка,
    Валюты.Наименование
ИЗ
    Справочник.Валюты КАК Валюты
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ

    Контрагенты.Ссылка,
    Контрагенты.Наименование
ИЗ
    Справочник.Контрагенты КАК Контрагенты

    SELECT
        CASE
            WHEN _Reference32_Q_000_T_001._IDRRef IS NOT NULL
            THEN 0x0000000A
            ELSE NULL
        END AS _Q_000_F_000TRef,
        _Reference32_Q_000_T_001._IDRRef AS _Q_000_F_000RRef,
        CAST(_Reference32_Q_000_T_001._Description AS NVARCHAR(100)) AS _Q_000_F_001
    FROM
        _Reference32 _Reference32_Q_000_T_001 WITH(NOLOCK)
UNION ALL
    SELECT
        CASE
            WHEN _Reference78_Q_001_T_001._IDRRef IS NOT NULL
            THEN 0x0000004E
            ELSE NULL
        END AS _Q_000_F_000TRef,
        _Reference78_Q_001_T_001._IDRRef AS _Q_000_F_000RRef,
        _Reference78_Q_001_T_001._Description AS _Q_000_F_001
    FROM
        _Reference78 _Reference78_Q_001_T_001 WITH(NOLOCK)
В запросе SQL у нас на выходе 3 поля  однако во временную таблицу необходимо вставить 4. Вот сдесь наблюдается небольшие расхождения в хранении составных типов. Временная таблица по непонятной лично мне причине требует еще одно поле *_Type, в котором фиксируется что текущая запись является ссылкой (не строкой, не датой и т.д) хотя в ТЗ мы не о каких других типов отличных от ссылок и не упоминали.
Поэтому полученный запрос приходится дорабатывать:
   SELECT
        0x08 AS _Q_000_F_000Type,                                                    
        CASE
            WHEN _Reference32_Q_000_T_001._IDRRef IS NOT NULL
            THEN 0x0000000A
            ELSE NULL
        END AS _Q_000_F_000TRef,
        _Reference32_Q_000_T_001._IDRRef AS _Q_000_F_000RRef,
        CAST(_Reference32_Q_000_T_001._Description AS NVARCHAR(100)) AS _Q_000_F_001
    FROM
        _Reference32 _Reference32_Q_000_T_001 WITH(NOLOCK)
UNION ALL
    SELECT
        0x08 AS _Q_000_F_000Type,                                                    
        CASE
            WHEN _Reference78_Q_001_T_001._IDRRef IS NOT NULL
            THEN 0x0000004E
            ELSE NULL
        END AS _Q_000_F_000TRef,
        _Reference78_Q_001_T_001._IDRRef AS _Q_000_F_000RRef,
        _Reference78_Q_001_T_001._Description AS _Q_000_F_001
    FROM
        _Reference78 _Reference78_Q_001_T_001 WITH(NOLOCK)

Выполнив этот запрос через интерфейс временных таблиц мы получим стандартный объект результат запроса, который:

  • может содержать ЛЮБЫЕ необходимые данные, в том числе и из других баз;

  • использовать группировки при выводе;

  • в 1000 раз быстрее перебора коллекции RecordSet (для получения результата);


Таким образом все недостатки прямых запросов (заявленные в целях) теперь в прошлом

Полностью (без контроля ошибок) данная конструкция на встроенном языке выглядит примерно так:

Conn= Новый COMОбъектADODB.Connection;
Conn.ConnectionString = "Provider=SQLOLEDB;Password=ooo;Persist Security Info=True;User ID=sa;InitialCatalog=dogovor_81;Data Source=1c";
Conn.Open();
Запрос = Новый Запрос("ВЫБРАТЬ
| ТЗ_Null.*
|ПОМЕСТИТЬ ТЗ
|ИЗ
| &ТЗ КАК ТЗ_Null");
МВТ = Новый МенеджерВременныхТаблиц;
Запрос.МенеджерВременныхТаблиц = МВТ;
ТЗ=Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("Ссылка",Новый ОписаниеТипов("СправочникСсылка.Валюты, СправочникСсылка.Контрагенты"));
ТЗ.Колонки.Добавить("Наименование",Новый ОписаниеТипов("Строка"),,Новый КвалификаторыСтроки(100));
Запрос.УстановитьПараметр("ТЗ", ТЗ);
RecSetRead=Conn.Execute("Select GETDATE() as CDate");
RecSetRead.MoveFirst();
ДатаОтсчета=RecSetRead.Fields(0).Value;
ДатаОтсчета="{ts '"+Формат(Год(ДатаОтсчета),"ЧЦ=4; ЧВН=; ЧГ=")+"-"+Формат(Месяц(ДатаОтсчета),"ЧЦ=2; ЧВН=")+"-"+Формат(День(ДатаОтсчета),"ЧЦ=2; ЧВН=")+" "+Формат(Час(ДатаОтсчета),"ЧЦ=2; ЧН=; ЧВН=")+":"+Формат(Минута(ДатаОтсчета),"ЧЦ=2; ЧН=; ЧВН=")+":"+Формат(Секунда(ДатаОтсчета),"ЧЦ=2; ЧН=; ЧВН=")+"'}";
//создаем временную таблицу
РезультатЗапроса = Запрос.Выполнить();
//переименовываем временную таблицу чтобы она была доступна из другой сессии
ТекстЗапросаTSQL="DECLARE @table_name varchar(100)
|--Перименовывем таблицу
|set @table_name = 'temp_'+Left(cast(newid() as varchar(36)),8)
|Select @table_name as table_name
|update tempdb..sysobjects
|set name = @table_name
|where id in (SELECT top 1
|id
|FROM
|tempdb..sysobjects
|WHERE
|Len(Name)=128 and info >= "+ТЗ.Колонки.Количество()+" and
|CRDate >= "+ДатаОтсчета+")";
RecSetRead=Conn.Execute(ТекстЗапросаTSQL);
RecSetRead.MoveFirst();
ИмяВТ=RecSetRead.Fields(0).Value;
Conn.Execute(СтрЗаменить("
|INSERT INTO tempdb..##tt
|SELECT
|0x08 AS _Q_000_F_000Type,
|CASE
|WHEN _Reference32_Q_000_T_001._IDRRef IS NOT NULL
|THEN 0x0000000A
|ELSE NULL
|END AS _Q_000_F_000TRef,
|_Reference32_Q_000_T_001._IDRRef AS _Q_000_F_000RRef,
|CAST(_Reference32_Q_000_T_001._Description AS NVARCHAR(100)) AS _Q_000_F_001
|FROM
|_Reference10 _Reference32_Q_000_T_001 WITH(NOLOCK)
|UNION ALL
|SELECT
|0x08 AS _Q_000_F_000Type,
|CASE
|WHEN _Reference78_Q_001_T_001._IDRRef IS NOT NULL
|THEN 0x0000004E
|ELSE NULL
|END AS _Q_000_F_000TRef,
|_Reference78_Q_001_T_001._IDRRef AS _Q_000_F_000RRef,
|_Reference78_Q_001_T_001._Description AS _Q_000_F_001
|FROM
|_Reference78 _Reference78_Q_001_T_001 WITH(NOLOCK)
|","tempdb..##tt","tempdb.."+ИмяВТ));
//возвращаем результат
Запрос.Текст = "ВЫБРАТЬ
| ТЗ.*
|ИЗ
| ТЗ КАК ТЗ";
Запрос.МенеджерВременныхТаблиц = МВТ;
Результат = Запрос.Выполнить();
Conn.Execute("DROP TABLE tempdb.."+ИмяВТ);

Резюме

Изложенный здесь подход  является своеобразным мостом между двумя системами 1С и SQL,  позволяющий объединить ресурсы этих систем для решения специфических,  и не очень задач. Основные сложности в данной технология связанны с правильным определением временной таблицы. Тут много причин .. основные это то что временные таблицы ведут себя не так как продекларировали разработчики 1С, в частности "..после уничтожения менеджера временных таблиц уничтожаются временные таблицы" - на физическом уровне никакого уничтожения временной таблицы не происходит (даже после УНИЧТОЖИТЬ ТЗ), таблица просто очишается и если в следующий раз при создании временной таблицы ее состав колонок будут схож с уже существующими(созданными ранее) временными таблицами,  данные будут помешены во временную таблицу созданную ранее. Поэтому после каждого цикла необходимо самостоятельно Conn.Execute("DROP TABLE tempdb.."+ИмяВТ); уничтожить временную таблицу, что бы не возникало  проблемм в последующих определениях.



Comments