Смекни!
smekni.com

MIDAS. Практическое применение (стр. 3 из 6)

create procedure CLIENT_IDreturns (ID integer)asbegin 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. Конечно, если потребуется хранить дополнительную информацию о документе, например, его номер, придется организовать в списке ссылки на записи, с выделением памяти под эту запись и уничтожением ненужных записей. При этом внешний вид функций не изменится, просто усложнится работа со списком, и может понадобиться обращение к БД для получения дополнительной информации.

Теперь все просто: все модули данных, которые работают с документами, используют эти две функции, и если RegisterDoc возвращает false (а это произойдет только в том случае, если номер уже есть в списке), то пользователю выдается сообщение, что с документом уже работают. Функция UnregisterDoc просто удаляет номер из списка.

На клиенте понадобится, кроме доступа к двум провайдерам, еще пара функций – получение нового значения CLIENT_ID для справочника клиентов и получение полного имени клиента. Для этого необходимо создать описание этих функций в библиотеке типов.

В зависимости от того, какой синтаксис используется в редакторе библиотеки типов (IDL или Pascal), объявление этих функций выглядит по-разному, ниже приведены их описания в protected-секции модуля данных:

protected class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override; function NewClientID: Integer; safecall; function Get_ClientName(ClientID: Integer): WideString; safecall;

На IDL это выглядит так:

[id(0x00000001)]HRESULT _stdcall NewClientID([out, retval] long * Result);[propget, id(0x00000004)]HRESULT _stdcall ClientName([in] long ClientID, [out, retval] BSTR * Value);

Реализация этих функций довольно проста. Надо вызвать хранимые процедуры, и выдать возвращаемое ими значение в качестве результата:

function TrdmCommon.NewClientID: Integer;begin lock; with spNewID do try ExecProc; Result := paramByName('ID').AsInteger; finally unlock; end;end;function TrdmCommon.Get_ClientName(ClientID: Integer): WideString;begin lock; try with spClientFullName do begin paramByName('ID').AsInteger := ClientID; ExecProc; Result := paramByName('FULL_NAME').AsString; end; finally unlock; end;end;

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

Рисунок 3.

Здесь уже все немного сложнее. Разумеется, здесь тоже есть соединение с базой ibdDoc, настроенное на сервер БД. Хранимая процедура spNewID выдает на этот раз номер для нового документа, используя процедуру DOC_TITLE_ID, аналогичную процедуре CLIENT_ID.

На этот раз в модуле данных, помимо компонентов запросов к серверу, присутствуют два компонента TСlientDataSet и два провайдера данных. Эти дополнительные компоненты предназначены именно для организации расчетов на сервере. Поскольку, как мы договорились, на сервере приложений должна рассчитываться сумма документа, то на нем должно быть известно содержимое документа до того, как оно будет сохранено в БД. Разумеется, это можно осуществить, используя события провайдера для предварительной обработки пакета изменений, поступившего от клиентской части, но мне хотелось показать возможность организации работы с документом как с полноценным объектом.

Идея простая: пусть весь удаленный модуль данных работает с одним документом. В таком случае этот модуль будет выглядеть для клиентской части как полноценный объект, владеющий всеми данными документа и предоставляющий клиентской части все необходимые свойства. Разумеется, от пакетов данных никто не отказывается.

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

К одному компоненту транзакции ibtDoc присоединено на этот раз два запроса ibqTitle и ibqBody, соответственно выбирающих одну строку заголовка документа (select * from DOC_TITLE where DOC_ID = :DocID) и все строки этого документа (select * from DOC_BODY where DOC_ID = :DocID).

ПРИМЕЧАНИЕХотя MIDAS требует наличия своей IBTransaction для каждой пары компонентов "IBQuery-провайдер", в данном случае это необязательно. Провайдеры не будут начинать и завершать транзакции, открываться и закрываться транзакция будет явно, в соответствующих методах.

К этим запросам присоединены провайдеры dspTitleInner и dspBodyInner, назначение которых – получить данные с сервера БД и передать их в соответствующие ClientDataSet. Свойство Exported у этих провайдеров установлено в false, они нужны только внутри сервера приложений, и видеть их на клиентской части незачем. Соответственно, клиентский набор данных cdsTitle (компонент TClientDataSet) получает одну строку заголовка из dspTitle и cdsBody, содержимое документа из dspBody.

Для того, чтобы клиентская часть могла получать и изменять данные документа, к клиентским наборам данных cdsTitle и cdsBody присоединены провайдеры данных, dspTitle и dspBody, соответственно. Свойству Exported этих провайдеров оставлено значение по умолчанию, True, зато свойство ResolveToDataSet установлено в True, для того, чтобы эти провайдеры не пытались работать с ClientDataSet с помощью запросов. Таким образом, клиентская часть может получать и изменять данные не из TIBQuery, но из TClientDataSet, причем совершенно об этом не догадываясь. По команде с клиентской части изменения, передаются серверу приложений, который и сохраняет их в БД.

Теперь посмотрим, что нам нужно для подобной реализации. Функции для синхронизации обработки документов RegisterDoc и UnregisterDoc уже есть, нужно их только использовать. С их помощью гарантируется, что одновременно один и тот же документ редактироваться не будет, поэтому у провайдеров данных dspTitleInner и dspTitleBody достаточно установить UpdateMode = upWhereKeyOnly, и указать ключевые поля у запросов. Содержимое документа может состоять из нескольких строк, поэтому у dspBodyInner и dspBody нужно установить флаг poAllowMultiRecordUpdates. Теперь нужно разобраться с полями клиентских наборов данных, установив у них соответствующие свойства. Я остановлюсь здесь только на свойстве ProviderFlags. Поскольку поле «Ссылка на документ» (DOC_ID) на клиентской части не нужно, ему можно задать флаг pfHidden. Разумеется, у всех ключевых полей (DOC_ID и LINE_NUM) и в наборе данных заголовка, и в содержимом документа надо указать флаг pfInKey. У провайдеров dspTitle и dspBody нужно установить политику обновления UpdateMode = upWhereKeyOnly, клиентская часть у модуля данных одна, и другие значения совершенно ни к чему.