Цели
Возможность
формирования прямых запросов к БД 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_F000RRef соответсвует (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.."+ИмяВТ); уничтожить временную таблицу, что бы не возникало проблемм в последующих определениях.
|