    ГЛАВА   14 

    Построение распределенных приложений 

    Роланд Бучероу 
    
    ——— Компонент ClientDataSet 

    Рассказывается, как заполнять и обновлять компонент clientDataSets. 

    ——I Компонент Provider 

    Вы узнаете об использовании компонента Providers для получения и 
применения данных компонента ClientDataSets. 

    ——— Компонент RemoteServer 

    Описывается использование компонента RemoteServers ДЛЯ СВЯЗИ компонента 
ClientDataSets в приложениях клиента с компонентом Providers на сервере приложений.; 

    •Развитие технологий СОМ, OLE, Intemet/intranet и высокая эффективность 
при их совместном использовании стимулировали разработки в среде Windows. 
Определенный интерес представляют распределенные приложения и приложения 
распределенных баз данных с небольшим количеством клиентов. В Delphi имеется 
три новых компонента, которые вместе с обычным набором компонентов доступа к 
данным предоставляют средства, с помощью которых можно легко реализовать 
распределенные приложения баз данных с небольшим количеством клиентов. 

    Компоненты ClientDataSet, Providers и RemoteServers 

    Модель распределенного приложения обычно обеспечивает три уровня: 
небольшая программа клиентской части, сервер приложений и сервер DBMS. В этой 
главе будут рассмотрены программы, находящиеся на первых двух уровнях. (Много 
ли вы знаете людей, которые написали хотя бы одну программу для сервера DBMS?) 
Небольшое приложение клиента— это единица с небольшой зоной влияния. Чтобы 
лучше поддерживать клиентские приложения, в Delphi 3 был введен компонент 
ClientDataSet. Если. например, вы создаете в Delphi стандартное приложение 
базы данных, использующее таблицы, запросы и хранимые процедуры, то для этого 
приложения требуется установка и применение системы Borland Database Engine 
(BDE). Зависимость от относительно большой зоны влияния BDE исчезает, если вы 
разрабатываете приложение, используя имен

    331 

    но компонент ClientDataSet. Вместо использования системы BDE вам нужна 
лишь библиотека DLL DBCIENT. DLL, которая предназначена для приложения 
клиента, использующего компонент ClientDataSet. В этой библиотеке DLL 
содержится реализация СОМ-интерфейсов IDSBase и IDSCursor. (Все три 
компонента, описываемые в этой главе, используют или содержат СОМ-интерфейсы.) 
Эти два интерфейса и интер4)ейс IProvider были созданы специально для 
разработки распределенных приложений баз данных. Кроме того, компонент 
ClientDataSet полностью сохраняется в памяти и не имеет непосредственной связи 
с устойчивыми объектами, такими как таблицы и взгляды. Подключение к первичным 
данным определяется пользователем во время разработки или выполнения приложения. 
    Компонент ClientDataSet— непосредственный потомок доработанного компонента 
DataSet. Поэтому функциональные возможности компонента ClientDataSet во многих 
отношениях совпадают с возможностями его "брата", компонента Table. Любые 
операции, выполняемые с какой-либо таблицей, могут быть подобным способом 
выполнены с компонентом ClientDataSet. Кроме того, компонент ClientDataSet 
поддерживает форму индекса в памяти и те же фильтрацию и возможности поиска, 
что и компонент Table. Преимущество компонента ClientDataSet заключается в 
том, что все алгоритмы поиска, индексации и фильтрации, оперирующие данными, 
размещаются в памяти, а методы, такие KaKAddIndex и Locate, и свойства Filter 
и Filtered быстро выполняются. Первоначально компоненты ClientDataSet пусты и 
имеют тип void для структуры и данных. Нужно обеспечить эти компоненты 
структурой и данными. Это обычно осуществляется с помощью интерфейса 
IProvider, получаемого с сервера приложений. Для предоставления такого 
интерфейса используется компонент IProvider, размещаемый на сервере 
приложений. Две программы связываются, используя третий новый компонент 
RemoteServer, который размещается в приложении клиента. Хотя может 
потребоваться много компонентов ClientDataSet для приложения клиента, вам 
необходимо использовать только один RemoteServer для каждого сервера приложений. 
    Работа с этими компонентами требует, чтобы вы применяли методологию 
"копирую-модифицирую-обновляю", подобную способу работы CachedUpdates. Первый 
компонент, ClientDataSet, заполняется данными или метаданными из сервера 
приложений. После изменений компонента ClientDataSet малый поток данных, 
называемый Delta, пересылается обратно в компонент Provider на сервер 
приложений, который поочередно применяет эти изменения к первичному объекту 
Table или Query. Хотя это и не строгое требование, обычно механизм передачи 
данных реализуется с помощью OLE Automation. Эта реализация относительно 
проста для выполнения, поскольку серверы Automation имеют встроенную поддержку 
COM/DCOM. Если же вы желаете отказаться от использования OLE, можете 
пересылать данные в приложение клиента и из него с помощью любых методов, 
которые вам нравятся. Почтовые слоты, называемые каналами, гнезда, а также 
файлы отображения памяти являются своеобразными транспортными механизмами. 

    Подключение приложений клиента к серверу приложений 

    Хотя компоненты RemoteServer размещаются в приложении клиента, они 
