Клиент многозвенного распределенного приложения (статья)

== Клиент многозвенного распределенного приложения ==
Клиентское ПО в распределенном многозвенном приложении имеет особенности архитектуры, продиктованные его ролью - ведь большая часть бизнес-логики и функций обработки данных сосредоточены в сервере приложений. Такая схема призвана обеспечить более высокую эффективность обработки запросов многочисленных удаленных клиентов, а также упрощает обслуживание клиентского ПО. Клиенты, выполняющие лишь необходимый минимум операций, называются "тонкими".
Клиенты многозвенных приложений обеспечивают выполнение следующих функций:
* соединение с сервером приложений, прием и передача данных; 
* отображение средствами пользовательского интерфейса; 
* простейшие операции редактирования; 
* сохранение локальных копий данных.
При разработке клиентских частей многозвенных приложений в Delphi используются компоненты DataSnap, а также компонент TClientoataSet, роль которого трудно переоценить.
Помимо новых компонентов в процессе разработки применяются стандартные компоненты отображения данных
=== Структура клиентского приложения ===
По своей структуре клиентское приложение подобно обычному приложению баз данных. Соединение клиента с сервером приложений осуществляется специализированными компонентами DataSnap. Эти компоненты взаимодействуют с удаленным модулем данных, входящим в состав сервера, при помощи методов интерфейса IAppServer.
Также в клиентском приложении могут использоваться дополнительные, определенные разработчиком, методы интерфейса удаленного модуля данных, унаследованного от интерфейса IAppServer. Подробнее об этих компонентах и способах их настройки на удаленный сервер приложений см. гл. 21.
; Внимание: Соединение с сервером приложений обеспечивает динамическая библиотека MIDAS.DLL, которая должна быть зарегистрирована на компьютере клиента.
Как и обычное приложение БД, клиент многозвенного распределенного приложения должен содержать компоненты, инкапсулирующие набор данных, которые связаны с визуальными компонентами отображения данных посредством компонентов TDataSource.
Очевидно, что набор данных сервера должен быть скопирован клиентским приложением в некий локальный буфер. При этом должен использоваться эффективный механизм загрузки данных сравнительно небольшими порциями, что позволяет значительно разгрузить транспортный канал между клиентом и сервером приложений.
Кэширование и редактирование данных в клиентском приложении обеспечивает специализированный компонент TclientDataSet, отдаленным предком которого является класс TDataSet. Помимо унаследованных от предков методов, класс TclientDataSet инкапсулирует ряд дополнительных функций, облегчающих управление данными.
; Примечание: Подобно обычному приложению БД, в "тонком" клиенте для размещения невизуальных компонентов доступа к данным необходимо использовать модули данных.
Для получения набора данных сервера компонент TclientDataSet взаимодействует с компонентом TDataSetProvider, используя методы интерфейса IProviderSupport (см. гл. 21).
По существу все уникальные функции клиентского приложения сосредоточены в компоненте TclientDataSet, изучением которого мы и займемся далее в этой главе. В остальном клиентское приложение не отличается от обычного приложения БД и при его разработке могут применяться стандартные методы.
=== Клиентские наборы данных ===
В Палитре компонентов Delphi представлено несколько компонентов, инкапсулирующих клиентский набор данных. В то же время при разработке настоящих удаленных клиентских приложений применяется компонент TClientDataSet. Внесем ясность в этот вопрос. Итак, помимо компонента TClientDataSet, расположенного на странице Data Access, существуют еще два компонента:
* TSimpleDataSet - разработан для технологии доступа к данным dbExpress и, по существу, является единственным полноценным средством для работы с набором данных в рамках этой технологии;
* TClientDataSet - используется в технологии доступа к данным сервера InterBase - InterBase Express.
Все перечисленные компоненты произошли от общего предка - класса TCustomClientDataSet. Они обеспечивают локальное кэширование данных и взаимодействие с серверным набором данных при посредстве интерфейса IProviderSupport.
Основное различие между компонентом TClientDataSet и другими клиентскими компонентами заключается в том, что первый предназначен для использования с внешним компонентом-провайдером данных. А значит, он может взаимодействовать с удаленным провайдером данных.
Остальные перечисленные компоненты инкапсулируют внутренний провайдер данных, предоставляя тем самым для использования в рамках соответствующих технологий доступа к данным эффективный механизм локального кэширования данных. Использование внутреннего провайдера данных обеспечивает общий класс-предок TCustomCachedDataSet. Для этого он имеет защищенное свойство Provider. 
Соединение с источником данных осуществляется не свойством RemoteServer (будет рассмотрено ниже применительно к компоненту TClientDataSet), задающим удаленный сервер, а стандартными средствами соответствующей технологии доступа к данным.
Таким образом, для работы с удаленными данными (т. е. внешними по отношению к клиенту) пригоден только компонент TClientDataSet, умеющий работать с внешним провайдером данных.
=== Компонент TClientDataSet ===
Компонент TClientDataSet используется в клиентской части многозвенного распределенного приложения. Он инкапсулирует набор данных, переданный при помощи компонента-провайдера из удаленного набора данных. Компонент обеспечивает выполнение следующих основных функций:
* получение данных от удаленного сервера и передача ему сделанных изменений с использованием удаленного компонента-провайдера;
* представление набора данных при помощи локального буфера и поддержка основных операций, унаследованных от класса TDataSet;
* объединение записей набора данных при помощи агрегатных функций для получения суммарных данных;
* локальное сохранение набора данных в файле и последующее восстановление набора данных из файла;
* представление набора данных в формате XML.
Предком компонента TClientDataSet является класс TDataSet, поэтому TClientDataSet обладает таким же набором функций, что и обычный компонент, инкапсулирующий набор данных. Основное же отличие заключается в том, источник данных для него доступен только через удаленный компонент-провайдер. Это означает, что сохранение изменений и обновление набора данных осуществляется локально, без обращения к источнику данных.
Например, выполнение метода Post приведет лишь к сохранению текущей записи набора данных в локальном кэше. Все изменения отсылаются на сервер только при необходимости и легко управляются разработчиком.
Как и обычный компонент, компонент TClientDataSet может использоваться совместно с визуальными компонентами отображения данных. Для этого нужен компонент TDataSource.
Рассмотрим основные функции, реализуемые компонентом TClientDataSet.
=== Получение данных от компонента - провайдера ===
Компонент TClientDataSet получает доступ к удаленным данным через компонент соединения DataSnap (см. гл. 20). В зависимости от используемой технологии, это могут быть технологии TDCOMConnection, TSocketConnection, TWebConnection ИЛИ TCorbaConnection.
Компонент TClientDataSet связывается с компонентом соединения при помощи свойства RemoteServer. Если соединение настроено правильно, то ссылка на интерфейс IAppServer в свойстве AppServer совпадает со свойством ClientDataSet.RemoteServer.AppServer;
После настройки соединения в свойстве ProviderName можно выбрать один из компонентов-провайдеров, которые доступны на сервере приложений, выбранном в компоненте соединения.
Если провайдер был подключен правильно, свойство только для чтения HasAppServer автоматически принимает значение True.
Теперь компонент готов к приему данных. При использовании метода Open() или свойства Active компонент получает от провайдера первый пакет данных. Размер пакета определяется свойством PacketRecords, которое задает число записей, передаваемое в одном пакете. Если свойство имеет значение -1 (это значение по умолчанию), передаются все записи набора данных. Если оно равно 0 - клиенту передаются только метаданные о наборе данных.
Если соединение клиента с сервером медленное, число записей в пакете можно уменьшить, но желательно так, чтобы при использовании компонентов TDBGrid полученные в одном пакете записи полностью заполняли рабочую область этого компонента.
Одновременно разработчик имеет возможность управлять доставкой следующих пакетов. Для этого используется метод GetNextPacket()
Например, это можно сделать следующим образом:
procedure TDataModulel.ClientDataSetAfterScroll(DataSet: TDataSet);
begin
 if ClientDataSet.EOF then ClientDataSet.GetNextPacket;
