воскресенье, 10 апреля 2016 г.

Caché: Прямой доступ через ODBC


Общие моменты

Рассмотрим возможность выполнения прямого доступа к Caché через ODBC-клиента.

К прямому доступу отнесем выполнение операций
  • чтение локальных и глобальных переменных
  • запись локальных и глобальных переменных
  • чтение результата выполнения подпрограмм
  • выполнение подпрограмм
  • косвенное выполнение выражения
  • получение результата выполнения подпрограмм, который выводится ими в текущее устройство с помощью команды write
В целом, приведенный список операций путем различного их комбинирования позволяет выполнить совершенно произвольные операции с базе данных. При этом основным направлением данной статьи выберем выполнение такого прямого доступа из клиентской программы, которая соединяется с базой через ODBC - шлюз.

Выбор способа соединения через 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 не усекается.
Создав запрос, в его sql - выражении ничего не пишем, поскольку оно нам совершенно не понадобится. Отмечаем запрос флагом "SQL Stored Procedure". Указываем тип запроса "%Library.Query". В спецификации выдаваемых запросом колонок указываем их в том же синтаксисе, как и обычные параметры функции, например "Name:%CacheString". Если колонок несколько, отделяем их спецификации запятыми. В нашем случае будем использовать только строковые значения. В случае использования иных типов следует обратиться к документации InterSystems. В качестве параметров запроса (что запрос принимает извне) указываем параметры также. Конечно, если они есть. Если параметров запроса нет, ничего не указываем.

Теперь создаем метод, ответственный за выполнение запроса. Он имеет один обязательный параметр, передаваемый по ссылке, обычно обозначается как &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();
};

Комментариев нет:

Отправить комментарий