Общие моменты
Рассмотрим возможность выполнения прямого доступа к 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();
};
Комментариев нет:
Отправить комментарий