end;
Свойство FetchOnDemand должно иметь значение False. При значении True оно разрешает компоненту получать новые пакеты данных по мере надобности, например, при необходимости прокрутки записей в компоненте TDBGrid.
До и после получения очередного пакета соответственно выполняются обработчики событий:
type
 TRemoteEvent = procedure(Sender: TObject; var OwnerData: OleVariant) of object;
 property BeforeGetRecords: TRemoteEvent;
 property AfterGetRecords: TRemoteEvent;
Содержимое очередного пакета представлено свойством Data. Данные в нем хранятся в транспортном формате, готовые для пересылки. Причем его можно использовать не только для чтения, но и для записи, формируя пакет данных для отправки провайдеру:
var
 OwnerData: OleVariant;
 MaxErrors, ErrorCount: Integer;
 MaxErrors := 0;
begin
 ResultDataSet.Data := SourceDataSet.AppServer.AS_ApplyUpdates(, SourceDataSet.Delta, MaxErrors, ErrorCount, OwnerData);
end;
Метод AS_AppiyUpdates() передает данные, содержащиеся в буфере Delta, провайдеру на сервер и возвращает записи, сохранить которые не удалось.
Размер буфера Data в байтах возвращает свойство DataSize
=== Кэширование и редактирование данных ===
После получения записей от провайдера набор данных сохраняется в локальном буфере памяти. И все вносимые изменения после применения метода Post также сохраняются локально и не пересылаются на сервер. Буфер изменений доступен при помощи свойства Delta
Для передачи изменений на сервер используется метод ApplyUpdates(), где параметр MaxErrors задает число ошибок, которые игнорируются при сохранении данных на сервере. Если параметр равен -1, сохранение на сервере прерывается при первой же ошибке. Метод возвращает число сохраненных записей.
После выполнения метода ApplyUpdates() все записи, сохранить которые не удалось, возвращаются клиенту в локальный буфер Delta.
Если клиентское приложение будет редко изменять свои наборы данных, сохранение изменений на сервере можно связать с методом-обработчиком AfterPost:
procedure TForml.ClientDataSetAfterPost(DataSet: TDataSet);
begin
 ClientDataSet.ApplyUpdates(-1);
