Программирование баз данных в Delphi

Многоуровневая архитектура. Создание тонкого клиента

Показывать лекцию целиком

На прошлой лекции мы рассматривали создание сервера приложений на основе удаленного модуля данных Remote Data Module. Эта лекция посвящена созданию клиентской части многоуровневых приложений на основе технологии DCOM. Связать клиентское приложение с одной таблицей очень просто. Связать две или более таблицы в отношении один-ко-многим несколько сложней, тут есть свои тонкости. Именно такую связь мы и будем делать. Кроме того, мы разберем способ реализации отложенной обработки, когда данные можно сохранить в файл, и при необходимости, загрузить их из файла без соединения с удаленным сервером приложений.

Связь клиентского приложения с удаленным модулем данных организуется одним из компонентов связи, расположенных на вкладке DataSnap Палитры компонентов, а для связи с наборами данных используется компонент ClientDataSet, который находится на вкладке Data Access Палитры компонентов. Каждый ClientDataSet соединяется с одним набором данных (таблица, запрос).

Разберем новый материал на практике, попутно останавливаясь на важных моментах.

Создание клиентского приложения

Начните новый проект. В свойстве Name главной формы напишите fMain, в свойстве Caption напишите "Клиент удаленного сервера", сохраните ее модуль под именем Main, а проект в целом - MyNewClient. На форму поместите три простых панели, сразу же очистив их свойство Caption. У верхней панели в свойстве Align выберите alTop, а в свойстве AutoSize установите True. Поместите на эту панель навигатор DBNavigator с вкладки Data Controls Палитры компонентов.

У нижней панели в свойстве Align выберите alBottom, чтобы она заняла всю нижнюю часть формы. На панель поместите сетку DBGrid и шесть простых кнопок.

Средняя панель в свойстве Align будет иметь значение alClient. На ней помещается только одна сетка DBGrid, которая в свойстве Align также имеет значение alClient. Внешний вид формы представлен на рисунке ниже:

(рис 29.1 ) Главная форма клиентского приложения

