Теперь создадим второй объект для управления созданной ранее в базе данных DBDEMOS таблицей delivery.db. Закроем все открытые проекты и создадим новый серверный объект, такой же, как и предыдущий (рис. 17):
Рис. 17. Серверный объект DelDM для управления таблицей delivery.db
В отличие от предыдущего случая компонент TDatabase свяжем с базой данных DBDEMOS (рис. 18):
Рис. 18. Свойства компонента TDatabase серверного объекта delDM
В качестве свойства SQL компонента TQuery используем следующее SQL-предложение:
delete from delivery where OrdNum=:d
Затем отредактируем библиотеку типов, добавив три метода GetDelivery, AddDeliery и DelDelivery для получения данных из таблицы, добавления и удаления записи (рис. 19):
Рис. 19. Библиотека типов серверного объекта, управляющего таблицей delivery.db
Реализация этих методов имеет следующий вид:
unit del1; //Another simple MTS server //By N.Elmanova //01.12.1998 interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComServ, ComObj, VCLCom, StdVcl, BdeProv, BdeMts, DataBkr, DBClient, MtsRdm, Mtx, dels_TLB, DBTables, Provider, Db; type TdelDM = class(TMtsDataModule, IdelDM) deltable: TTable; DelProvider: TProvider; Database2: TDatabase; Query4: TQuery; Session2: TSession; private { Private declarations } public { Public declarations } protected function GetDelivery: OleVariant; safecall; procedure AddDelivery(OrdNum: Integer; const OrdName: WideString; const OrdAddr: WideString); safecall; procedure DelDelivery(OrdNum: Integer); safecall; end; var delDM: TdelDM; implementation {$R *.DFM} function TdelDM.GetDelivery: OleVariant; begin Result:=DelProvider.Data; SetComplete; end; procedure TdelDM.AddDelivery(OrdNum: Integer; const OrdName: WideString; const OrdAddr: WideString); begin try deltable.open; deltable.append; deltable.fieldbyname('OrdNum').Value:=OrdNum; deltable.fieldbyname('GoodsName').Value:=OrdName; deltable.fieldbyname('Address').Value:=OrdAddr; deltable.post; deltable.close; SetComplete; except SetAbort; raise; end; end; procedure TdelDM.DelDelivery(OrdNum: Integer); begin try database2.open; deltable.open; deltable.SetRangeStart; deltable.FieldByName('OrdNum').AsInteger:=OrdNum; deltable.SetRangeEnd; deltable.FieldByName('OrdNum').AsInteger:=OrdNum; deltable.ApplyRange; deltable.Delete; deltable.close; database2.close; except SetAbort; raise; end; end; initialization TComponentFactory.Create(ComServer, TdelDM, Class_delDM, ciMultiInstance, tmApartment); end.
Скомпилируем и установим данный объект в тот же "пакет", что и предыдущий.
Рекомендуется протестировать данный объект, создав клиентское приложение, более или менее аналогичное предыдущему. При тестировании следует помнить, что в этой таблице есть уникальный первичный ключ, поэтому при вводе записей с одинаковым значением поля OrdNum транзакции завершаться не будут.
И, наконец, создадим третий серверный объект, который будет управлять распределенными транзакциями и с этой целью порождать два предыдущих серверных объекта внутри своих транзакций. Вначале создадим объект, аналогичный двум предыдущим (рис. 20):
delete from ord where ordnum=:d
Рис. 20. Модуль данных серверного объекта pays для управления таблицей ord.dbf
Теперь компонент TDatabase свяжем с созданной нами базой данных dbpay, содержащей таблицу ord.dbf (с ней мы свяжем компонент TTable, рис. 21):
Рис. 21. Свойства компонента TDatabase серверного объекта, управляющего распределенными транзакциями
Значение свойства SQL компонента TQuery будет выглядеть следующим образом:
delete from ord where ordnum=:d
Теперь добавим в проект библиотеки типов двух созданных ранее серверов. Для этого следует выбрать из меню Delphi опцию Project/Import type library, нажать кнопку Add и выбрать соответствующий файл с расширением *.tlb (рис. 22):
Рис. 22. Импорт библиотек типов серверных объектов - участников распределенной транзакции
Далее отредактируем библиотеку типов данного серверного объекта, создав методы GetPays. AddPay, DelPay для доставки данных клиентскому приложению, добавления и удаления записей, а также метод DoTrans, реализующий распределенную транзакцию (удаление записи о выбранном товаре из таблицы STOCKTABLE в базе данных IBLOCAL и добавление по одной записи в таблицу заказов на доставку delivery.db в базе данных DBDEMOS и в таблицу счетов за заказы ord.dbf в базе данных paydb, рис. 23).
Рис. 23. Библиотека типов серверного объекта, управляющего распределенными транзакциями
Реализация этих методов имеет следующий вид:
unit pay1; //MTS server for managing distributed transactions //By N.Elmanova //04.12.1998 interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComServ, ComObj, VCLCom, StdVcl, BdeProv, BdeMts, DataBkr, DBClient, MtsRdm, Mtx, paysrv_TLB, dels_TLB, st_TLB, DBTables, Provider, Db; type Tpays = class(TMtsDataModule, Ipays) paytable: TTable; PayProvider: TProvider; Database3: TDatabase; Query6: TQuery; Session3: TSession; Query5: TQuery; private FStockDM1: IStockDM1; FDelDM: IDelDM; { Private declarations } public { Public declarations } protected function GetPays: OleVariant; safecall; procedure AddPay(Pnum: Integer; Pval: Double; const Address: WideString); safecall; procedure DelPay(Pnum: Integer); safecall; procedure DoTrans(Num: Integer; Val: Double; const Addr, Gname: WideString); safecall; end; var pays: Tpays; implementation {$R *.DFM} function Tpays.GetPays: OleVariant; begin Result:=PayProvider.Data; SetComplete; end; procedure Tpays.AddPay(Pnum: Integer; Pval: Double; const Address: WideString); begin try paytable.open; paytable.append; paytable.fieldbyname('OrdNum').Value:=PNum; paytable.fieldbyname('Payment').Value:=Pval; paytable.fieldbyname('Address').Value:=Address; paytable.post; paytable.close; SetComplete; except SetAbort; end; end; procedure Tpays.DelPay(Pnum: Integer); begin try Database3.Open; paytable.Open; paytable.SetRangeStart; paytable.FieldByName('ordnum').AsInteger:=Pnum; paytable.SetRangeEnd; paytable.FieldByName('ordnum').AsInteger:=Pnum; paytable.ApplyRange; paytable.Delete; paytable.Close; Database3.Close; SetComplete; except SetAbort; raise; end; end; procedure Tpays.DoTrans(Num: Integer; Val: Double; const Addr, Gname: WideString); begin try OleCheck(ObjectContext.CreateInstance(CLASS_StockDM1, IStockDM1, FStockDM1)); OleCheck(ObjectContext.CreateInstance(CLASS_DelDM, IDelDM, FDelDM)); FStockDM1.DeleteGoods(Num); FDelDM.AddDelivery(Num,Gname,Addr); AddPay(Num,Val,Addr); except DisableCommit; raise; end; EnableCommit; end; initialization TComponentFactory.Create(ComServer, Tpays, Class_pays, ciMultiInstance, tmApartment); end.
Прокомментируем приведенный выше код для метода DoTrans. Этот код реализует распределенную транзакцию, вызывая методы двух порожденных ей серверных объектов и выполняя собственные манипуляции с таблицей счетов. При вызове метода DoTrans клиентским приложением все три серверных объекта функционируют согласованно.
В начале выполнения создается так называемый контекст транзакции - интерфейс ITransactionContextEx. Этот интерфейс контролирует выполнение транзакции и обладает методами CreateInstance (создание экземпляра порожденного объекта), Commit (завершение транзакции) и Abort (откат транзакции). Отметим, что если для порождения серверного объекта MTS клиентским приложением используется компонент TDCOMConnection, то для порождения серверного объекта другим серверным объектом используется вызов метода CreateInstance интерфейса ITransactionContextEx. Параметрами этого метода являются CLSID объекта, интерфейс объекта и указатель на объект (возвращаемый параметр).
Далее следуют вызовы методов порожденных серверных объектов и собственные манипуляции с данными. Если все операции были успешны, транзакция завершена, и может быть выполнен метод Commit интерфейса ITransactionContextEx. Если же операции были неуспешны, и в одном или обоих порожденных серверах либо во время собственных манипуляций с данными возникнут исключения (например, другой пользователь уже удалил запись из списка товаров, сделав заказ, или какая-то из таблиц заблокирована), будет вызван метод Abort.
Для тестирования распределенных транзакций установим все три серверных объекта в один и тот же "пакет" (рис. 24):
Рис. 24. Серверные объекты, участвующие в распределенной транзакции
Для тестирования созданного ранее сервера и инициации распределенных транзакций создадим клиентское приложение, имитирующее процесс оформления заказов. На главной форме приложения поместим кнопку с надписью "Connect", три компонента TDCOMConnection, связанные с соответствующими серверами, три компонента TClientDataSet, связанные с соответствующими компонентами TDCOMConnection, три компонента TDataSource, связанные с компонентами TClientDataSet и блокнот из двух страниц. На одной из страниц блокнота разместим компонент TDBGrid, отображающий данные из таблицы со списком товаров на складе, компонент TEdit для ввода пользователем адреса доставки, и кнопку для инициирования транзакции - принятия заказа. На второй странице поместим два компонента TDBGrid для отображения данных из двух других таблиц и компонент TSplitter между ними (рис. 25):
Рис. 25. Клиентское приложение для тестирования распределенных транзакций
Создадим обработчики событий, связанных с нажатием на кнопки:
unit allcl1; //Client application for using distributed transactions //By N.Elmanova //05.12.1998 interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Grids, DBGrids, Db, DBClient, StdCtrls, MConnect, ExtCtrls, ComCtrls; type TForm1 = class(TForm) PageControl1: TPageControl; TabSheet1: TTabSheet; TabSheet2: TTabSheet; DCOMConnection1: TDCOMConnection; ClientDataSet1: TClientDataSet; DataSource1: TDataSource; DBGrid1: TDBGrid; Edit1: TEdit; Label1: TLabel; Button1: TButton; DBGrid2: TDBGrid; DBGrid3: TDBGrid; Splitter1: TSplitter; DCOMConnection2: TDCOMConnection; ClientDataSet2: TClientDataSet; DataSource2: TDataSource; DCOMConnection3: TDCOMConnection; ClientDataSet3: TClientDataSet; DataSource3: TDataSource; Button2: TButton; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.Button1Click(Sender: TObject); var n:integer;val:double;gnam,addr:widestring; begin try n:= ClientDataSet1.FieldByName('GOODSNUMBER').Value; val:= ClientDataSet1.FieldByName('PRICE').Value; gnam:= ClientDataSet1.FieldByName('GOODSNAME').Value; addr:=Edit1.Text; DcomConnection2.Connected:=true; DCOMConnection2.AppServer.DoTrans(n,val,addr,gnam); ShowMessage('Заказ принят'); except ShowMessage('Заказ не принят '); end; DcomConnection2.Connected:=false; end; procedure TForm1.Button2Click(Sender: TObject); begin DCOMConnection1.Connected:=true; DCOMConnection2.Connected:=true; DCOMConnection3.Connected:=true; CLientdataset1.data:=Dcomconnection1.Appserver.GetGoods; CLientdataset2.data:=Dcomconnection2.Appserver.GetPays; CLientdataset3.data:=Dcomconnection3.Appserver.GetDelivery; DCOMCOnnection1.Connected:=false; DCOMCOnnection2.Connected:=false; DCOMCOnnection3.Connected:=false; end; end.
Для тестирования распределенных транзакций запустим приложение. Введем адрес в компонент TEdit, выберем строку в списке товаров и нажмем на кнопку "Заказать" (рис. 26).
Рис. 26. Тестирование распределенной транзакции
В результате получим сообщение о том, что заказ принят
Нажав на кнопку Connect, обновим данные в компонентах TDBGrid. При этом запись, выбранная ранее, исчезнет, а в двух других компонентах TDBGrid появятся две новых (рис. 27):
Рис. 27. Результат выполнения распределенной транзакции
Отметим, что, если не нажать на кнопку Connect, данные в компонентах TDBGrid останутся прежними (в данном примере не предусмотрено обновление данных после выполнения транзакции), у пользователя есть возможность попытаться повторно выбрать для заказа уже выбранную ранее запись (то есть заказать товар, заказ на который уже оформлен). В этом случае один из трех серверных объектов будет пытаться удалить уже удаленную запись (если вспомнить текст соответствующего SQL-запроса, она идентифицируется значением первичного ключа), и в этом случае соответствующая часть транзакции не завершится. Соответственно, произойдет откат назад всей распределенной транзакции.
Сведения о завершенных и отмененных транзакциях можно получить, выбрав в MTS Explorer опцию Transaction Statistics (рис. 28):
Рис. 28. Просмотр статистики выполнения и отката распределенных транзакций
Если вспомнить, что первичные ключи в нашей таблице товаров создаются с использованием генератора, созданного нами на сервере IB Database, становится очевидным, что создаваемые в этой таблице новые записи будут иметь значения первичного ключа, не совпадающие со значениями первичных ключей уже удаленных записей. Поэтому вероятность коллизий, связанных с удалением не той записи, в данном случае равна нулю.
Отметим, однако, что при создании подобного рода серверных объектов и их клиентов следует всегда пытаться исключить возможность ошибочных действий пользователя, поэтому более корректным было бы иметь следующий обработчик события, связанного с нажатием на кнопку "Заказать":
procedure TForm1.Button1Click(Sender: TObject); var n:integer;val:double;gnam,addr:widestring; begin try n:= ClientDataSet1.FieldByName('GOODSNUMBER').Value; val:= ClientDataSet1.FieldByName('PRICE').Value; gnam:= ClientDataSet1.FieldByName('GOODSNAME').Value; addr:=Edit1.Text; DcomConnection2.Connected:=true; DCOMConnection2.AppServer.DoTrans(n,val,addr,gnam); ShowMessage('Заказ принят'); Button2Click(self); except ShowMessage('Заказ не принят '); end; DcomConnection2.Connected:=false; end;Отметим также, что при нажатии на кнопку "Заказать" в случае отсутствия данных в компонентах TDBGrid в клиентском приложении возникнет исключение, связанное с отсутствием нужного поля в компоненте TClientDataSet. Следовательно, данная кнопка в такой ситуации должна быть невыбираемой. Поэтому установим значение ее свойства Enabled равным False и перепишем обработчик события,связанного с нажатием на кнопку Connect::
procedure TForm1.Button2Click(Sender: TObject); begin try DCOMCOnnection1.Connected:=true; DCOMCOnnection2.Connected:=true; DCOMCOnnection3.Connected:=true; CLientdataset1.data:=Dcomconnection1.Appserver.GetGoods; CLientdataset2.data:=Dcomconnection2.Appserver.GetPays; CLientdataset3.data:=Dcomconnection3.Appserver.GetDelivery; Button1.Enabled:=true; except Button1.Enabled:=false; ShowMessage('Один из серверных объектов недоступен'); end; DCOMCOnnection1.Connected:=false; DCOMCOnnection2.Connected:=false; DCOMCOnnection3.Connected:=false; end;
Итак, мы создали три серверных объекта, реализующих распределенную транзакцию, связанную с удалением записи из одной таблицы и добавлением записи в две другие таблицы, и клиентское приложение, инициирующее выполнение таких транзакций. Следует обратить внимание на то, что таблицы, участвующие в транзакции, содержатся в трех физически разных базах данных.
Итак, на примере Delphi 4 и Microsoft Transaction Server мы рассмотрели возможность и способы использования технологий COM и COM+ для организации распределенных вычислений и управления распределенными транзакциями.
Следующая статья данного цикла будет посвящена другой технологии, используемой при организации распределенных вычислений - DCE (Distributed Computing Environment), и ее реализации в многоплатформенном сервере приложений Inprise Entera.
<< Назад | Содержание