end;
Свойство только для чтения ChangeCount возвращает общее число изменений, содержащееся в буфере Delta. Для очистки буфера изменений используется метод CancelUpdates(). После вызова метода свойство ChangeCount принимает значение 0.
До и после сохранения изменений на сервере соответственно вызываются методы-обработчики BeforeApplyUpdates и AfterApplyUpdates.
Несмотря на сделанные локально многократные изменения, запись может быть восстановлена в первоначальном виде. Метод RefreshRecord() получает от провайдера первоначальный вариант текущей записи, сохраненный на сервере. При этом (и при всех других случаях, когда компонент запрашивает обновление текущей записи) вызываются методы-обработчики BeforeRowRequest и AfterRowRequest
Но что делать, если необходимо восстановить удаленную запись? В обычном наборе данных после сохранения это невозможно. В компоненте TClientDataSet существует метод UndoLastChange(), который возвращает набор данных к состоянию до последней выполненной операции редактирования, добавления или удаления записи. Если параметр FollowChange имеет значение True, курсор набора данных будет установлен на восстановленную запись.
О состоянии текущей записи позволяет судить метод UpdateStatus(), который возвращает значение типа TUpdateStatus, означающее состояние текущей записи:
* usUnmodified - запись осталась неизменной;
* usModified - запись была изменена;
* uslnserted - запись была добавлена;
* usDeleted - запись была удалена.
Например, при закрытии набора данных можно выполнить проверку:
if ClientDataSet.UpdateStatus = usModified then
 ShowMessage('Record was changed');