обеспечивают концептуальный конвейер, по которому передаются данные 
компонентов ClientDataSet и Provider. 

    TRemoteServer = class(TCustomRemoteServer) 
    public 
    function GetProvider(const ProviderName: string): IProvider; override; 
    published 
    property ComputerName: string ... ; 
    property Connected: Boolean ... ; 
    property ServerName: string ...; 
    Beforeproperty AfteDisconnect: TNotifyEvent ...; 
    end; 

    Вы подключаете компонент RemoteServer к серверу приложений, указывая имя 
приложения и, возможно, место его расположения. Свойство ServerName, которое 
заполнено пробелами по умолчанию, идентифицирует имя сервера приложений. Это 
имя обычно является программным идентификатором сервера OLE Automation, 
который предоставляет доступ к интерфейсу Provider, выполняя метод 
GetProvider. Этот метод использует один строковый аргумент, ProviderName, 
который идентифицирует имя свойства OLE Automation, предоставляющего интерфейс IProvider. 
    Кроме того, компонент RemoteServer имеет два обработчика событий, 
OnConnect и OnDisconnect, которые запускаются после соединения или 
разъединения с сервером приложений. 

    332                                     Часть III. Работа с базами данных 


    Заполнение компонента ClientDataSet 

    Как уже упоминалось, компонент ClientDataSet первоначально пуст, без 
структуры и содержимого. Необходимо извлечь структуру (метаданные) из объекта 
Table или Query или построить ее самостоятельно. Явно или неявно, это будет 
интерфейс IProvider, который получает поток данных из сервера приложений. Код 
в листинге 14.1 взят непосредственно из STDVCL. РАЗ, раздела, в котором 
определен интерфейс IProvider. 

    Листинг 14.1. STDVCL. PAS. Объявление интерфейса IProvider                 

    IProvider = interface(IDispatch) 
    function Get_Data: OleVariant; safecall; 
    function ApplyUpdates(Delta: OleVariant; MaxErrors: Integer; 
    out ErrorCount: Integer): OleVariant; safecall; 
    function GetMetaData: OleVariant; safecall; 
    function GetRecords(Count: Integer; out RecsOut: Integer): 
    OleVariant; safecall; 
    function DataRequest(Input: OleVariant): OleVariant; safecall; 
    function Get_Constraints: WordBool; safecall; 
    procedure Set Constraints(Value: WordBool); safecall; 
    procedure Reset; safecall; 
    procedure SetParams(Values: OleVariant); safecall; 
    property Data: OleVariant read Get_Data; 
    property Constraints: WordBool read Get Constraints write 
    Set Constraints; 
    end; 


    Соединение с компонентом RemoteServer 

    Если определить свойство Provide rName и указать текущий компонент 
RemoteServer в свойстве RemoteServer компонента ClientDataSet, то, вызвав 
метод Open или установив свойство Active равным True, можно заполнить 
компонент ClientDataSet автоматически. 

    TClientDataSet = class (TDataSet) 
    published property Pro viderName: string ...; 
    property RemoteServer: TCustomRemoteServer ...; 
    end; 

    При заполнении компонента ClientDataSet таким способом происходит следующее. 
    1. Присоединенный RemoteServer связывается с сервером приложений (если он 
еще не связался). 
    2. Интерфейс IProvider обеспечивается путем вызова метода GetProvider, в 
котором в качестве аргумента используется свойство ProviderName компонента ClientDataSet. 
    3. Интерфейс IProvider присваивается свойству IProvider компонента ClientDataSet. 
    4. Поток данных предоставляется методом IProvider. GetRecords как OleVariat. 
    5. Этот поток данных размещается в компоненте ClientDataSet с помощью 
метода AppendData. 
    Соединение компонента ClientDataSet с компонентами RemoteServer— наиболее 
