servitRM
Несмотря на перманентные похороны Delphi, эта платформа построения Desktop приложений живёт и здравствует, а со сменой владельца даже обретает второе дыхание и продолжает оставаться основным инструментом для тысяч разработчиков во всём мире.
Как и с любыми другими СУБД, Delphi прекрасно взаимодействует с СУБД Caché.
Из Delphi можно подключиться к Caché, используя следующие интерфейсы:
В данной статье будут рассмотрены примеры использования объектного интерфейса при работе с СУБД Caché.
Но для начала приведу несколько коротких примеров на VBScript, которые вы можете запустить непосредственно из Windows Проводника.
Пример прямого доступа:
Set f = CreateObject("VISM.VisMCtrl.1")
f.Server="CN_IPTCP:localhost[1972]:_system:@ SYS"
f.NameSpace="SAMPLES"
f.Execute("=$zv")
WScript.Echo f.VALUE
Пример реляционного доступа:
Set cn=Createobject("ADODB.Connection")
cn.ConnectionString="DRIVER={InterSystems ODBC35}; SERVER=127.0.0.1; PORT=1972; DATABASE=SAMPLES; UID=_system; PWD=SYS"
cn.open
WScript.Echo "Succesfully!"
Пример объектного доступа:
Set f = CreateObject("CacheActiveX.Factory")
Set rs = CreateObject("CacheActiveX.ResultSet")
If Not f.IsConnected() Then
f.Connect("cn_iptcp:127.0.0.1[1972]:SAMPLES:_SYSTEM:SYS")
Set rs=f.DynamicSQL("select TOP 3 * from Sample.Person")
rs.Execute()
while rs.Next
WScript.Echo rs.Get("SSN")
wend
rs.Close()
Set person = f.Static("Sample.Person")
age=person.CurrentAge(45678)
WScript.Echo age
End If
Похожим образом вы можете работать с СУБД Caché, использя JScript, Visual Basic, C++ Builder, и т.д.
Примечание: Для Java и .NET предусмотрено несколько нативных интерфейсов, которые обеспечивают гораздо больше возможностей.
Позднее и раннее связывание
Существует два подхода при работе с объектами Caché из Delphi:
У каждого из этих подходов есть свои преимущества и недостатки, которые впрочем компенсируют друг друга.
При позднем связывании разработчику недоступен подсказчик кода, соответственно высока вероятность допустить ошибку в коде, которая будет обнаружена лишь во время выполнения программы. Скорость работы несколько ниже, чем при раннем связывании, так как код выполняется динамически.
Раннее связывание лишено этих недостатков, но за это приходится платить тем, что при изменении интерфейса пользовательских классов в БД или при переходе на новую версию СУБД Caché необходимо перегенерировать прокси-классы для клиентского приложения.
Комбинируя оба этих подхода можно добиться оптимальной производительности и удобства в работе.
В данной статье будет в основном использоваться раннее связывание, так как данный подход меньше описан по сравнению с поздним связыванием.
Для работы Caché Objects используются две библиотеки, которые по умолчанию устанавливаются вC:\Program Files\Common Files\InterSystems\Cache\:
- CacheObject.dll (устаревшая);
- CacheActiveX.dll (рекомендуемая).
Начиная с версии Caché 5.1, разработчики InterSystems настоятельно рекомендуют использовать новую библиотеку CacheActiveX.dll. Библиотека CacheObject.dll оставлена лишь для совместимости со старыми приложениями. Поэтому в данной статье я буду основываться на CacheActiveX.dll.
Об отличиях данных библиотек и нюансах, которые нужно учитывать при переходе на новую версию, можно почитать в Upgrading from CacheObject.dll.
В каталоге выше можно найти и другие файлы, которые могут Вам пригодиться:
- DelphiCallback.dll;
- CacheList.ocx;
- CacheQuery.ocx;
- VISM.ocx.
Импорт и установка компонент Caché Objects ActiveX в Delphi
В данном разделе описывается установка основных классов и интерфейсов в среде Delphi для работы с объектами в СУБД Caché, используя раннее связывание. Для работы с поздним связыванием этот раздел можно пропустить.
Итак, по порядку:
- выбираем пункт меню Component > Import Component...;
- выбираем для начала библиотеку типов CacheActiveX 2.0 Type Library;
- задаём имя нашей закладки, куда мы хотим установить наши компоненты, а также другие параметры;
- создаём наш модуль, пока без установки;
- повторяем пункты 2-4 для следующих библиотек:
- CacheActiveX 2.0 Type Library;
- CacheList ActiveX Control module;
- CacheQuery ActiveX Control module;
- DelphiCallback 1.0 Type Library;
- VisM 7.2 ActiveX Control;
- TL 1.0 Type Library.
Примечание: Создать модули можно и с помощью утилиты tlibimp.exe, входящей в поставку Delphi.
- создаём новый проект типа Package и добавляем в него все нами ранее созданные модули. Компилируем проект и инсталлируем наш пакет. Вот что в итоге у нас должно получиться:
Примечание: Для более ранних версий Delphi процесс создания модулей отличается незначительно:
- выбираем пункт меню Project > Import Type Library...;
- далее см. пункты выше.
Генерация пользовательских прокси-классов
В данном разделе описывается генерация пользовательских прокси-классов для работы с объектами в СУБД Caché, используя раннее связывание. Для работы с поздним связыванием этот раздел можно пропустить.
Для начала необходимо сгенерировать ODL-файл, содержащий все необходимые нам пользовательские классы. Для этого следует воспользоваться утилитой odl_generator.exe, поставляемой с СУБД Caché.
ПРИМЕР:
odl_generator.exe -conn cn_iptcp:localhost[1972]:USER:_system:SYS -class-list test.txt -lib-name test -dir MIDL
В данном примере утилита подключается к области USER, генерирует прокси-классы для классов перечисленных в файле test.txt, и сохраняет результат в файл MIDL\test.odl
Внимание: Полученный таким образом файл предназначен для использования с библиотекойCacheActiveX.dll. Чтобы сгенерировать ODL-файл, предназначенный для работы со старой библиотекой, следует воспользоваться методом ExportODL класса %SYSTEM.OBJ
ПРИМЕР:
set list="%Library.Status,Sample.Person"
do $system.OBJ.ExportODL(list,"c:\MIDL\test.odl","-d",.err)
Итак, ODL-файл получен, теперь следует его скомпилировать, чтобы получить TLB-файл, а затем и PAS-файл. Для этого можно воспользоваться утилитами midl.exe или mktypelib.exe, входящими в комплект разработчика Visual C++.
ПРИМЕР:
midl /I . test.odl /tlb test.tlb
tlibimp.exe -C- -P+ -Hr- -Ha- -Hs- -XM- test.tlb
Примечание: Все вышеперечисленные этапы вы можете автоматизировать с помощью MAC-программы СУБД Caché.
Подготовительные работы на сервере
Создадим в нашей тестовой базе следующие классы данных:
/// Встраиваемый класс
Class pas.s Extends %SerialObject
{
/// Целое число (64-бит).
Property aInteger As %Integer;
/// Строка. Максимальная длина по умолчанию - 50 символов.
Property aString As %String;
}
/// Вспомогательный хранимый класс.
Class pas.a Extends %Persistent
{
/// Индекс на поле aA;
Index aAIndex On aA;
/// Отношение один-ко-многим, мощность "один". В SQL преобразуется в foreign key.
Relationship aA As pas.test [ Cardinality = one, Inverse = aChilds ];
Property aInteger As %Integer;
Property aString As %String;
}
/// Подключение вспомогательного файла %occIO.inc с макросами.
Include %occIO
/// Основной хранимый класс.
Class pas.test Extends %Persistent
{
/// Задаём поля, возвращаемые при выборке всех экземпляров данного класса;
Parameter EXTENTQUERYSPEC As ROWSPEC [ Flags = LIST ] = aBoolean,aInteger,aString,aDate,aTimeStamp";
/// Булево значение (true/false/null);
Property aBoolean As %Boolean;
Property aInteger As %Integer;
Property aString As %String;
/// Дата;
Property aDate As %Date;
/// Дата+время;
Property aTimeStamp As %TimeStamp;
/// Символьный поток (CLOB);
Property aMemo As %GlobalCharacterStream;
/// Двоичный поток (BLOB);
Property aPhoto As %GlobalBinaryStream;
/// Отношение один-ко-многим, мощность "много". В SQL отсутствует аналог.
Relationship aChilds As pas.a [ Cardinality = many, Inverse = aA ];
/// Свойство-объект встраиваемого класса;
///<br>В SQL каждое свойство встраиваемого класса становится отдельным
///<br>свойством класса-контейнера.
Property aS As pas.s;
/// Коллекция-список строк;
///<br>В SQL это поле содержит значения, разделённые заданным разделителем, например запятой.
Property aListOfString As list Of %String;
/// Коллекция-список объектов вспомогательного класса;
///<br>В SQL это поле содержит значения первичных ключей объектов, разделённые заданным разделителем, например запятой.
Property aListOfA As list Of pas.a;
/// Коллекция-массив строк;
///<br>В SQL формируется виртуальная таблица.
Property aArrOfString As array Of %String;
/// Коллекция-массив объектов вспомогательного класса;
///<br>В SQL формируется виртуальная таблица.
Property aArrOfA As array Of pas.a;
/// Метод экземпляра класса.
/// <br>Переопределяем встроенное событие, возникающее перед сохранением объекта.
Method %OnBeforeSave(insert As %Boolean) As %Status [ Private,ServerOnly = 1 ]
{
; выводим имя класса и метода текущего контекста
write "Hello from Cache! (",$$$CurrentClass,":",$$$CurrentMethod,")",!
quit $$$OK
}
/// Запрос - хранимая процедура: демонстрация передачи простых типов данных.
Query test1(ABoolean As %Boolean, AInteger As %Integer, AString As %String,ADate As %Date, ATimeStamp As %TimeStamp) As %SQLQuery(CONTAINID = 1, ROWSPEC = "ID:%String,aBoolean:%Boolean,aInteger:%Integer,aString:%String,aDate:%Date,aTimeStamp:%TimeStamp") [ SqlProc ]
{
SELECT %ID,aBoolean,aInteger,aString,aDate,aTimeStamp FROM pas.test WHERE
(aBoolean=:ABoolean or :ABoolean is null)
AND (aInteger=:AInteger or :AInteger is null)
AND (aString=:AString or :AString is null)
AND (aDate<:ADate or :ADate is null)
AND (aTimeStamp<=:ATimeStamp or :ATimeStamp is null)
}
/// Запрос - хранимая процедура: демонстрация передачи параметра встроенного типа данных "список"
///<br>и использование его в запросе совместно с конструкцией %INLIST.
Query test2(AList As %List) As %SQLQuery(CONTAINID = 1, ROWSPEC = "ID:%String,aBoolean:%Boolean,aInteger:%Integer,aString:%String,aDate:%Date,aTimeStamp:%TimeStamp") [ SqlProc ]
{
SELECT %ID,aBoolean,aInteger,aString,aDate,aTimeStamp FROM pas.test WHERE ID %INLIST :AList
}
/// Метод класса.
ClassMethod test3(AList As %List) As %Status
{
; выводим на текущее устройство значение параметра AList
write AList
; сохраняем значение в глобал
set ^pastest=AList
quit $$$OK
}
/// Демонстрация генерации ошибки.
ClassMethod test4() As %Status
{
quit $$$ERROR($$$GeneralError,"My error!")
}
/// Демонстрация передачи заранее неизвестного количества параметров.
ClassMethod test5(Arg1... As %List) As %Status
{
; выводим общее количество переданных параметров и их значения
write "Invocation has ",$get(Arg1, 0)," element",$select(($get(Arg1, 0)=1):"", 1:"s"),!
for i = 1 : 1 : $get(Arg1, 0)
{
write:($data(Arg1(i))>0) "Argument[",i,"]:",?15,$get(Arg1(i),"<NULL>"),!
}
quit $$$OK
}
/// Демонстрация передачи параметров более сложных типов: объекта нашего класса и потоков.
/// <br>Также демонстрируется передача параметра по ссылке и выходных параметров.
/// <br>Параметры:
/// <br><var>ID</var> - строка;
/// <br><var>A</var> - объект вспомогательного класса;
/// <br><var>BLOB</var> - двоичный поток;
/// <br><var>RS1</var> - символьный поток, содержащий данные запроса в формате Borland ® MyBase (DataSnap (TM)) XML DataSet;
/// <br><var>RS2</var> - символьный поток, содержащий данные запроса в формате Borland ® MyBase (DataSnap (TM)) XML DataSet;
ClassMethod test6(
ID As %String,
ByRef A As pas.a,
Output BLOB As %BinaryStream,
Output RS1 As %CharacterStream,
Output RS2 As %CharacterStream) As %Status
{
// меняем одно из свойств объекта, переданного по ссылке
set A.aString=999
// создаём объект двоичного потока
set BLOB=##class(%GlobalBinaryStream).%New()
// записываем в поток данные
do BLOB.Write("123")
// создаём объекты символьного потока
set RS1=##class(%GlobalCharacterStream).%New()
set RS2=##class(%GlobalCharacterStream).%New()
// создаём объект пользовательского класса, позволяющего выгружать данные в формат XML для TClientDataSet
set cds=##class(%XML.ZMyBaseDataSet).%New()
// подготавливаем запрос
do cds.Prepare("select * from pas.a where id %inlist ?")
// передаём данные в запрос
do cds.SetArgs($listbuild(1,2,3,9))
// выгружаем данные в символьный поток в формате XML
do cds.XMLExportToStream(.RS1)
// закрываем (инициализируем заново) объект для выполнения другого запроса
do cds.Close()
do cds.Prepare("select ID,aBoolean,aInteger,aString,aDate,aTimeStamp from pas.test")
do cds.XMLExportToStream(.RS2)
do cds.Close()
// возвращаем статус "Успешно"
quit $$$OK
}
}
Сгенерируем для них прокси-классы, а также для следующих классов:
- %Library.ArrayOfDataTypes
- %Library.ArrayOfObjects
- %Library.ListOfObjects
- %Library.ListOfDataTypes
- %Library.RelationshipObject
Теперь, если подключить к проекту сгенерированный PAS-файл, становятся доступны наиболее важные методы и свойства наших классов:
увеличить
Внимание: Классы по работе с потоками и некоторые другие не следует импортировать из СУБД Caché, так как они уже зашиты в библиотеку CacheActiveX.dll и несовместимы со сгенерированными прокси-классами.
Подключение к СУБД Caché
Позднее связывание (старая библиотека):
var _f:variant;
begin
_f:=CreateOleObject('CacheObject.Factory');
if _f.Connect(_f.ConnectDlg('+%up')) then ShowMessage('OK') else ShowMessage('ERROR');
Позднее связывание (новая библиотека):
var _f:variant;
begin
_f:=CreateOleObject('CacheActiveX.Factory');
if _f.Connect(_f.ConnectDlg('+%up')) then ShowMessage('OK') else ShowMessage('ERROR');
Раннее связывание (новая библиотека):
type
Tfm = class(TForm)
f: TFactory;
...
begin
...
if f.Connect1(f.ConnectDlg('+%up')) then ShowMessage('OK') else ShowMessage('ERROR');
...
Внимание: Старая библиотека использует сервис %Service_CacheDirect и только неаутентифицированный доступ, новая - %Service_Bindings и другие методы доступа.
Подробное описание методов класса TFactobry, а также других классов, можно найти в ActiveX API Reference
Примеры вызова запросов и методов, используя раннее связывание
Использование функциональности CallBack
Для использования функциональности CallBack из Delphi следует воспользоваться готовым классомTCallback из файла DelphiCallback.dll.
var f:TFactory;
Callback1: TCallback;
mm: TMemo;
...
f.SetOutput(Callback1.OleObject);
...
procedure Tfm.Callback1TextChanged(Sender: TObject; const p_bstrText: WideString);
begin
mm.Lines.Append(p_bstrText);
end;
Удаление всех данных
mm.Lines.Text:='KillExtent'#10#13;
test_(f.Static('pas.test')).SYS_KillExtent(1);
a(f.Static('pas.a')).SYS_KillExtent(1);
Пример создания, заполнения, сохранения и освобождения объектов различных типов
Про особенности закрытия объектов при работе через ActiveX можно почитать в статье Сергея Кудинова:Особенности закрытия объектов при работе через ActiveX, CPP-binding.
Пример кода:
...
uses test_TLB, AxCtrls, ComObj, ActiveX, Types;
...
const
N = 3;
var
i: integer;
_t: test_;
_a: A;
_s: s;
rel: RelationshipObject;
listStr: ListOfDataTypes;
listA: ListOfObjects;
arrStr: ArrayOfDataTypes;
arrA: ArrayOfObjects;
stream: IDispatch;
begin
Screen.Cursor := crSQLWait;
mm.Lines.Text:='Save'#10#13;
try
try
_t := test_(f.New('pas.test'));
_t.aBoolean := true;
_t.aInteger := 50;
_t.aString := 'Тестовая строка';
Variant(_t).aDate := nil;
_t.aDate := _t.aDateDisplayToLogical('02.03.2001');
_t.aTimeStamp := _t.aTimeStampDisplayToLogical('1900-01-02 12:34:55');
stream := _t.aMemo;
ICharStream(stream).Write('Символьный поток');
stream := nil;
stream := _t.aPhoto;
IBinaryStream(stream).FileRead('C:\test.jpg');
stream := nil;
rel := RelationshipObject(_t.aChilds);
for i := 1 to N do
begin
_a := A(f.New('pas.a'));
_a.aInteger := i;
_a.aString := 'rel' + IntToStr(i);
rel.Insert(_a);
_a.SYS_Close;
end;
_t.aChilds := rel;
rel.SYS_Close;
_s := s(_t.aS_);
_s.aInteger := 1;
_s.aString := 's1';
_s.SYS_Close;
listStr := ListOfDataTypes(f.New('%ListOfDataTypes'));
for i := 1 to N do
listStr.Insert('str' + IntToStr(i));
_t.aListOfString := listStr;
listStr.SYS_Close;
listA := ListOfObjects(f.New('%ListOfObjects'));
for i := 1 to N do
begin
_a := A(f.New('pas.a'));
_a.aInteger := i;
_a.aString := 'listA' + IntToStr(i);
listA.Insert(_a);
_a.SYS_Close;
end;
_t.aListOfA := listA;
listA.SYS_Close;
arrStr := ArrayOfDataTypes(f.New('%ArrayOfDataTypes'));
for i := 1 to N do
arrStr.SetAt('astr' + IntToStr(i), 'arraykey' + IntToStr(i));
_t.aArrOfString := arrStr;
arrStr.SYS_Close;
arrA := ArrayOfObjects(f.New('%ArrayOfObjects'));
for i := 1 to N do
begin
_a := A(f.New('pas.a'));
_a.aInteger := i;
_a.aString := 'arrayA' + IntToStr(i);
arrA.SetAt(_a, 'arraykey' + IntToStr(i));
_a.SYS_Close;
end;
_t.aArrOfA := arrA;
arrA.SYS_Close;
_t.SYS_Save(0);
stream := _t.aPhoto;
SetOlePicture(img.Picture, IBinaryStream(stream).GetPicture);
stream := nil;
_t.SYS_Close;
_t := nil;
mm.Lines.Append('OK');
except
on E: Exception do
begin
mm.Lines.Append(E.Message);
end;
end;
finally
Screen.Cursor := crDefault;
f.ForceSync;
end;
Запрос Extent (выборка всех экземпляров хранимого класса)
var mm: TMemo;
rs: TResultSet;
...
mm.Lines.Text:='Extent'#10#13;
rs.ConnectTo(IResultSet(f.ResultSet('pas.test', 'Extent')));
rs.Execute;
while rs.Next do
begin
mm.Lines.Append(Format('ID = %s',[rs.GetDataAsString(1)]));
mm.Lines.Append(Format('aBoolean = %s',[rs.Get('aBoolean')]));
mm.Lines.Append(Format('aInteger = %s',[rs.Get('aInteger')]));
mm.Lines.Append(Format('aString = %s',[rs.Get('aString')]));
mm.Lines.Append(Format('aDate = %s',[rs.Get('aDate')]));
mm.Lines.Append(Format('aTimeStamp = %s',[rs.Get('aTimeStamp')]));
mm.Lines.Append('-----');
end;
rs.Close;
rs.Disconnect;
Запрос test1
var
i: integer;
...
mm.Lines.Text:='test1'#10#13;
rs.ConnectTo(IResultSet(f.ResultSet('pas.test', 'test1')));
rs.SetParam(1, null);
rs.SetParam(2, 50);
rs.SetParam(3, null);
rs.SetParam(4, '03.03.2001');
rs.SetParam(5, '1900-01-02 12:34:55.0');
rs.Execute;
while rs.Next do
begin
for i := 1 to rs.GetColumnCount do
mm.Lines.Append(rs.GetColumnName(i)+' = '+rs.GetDataAsString(i));
mm.Lines.Append('-----');
end;
rs.Close;
rs.Disconnect;
Запрос test2
var
i: integer;
syslist: TSyslist;
...
mm.Lines.Text:='test2'#10#13;
syslist.Clear;
syslist.Add(1);
syslist.Add(2);
syslist.Add(3);
rs.ConnectTo(IResultSet(f.ResultSet('pas.test', 'test2')));
rs.Execute(syslist.DefaultInterface);
while rs.Next do
begin
for i := 1 to rs.GetColumnCount do
mm.Lines.Append(rs.GetColumnName(i)+' = '+rs.GetDataAsString(i));
mm.Lines.Append('-----');
end;
rs.Close;
rs.Disconnect;
Метод test3. Работа с объектом класса TSysList (тип %List в Caché)
mm.Lines.Text:='test3'#10#13;
syslist.Clear;
syslist.Add('16');
syslist.Add('42');
syslist.Add('35');
test_(f.Static('pas.test')).test3(syslist.DefaultInterface);
Метод test4. Обработка ошибок
mm.Lines.Text:='test4'#10#13;
try
test_(f.Static('pas.test')).test4();
except
on E: Exception do
begin
mm.Lines.Append(E.Message);
end;
end;
Метод test5
mm.Lines.Text:='test5'#10#13;
syslist.Clear;
syslist.Add('16');
syslist.Add('42');
syslist.Add('35');
test_(f.Static('pas.test')).test5(syslist.DefaultInterface);
Метод test6
В данном примере используется класс %XML.ZMyBaseDataSet, который можно найти здесь. С его помощью можно на сервере формировать данные в формате Borland ® MyBase (DataSnap (TM)) XML DataSet. В том числе и для веб-сервисов.
cds1,cds2:TClientDataSet;
...
var _a:a;
__a,blob,rs1,rs2:IDispatch;
cs1,cs2:ICharStream;
begin
mm.Lines.Text:='test6'#10#13;
try
_a:=a(f.OpenId('pas.a','1'));
__a:=_a;
test_(f.Static('pas.test')).test6('1',__a,blob,rs1,rs2);
cs1:=ICharStream(rs1);
cs2:=ICharStream(rs2);
mm.Lines.Append('A.aString = '+_a.aString);
mm.Lines.Append('BLOB.Size = '+IntToStr(IBinaryStream(blob).size));
mm.Lines.Append('RS1.Size = '+IntToStr(cs1.size));
mm.Lines.Append('RS2.Size = '+IntToStr(cs2.size));
cds1.XMLData:=cs1.Data;
cds2.XMLData:=cs2.Data;
finally
_a.SYS_Close;
_a:=nil;
__a:=nil;
blob:=nil;
rs1:=nil;
rs2:=nil;
cs1:=nil;
cs2:=nil;
f.ForceSync;
end;
Для поддержки классом %XML.ZMyBaseDataSet данных в Unicode, а также других нереализованных типов данных Вам необходимо будет его доработать самостоятельно.
Распространение приложения
Для установки с приложением всех необходимых драйверов на новый компьютер следует скопировать все (для простоты. Набор нужных файлов можно ограничить) файлы из каталога C:\Program Files\Common Files\InterSystems\Cache\ и зарегистрировать в системе некоторые из них с помощью утилиты regsvr32.exe.
ПРИМЕР:
regsvr32.exe /s "C:\Program Files (x86)\Common Files\Intersystems\Cache\CacheQuery.ocx"
regsvr32.exe /s "C:\Program Files (x86)\Common Files\Intersystems\Cache\CacheFormWizard.dll"
regsvr32.exe /s "C:\Program Files (x86)\Common Files\Intersystems\Cache\CacheList.ocx"
regsvr32.exe /s "C:\Program Files (x86)\Common Files\Intersystems\Cache\CacheActiveX.dll"
regsvr32.exe /s "C:\Program Files (x86)\Common Files\Intersystems\Cache\vism.ocx"
regsvr32.exe /s "C:\Program Files (x86)\Common Files\Intersystems\Cache\TL.dll"