Свойство Name у кнопок переименуйте, соответственно, в:

  • bConnect
  • bRefresh
  • bSaveToDB
  • bUnConnect
  • bSaveToFile
  • bLoadFromFile
  • Свойство Caption кнопок измените, как на рисунке.

    Верхнюю сетку и навигатор позже соединим с главной таблицей LichData, нижняя сетка отобразит данные подчиненной таблицы Telephones.

    Для невизуальных компонентов доступа создадим обычный модуль данных. Выберите команду File -> New -> Data Module. В свойстве Name модуля данных укажите fDM, а сам модуль сохраните на диск под именем DM. Не забудьте в главной форме командой File -> Use Unit подключить этот модуль.

    Перейдем на вкладку DataSnap. Вы видите целый ряд компонентов, реализующих подключение с удаленным модулем данных различными технологиями. Нам нужен компонент DCOMConnection, который использует технологию DCOM, поместите его в модуль данных. Для краткости обращений к компоненту переименуйте его свойство Name в DCOMC. Теперь нужно подключить его к удаленному серверу. В свойстве ComputerName нужно указать сетевое имя или IP -адрес компьютера, на котором расположен сервер приложений. Поскольку у нас все хранится на одном компьютере, укажем адрес 127.0.0.1, то есть, локальный IP -адрес.

    Теперь щелкнем по выпадающему свойству ServerName, которое хранит список всех доступных серверов на указанном компьютере. Если в прошлой лекции вы все сделали правильно, сейчас список должен содержать одну строку - имя серверного проекта и имя удаленного модуля данных. У нас это

    MyNewServer.MyRDM

    Выберем эту строку. В свойстве ServerGUID автоматически должен появиться уникальный идентификатор сервера. Убедимся, что свойство LoginPrompt установлено в False, чтобы при соединении не запрашивались имя пользователя и пароль, и переведем свойство Connected в True, чтобы физически подключить клиентское приложение к серверу. Если вы все сделали правильно, то соединение произойдет, при этом Windows автоматически загрузит сервер приложений, который мы делали на прошлой лекции. Вот таким простым способом клиентское приложение связывается с удаленным модулем данных. Как видите, компонент содержит минимум необходимых свойств.

    Теперь нам нужно подключиться к наборам данных на сервере. Для этого используется компонент ClientDataSet из вкладки Data Access Палитры компонентов. Для каждого набора данных требуется установить по одному такому компоненту. Это важный компонент, поэтому рассмотрим его подробней.

    Компонент ClientDataSet предназначен для:

  • Предоставления удаленных данных, расположенных на сервере приложений;
  • Загрузки данных из локального файла;
  • Сохранение набора (или связанных наборов) данных в локальный файл.
  • Компонент ClientDataSet является наследником объекта DataSet, как таблицы или запросы, поэтому он имеет почти те же свойства, методы и события, что и эти наборы данных. Например, открытие этого НД можно осуществить как методом Open, так и присвоением True свойству Active. Однако, в связи с удаленным способом связи, имеются и различия. Прежде всего, различие связано с тем, что компонент считывает данные в буфер (или кэш), после чего работает с ними. Выполнение метода Post, к примеру, сохраняет данные не в базе данных на сервере, а в буфере (в оперативной памяти клиентского приложения). Чтобы сохранить эти данные в базу, нужно явно вызвать метод ApplyUpdates. Это позволяет существенно снизить нагрузку на сетевой трафик. Таким способом и реализуется отложенная обработка.

    Рассмотрим основные свойства и методы компонента ClientDataSet.

    Основные свойства и методы компонента ClientDataSet

    Важные свойства компонента ClientDataSet
    Свойство Описание
    ChangeCount Содержит количество изменений в буфере. После явного сохранения изменений в базу данных, это свойство содержит 0.
    CommandText Содержит текст SQL -запроса, который может быть выполнен методом Execute компонента. Таким образом, можно изменять НД, предоставляемый сервером.
    Data Свойство содержит пакет данных, предназначенный для передачи по сети в специальном транспортном формате.
    DataSize Размер в байтах пакета Data.
    DataSetField При отношении один-ко-многим, свойство у подчиненного ClientDataSet позволяет выбрать специальное поле типа TDataSetField, которое инкапсулирует данные подчиненного НД. Подробней об этом поговорим ниже.
    Delta Пакет с измененными данными, которые еще не сохранены на сервере.
    FetchOnDemand При значении True (по умолчанию) разрешает компоненту получать очередной пакет данных по мере надобности, например при прокрутке сетки DBGrid.
    FileName Может содержать имя файла, в которое при необходимости будет сохранять данные, или считывать их. Если этого имени нет, можно использовать SaveDialog или OpenDialog для получения этого имени от пользователя.
    PacketRecords Указывает количество записей, получаемых из серверного НД в одном пакете. По умолчанию равно -1, то есть считываются все записи. Если значение равно 0, то считываются только метаданные.
    ProviderName Имя компонента-провайдера на стороне сервера ( DataSetProvider ), который предоставляет доступ к нужному набору данных.
    RemoteServer Имя компонента соединения (например, DCOMConnection ), с помощью которого компонент подключается к удаленному модулю данных. Это свойство нужно настраивать в первую очередь.

    Теперь рассмотрим основные методы компонента ClientDataSet.

    Важные методы компонента ClientDataSet
    Метод Описание
    AddIndex Создает локальный индекс. ClientDataSet не может эффективно управлять индексами на стороне сервера, однако он позволяет создавать локальные индексы, которые существенно облегчают работу с данными. Однако эти индексы невозможно сохранить вместе с набором данных, поэтому их нужно перестраивать при каждом открытии
    ApplyRange Включает механизм фильтрации по созданному ранее диапазону.
    ApplyUpdates Обновляет записи в базе данных. Имеет параметр - максимальное количество ошибок (по умолчанию 0), после которых обновление прекращается. Возвращает целое число - количество действительно допущенных ошибок.
    Cancel Отменяет все неподтвержденные изменения записи.
    CancelRange Отменяет механизм фильтрации по указанному диапазону.
    CancelUpdates Отменяет все неподтвержденные изменения из пакета Delta, предназначенные для дальнейшей передачи на сервер.
    DeleteIndex Удаляет локальный индекс.
    EmptyDataSet Очищает буфер от всех записей.
    Execute Выполняет SQL -запрос из свойства CommandText, меняя НД на стороне сервера.
    FetchBlobs По умолчанию, большие BLOB -столбцы, которые могут содержать изображение, музыку или какие либо двоичные данные, в целях разгрузки трафика клиенту не передаются. Метод FetchBlobs явно запрашивает с сервера содержимое текущего BLOB -столбца.
    FetchDetails Запрашивает с сервера недостающие данные из вложенных или подчиненных таблиц. Если свойство FetchOnDemand имеет значение True, то эти данные подгружаются автоматически, и применять метод не нужно.
    FetchParams Получает с сервера приложений текущие параметры набора данных провайдера. Если параметров нет, метод не делает ничего.
    GetNextPacket Запрашивает с сервера очередной пакет записей, который будет добавлен к свойству Data. Длина пакета в записях определяется свойством PacketRecords.
    LoadFromFile Загружает данные из локального файла. Вместе с SaveToFile позволяет организовать отложенную обработку данных. Подробнее об этом поговорим ниже.
    LoadFromStream Загружает данные из потока.
    Post Подтверждает сделанные изменения в буфере. На сервере данные при этом не меняются, для этого нужно явно вызвать метод ApplyUpdates.
    RefreshRecord Обновляет текущую запись, запрашивая ее с сервера.
    RevertRecord Восстанавливает текущую запись, если она была изменена, но еще не сохранена на сервере.
    SaveToFile Сохраняет данные в локальный файл. Отличается от аналогичного метода других компонентов наличием второго параметра - формата данных файла. Может быть три формата: dfBinary - двоичный формат, dfXML - формат XML и dfXMLUTF8 - формат XML в кодировке UTF8.
    SaveToStream Сохраняет данные в поток.

    Реализация подключений к серверным наборам данных

    Вернемся к нашему проекту. Поместите в модуль данных два компонента ClientDataSet. Для подключения этих компонентов к наборам данных с сервера приложений, прежде всего, в свойстве RemoteServer нужно выбрать проводника, у нас это DCOMC. Далее в свойстве ProviderName следует выбрать нужного провайдера, который обеспечит связь с НД сервера. Для первого ClientDataSet это будет DSPLichData, для второго - DSPTelephones. Соответственно, у первого компонента ClientDataSet в свойстве Name впишите CDSLichData, у второго - CDSTelephones.

    Для того, чтобы связать навигатор DBNavigator и сетки DBGrid на главной форме, нам потребуются два компонента DataSource с вкладки Data Access. Первый назовите dsLichData и в свойстве DataSet свяжите его с набором данных CDSLichData, второй назовите dsTelephones и свяжите его с CDSTelephones. Откройте оба ClientDataSet, переведя их свойство Active в True.

    Вернитесь на главную форму. Навигатор и верхнюю сетку через свойство DataSource подключите к fDM.dsLichData, нижнюю сетку подключите к fDM.dsTelephones. Сетки при этом должны отобразить данные. Сохраните проект, скомпилируйте и загрузите. Сразу же при этом проявляется ошибка: нижняя сетка содержит подчиненные данные из таблицы Telephones, относящиеся к первой записи главной таблицы LichData. При смене записи в главной таблице, подчиненная таблица записей не меняет, несмотря на то, что мы организовали связь один-ко-многим на стороне сервера. Обусловлено это тем, что при начале работы подчиненная таблица получила нужные записи в локальный буфер, и теперь с ними работает, не изменяя его. Точно также, если мы сейчас попробуем сохранить данные таблиц в локальный файл, то записи главной таблицы сохранятся полностью, подчиненная же таблица сохранит лишь те записи, которые есть в кэше. Исправим эту ситуацию.

    Вернитесь в модуль данных. Щелкните дважды по компоненту CDSLichData, который содержит данные главной таблицы. Откроется редактор полей. Щелкните по нему правой кнопкой, и командой AddFields (добавить поля) добавьте все имеющиеся там поля. Как вы можете заметить, помимо всех полей таблицы LichData редактор отобразил еще одно поле: TTelephones, которое имеет тип TDataSetField. Это специальное поле, которое содержит данные подчиненной таблицы, через него и будет создаваться нужная связь один-ко-многим. Кнопкой OK закройте редактор полей, добавив все имеющиеся там поля.

    Перейдем к подчиненному компоненту CDSTelephones. В свойстве DataSetField выберите полученное поле главной таблицы CDSLichDataTTelephones. При этом автоматически очистятся свойства RemoteServer и ProviderName. Теперь этот НД связан с сервером через главную таблицу, а не напрямую. Снова сохраните проект, скомпилируйте и загрузите его. Теперь полный порядок: при изменении записи в главной таблице, изменяются записи подчиненной таблицы.

    Однако это не все. На модуль данных установите также два диалога: SaveDialog и OpenDialog. Переименуйте их соответственно, в SD1 и OD1. Выделите оба диалога и откройте редактор свойства Filter. В редакторе укажите единственную строчку:

    Файлы данных | *.dat

    В свойстве DefaultExt диалогов укажите расширение по умолчанию, "dat" (разумеется, без кавычек). В свойстве FileName укажите имя файла по умолчанию: "LichData.dat". В свойстве InitialDir укажите папку по умолчанию: "C:\". Таким образом, мы настроили оба диалога на работу с файлом

    C:\LichData.dat

    Конечно, пользователь сможет изменить и адрес, и имя файла, но по умолчанию будут использованы наши настройки. Осталось изменить свойство Title диалогов. У OD1 укажите "Открыть файл с личными данными", у SD1 - "Сохранить файл с личными данными".

    Вернемся к главной форме. Создайте обработчик нажатия на кнопку "Подключиться к базе". Код обработчика приведен ниже:

    procedure TfMain.bConnectClick(Sender: TObject);
    begin
      //если нет подключения:
      if not fDM.DCOMC.Connected then begin
        // подключаем DCOMConnected
        fDM.DCOMC.Connected:= True;
        //открываем наборы данных:
        fDM.CDSLichData.Open;
        fDM.CDSTelephones.Open;
      end; //if
    end;

    Комментарии достаточно подробны, чтобы вы смогли разобраться с кодом. Обработчик нажатия на "Обновить данные" будет таким:

    procedure TfMain.bRefreshClick(Sender: TObject);
    begin
      //отменяем все неподтвержденные изменения:
      fDM.CDSLichData.CancelUpdates;
      fDM.CDSTelephones.CancelUpdates;
    end;

    При отмене неподтвержденных изменений с сервера считывается текущий набор данных, и сетки будут отображать действительное положение дел. Кнопка "Сохранить изменения в базу" вызовет следующий обработчик:

    procedure TfMain.bSaveToDBClick(Sender: TObject);
    var i,k: Integer; //для подсчета ошибок
    begin
      i:= fDM.CDSLichData.ApplyUpdates(0);
      k:= fDM.CDSTelephones.ApplyUpdates(0);
      if i + k > 0 then
        ShowMessage('При сохранении данных выявлено ошибок: '+
           IntToStr(i+k))
      else ShowMessage('Данные сохранены');
    end;

    Переменные целого типа нам нужны для подсчета возможных ошибок при сохранении данных на сервер. Метод ApplyUpdates, вызванный для каждого НД, не только сохраняет данные на сервер, но и возвращает количество ошибок, если они были. Параметр 0 указывает, что сохранение данных не нужно прекращать, если при сохранении выявляются ошибки. Можете здесь указать 1. В этом случае сохранение прекратится сразу же, как будет выявлена какая либо ошибка. Если сумма переменных i и k окажется больше 0, ошибки были допущены. Иначе ошибок не было.

    Код кнопки "Отключиться от базы" будет таким:

    procedure TfMain.bUnConnectClick(Sender: TObject);
    begin
      //отключаем таблицы
      fDM.CDSLichData.Close;
      fDM.CDSTelephones.Close;
      //отключаем DCOMConnection
      fDM.DCOMC.Close;
    end;

    Вначале мы закрываем наборы данных, после чего прерываем подключение к серверу. Теперь сохраните проект, скомпилируйте его и загрузите. Попробуйте отредактировать какую-нибудь запись. Затем нажмите "Обновить данные", либо последовательно, отключитесь от сервера и снова подключитесь к нему. Вы убедитесь, что сделанные изменения не сохранились. Это подтверждает, что при редактировании мы изменяем данные в кэше, а вовсе не на сервере. Если вы снова измените запись, затем нажмете "Сохранить изменения в базу", то изменения передадутся на сервер. Повторное обновление данных убедит вас в этом.

    Сохранение данных в локальный файл, и чтение из файла

    Теперь нам осталось написать обработчики для кнопок "Сохранить данные в файл" и "Загрузить данные из файла". Однако здесь следует сделать одно замечание. Как уже говорилось, метод SaveToFile () компонента ClientDataSet имеет два параметра: имя файла, куда нужно сохранять данные, и формат файла, который имеет тип TDataPacketFormat. Этот тип может содержать три значения (см. табл.29.2), описание этого типа находится в модуле DBClient. Этот модуль уже подключен к нашему модулю данных DM, однако он не подключен к главной форме. Следовательно, в главной форме мы не сможем реализовать сохранение данных в файл и чтение их из файла, если не подключим этот модуль в раздел uses. Чтобы дважды не подключать его к проекту, реализуем методы сохранения в файл и чтения из файла в модуле данных, а в главной форме просто вызовем их.

    Перейдите к модулю данных. В разделе Public опишите две процедуры:

    public
        { Public declarations }
        procedure SaveDataToFiles;
        procedure LoadDataFromFiles;

    Установите курсор на одну из них и нажмите <Ctrl+Shift+C>, чтобы сгенерировать обе эти процедуры. Эти процедуры будут такими:

    {Читаем данные из файла}
    procedure TfDM.LoadDataFromFiles;
    begin
      if OD1.Execute then begin
        CDSLichData.LoadFromFile(OD1.FileName);
        ShowMessage('Процедура чтения данных выполнена!');
      end; //if
    end;
    //**************************************
    {Сохраняем данные в файл}
    procedure TfDM.SaveDataToFiles;
    begin
      if SD1.Execute then begin
        CDSLichData.SaveToFile(SD1.FileName, dfBinary);
        ShowMessage('Процедура сохранения данных выполнена!');
      end; //if
    end;

    Комментарии достаточно подробны и не нуждаются в дополнительных пояснениях. Код кнопки "Сохранить данные в файл" на главной форме просто вызывает нужную процедуру:

    procedure TfMain.bSaveToFileClick(Sender: TObject);
    begin
      fDM.SaveDataToFiles;
    end;

    А код кнопки "Загрузить данные из файла" будет следующим:

    procedure TfMain.bLoadFromFileClick(Sender: TObject);
    begin
      fDM.LoadDataFromFiles;
    end;

    Сохраните проект, скомпилируйте и загрузите его. Соединитесь с базой данных, чтобы считать данные. Сохраните их в файл и отключитесь от базы данных. Считайте данные из локального файла, не подключаясь к БД. Вы видите, что сохранились данные не только главной, но и подчиненной таблицы. Фактически, через поле TTelephones, каждая запись главной таблицы содержит также связанные данные из подчиненной таблицы, поэтому нам пришлось сохранять лишь один файл. Вы реализовали "метод Портфеля".

    На этом наш курс "Программирование баз данных в Delphi " окончен. Желаю всевозможных успехов в программировании!

    Вернуться к учебному плану