общий способ создания и заполнения компонента ClientDataSet. 
    Явное использование свойства (Provider 
    Компоненты Provider и ClientDataSet, так же как и производные компоненты 
компонента DBDataSet, имеют свойство IProvider, которое называют Provider. 

    TClientDataSet = class (TDataSet) 


    Глава 14. Построение распределенных приложений                         333 


    public 
    property Provider: IProvider ...; 
    end; 
    TDBDataSet = class (TDBEDataSet) 
    public 
    property Provider: IProvider ...; 
    end; 
    TProvider = class(TCustomProvider) 
    public 
    property Provider: IProvider ... ; 
    end; 

    Свойство Provider для всех таких компонентов открыто (объявлено как 
public), но используется только для чтения в компоненте Provider и в 
компонентах, производных от DBDataSet. 

    TProvider = class(TCustomProvider) 
    published 
    property DataSet: TDBDataSet ...; 
    end; 

    Чтобы связать компонент DBDataSet с компонентом Provider, укажите в 
свойстве DataSet компонента Provider ссылку на Table или Query. В следующем 
коде для компонента Provideri указано, что Tablel является исходным 
компонентом класса DBDataSet для потока данных. 

    Provideri.DataSet := Tablel; 

    Если компонент Provider реализует интерфейс IProvider, использовать сам 
компонент Provider вовсе не обязательно. Однако вы, возможно, захотите 
использовать его, чтобы лучше контролировать сервер приложений, особенно если 
вы выбрали СОМ для своей распределенной инфраструктуры. 

    ПРЕДОСТЕРЕЖЕНИЕ 

    Чтобы избежать явного использования компонента provider на сервере 
приложений, необходимо вручную включить модуль BDEProv в раздел uses 
рассматриваемого модуля. Если этого не сделать, то ваше приложение, возможно, 
вызовет нарушение доступа, поскольку без этого модуля экземпляры интерфейса 
IProvider не смогут быть созданы, а все ссылки на свойство provider компонента 
будут заполнены nil. 
    После того как вы получите и назначите правильный интерфейс IProvider, 
компонент ClientDataSet будет заполнен автоматически благодаря выполнению 
метода Open, точно так, как будто был подключен компонент RemoteServer. 

    ClientDataSet.Provider := Table.Provider; 
    ClientDataSet.Open; 

    Обеспечение интерфейса IProvider таким путем является методом, применяемым 
во многих примерах кода, рассматриваемых в этой главе. 
    отя компоненты Provider и DBDataSet предназначены для использования на 
сервере приложений, во многих примерах данной главы они применяются в одном и 
том же проекте, представленном компонентом ClientDataSet. Это сделано, чтобы 
упростить примеры. 
    По умолчанию после предоставления интерфейса IProvider для компонента 
ClientDataSet и открытия компонента все записи копируются из первичного 
компонента DBDataSet. Эта операция может быть запрещена из-за размера таблицы 
или низкоскоростного канала между приложением клиента и сервером приложений. 
Чтобы смягчить это, можно установить фильтр или диапазон ограничений на 
первичный компонент DBDataSet. Однако при использовании фильтров и диапазонов 
предоставляются неточные средства контроля числа выбранных строк. 

    334                                     Часть III. Работа с базами данных 


    TClientDataSet = class (TDataSet) 
    public 
    function GetNextPacket: Integer; 
    published 
    property FetchOnDemand: Boolean ... default True; 
    property PacketRecords: Integer ... default -1; 
    end; 

    Свойство FetchOnDemand используется для определения, выбираются ли данные 
автоматически. Если оно установлено равным True по умолчанию, данные 
возвращаются из интерфейса IProvider автоматически, когда компонент 
ClientDataSet открыт и по нему происходит перемещение. Если вам нужно 
контролировать выборку данных, установите свойство FetchOnDemand равным False 
и выполните метод GetNextPacket для возвращения невыбранных строк. Если 
последняя запись выбрана из исходного компонента DBDataSet, метод 
GetNextPacket будет возвращать значение, которое меньше указанного свойством PacketRecords. 
    Происходит ли наполнение компонента ClientDataSet автоматически или вы это 
делаете явно, число выбранных строк в любой момент времени определяется 
свойством PacketRecords. Если установить это свойство равным -1, то все строки 
будут выбраны в компонент ClientDataSet. Если установить свойство 
PacketRecords равным 0, то будут возвращены только метаданные. Это аналогично 
вызову метода IProvider. GetMetaData. Любое положительное значение этого 
свойства ограничивает количество строк, которые будут выбраны одновременно. В 
примере PACKETSAMPLE. PAS показано использование свойств FetchOnDemand и 
PacketRecords и метода GetNextPacket. 

    Использование свойства Data и метода Get_Data 

    Свойство Data в интерфейсах компонентов ClientDataSet и IProvider 
определено как поток данных и представлено типом OleVariant следующим образом. 

    TClientDataSet = class(TDataSet) 
    public 
    property Data: OleVariant ...; 
    end; 
    TClientDataSet = class(TDataSet) 
    public 
    property Data: OleVariant ...; 
    end; 

    Компонент ClientDataSet можно заполнить, присвоив свойству Data подходящий 
поток данных. Этот метод возвращает все записи набора данных, не ограниченные 
значением свойства PacketRecords. Компонент ClientDataSet можно заполнить 
путем простого присвоения значения свойству Data: 

    ClientDataSet.Data := Table.Povider.Data; 

    Более того, присвоив свойству Data значение Null, вы возвратите компоненту 
ClientDataSet его незаполненное состояние. Для компонента Provider доступ к 
свойству Data равносилен вызову метода IProvider. Get_Data. Компонент Provider 
также реализует метод Get_Data, но он скрыт в защищенном разделе объявлений. 

    Выборка групп строк из компонента DBDataSet 

    В сердцевине всех процедур выборки записей интерфейс IProvider реализует 
метод GetRecords, как показано в следующем примере. 
    TProvider = class(TCustomProvider) 
    public 


    Глава 14. Построение распределенных приложений                         335 


    function GetRecords(Count: Integer; out RecsOut: Integer): 
    OleVariant; override; 
    procedure Reset; ... 
    end; 

    Принимая два простых аргумента (coynf-параметр, количество строк для 
выборки и ок<-параметр, используемый для отображения текущего числа выбранных 
записей), метод GetRecords возвращает результат типа OleVariant, который 
содержит количество запрошенных строк. Можно передать нуль для 
count-параметра, чтобы выбрать поток данных, содержащий только метаданные. Это 
точно такой же способ, которым оперирует метод IProvider.GetMetaData. Метод 
GetData делает то же самое, но с count-параметром, равным -1. Это, конечно, 
вызывает возврат всех строк. Если вы применяете метод GetRecords 
непосредственно, используйте метод ClientDataSet .AppendData в приложении 
клиента для добавления новых отобранных строк. Это, по существу, способ, 
который используется методом GetNextPacket. Метод AppendData объявляется так. 

    TClientDataSet = class(TDataSet) 
    public 
    procedure AppendData(Data: OleVariant; HitEOF: Boolean); 
    end; 

    Второй параметр метода GetRecords, HitEOF, информирует компонент 
ClientDataSet о том, что была добавлена последняя группа записей, т.е. был 
достигнут конец файла. Обратите внимание, что, назначив потоку данных тип 
OleVariant для свойства Data компонента ClientDataSet, вы будете выполнять 
скорее замену, а не добавление строк уже в компоненте ClientDataSet. И, 
наконец, метод Reset используется для того, чтобы компонент Provider начал 
повторную выборку с первой записи связанного с ним компонента DBDataSet. 
    В ответ на любую попытку получить данные из компонента Provider 
выполняется событие OnGetData. В следующем фрагменте кода показано объявление 
события OnGetData в компоненте Provider. 

    TProviderDataEvent = procedure (DataSet: TClientDataSet) 
    of object; 
    TProvider = class(TCustomProvider) 
    published 
    property OnGetData: TProviderDataEvent ...; 
    end; 

    Явно или косвенно, но метод GetRecords вызывает обработчика события 
OnGetData сразу после успешного возвращения запрашиваемых данных. 

    Реализация пользовательского метода поиска с помощью метода DataRequest 

    Метод DataRequest реализован и в интерфейсе IProvider, и в компоненте 
Provider. Он используется в качестве некоторого общего метода для поддержки 
связи с компонентом Provider на сервере приложений. В ответ на вызов метода 
DataRequest запускается обработчик события OnDataRequest компонента Provider. 
Никаких других действий, кроме запуска обработчика события, не предпринимается. 

    TDataRequestEvent = function (Sender: TObject; Input: 
    OleVariant) of object; 
    TProvider = class(TCustomProvider) 
    published 
    property OnDataRequest: TDataRequestEvent ...; 
    end; 

    Обработчик события OnDataRequest необычен тем, что обрабатывает одно из 
двух VCL-событий, обработка которых реализована как функция. Это нарушает 
неписаные правила программирования, на которых базируется механизм обработки 
событий VCL. Этот важный момент нужно запомнить. Решив указать свой 
собственный обработчик события OnDataRequest, вы должны будете возвращать значение. 

    336                                     Часть III. Работа с базами данных 

    Кроме вездесущего параметра Sender, обработчик события OnDataRequest 
получает параметр OleVariant, удачно названный Input. Что будет содержаться в 
этих параметрах, решать только вам. Более того, вам следует подумать о 
содержании возвращаемого значения. Если это значение не имеет значения в вашей 
реализации, возвращайте Null. 

    Использование потоков и файлов 

    Реализация методов компонента ClientDataSet зеркально отражает процедуры 
загрузки и сохранения, которые должны быть доступны для класса TStrings. С 
помощью методов LoadFromFile, LoadFromStream, SaveToFile и SaveToStream можно 
сохранять и загружать поток данных ClientDataSet в файлы и потоки и из них 
обычным способом. 

    TClientDataSet = class(TDataSet) 
    public 
    procedure LoadFromFile(const FileName: string); 
    procedure LoadFromStream(Stream: TStream); 
    procedure SaveToFile(const FileName: string); 
    procedure SaveToStream(Stream: TStream) ; 
    end; 

    Методы загрузки оперируют непосредственно свойством ClientDataSet. Data 
типа OleVariant. Внутри себя они создают пустой массив типов, блокируют его с 
помощью функции VarArrayLock, считывают поток (или файл) в результирующий 
массив байтов, а затем разблокируют массив типов. 

    В листинге 14.2, который является фрагментом файла LOADSAVESAMPLE. PAS, 
используются оба метода— SaveToFile и LoadFromFile. 

    Листинг 14.2. LOADSAVESAMPLE . PAS. Сохранение и загрузка потока данных 
компонента ClientDataSet методами 

    SaveToFileиLoadFromFile 
    procedure TLoadSaveSampleForm.SaveItemClick(Sender: TObject); 
    begin 
    if SaveDialog.Execute then begin 
    ClientDataSet.SaveToFile(SaveDialog.FileName) ; 
    ClientDataSet.Data := Null; 
    ClientDataSet.IndexName := ''; 
    Saveltem.Enabled := False; 
    Loadltem.Enabled := True 
    end 
    end; 
    procedure TLoadSaveSampleForm.LoadItemClick(Sender: TObject); 
    begin 
    with ClientDataSet do begin 
    LoadFromFile(SaveDialog.FileName); 
    Addlndex('CustNo', 'CustNo', [ixPrimary]) ; 
    Addlndex('ByCompany', 'Company', [ixCaseInsensitive]); 
    IndexName := 'ByCompany' 
    end; 
    Saveltem.Enabled := True; 
    Loadltem.Enabled := Falser
    end; 

    Обратите внимание, что в предыдущем фрагменте кода свойство IndexName 
очищается после сохранения потока данных в файле, а индексы вновь создаются 
после перезагрузки потока данных. Это необходимо, поскольку данные свойства не 
записаны в файл или поток. Перед заполнением компонента ClientDataSet 
убедитесь в том, что ссылки на элементы, такие как поля и индексы, удалены. В 
противном случае будет выдано сообщение о неизвестном элементе, когда 
компонент ClientDataSet будет активным. 

    Глава 14. Построение распределенных приложений                         337 


    ПРЕДОСТЕРЕЖЕНИЕ 

    Не путайте использование файла в процедурах сохранения и загрузки с 
записью и чтением таблиц, которые размещены на диске. Формат потока данных, 
используемый компонентом clientDataSet, является собственностью и полностью не 
документирован. Методы saveToFile и LoadFroraFile предназначены для простого 
использования, такого как динамическое сохранение работы, и, конечно, не для 
повторного доступа к файлу результатов, как в случае с обычными таблицами. 
    Хотя методы Load и Save нельзя сравнить в этом отношении с методами 
таблиц, они предоставляют мощные средства передачи потоков данных. С помощью 
методов LoadFromStream и SaveToStream можно загружать и сохранять данные 
любого потока, включая пользовательские потоки на основе класса TStream. 
Применяя пользовательские потоки данных, можно заполнять компоненты 
ClientDataSet самыми разнообразными данными. Например, переопределив всего 
четыре метода класса TStream, можно создать поток, поддерживающий чтение и 
запись на определенный Webузел через устойчивую связь с этой сетью. 

    Использование метода CreateDataSet 

    Используя свойство FieldDefs или создавая компоненты полей, можно создать 
структуру несуществующего компонента ClientDataSet. После определения полей 
вам остается только вызвать метод CreateDataSet. 

    TClientDataSet = class(TDataSet) 
    public 
    procedure CreateDataSet; 
    end; 

    Таким образом, создавая список IndexDefs, можно одновременно создавать 
индексы. Но проще сначала во время разработки программы создать компоненты 
полей, а затем при необходимости вызвать метод CreateDataSet. Того же 
результата можно достичь, создавая компоненты полей во время выполнения 
программы. На дополнительно распространяемой дискете есть модули 
CREATESAMPLE1. PAS, CREATESAMPLE2. PAS, CREATESAMPLE3. PAS и связанные с ними 
модули, в которых такие методы применяются для создания функционирующего 
набора данных ClientDataSet с нуля. В листинге 14.3 показано создание полей 
FieldDefs и индексов IndexDefs. 

    Листинг 14.3. \UDELPHI3\CH14\CREATESAMPLE3. PAS. Определение полей и 
индексов для свойств FieldDefs и 

    IndexDefs компонента ClientDataSet 
    procedure TCreationSampleForm.FormCreate(Sender: TObject); 
    begin 
    with ClientDataSet.FieldDefs do begin 
    Add('CustNo', ftFloat, 0, False); 
    Add('Company', ftString, 30, False); 
    AddfAddrl', ftString, 30, False); 
    Add('Addr2', ftString, 30, False); 
    Add('City', ftString, 15, False); 
    Add('State', ftString, 20, False); 
    Add('Zip', ftString, 10, False); 
    Add('Country', ftString, 20, False); 
    Add('Phone', ftString, 15, False); 
    Add('FAX', ftString, 15, False); 
    Add('TaxRate', ftFloat, 0, False); 
    Add('Contact', ftString, 20, False); 
    Add('LastInvoiceDate', ftDateTime, 0, False); 
    end; 
    with ClientDataSet.IndexDefs do begin 
    Add('CustNo', 'CustNo', [ixPrimary]); 
    Add('ByCompany', 'Company', [ixCaseInsensitive]); 
    end; 
    ClientDataSet.IndexName := 'CustNo'; 
    ClientDataSet.CreateDataSet 
    end; 


    338                                     Часть III. Работа с базами данных 


    Обработка изменений набора данных ClientDataSet 

    После того как ClientDataSet заполнен, приложение клиента изменяет (или 
позволяет пользователю изменять) полученные данные и направляет изменения к 
серверу приложения. Чтобы дать возможность отслеживать эти изменения и, 
возможно, отменять их, в компоненте ClientDataSet предусмотрены специальные 
свойства и методы. В этом разделе  рассматриваются   все  эти  возможности.   
Примеры их использования вы найдете в файле CHANGETRACKINGSAMPLE.PAS. 

    Регистрация изменений 

    Как упоминалось ранее, изменения, внесенные в ClientDataSet регистрируются 
в другом внутреннем потоке, на который ссылается свойство Delta. Все изменения 
можно получить с помощью этого свойства. 

    TUpdateStatus =(usUnmodified, usModified, usinserted, usDeleted); 
    TClientDataSet = class(TDataSet) 
    public 
    function UpdateStatus: TUpdateStatus; 
    property ChangeCount: Integer ... ; 
    property Delta: OleVariant ...; 
    property LogChanges: Boolean ... ; 
    end; 

    Регистрация изменений включена по умолчанию и происходит автоматически. Ее 
можно отключить, установив свойство LogChanges равным False, однако, сделав 
это, вы не сможете обновлять реальные данные с помощью компонента 
ClientDataSet. Свойство ChangeCount позволяет определить количество сделанных изменений. 


    ПРЕДОСТЕРЕЖЕНИЕ 
    Если внести три отдельных изменения в одной записи, для компонента 
ClientDataSet это будет одна измененная запись. Вставка или удаление также 
считается одной измененной записью независимо от количества изменений, 
вносимых в нее позже. 

    На рис. 14.1 показана таблица Customer. DB, представленная компонентом 
ClientDataSet перед внесением в нее изменений. 
    Метод UpdateStatus возвращает значение перечислимого типа TUpdateStatus. 
(Этот же тип используется для отображения статуса обновлений записей при 
кэшированном обновлении записей компонента DBDataSet.) С помощью метода 
UpdateStatus можно для каждой записи узнать ее статус— измененная, не 
измененная или вставленная. Для невидимых и удаленных строк значение 
UpdateStatus также определено. Статус удаленных строк можно узнать, скопировав 
поток Delta в другой компонент ClientDataSet или активизировав специальный 
встроенный индекс. На рис. 14.2 показан тот же набор данных ClientDataSet 
после одного удаления, одной вставки и одного изменения. Обратите внимание на 
колонку статуса изменений, отображающую состояние видимых строк. 
    После заполнения компонента ClientDataSet автоматически создается два 
встроенных индекса. Первый индекс называется CHANGEINDEX и позволяет 
просматривать записи, которые изменились каким-то определенным способом. Когда 
этот индекс активен, вы видите как начальную версию записи, так и измененную 
версию после каждого изменения. Кроме того, этот индекс — единственное 
средство, с помощью которого можно просматривать удаленные записи. Второй 
индекс называется DEFAULT_ORDER и обеспечивает нормальный вид набора данных с 
последними изменениями и в естественном порядке. На рис. 14.3 показан набор 
данных ClientDataSet после активизации индекса CHANGEINDEX. 

    Глава 14. Построение распределенных приложений                         339 
    Часть III. Работа с базами данных 


    Рис. 14.1. Таблица Customer. DB, представленная компонентом clientDataSet 
перед внесением в нее изменений 

    Рис. 14.2. Набор данных clientDataSet после одного удаления, одной вставки 
и одного изменения 

    Рис. 14.3. Набор данных ClientDataSet с активизированным индексом CHANGE INDEX 


    Отмена изменений 

    Отменить изменения, внесенные в набор данных ClientDataSet, можно с 
помощью трех методов: CanceIUpdates, RevertRecord и UndoLastChange. Вот как 
объявлены эти методы в файле DBCLIENT. РАЗ. 

    TClientDataSet = class (TDataSet) 
    public 
    procedure CancelUpdates; 
    procedure RevertRecord; 
    function UndoLastChange(FollowChange: Boolean): Boolean
    end; 

    Использование этих методов (или предоставление их пользователю) очень 
важно, поскольку возвращение "чистого" набора строк с сервера приложений — 
сравнительно дорогостоящая операция с точки зрения циклов и, возможно, ширины 
строки. Отменить последнее изменение в последней модифицированной записи 
можно, вызвав метод UndoLastChange. При такой отмене последнего изменения 
запись вовсе не обязательно возвратится в начальное состояние, она станет 
такой, какой была перед последним изменением. Например, если запись сначала 
была вставлена в набор данных, а затем удалена, то, вызвав метод 
UndoLastChange впервые, мы возвратим запись, а вызвав второй раз, снова 
удалим, так как изначально эта запись не существовала. Аналогично этот метод 
действует и для записей, которые изменялись несколько раз. С каждым вызовом 
процедура UndoLastChange удаляет последнюю версию записи и заменяет ее 
предыдущей. С помощью этой функции можно реализовать простое средство отмены 
изменений для клиентского приложения. Методу UndoLastChange в качестве 
параметра передается логическое значение FollowChange, которое указывает, 
перемещать ли указатель текущей записи к восстановленной записи. 
    С помощью методов CancelUpdates и RevertRecord можно восстановить 
изначальные версии записей. Метод CancelUpdates отменят все изменения для всех 
записей, изменения которых были зарегистрированы. Метод RevertRecord отменят 
все изменения только для текущей записи. При этом все версии измененной записи 
удаляются из регистрации. Метод RevertRecord нельзя применить к удаленной 
записи, так как ее нельзя сделать текущей. 

    ПРЕДОСТЕРЕЖЕНИЕ 
    Если отключить регистрацию изменений после того, как сделано несколько 
изменений, то дальнейшие изменения, как и ожидалось, не зарегистрируются и 
методы CancelUpdates и ondoLastChange не будут работать даже для записей с 
изменениям, сделанными до отключения регистрации изменений. Чтобы отменить 
изменения для этих записей, нужно перейти к каждой из них и вызвать метод RevertRecord. 


    Обработка изменений 

    Один из основных этапов создания распределенной системы — реальное 
изменение исходных данных из клиентского приложения. К счастью, эта задача 
решена довольно просто. В ходе операции обновления могут возникнуть ошибки. 
Они нуждаются в обработке. И снова компоненты ClientDataSet и Provider 
предоставляют методы и события, позволяющие указывать действия, которые будут 
предприняты при возникновении ошибок. 

    Использование метода ApplyUpdates 

    Для применения изменений набора данных ClientDataSet в компонентах 
ClientDataSet и Provider метод ApplyUpdates реализован по-разному. 

    TProvider = class(TCustomProvider) 
    public 
    function ApplyUpdates(Delta: OleVariant; MaxErrors: Integer; 
    out ErrorCount: Integer): OleVariant; 
    end; 
    TClientDataSet = class(TDataSet) 


    Глава 14. Построение распределенных приложений                         341 


    public 
    function ApplyUpdates(MaxErrors: Integer) : Integer
    procedure MergeChangeLog; 
    function Reconcile(Results: OleVariant): Boolean; 
    end; 

    В методе ApplyUpdates компонента Provider ключевым аргументом является 
параметр Delta. Это тот же самый параметр Delta, определенный в компоненте 
TClientDataSet. Второй целый параметр MaxErrors указывает максимальное 
разрешенное количество ошибок перед прекращением изменения. Третьему 
параметру, ErrorCount, присваивается количество ошибок, произошедших перед 
завершением метода ApplyUpdates. А возвращает этот метод подмножество 
переданного потока Delta, содержащее только записи, которые привели к 
появлению ошибки обновления. 
    Используйте метод ApplyUpdates компонента ClientDataSet, если его свойству 
Provider присвоен интерфейс IProvider. Этого можно достичь путем 
непосредственного присвоения или соединения компонента ClientDataSet с 
компонентом RemoteServer. Метод ClientDataSet .ApplyUpdates, в конечном счете, 
использует метод ApplyUpdates компонента Provider через интерфейс IProvider. 
Метод ApplyUpdates компонента ClientDataSet требует только одного аргумента 
(он же используется и в методе интерфейса IProvider) и возвращает количество 
записей, вызвавших ошибки обновления. 
    Кроме различия в аргументах и типах возвращаемых значений, существует 
принципиальная разница в работе двух версий метода Apply Updates. При 
выполнении ApplyUpdates компонента ClientDataSet автоматически вызывается 
метод ClientDataSet. Reconcile. Если во время выполнения метода ApplyUpdates 
не произошло никаких ошибок, то метод Reconcile вызывает метод ClearChangeLog 
для очистки свойства Delta компонента ClientDataSet. Но ошибки обязательно 
когда-нибудь возникнут. 

    Обработка ошибок обновления с помощью события OnUpdateError 

    У вас есть две возможности обработки конфликтных попыток обновления 
(например, запись, измененная компонентом ClientDataSet, была в это же время 
удалена или изменена еще раз). Первая возможность представляется сервером 
приложения; это использование события OnUpdateError компонента Provider, 
объявленного следующим образом. 

    TUpdateKind = (ukModify, uklnsert, ukDelete); 
    TResolverResponse = (rrSkip, rrAbort, rrMerge, rrlgnore) ; 
    TResolverErrorEvent = procedure(DataSet: TClientDataSet; 
    E: EUpdateError; UpdateKind: TUpdateKind; 
    var Response: TResolverResponse) of object; 
    TProvider = class(TCustomProvider) 
    published 
    property OnUpdateError: TResolverErrorEvent ...; 
    end; 

    Обработчик события OnUpdateError используется для контроля ошибок по мере 
их возникновения. Это событие выполняется один раз для каждой записи, 
вызвавшей ошибку обновления. Обработчику события передается четыре параметра. 
Первый — это набор данных ClientDataSet, который содержит целых три копии 
записи, вызвавшей ошибку. Доступ к исходным значениям можно получить через 
свойство каждого поля OldValue, доступ к текущему конфликтующему значению— 
через свойство CurValue и доступ к значению, возвращаемому клиентским 
приложением, — через свойство NewValue. 
    Второй параметр, передаваемый обработчику события OnUpdateError, — это 
ссылка на исключительную ситуацию, возникшую при попытке обновления. Третий 
параметр дает тип попытки обновления. С его помощью можно проинструктировать 
компонент Provider о том, какие действия следует предпринять в случае 
возникновения ошибки. Эти инструкции представляют собой одно из пяти значений 
параметра-переменной Response (табл. 14.1). 
    При входе в OnUpdateError параметр Response будет иметь предопределенное 
значение rrAbort, если параметр MaxErrors метода ApplyUpdates будет равен 0, и 
значение rrSkip, если параметр MaxErrors больше нуля. 
    После того как обновления будут выполнены или прерваны, клиентское 
приложение должно полностью утвердить или не утвердить поток данных 
ClientDataSet. Если нужно сохранить изменения, внесенные в набор данных 
ClientDataSet, воспользуйтесь методом MergeChangeLog. При вызове этого метода 
свойство ChangeCount станет равным нулю, поток данных Delta будет очищен, а 
другие данные останутся нетронутыми. Как упоминалось ранее, присвоив свойству 
ClientDataSet. Data значение Null, вы очистите структуру и данные компонента 
ClientDataSet. В некоторых случаях нужно поступить так, а затем вновь 
заполнить компонент данными сервера приложения. 

    342                                     Часть III. Работа с базами данных 


    Таблица 14.1. Возможные значения параметра Response 

    Значение	Описание 

    rrSkip	Проблемная запись пропускается. Обработка записей продолжается. 
Измененная запись остается в свойстве Delta 
    rrAbort	Операция обновления полностью отменяется. Записи больше не 
обрабатываются. Все необработанные записи остаются в свойстве Delta 
    rrMerge	Измененная запись сливается с записью в конечной таблице и 
удаляется из свойства Delta 
    rrApply	Текущая запись переписывается, используя новые значения. 
Измененная запись удаляется из свойства Delta 
    rrlgnore	Проблемная запись пропускается. Обработка записей продолжается. 
Измененная запись удаляется из свойства Delta 


    Обработка ошибок обновления с помощью события OnReconcileError 

    Вторая возможность обработки ошибок обновления появляется, когда в 
клиентском приложении вызывается мета Reconcile. Можно присвоить обработчик 
события свойству OnReconcileError компонента ClientDataSet cni дующим образом. 

    TReconcileAction = (raSkip, raAbort, raMerge, raCorrect, 
    raCancel, raRefresh) ; 
    TReconcileErrorEvent = procedure(DataSet: TClientDataSet; 
    E: EReconcileError; UpdateKind: TUpdateKind; 
    var Action: TReconcileAction) of object; 
    TClientDataSet = class(TDataSet) 
    published 
    property OnReconcileError: TReconcileErrorEvent ...; 
    end; 

    Это событие вызывается для каждой записи, которая не обновляется на 
сервере приложения. Параметры этого собь тия очень похожи на соответствующие 
параметры события Provider. OnUpdateError. Параметр Action принимае одно из 
шести возможных значений. Эти значения описаны в табл. 14.2. 

    Таблица 14.2. Возможные значения параметра Action 

    Значение          Описание 

    raSkip	Проблемная запись пропускается. Обработка записей продолжается. 
Измененная запись остается в свойстве Delta 
    raAbort	Операция проверки полностью прекращается. Все необработанные 
записи остаются в свойствеDelta 
    raMerge	Измененная запись сливается с исходным образом записи и удаляется 
из свойства Delta 
    raCorrect	Измененная запись переписывается, используя новые значения и 
удаляется из свойства Delta 
    raCancel	Никакие изменения исходной записи не выполняются. Измененная 
запись удаляется из свойства Delta 
    raRefresh	Исходная запись восстанавливается. Измененная запись удаляется 
из свойства Delta 

    При входе в OnReconcileError параметр Action будет иметь значение raAbort, 
если метод ApplyUpdates был аварийно завершен, и значение raSkip — в противном 
случае. Метод Reconcile возвращает значение True, если все записи или 
зарегистрированы, или проверены, или и то, и другое. Кроме того, свойство 
ChangeCount имеет значение, равное нулю, а поток данных Delta будет иметь 
значение Null. 

    Глава 14. Построение распределенных приложений                         343 


    Резюме 

    В этой главе представлена разработка распределенных приложений в Delphi. 
Вы узнали, как пополнить компонент ClientDataSet с помощью интерфейса 
IProvider и без него, как идентифицировать, перехватывать и отменять 
изменения, выполненные для компонента ClientDataSet, как применять и проверять 
эти изменения в приложениях сервера и клиента. Это основная информация, 
необходимая для написания распределенных приложений в Delphi. 
    И наконец, вы должны проверить примеры проектов CLNTPROJ. DPR и 
SERVER.DPR, которые поставляются с Delphi. Эти два проекта реализуют оба 
уровня (клиентский и сервера) в простом распределенном приложении. Вместе они 
формируют основы системы COM/OLE Automation, базирующейся на распределенном обеспечении. 

    344                                     Часть III. Работа с базами данных