На основе типа можно управлять видимостью записей в наборе данных. Свойство StatusFilter определяет, какой тип записей будет отображаться в наборе данных. 
Например:
ClientDataSet.StatusFilter := usDeleted;
отобразит в наборе данных только удаленные записи (при этом изменения не сохранены на сервере).
=== Управление запросом на сервере ===
Компонент TClientDataSet может не только эффективно управлять своим набором данных, но и влиять на выполнение серверного компонента, с которым он связан через провайдер.
Свойство CornmandText содержит текст запроса SQL, имя таблицы или хранимой процедуры в зависимости от типа серверного компонента. Изменив значение этого свойства на клиенте, можно, например, модифицировать запрос SQL на сервере. Но для этого в свойстве Options соответствующего компонента-провайдера TDataSetProvider должно быть установлено значение
poAliowCommandText := True;
Новое значение свойства CommandText отправляется на сервер только после открытия клиентского набора данных или выполнения метода Execute()
Для запросов или хранимых процедур можно задавать параметры, которые сохраняются в свойстве Params. До выполнения запроса присваиваются значения входным параметрам. После выполнения хранимой процедуры в выходных параметрах размещаются полученные от сервера значения.
Обратите внимание, что при выполнении запросов или хранимых процедур может измениться порядок следования параметров. Поэтому обращаться к параметрам желательно по их именам. Например, так:
Editl.Text := ClientDataSet.Params.ParamByName('OutputParam').AsString;
Для того чтобы получить текущие значения параметров компонента набора данных на сервере, достаточно использовать метод FetchParams()
Перед и после получения параметров от провайдера, клиентский набор данных вызывает методы-обработчики событий:
property BeforeGetParams: TRemoteEvent;
property AfterGetParams: TRemoteEvent;
=== Использование индексов ===
Обычно использование индексов - прерогатива сервера БД. Из компонентов Delphi только табличные компоненты могут в какой-то степени управлять использованием индексов. Очевидно, что удаленное соединение не способствует эффективному управлению индексами набора данных на сервере. Поэтому компонент TClientDataSet предоставляет разработчику возможность создавать и использовать локальные индексы.
Правильно созданные и используемые локальные индексы могут существенно ускорить выполнение операций с набором данных. В то же время их невозможно сохранить вместе с набором данных локально, их необходимо перестраивать при каждом новом открытии набора данных и его обновлении с сервера.
Для создания локального индекса используется метод
procedure Addlndex(const Name, Fields: string;
 Options: TIndexOptions;
 const DescFields: string = ;
 const CaselnsFields: string = ;
 const GroupingLevel: Integer = 0);
Параметр Name определяет имя нового индекса. Параметр Fields должен содержать имена полей, которые разработчик хочет включить в индекс. Имена полей должны разделяться точкой с запятой. Параметр Options позволяет задать тип индекса:
TIndexOption = (ixPrimary, ixUnique, ixDescending, ixCaselnsensitive, ixExpression, ixNonMaintained);
TIndexOptions = set of TIndexOption;
* ixPrimary - первичный индекс;
* ixUnique - значения индекса уникальны;
* ixDescending - индекс сортирует записи в обратном порядке;
* ixCaselnsensitive - индекс сортирует записи без учета регистра символов;
* ixExpression - в индексе используется выражение (для индексов dBASE);
* ixNonMaintained - индекс не обновляется при открытии таблицы.
При этом можно задать поля, порядок сортировки которых будет обратным. Для этого их необходимо перечислить через точку с запятой в параметре DescFields. Параметр CaselnsFields аналогичным образом позволяет задать поля, на сортировку которых не влияет регистр символов.
Параметры DescFields и CaselnsFields используются вместо параметра Options.
Параметр GroupingLevel задает уровень группировки полей индекса. Подробнее об этом см. ниже в разд. "Агрегаты" этой главы.
Основные свойства компонента, обеспечивающие управление индексами, совпадают с аналогичными свойствами табличных компонентов (подробнее об этом см. гл. 12). Поэтому лишь кратко перечислим их.
При работе с компонентом разработчик имеет возможность управлять индексами. Созданный индекс подключается к набору данных свойством IndexName, которое должно включать имя индекса или использовать свойство IndexFieldNames, в котором можно задать произвольное сочетание имен индексированных полей таблицы. Имена полей разделяются точкой с запятой. Свойства IndexName и IndexFieldNames нельзя использовать одновременно.
Число полей, используемых в текущем индексе табличного компонента, возвращает свойство IndexFieldCount. Cвойство IndexFields представляет собой индексированный список полей, входящих в текущий индекс.
Параметры созданных индексов доступны в свойстве IndexDefs. Класс TIndexDefs подробно рассматривается в гл. 12.
После создания и подключения индекса записи набора данных "переупорядочиваются" в соответствии со значениями индексированных долей.
Удаление локального индекса обеспечивает метод DeleteIndex()
После удаления текущего индекса или его отмены (обнуления свойства IndexName) записи набора данных "переупорядочиваются" в исходном порядке, соответствующем порядку записей набора данных на сервере.
Имена всех существующих в наборе данных индексов можно загрузить в список при помощи метода GetlndexNames()
Например:
Memol.Lines.Clear;
ClientDataSet.GetlndexNames(Memol.Lines);
=== Сохранение набора данных в файлах ===
Клиентское приложение может использовать одну очень удобную функцию компонента TClientDataSet. Представим, что соединение между сервером и клиентом обладает малой пропускной способностью и к тому же часто обрывается. Что в этом случае делать пользователю, который внес много изменений и не может сохранить их на сервере?
В этом случае можно сохранить набор данных клиента в файле на локальном диске, а при удобной возможности - загрузить обратно и переслать на сервер.
Для сохранения данных (по существу это буфер Data) в файле используется метод
procedure SaveToFile(const FileName: string = ; Format: TDataPacketFormat = dfBinary);
Причем, если параметр FileName пуст, имя файла берется из свойства FileName.
Также можно передать данные в поток:
procedure SaveToStream(Stream: TStream; Format: TDataPacketFormat = dfBinary);
Формат, в котором данные будут сохранены, определяется параметром Format
* dfBinary - бинарный вид,
* dfXML - формат XML,
* dfXMLUTF8 - формат XML в кодировке UTF8.
Обратная загрузка данных, соответственно, выполняется методами LoadFromFile() и LoadFromStreamfStream()
После загрузки набор данных полностью готов к работе:
 
