Смекни!
smekni.com

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

create procedure REP_INOUT(FROM_DATE date, TO_DATE date)returns (FROM_ID integer, FROM_NAME varchar(180), TO_ID integer, TO_NAME varchar(180), FULL_SUM numeric(15,4))asbegin for select FROM_ID, TO_ID, sum(DOC_SUM) from DOC_TITLE where DOC_DATE >= :FROM_DATE and DOC_DATE <= :TO_DATE group by FROM_ID, TO_ID into :FROM_ID, :TO_ID, :FULL_SUM do begin FROM_NAME = NULL; TO_NAME = NULL; select NAME from client where CLIENT_ID = :FROM_ID into :FROM_NAME; select NAME from client where CLIENT_ID = :TO_ID into :TO_NAME; if (FULL_SUM is NULL) then FULL_SUM = 0; suspend; endend

Процедура выдает то, что нужно для отчета, но, к сожалению, не в виде перекрестного отчета, а по строкам:

От кого Кому На сумму
<Поставщик> <Получатель> Сумма ...
...

Приводить к нормальному виду все это будет сервер приложений.

Все готово для написания сервера приложений. Приступим.

Сервер приложений

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

Сервер приложений должен обеспечивать обработку документа как единого объекта, поэтому разумным будет выделить работу с ним в отдельный класс, в данном случае потомок 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:&bsol;projects&bsol;docmidas&bsol;data&bsol; и называется 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 <Table> SET <Field1>=<NewValue1>, ... WHERE <Field1>=<OldValue1> AND ..., в полном соответствии со стандартом SQL (при ResolveToDataset=True производится поиск и обновление прямо в таблице).Имя таблицы <Table> берется из Dataset (провайдер великолепно понимает запросы SQL вида Select from...), либо задается в обработчике OnGetTableName. Значения NewValue и OldValue для каждого поля берутся из пакета обновления, посылаемого провайдеру. Имена полей в выражениях SET и FROM формируются автоматически, как раз на основе свойств ProviderFlags и UpdateMode того набора данных, через который провайдер работает с базой. Алгоритм следующий:В предложение 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, который предоставляет доступ к хранимой процедуре на сервере БД. Процедура описана в базе данных следующим образом: