45803 (MIDAS. Практическое применение), страница 2
Описание файла
Документ из архива "MIDAS. Практическое применение", который расположен в категории "". Всё это находится в предмете "информатика" из , которые можно найти в файловом архиве . Не смотря на прямую связь этого архива с , его также можно найти и в других разделах. Архив можно найти в разделе "рефераты, доклады и презентации", в предмете "информатика, программирование" в общих файлах.
Онлайн просмотр документа "45803"
Текст 2 страницы из документа "45803"
Сервер приложений
Создаваемый нами сервер приложений будет отдельным исполняемым модулем. Этот модуль затем можно будет расположить на отдельном компьютере, который сможет производить расчеты для нескольких клиентов и синхронизировать их работу.
Сервер приложений должен обеспечивать обработку документа как единого объекта, поэтому разумным будет выделить работу с ним в отдельный класс, в данном случае потомок TRemoteDataModule. Нам также понадобится модуль данных для работы со справочником поставщиков и получателей, и выдачи списка документов. Отчет я решил также выделить в отдельный модуль. В итоге на сервере необходимо создать три потомка TRemoteDataModule: rdmCommon (общий модуль со списками поставщиков/получателей и документов), rdmDoc и rdmReport – соответственно для документа и отчета.
Мастер создания удаленного модуля данных предлагает по умолчанию политику загрузки исполняемого модуля Multiple instance и модель потоков Apartment. Это именно то, что нам нужно! Действительно, Instancing = Internal приведет к созданию серверного компонента в клиентском процессе (это распространяется только на сервер, создаваемый в виде DLL). При Single instance каждая клиентская часть будет соединяться со своим собственным экземпляром сервера приложений, а синхронизацию проще сделать, если все клиенты подсоединяются к одному экземпляру сервера приложений. Выбор модели потоков Apartment позволит избежать ручной синхронизации доступа к данным компонента.
Теперь остается создать три (опять три, это тоже случайно) потомка TRemoteDataModule, расположить на них компоненты доступа к данным и написать код для обработки данных.
При этом необходимо учитывать, что при использовании модели потоков Apartment каждый модуль данных работает в своем потоке, и поэтому в каждом модуле должен находится отдельный компонент TIBDatabase.
При прямом доступе провайдера к базе (свойство ResolveToDataset = false) MIDAS также требует наличия отдельной копии объекта TIBTransaction для каждого компонента доступа к данным, то есть у каждого провайдера должна быть своя транзакция. Компонент TIBTransaction специфичен для компонентов прямого доступа к Interbase, обычно работа с транзакциями возложена на компонент соединения с базой данных.
ПРИМЕЧАНИЕ При использовании сервера Interbase для доступа к данным по технологии MIDAS логично использовать IBX, провайдеры данных великолепно работают с этими компонентами. Единственное замечание – Borland сертифицировала на момент написания статьи версию IBX 4.52. Более поздние версии работают в составе MIDAS несколько иначе, чем раньше. В частности, транзакции теперь не закрываются автоматически после выборки данных. |
Рассмотрим удаленные модули данных по порядку, и начнем с модуля справочников (rdmCommon) (рисунок 2).
Рисунок 2. Общий модуль rdmCommon.
Компонент ibqDocs имеет тип TIBDatabase и обеспечивает соединение модуля с сервером БД. У меня БД находится в каталоге d:\projects\docmidas\data\ и называется doc.gdb. В прилагающемся к статье проекте сервер приложений позволяет указать произвольное местонахождение сервера БД и файла базы данных.
Для того, чтобы при каждом соединении сервер приложений не запрашивал имя пользователя и пароль, они просто указаны в параметрах соединения. Имя пользователя SYSDBA и пароль masterkey являются установками по умолчанию при инсталляции сервера Interbase.
Перечислим компоненты модуля. К компоненту транзакции ibtClient подсоединен запрос ibqClient (компонент TIBQuery), к которому, в свою очередь, присоединен провайдер dspClient. Соответственно, у транзакции и запроса указано соединение с БД ibdDocs. Остается только установить тип транзакции read committed (удобнее всего это сделать, дважды щелкнув на соответствующем компоненте, и выбрав его тип), и в свойстве SQL-запроса записать “select * from client”. Теперь провайдер может предоставлять клиентской части возможность работать со справочником клиентов. Но для повышения комфорта нужно добавить возможность нескольким пользователям изменять одновременно разные поля в одной и той же записи в таблице (их два: Name и Phone). Делается это довольно просто, в редакторе полей (Fields Editor) ibqClient нужно создать постоянный список всех полей запроса, и у поля CLIENT_ID в его свойство ProviderFlags добавить опцию pfInKey. Затем у провайдера dspClient установить свойство UpdateMode в upWhereChanged. В этом случае, если разные клиентские части изменят разные поля одной записи в таблице CLIENT, сервер приложений примет эти изменения. В случае, если будут изменены одни и те же поля одной записи, клиентской части будет выдано сообщение вида «Запись изменена другим пользователем».
ПРИМЕЧАНИЕ Здесь мне хотелось бы остановиться на свойствах TField.ProviderFlags и TDataSetProvider.UpdateMode. Дело в том, что меня часто спрашивают, что зависит от значений этих свойств, а зависит от них довольно много. В справке по VCL эти свойства описаны, на мой взгляд, недостаточно подробно, а связь между ними достаточно тесная. Итак, пусть имеется компонент TQuery, TIBQuery или какой-то другой (запрос), соединенный с сервером БД, и к нему присоединен TDataSetProvider. В этом случае на логику работы оказывают влияние именно значения свойства ProviderFlags полей этого запроса, аналогичные свойства полей на клиентской стороне никакого влияния не оказывают. Комбинация значений этих свойств полностью определяет, как будут производиться операции обновления данных на сервере БД. Рассмотрим обновление данных в таблице. Добавление и удаление записи происходит аналогично. Провайдер с установленным свойством ResolveToDataset = false при обновлении записи формирует SQL-запрос вида UPDATE Имя таблицы В предложение SET входят только те поля, у которых установлен флаг pfUpdate в свойстве ProviderFlags (требуется обновлять в базе данных) и OldValue <> NewValue (значение поля было изменено). Предложение WHERE формируется следующим образом: Берутся все поля, у который установлены [pfInKey, pfInWhere], фактически это первичный ключ. При UpdateMode=upWhereKeyOnly больше никаких полей не берется. При UpdateMode=upWhereChanged к полям первичного ключа добавляются те поля, у которых OldValue <> NewValue и pfWhere in ProviderFlags, что позволяет делать проверку на изменение тех же полей другим пользователем. При UpdateMode=upWhereAll в список полей WHERE входят все поля записи, у которых pfWhere in ProviderFlags. В случае, если запись в таблице на сервере не найдена (нет записей, удовлетворяющих условию WHERE), пользователю выдается сообщение вида "Запись изменена другим пользователем", вне зависимости от причины. Остается одно значение флага, pfHidden. Поля с этим флагом не передаются клиентскому приложению, и не принимаются от него, флаг указывает, что эти поля - только для использования на стороне сервера. |
Если уж создан постоянный список полей, можно установить параметры их отображения на клиентской части, в частности, DisplayLabel, DisplayWidth и Visible, а у провайдера - флаги poIncFieldProps. При этом на клиентской части можно не заботиться о списке полей – значения, полученные с сервера приложений, переопределяют заданные на клиенте в любом случае. Заодно у провайдера надо установить опцию poMultiRecordUpdates, чтобы на клиентской части можно было изменять сразу несколько записей в справочнике до отправки изменений на сервер.
Поле CLIENT_ID в справочнике поставщиков и получателей является первичным ключем, а стало быть, в нем должны содержаться уникальные значения. Для получения уникальных значений удобно использовать автоинкрементальные поля (autoincrement field). В IB собственно автоинкрементных полей нет, нарастающие значения получают от генератора с помощью функции Gen_ID, и как правило, присваивают это значение полю в триггере. Мне нравится ситуация, когда новое уникальное значение появляется на клиентской части сразу после добавления записи. Поэтому вместо присвоения значения, полученного от генератора, в триггере, используется хранимая процедура, результатом работы которой и является это значение. Для этого в удаленном модуле данных расположен компонент spNewID: TIBStoredProc, присоединенный к компоненту транзакции ibtDefault, который предоставляет доступ к хранимой процедуре на сервере БД. Процедура описана в базе данных следующим образом:
create procedure CLIENT_ID returns (ID integer) as begin ID = Gen_ID(CLIENT_ID_GEN,1); end |
Как видно, процедура просто выдает следующее значение генератора. Это гарантирует, что при последовательных запросах к ней это значение повторяться не будет. Получение значения на клиентской части обеспечивается методом сервера, об этом немного ниже.
Вторая хранимая процедура, spClientFullName, присоединена к компоненту транзакции ibtClient и предназначена для выдачи имени и телефона поставщика или получателя в виде единой строки «Имя (телефон)», возвращаемой процедурой сервера БД CLIENT_FULL_NAME. Эта строка также передается на клиентскую часть через метод сервера.
Группа компонентов ibtDocList, ibqDocList, dspDocList и ibqDelDoc предназначена для работы со списком документов. У IbtDocList, компонента транзакции, установлен режим read committed, а в компоненте ibqDocList содержится SQL-запрос «select * from List_doc(:FromDate, :ToDate)». Весь список документов сразу выводить довольно бессмысленно, их может быть много. Поэтому запрос выбирает список документов, даты которых лежат в промежутке от FromDate до ToDate. Провайдер dspDocList выдает этот список клиентской части.
Дополнительный компонент, ibqDelDoc, как, думаю, видно из его названия, предназначен для удаления документа, в его свойстве SQL стоит запрос «delete from DOC_TITLE where DOC_ID = :DOC_ID». Несмотря на то, что для создания и изменения документа планируется использовать отдельный модуль, rdmDoc, для удаления документа вовсе необязательно его открывать, и с точки зрения интерфейса пользователя удобно делать это прямо из списка документов. На первый взгляд, использование отдельного запроса для удаления кажется излишним, для этого обычно достаточно объявить в обработчике dspDocList.OnGetTableName имя таблицы (DOC_TITLE), и удаление будет автоматически обеспечено. Однако в постановке задачи стоит условие, что открытый в одной клиентской части документ должен быть недоступен для изменения (а значит, и удаления) из других клиентских частей. Поэтому приходится делать это в обработчике события dspDocList.OnBeforeUpdateRecord следующим образом:
procedure TrdmCommon.dspDocListBeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean); var DocID: Integer; begin if UpdateKind = ukDelete then //Только если запись удаляется begin DocID := DeltaDS.FieldByName('DOC_ID').AsInteger; try if not RegisterDoc(DocID) then //Пытаемся зарегистрировать raise Exception.Create('Документ редактируется'); with ibqDelDoc do //Удаляем begin paramByName('DocID').AsInteger := DocID; ExecSQL; end; Applied := True; finally UnregisterDoc(DocID); //Изменение закончено, удалили end; end; end; |
Если удаляется документ, попытаемся его зарегистрировать в списке редактируемых функцией RegisterDoc, затем, если это получилось, удаляем его с помощью запроса ibqDelDoc и удаляем из списка редактирования (UnregisterDoc). Устанавливаем Applied := true, чтобы сказать провайдеру, что все уже сделано.
Конечно, одновременно может редактироваться (удаляться, добавляться) довольно много документов, поэтому нужен единый список этих документов, к которому будут обращаться процедуры RegisterDoc и UnregisterDoc. Поскольку обращение к нему будет производиться из модулей данных, работающих в разных потоках, то наилучшим образом для этого подходит TThreadList (потокобезопасный класс списка). Список документов должен быть единым для всех клиентских частей, поэтому расположить его нужно в отдельном модуле, например, в модуле главной формы сервера. На ней потом можно вывести, например, список редактируемых на данный момент документов. Так и сделаем.
В модуле главной формы сервера в разделе implementation объявим переменную DocList: TThreadList; Этот список лучше инициализировать сразу при запуске сервера и уничтожать при выходе:
initialization DocList := TThreadList.Create; finalization if Assigned(DocList) then begin DocList.Free; DocList := nil; end; end. |
С этим списком работают две функции: RegisterDoc и UnregisterDoc :
function RegisterDoc(DocID: integer): boolean; begin Result := False; if DocID = 0 then Exit; with DocList.LockList do try if IndexOf(Pointer(DocID)) < 0 then begin Add(Pointer(DocID)); Result := True; end; finally DocList.UnlockList; end; end; function UnregisterDoc(DocID: integer): boolean; begin Result := False; if DocID = 0 then Exit; with DocList.LockList do try if IndexOf(Pointer(DocID)) >= 0 then begin Remove(Pointer(DocID)); Result := True; end; finally DocList.UnlockList; end; end; |
В списке хранятся идентификаторы документов. Но TThreadList предназначен для хранения указателей. Поэтому для хранения в этом списке идентификатора, имеющего тип Integer, придется привести его к типу pointer. Конечно, если потребуется хранить дополнительную информацию о документе, например, его номер, придется организовать в списке ссылки на записи, с выделением памяти под эту запись и уничтожением ненужных записей. При этом внешний вид функций не изменится, просто усложнится работа со списком, и может понадобиться обращение к БД для получения дополнительной информации.