if LoadFileDialog.Execute then
 begin
 ClientDataSet.LoadFromFile(LoadFileDialog.FileName);
 ClientDataSet.Open;
 end;
=== Работа с данными типа BLOB ===
Если набор данных сервера содержит большие поля (например, изображения), передача данных по медленному каналу займет очень много времени, что, несомненно, снизит эффективность приложения. Простейшее решение проблемы - передача клиенту данных типа BLOB только в том случае, когда это ему действительно необходимо - т. е. исключительно по его запросу.
В компоненте TCHentDataSet процессом передачи полей типа BLOB можно управлять, используя свойство FetchOnDemand. По умолчанию оно равно значению True и клиентский набор данных "выкачивает" данные BLOB по мере необходимости автоматически. Это означает, что приложение будет останавливаться и заново получать данные при любом просмотре данных, прокрутке и т. д. Если свойство имеет значение False, для получения данных клиент должен явно вызвать метод FetchBlobs(). Но, кроме этого, в свойстве options компонента-провайдера TDataSetProvider обязательно должно быть установлено значение: poFetchBlobsOnDemand := True;
=== Представление данных в формате XML ===
Набор данных клиента легко можно представить в формате XML. Для этого достаточно использовать свойство XMLData, которое возвращает данные, содержащиеся в буфере Data (см. выше) в бинарном виде, в формате XML.
Например, клиентский набор данных можно сохранить в файле формата XML:
 if SaveDialog.Execute then
 with TFileStream.Create(SaveDialog.FileName, fmCreate) do
 try
 Write(Pointer(ClientDataSet.XMLData)^, Length(ClientDataSet.XMLData));
 finally
 Free;
 end;
== Агрегаты ==
Наличие локального буфера данных позволяет компоненту TClientDataSet реализовать ряд дополнительных функций, основанных на использовании агрегатных функций применительно к полям всего набора данных, загруженного в локальный буфер.
К агрегатным функциям относятся:
* AVG - вычисляет среднее значение;
* COUNT - возвращает число записей;
* MIN - вычисляет минимальное значение;
* МАХ - вычисляет максимальное значение;
* SUM - вычисляет сумму.
Для их применения в компоненте TClientDataSet предусмотрены:
* индексированный список объектов, инкапсулирующих агрегатные выражения - агрегаты;
* агрегатные поля, обеспечивающие получение новых значений подобно вычисляемым полям, но с группированием записей на основе использования агрегатных функций.
=== Объекты-агрегаты ===
Для вычисления агрегатных выражений для всех записей набора данных используются объекты класса TAggregate. Индексированный список этих объектов содержится в свойстве Aggregates компонента TClientDataSet. Прямым предком класса TAggregates является класс TCollection, поэтому для него можно использовать все основные приемы работы с коллекциями.
Для создания нового агрегата необходимо щелкнуть на кнопке свойства в Инспекторе объектов и, в появившемся Редакторе агрегатов, выбрать пункт Add во всплывающем меню или щелкнуть на кнопке Add New.
Новый агрегат может быть добавлен и динамически:
var NewAgg: TAggregate;
NewAgg := ClientDataSet.Aggregates.Add;
Рассмотрим свойства класса TAggregate.
Имя агрегата содержится в свойстве AggregateName, которое может быть использовано при отображении агрегата в визуальных компонентах.
Вычисляемое выражение с применением агрегатных функций должно находиться в свойстве Expression. Например, для таблицы COUNTRY.DB из демонстрационной базы данных Delphi можно вычислять общую площадь государств Северной и Южной Америки (площадь государства содержится в поле Area):
ClientDataSet.Aggregates[SomeIndex].Expression := 'SUM(Area)';
Вычислением агрегата управляет свойство Active, а вычисленное значение возвращает функция Value()
Если пользователь редактирует набор данных, то для всех включенных агрегатов (Active = True) возвращаемое значение автоматически пересчитывается.
Например, после сохранения изменений в наборе данных можно визуализировать новое значение агрегата:
SomeLabel.Caption := ClientDataSet.Aggregates[0].AggregateName;
SomeEdit.Text := ClientDataSet.Aggregates[0].Value;
Для проверки активности агрегата, помимо проверки значения свойства Active, можно также использовать свойство InUse. Если оно возвращает значение True - вычисляемое выражение агрегата рассчитывается.
Видимость агрегата в визуальных компонентах управляется свойством Visible
Для того чтобы снизить вычислительную нагрузку на набор данных, можно отключить все агрегаты одновременно. Для этого свойству AggregatesActive необходимо присвоить значение False.
Если же AggregatesActive = True, вычисляются только активные агрегаты, для которых свойство Active имеет значение True.
Если вам необходимо использовать все активные агрегаты, то вместо их последовательного перебора с проверкой свойства Active можно использовать свойство ActiveAggs[] компонента TClientDataSet, которое представляет собой список активных агрегатов.
=== Агрегатные поля ===
Агрегатные поля не входят в структуру полей набора данных, т. к. агрегатные функции подразумевают объединение записей таблицы для получения результата. Следовательно, значение агрегатного поля нельзя связать с какой-то одной записью, оно относится ко всем или группе записей.
Агрегатные поля не отображаются вместе со всеми полями в компонентах TDBGrid, в Редакторе полей они расположены в отдельном списке. Для представления значения агрегатного поля можно воспользоваться одним из компонентов отображения данных, который визуализирует значение одного поля (например, TDBText или TDBEdit) или свойствами самого поля:
LabelI.Caption := MyDataSetAGGRFIELDl.AsString;
Подробно вопросы создания агрегатных полей рассмотрены в гл. 13.
Класс TAggregateField предназначен для инкапсуляции свойств и методов агрегатных полей.
Его свойство Expression задает вычисляемое выражение. Вычисление значения проводится только для тех агрегатных полей, свойство Active которых имеет значение True.
Вычисление включенных свойством Active агрегатных полей выполняется только в том случае, если булевское свойство AggregatesActive клиентского компонента набора данных имеет значение True.
По умолчанию экземпляр класса TAggregateField создается со свойством Visible = False
=== Группировка и использование индексов ===
Каждый агрегат (объект или поле) имеет свойство GroupingLevel, которое задает уровень группировки полей набора данных при вычислении. При значении 0 расчет проводится для всех записей набора данных. При значении 1 записи группируются по первому полю набора данных и расчет осуществляется для каждой группы. При значении 2 записи разбиваются на группы по первому и второму полям и т. д.
Однако группировка по уровням выше нулевого возможна, только если в наборе данных используется индекс по группирующим полям. Например, если свойство GroupingLevel = 2 и набор данных начинается с полей CustNo и OrderNo, в свойстве IndexName компонента TClientDataSet и свойств property IndexName: String; агрегата (объекта или поля) должно быть имя индекса, включающего оба эти поля.
== Вложенные наборы данных ==
При организации между таблицами отношения "один-ко-многим", когда через одинаковое значение поля внешнего ключа одна запись главной таблицы связывается с несколькими записями подчиненной таблицы. Этот широко распространенный в практике программирования приложений БД механизм реализован и в компоненте TClientDataSet.
Для этого используется класс поля TDataSetField.
На стороне клиента для создания отношения "один-ко-многим" необходимо использовать как минимум два компонента TClientDataSet, главный из которых инкапсулирует основной набор данных, а подчиненный - вложенный набор данных.
Итак, на стороне сервера есть два табличных компонента, связанных отношением "один-ко-многим" при помощи свойств MasterSource и MasterFields (см. гл. 14). Также это могут быть и два компонента запросов SQL, связанные параметрами подчиненного запроса с одноименными полями главного запроса и свойством DataSource.
Теперь на стороне клиента необходимо при помощи компонента-провайдера связать компонент TClientDataSet с главным серверным компонентом отношения "один-ко-многим" и создать для него статические объекты для всех полей. Для этого достаточно дважды щелкнуть на компоненте и в окне Редактора полей (см. рис. 22.3) из всплывающего меню выбрать пункт Add Field. В результате в окне Редактора полей появятся имена объектов для всех полей серверного набора данных, а также еще одно дополнительное поле объектного типа TDataSetFieid. Его имя совпадает с именем подчиненного серверного компонента отношения "один-ко-многим".
Это поле связано с подчиненным компонентом на сервере. Чтобы убедиться в этом, достаточно просмотреть значение его свойства только для чтения NestedDataSet
Индексированный список всех полей, передаваемых из серверного подчиненного компонента, содержится в свойстве только для чтения Fields. В дальнейшем связь между компонентами на клиенте настраивается именно через это поле. В подчиненном компоненте TClientDataSet в Инспекторе объектов необходимо выбрать свойство DataSetField
В списке этого свойства вы увидите имя только что созданного поля объектного типа TDataSetField. Выберите его и отношение "один-ко-многим" для клиентских наборов данных готово. При этом в компоненте вложенного набора данных автоматически очищаются свойства RemoteServer и FroviderName, т. к. их значения утрачивают значение и компонент оказывается связан только с главным компонентом отношения "один-ко-многим".
Теперь при навигации по записям основного набора данных во вложенном наборе данных автоматически будут появляться связанные записи. Также вы можете использовать все возможности, предоставляемые компонентом TClientDataSet как для основного, так и для вложенного набора данных.
== Дополнительные свойства полей клиентского набора данных ==
Как известно, все классы полей имеют одного общего предка - класс TField. Остановимся лишь на нескольких дополнительных свойствах полей, которые работают только в режиме кэширования в обычных компонентах, инкапсулирующих набор данных, и в компоненте TClientDataSet. Причем в компоненте TClientDataSet реализация этих свойств обеспечена локальным кэшем.
Итак, для разработчика могут быть полезны свойства объектов полей, содержащие не только текущее, но и предыдущее значение поля.
Свойство CurValue возвращает текущее значение поля. Свойство OldValue содержит значение поле, которое было до начала редактирования. Свойство NewValue содержит новое значение, которое может быть присвоено при обработке ошибки сервера методом-обработчиком onReconclieError (см. ниже).
== Обработка ошибок ==
Особенности использования компонента TClientDataSet распространяются также и на обработку ошибок. Ведь клиентский набор данных должен реагировать не только на ошибки, возникшие локально, но и на ошибки сохранения изменений на сервере.
В первом случае разработчик может применить стандартные способы. Это использование блоков try..except или методов обработчиков, унаследованных от класса TDataSet:
* OnDeleteError - вызывается при ошибках удаления записей;
* OnEditError - вызывается при ошибках редактирования записей;
* OnPostError - вызывается при ошибках локального сохранения записей.
Все они используют процедурный тип
type
 TDataSetErrorEvent = procedure(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction) of object;
