Общие моменты
Рассмотрим возможность выполнения прямого доступа к Caché через ODBC-клиента.К прямому доступу отнесем выполнение операций
- чтение локальных и глобальных переменных
- запись локальных и глобальных переменных
- чтение результата выполнения подпрограмм
- выполнение подпрограмм
- косвенное выполнение выражения
- получение результата выполнения подпрограмм, который выводится ими в текущее устройство с помощью команды write
Выбор способа соединения через ODBC сделан по причине его доступности. Соединение через ODBC может быть настроено с помощью средств, входящих в дистрибутив Caché. Демонстрировать ODBC - клиента будем на примере ActivePerl реализации Perl, с использованием пакета Win32::ODBC. Такого же клиента можно составить, используя C - программу, напрямую вызывающую функции в odbc32.dll, с помощью компонент modbc для Delphi / BCB или с помощью штатных компонент доступа к данным Delphi / BCB, настроив доступ через ODBC. Выбор Perl сделан по причине простоты использования его в демонстрационных целях.
Пример выполнен для версии Caché 4.1.
В качестве технологической основы, позволяющей вызвать прямой код CacheObjectScript, выберем возможность составить запрос класса таким образом, что у него переопределены операции выполнения, выборки и закрытия запроса, при этом сам запрос объявлен как хранимая процедура.
Краткое технологическое введение в написание запросов с переопределением операций выполнения, выборки данных и закрытия запроса
Создадим класс, наследник класса %Library.RegisteredObject. В этом классе в качестве его члена создадим запрос с именем, отражающим смысл выполняемых этим запросом действий. Пусть в примере этот запрос называется TestQuery. Это слово целиком будет идентифицирующим запрос и остальные идентификаторы, которые будут использоваться, будут являться производными от него.Запрос для его использования в качестве хранимой процедуры в наших целях имеет несколько пунктов, на которые должен обратить внимание программист:
- Название запроса. Это строка, например, TestQuery.
- Флаг, объявляющий этот запрос хранимой процедурой.
- Параметры запроса, передаваемые ему извне.
- Спецификация колонок выдаваемых в ответ.
- Метод класса с именем, производным от имени запроса, ответственный за открытие запроса. Имя образуется как "QueryName"_Execute. В нашем примере это TestQueryExecute.
- Метод класса с именем, производным от имени запроса, ответственный за выборку данных. Имя класса образуется как "QueryName"_Fetch. В нашем примере это TestQueryFetch.
- Метод класса с именем, производным от имени запроса, ответственный за закрытие запроса. Имя класса образуется как "QueryName"_Close. В нашем примере это TestQueryClose.
- Типы данных следует объявлять совместимыми с ODBC. В нашем случае передаваться будут только строки, при этом их усечение нецелесообразно, поэтому вместо обычного типа %String будем использовать тип %CacheString. Он при передаче через ODBC не усекается.
Теперь создаем метод, ответственный за выполнение запроса. Он имеет один обязательный параметр, передаваемый по ссылке, обычно обозначается как &QHandle:%Binary. После него указываем те же параметры, что указывали при спецификации параметров запроса, в том же порядке. Например, если параметры запроса Arg1:%CacheString,Arg2:%CacheString, то параметры метода &QHandle:%Binary,Arg1:%CacheString,Arg2:%CacheString. Имя метода следует объявить производным от имени запроса точно так же как объявляются производными имена методов геттеров и сеттеров свойств. В нашем примере это TestQueryExecute. Методу следует указать флаг "метод класса".
Метод TestQueryExecute вызывается при выполнении хранимой процедуры один раз и должен сохранить данные, специфичные для запроса, в переменной, переданной косвенно через QHandle. Эта переменная будет также косвенно передаваться в два другие метода в качестве первого параметра - TestQueryFetch и TestQueryClose. В ней следует хранить состояние запроса - начат - промежуточное состояние - закончен.
Аналогично создаются методы класса TestQueryFetch и TestQueryClose. Параметры метода TestQueryFetch фиксированы - &QHandle:%Binary,&Row:%List,&AtEnd:%Integer=0. Все эти параметры передаются по ссылке. При этом переменная QHandle должна хранить состояние запроса, Row должен быть списком значений, выдаваемых при каждом fetch, AtEnd используется в качестве флага окончания данных для их выдачи. После того, как метод TestQueryFetch вернет индикатор окончания выдачи данных, будет вызван метод TestQueryClose. Этот метод ответственен за закрытие использованных в запросе объектов, освобождение использованных ресурсов и очистки использованных переменных. При этом формат переменной QHandle предоставляется на усмотрение программиста, но метод TestQueryClose должен установить ее в значение пустой строки.
Запрос прямого чтения переменной
Для выполнения чтения переменной создадим запрос с именем Read и соответствующие методы класса ReadExecute, ReadFetch и ReadClose. Сценарий вызова методов следующий - сначала вызывается ReadExecute, ему передаются параметры запроса. Потом для выдачи данных вызывается метод ReadFetch, пока он не вернет индикатор окончания набора данных. Потом вызывается ReadClose. Поэтому в нашем случае на ReadExecute запомним выражение, которое следует прочитать в переменной состояния запроса, в методе ReadFetch вернем косвенно прочитанное выражение и на следующем выполнении ReadFetch вернем индикатор окончания данных.Итого в нашем случае транспортный класс, созданные специально для поддержки прямого доступа через ODBC, получит запрос и методы класса:
method ReadExecute(&QHandle:%Binary,What:%CacheString) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : s QHandle=$lb(What) : q $$$OK } } method ReadClose(QHandle:%Binary) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : Set QHandle="" : Quit $$$OK } } method ReadFetch(&QHandle:%Binary,&Row:%List,&AtEnd:%Integer=0) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : n What,Value s What=$li(QHandle) : i What="" d : . s Row="" : . s AtEnd=1 : . s QHandle=$lb("") : e d : . s AtEnd=0 : . s @("Value="_What) : . s Row=$lb(Value) : . s QHandle=$lb("") : Quit $$$OK } } query Read(What:%CacheString) { type = %Library.Query(ROWSPEC="Value:%CacheString"); sqlquery = { : } sqlproc; sqlview = 0; }После трансляции класса получаем результат компиляции примерно в таком виде:
Including classes ... Building dependencies ... Compiling class User.DODBC ............................. done. Compiling routine User.DODBC.1 ... done. Compiling procedure info SQLUser.DODBC_Read ... done. Creating descriptor for class User.DODBC ... done.Таким образом, имя хранимой процедуры, которую следует вызвать, есть SQLUser.DODBC_Read. При этом DODBC - имя класса. Для проверки прямого доступа на чтение к глобальной переменной составим проверочный скрипт:
#!/usr/bin/perl $dsn = 'CACHE41User'; $login = '_system'; $pwd = 'SYS'; use Win32::ODBC; $connectstring = "DSN=$dsn;UID=$login;PWD=$pwd;"; if ( !($db = new Win32::ODBC( $connectstring))) { print "Cannot connect to database!\n"; print "Error: " . Win32::ODBC::Error() . "\n"; } else { $db->Sql("call SQLUser.DODBC_Read('^A')"); $db->FetchRow(); %data = $db->DataHash(); print "Value of ^A is: " . $data{ "Value"}; $db->Close(); };Здесь
$db->Sql("call SQLUser.DODBC_Read('^A')");вызывает созданную хранимую процедуру, один раз вызывается выборка данных (мы знаем, что будет только одна строка), на
%data = $db->DataHash();получаем всю строку данных в виде хэша, и на
print "Value of ^A is: " . $data{ "Value"};выводим значение колонки "Value" (мы знаем ее название).
Таким образом, мы уже получили расширение возможностей ODBC-клиента возможностью прямого чтения переменных. Эта же самая хранимая процедура может быть использована для чтения системных переменных и результатов возврата подпрограмм, например чтение имени хоста сервера
$db->Sql("call SQLUser.DODBC_Read('\$zu(110)')");
Запрос, выполняющий прямую запись
Для выполнения операции прямой записи переменных создадим второй запрос с именем Write и соответствующие методы WriteExecute, WriteFetch и WriteClose. С тем отличием, что операция записи принимает два параметра - куда записать и что записать и не возвращает данных. Выполним операцию записи непосредственно в методе WriteExecute. При этом наш класс получит еще три метода класса и запрос:method WriteClose(QHandle:%Binary) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : Set QHandle="" : Quit $$$OK } } method WriteExecute(&QHandle:%Binary, What:%CacheString,Value:%CacheString) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : s @(What_"="_Value) : s QHandle=$lb("") : Quit $$$OK } } method WriteFetch(&QHandle:%Binary,&Row:%List,&AtEnd:%Integer=0) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : s Row="" : s AtEnd=1 : s QHandle=$lb("") : Quit $$$OK } } query Write(What:%CacheString,Value:%CacheString) { type = %Library.Query( ROWSPEC="Empty:%CacheString",CONTAINID=""); sqlquery = { : } sqlproc; sqlview = 0; }Выполним проверку скриптом:
#!/usr/bin/perl $dsn = 'CACHE41User'; $login = '_system'; $pwd = 'SYS'; use Win32::ODBC; $connectstring = "DSN=$dsn;UID=$login;PWD=$pwd;"; if ( !($db = new Win32::ODBC( $connectstring))) { print "Cannot connect to database!\n"; print "Error: " . Win32::ODBC::Error() . "\n"; } else { $db->Sql("call SQLUser.DODBC_Write('^A','123456')"); $db->Close(); };При этом в глобали ^A запишется значение "123456".
Выполнение выражения
Для выполнения запроса, выполняющего выражение косвенно, составим аналогичный набор методов и запрос с именем Exec:method ExecClose(QHandle:%Binary) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : Set QHandle="" : Quit $$$OK } } method ExecExecute(&QHandle:%Binary,What:%CacheString) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : x What : s QHandle=$lb("") : Quit $$$OK } } method ExecFetch(&QHandle:%Binary,&Row:%List,&AtEnd:%Integer=0) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : s Row="" : s AtEnd=1 : s QHandle=$lb("") : Quit $$$OK } } query Exec(What:String) { type = %Library.Query(CONTAINID="",ROWSPEC="Empty:%CacheString"); sqlquery = { : } sqlproc; sqlview = 0; }Выполним проверку скриптом:
#!/usr/bin/perl $dsn = 'CACHE41User'; $login = '_system'; $pwd = 'SYS'; use Win32::ODBC; $connectstring = "DSN=$dsn;UID=$login;PWD=$pwd;"; if ( !($db = new Win32::ODBC( $connectstring))) { print "Cannot connect to database!\n"; print "Error: " . Win32::ODBC::Error() . "\n"; } else { $db->Sql("call SQLUser.DODBC_Exec('s ^A=\"abcdef\"')"); $db->Close(); };Таким образом, получили у ODBC - клиента возможность выполнения выражения на сервере.
Выполнение выражения, выводящего результат работы в текущее устройство
Осталось реализовать самое интересное - выполнение выражения, которое выдает результат в текущее устройство. Например, компиляция класса. Компиляция выполняется с помощью d CompileList^%apiOBJ(classname,"C",.tmp), где в переменной classname содержатся имена классов для компиляции, перечисленные через запятую. В нашем случае мы можем вызвать это выражение на выполнение, но ничего хорошего из этого не выйдет - компилятор классов выдает отчет о трансляции в текущее устройство, а нам нужно получить его в виде набора данных ODBC. Поэтому используем спулер - в функции Execute запроса переключим текущее устройство на спулер, выполним компиляцию и переключимся на предыдущее устройство. После этого отчет о компиляции (в общем случае - то, что вызванное выражение выводит в текущее устройство) оказывается в данных спулера. В функции Fetch нам остается только пройти с помощью $O по данным спулера и выдать строку за строкой. Итого, к нашему транспортному классу добавятся:method SpoolerExecClose(QHandle:%Binary) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : n file : s file=$li(QHandle) : k ^SPOOL(file) : Set QHandle="" : Quit $$$OK } } method SpoolerExecExecute(&QHandle:%Binary,What:%CacheString) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : n io,file,start,last : s io=$IO : s file=$J : s start=1 : k ^SPOOL(file) : o 2:(file:start) : u 2 : : x What : : u io : c 2 : s last=$O(^SPOOL(file,""),-1) : k ^SPOOL(file,last) : s QHandle=$lb(file,"") : Quit $$$OK } } method SpoolerExecFetch( &QHandle:%Binary,&Row:%List,&AtEnd:%Integer=0) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : n index,Value,file : s index=$li(QHandle,2) : s file=$li(QHandle,1) : : s index=$O(^SPOOL(file,index)) : : i index="" d : . s AtEnd=1 : . s Row="" : e d : . s AtEnd=0 : . s Row=$lb($G(^SPOOL(file,index))) : s $li(QHandle,2)=index : Quit $$$OK } } query SpoolerExec(What:%String) { type = %Library.Query(ROWSPEC="Row:%CacheString"); sqlquery = { : } sqlproc; sqlview = 0; }Здесь в функции Execute выполняется удаление последней записи спулера. В этой записи спулер сохраняет свои данные и они при выдаче клиенту не нужны. Их удаление упрощает код выборки данных. Проверим выполнение запроса используя скрипт:
#!/usr/bin/perl $dsn = 'CACHE41User'; $login = '_system'; $pwd = 'SYS'; use Win32::ODBC; $connectstring = "DSN=$dsn;UID=$login;PWD=$pwd;"; if ( !($db = new Win32::ODBC( $connectstring))) { print "Cannot connect to database!\n"; print "Error: " . Win32::ODBC::Error() . "\n"; } else { $db->Sql("call SQLUser.DODBC_SpoolerExec(" . "'d CompileList^\%apiOBJ(\"User.NewClass1\",\"C\",.tmp)')"); while( $db->FetchRow()) { undef %data; %data = $db->DataHash(); foreach $fieldname( keys %data) { print $data{ $fieldname}; }; }; $db->Close(); };Можно отметить, что при работе со спулером спулер помещает в конец строки символы перевода строк, поэтому в скрипте эти символы не вставляются.
Шлюз Caché ODBC изнутри
В качестве совсем уж необычного применения прямого доступа из ODBC - клиента посмотрим на ODBC - шлюз Caché изнутри. Посмотрим контекст выполнения метода SpoolerExecExecute, на время заменив в нем код x What на d ##class(User.DODBC).ShowContext() и добавив метод просмотра контекста:method ShowContext() { classmethod; not final; public; sqlproc = 0; code = { : n sl,i,var s sl=$zu(41) : f i=1:1:sl d : . w "Stack Level: "_i,! : . s var="" : . f s var=$zu(42,sl,var) q:var="" d : . . w "Variable "_var_" = ",$G(@var),! : . w "Context: "_$st(i),! : . w "Place: "_$st(i,"PLACE"),! : . w "MCODE"_$st(i,"MCODE"),! : q } }При этом, поскольку в частном случае параметр хранимой процедуры не используется, в него можно передать любую строку, заменив в предыдущем скрипте вызов на:
$db->Sql("call SQLUser.DODBC_SpoolerExec('')");Таким образом, мы добились чего хотели - клиент, использующий общедоступное средство коннекта (ODBC - шлюз), получил прямой доступ к базе данных Caché и может выполнить любой код. Предварительно это потребует установки на сервере соответствующего транспортного класса, в котором определены приведенные выше запросы и методы, реализующие хранимые процедуры. Способ доступа, конечно, необычный, но по-моему его имеет смысл иметь в виду на случай если штатных возможностей ODBC не хватит.
Раскланиваемся
Этот способ описан не с целью предоставления несанкционированного доступа к базе данных, а с целью предоставления программистам более гибких средств. Этот способ доступа я не использовал в коммерческих проектах, поэтому его публикация не должна нанести ущерб никаким фирмам.Спустя некоторое время после публикации вышеприведенного метода Mark Noten задал интересный вопрос:
Do you have any documentation how I could extend the procedure read so that I can read more than one value. Something like $db->Sql("call SQLUser.DODBC_Read('^A(1:100')"); for all the values of the global A with index from 1 tot 100.В результате в класс добавился еще один запрос, принимающий имя глобали, первый и последний индекс и возвращающий пару индекс - значение. В класс нужно добавить запрос вида
query OrderRead( Global:%CacheString,FirstIndex:%CacheString,LastIndex:%CacheString) { type = %Library.Query( ROWSPEC="Index:%CacheString,Value:%CacheString",CONTAINID=""); sqlquery = { : } sqlproc; sqlview = 0; }И три функции реализующие собственно доступ:
method OrderReadClose(QHandle:%Binary) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : Set QHandle="" : Quit $$$OK } } method OrderReadExecute(&QHandle:%Binary, Global:%CacheString,FirstIndex:%CacheString,LastIndex:%CacheString) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : s QHandle=$lb(Global,FirstIndex,LastIndex) : q $$$OK } } method OrderReadFetch(&QHandle:%Binary,&Row:%List,&AtEnd:%Integer=0) { returntype = %Library.Status; classmethod; not final; public; sqlproc = 0; code = { : n Global,Index,LastIndex,Value,OutIndex : s Global=$lg(QHandle,1) : s Index=$lg(QHandle,2) : s LastIndex=$lg(QHandle,3) : s:(Global'="")&(Index="") Index=$O(@Global@(Index)) : i (Global="")!(Index]]LastIndex) d : . s Row="" : . s AtEnd=1 : e d : . s AtEnd=0 : . s Value=$G(@Global@(Index)) : . s OutIndex=Index : . s Row=$lb(Value) : . s Index=$O(@Global@(Index)) : . s $li(QHandle,2)=Index,Row=$lb(OutIndex,Value) : . i Index="" s $li(QHandle,1)="" : Quit $$$OK } }Код проверялся скриптом вида
#!/usr/bin/perl $dsn = 'CACHE41User'; $login = '_system'; $pwd = 'SYS'; use Win32::ODBC; $connectstring = "DSN=$dsn;UID=$login;PWD=$pwd;"; if ( !($db = new Win32::ODBC( $connectstring))) { print "Cannot connect to database!\n"; print "Error: " . Win32::ODBC::Error() . "\n"; } else { print "Call to OrderRead\n"; $db->Sql("call SQLUser.DODBC_OrderRead('^A','1','100')"); while( $db->FetchRow()) { print "New row\n"; undef %data; %data = $db->DataHash(); foreach $fieldname( keys %data) { print $data{ $fieldname}, "\n"; }; }; $db->Close(); };
Комментариев нет:
Отправить комментарий