Здесь, помимо параметров DataSet и Е, определяющих соответственно набор данных и тип ошибки, параметром Action можно задать вариант реакции на ошибку:
* daFail - прервать операцию и показать сообщение об ошибке;
* daAbort - прервать операцию без сообщения об ошибке;
* daRetry - повторить операцию
Например, при возникновении ошибки редактирования набора данных код обработчика может выглядеть следующим образом:
procedure TForml.ClientDataSetEditError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction);
begin
 if Not (DataSet.State in [dsEdit, dsInsert]) then
 begin
 DataSet.Edit;
 Action := daRetry;
 end
 else
 Action := daAbort;
end;
Здесь, если набор данных не находится в состоянии редактирования, это упущение исправляется и операция повторяется.
Итак, с локальными ошибками все обстоит достаточно просто. А как клиентский набор данных "узнает" об ошибке на удаленном сервере? Очевидно, при помощи своего компонента-провайдера. Действительно, компонент TDataSetProvider не только возвращает клиенту несохраненные изменения в пакете Delta (см. выше), но и обеспечивает генерацию события, реакцией на которое является метод-обработчик
type
TReconcileErrorEvent = procedure(DataSet: TCustomClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction) of object;
property OnReconcileError: TReconcileErrorEvent;
Обратите внимание, что все параметры похожи на соответствующие параметры локальных обработчиков, но имеют собственные типы. Рассмотрим их.
Параметр UpdateKind содержит указание на тип операции, вызвавшей ошибку на сервере:
* ukModify - изменение данных;
* uklnsert - добавление записей;
* ukDelete - удаление записей.
Параметр Action позволяет разработчику предусмотреть реакцию клиентского набора данных на ошибку:
* raSkip - отменить операцию для записей, вызвавших ошибку, с их сохранением в буфере;
* raAbort - отменить все изменения для операции, вызвавшей ошибку;
* raMerge - совместить измененные записи с аналогичными записями сервера;
* raCorrect - сохранить изменения, сделанные в данном методе-обработчике;
* raCancel - отменить изменения, вызвавшие ошибку, заменив их исходными локальными значениями клиентского набора данных;
* raRefresh - отменить изменения, вызвавшие ошибку, заменив их исходными значениями серверного набора данных.
Как видите, выбор возможных реакций на ошибку сервера несколько шире, чем на локальные ошибки.
Тип ошибки возвращается параметром Е, для которого предусмотрен специальный класс EReconcileError, имеющий несколько полезных свойств.
Свойство ErrorCode возвращает код ошибки. Используемые коды ошибок можно найти в файле \Source\Vcl\DSIntf.pas. Код предыдущей ошибки возвращается свойством PreviousError
Используя представленную здесь информацию, вы можете самостоятельно управлять обработкой ошибок сервера на клиенте. Но можно поступить и более просто - использовать стандартный диалог обработки удаленных ошибок (рис. 22.4). Этот диалог можно подключить к вашему проекту (он содержится в модуле \ObjRepos\RecError.pas) и вызвать при помощи процедуры:
function HandleReconcileError(DataSet: TDataSet; UpdateKind: TUpdateKind; ReconcileError: EReconcileError): TReconcileAction;
В параметры этой функции подставляются параметры метода-обработчика OnReconciieError, а возвращает данная функция действие, выбранное пользователем в диалоге (см. рис. 22.4). Таким образом, ее использование очень просто:
procedure TForml.ClientDataSetReconcileError(DataSet: TCustomClientDataSet;
 E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
 Action := HandleReconcileError(DataSet, UpdateKind, E);
end;
ID: 04364
Category:Статьи
Category:Базы данных