Учебная работа. Реферат: MIDAS. Практическое применение
Роман Игнатьев
Введение
Нужные познания: Перед чтением рекомендуется ознакомиться с технологией MIDAS хотя бы на уровне демо приложений, поставляющихся с Delphi.
разработка MIDAS (Multi-tier Distributed Application Services Suite, Сервис для сотворения многоуровневых распределенных приложений) была предложена компанией Borland уже достаточно издавна, 1-ое приложение с ее внедрением я написал еще в 98 году, на Delphi 4. И с того времени фактически все приложения для работы с базами данных создаются мной конкретно на базе MIDAS. О преимуществах, думаю, гласить не нужно – даже обычное разделение приложения на две части, одна из которых работает с базой данных ( приложений), а иная обеспечивает интерфейс юзера, делает значимые удобства как при разработке приложения, так и при его использовании.
на данный момент существует огромное количество статей и книжек по технологии сотворения многозвенных приложений на Delphi, но, к огорчению, мне не удалось отыскать литературы, в какой бы рассматривались некие интересующие меня вопросцы. Дело в том, что во всех примерах создается трехзвенное приложение, в каком сервер приложений просто соединяет БД с клиентской частью, просто абстрагируя работу с базой данных.
С одной стороны, это дает некое преимущество при переходе с двухуровневой технологии (клиент-сервер) на трехуровневую, для чего же составляющие доступа к базе данных из клиентской части переносятся в сервер приложений. С иной стороны, охото большего, а конкретно переноса на не только лишь работы с таблицами базы данных, да и главный логики приложения, оставляя клиентской части лишь задачку взаимодействия с юзером.
Ниже рассматривается весьма обычное приложение для работы с сервером БД. База данных, с которой работает это приложение, содержит всего 3 таблицы. С его помощью мне хотелось бы показать некие методы сотворения настоящего сервера приложений, обеспечивающего полную обработку данных.
Все, что написано ниже, относится к Delphi 5, в качестве сервера избран Interbase 5.6. Конкретно эти продукты я употреблял в большинстве проектов. Но база данных работает и на наиболее старших версиях Interbase, я инспектировал ее работоспособность, а именно, на IB6, а начальные тексты приложения с минимальными переменами можно составлять на старших версиях Delphi. К огорчению, некие конфигурации созодать все таки придется, потому что MIDAS повсевременно развивается. Но, как правило, такие конфигурации носят косметический нрав, и создать их нетрудно. Как поменять проект для компиляции на Delphi 6, будет поведано в заключительной части. Невзирая на то, что в сервере приложений употребляются составляющие прямого доступа Interbase Express (IBX), несложно перейти на иной БД, просто заменив составляющие доступа и мало изменив текст способов.
Исходя из практических суждений ниже описано создание трехуровневого приложения, включая разработку структуры базы данных. Мне не хотелось воспользоваться готовыми примерами, входящими в поставку Interbase и Delphi, чтоб не придерживаться шаблонов, навязываемых ими.
Постановка задачки
Я собираюсь на ординарном примере показать способности технологии, потому приложение будет работать всего с одним логическим объектом – неким абстрактным документом, состоящим из заголовка и содержимого. Таковая структура была выбрана из-за того, что связь “мастер-таблица — подчиненная таблица” (master-detail) весьма нередко встречается на практике, и также нередко в приложениях встречается обработка разных документов.
“Документ” тут понимается как некая абстракция, напоминающая настоящие документы, такие, как затратная либо счет.
Очевидно, нужно обеспечить одновременную работу с документами нескольких юзеров.
Описание документа
Заголовок документа прост, и состоит из последующих полей:
Номер – номер документа.
Дата – дата выписки, по дефлоту текущая.
Поставщик – имя, телефон.
Получатель – также имя и телефон.
Сумма – общая сумма по документу.
Поставщик и получатель будут выбираться из одного справочника клиентов, содержащего лишь два поля: имя и телефон.
Документ содержит записи со сведениями о перемещении некоторых продуктов. содержимое документа – это таблица в базе данных, записи которой состоят из последующих полей:
Порядковый номер
Номер документа, к которому относится.
Наименование — наименование продукта.
количество.
Стоимость.
Сумма.
Итог может смотреться последующим образом:
номер п/п
наименование
количество
стоимость
сумма
1
Что-то
1
30.00
30.00
2
Что-то еще
2
10.50
21.00
…
Юзеру должен выдаваться перечень документов с указанием реквизитов документа и его итоговой суммы.
Сумма документа будет рассчитываться сервером приложений на базе его содержимого.
Не считая этого, сделаем итоговый отчет – «шахматную» ведомость, где по одной оси размещены поставщики, а по иной – получатели:
За период с <дата> по <дата> (по дате документа)
От кого
Кому
Получатель1(имя)
Получатель2(имя)
…
Поставщик1(имя)
Сумма
Сумма
Поставщик2(имя)
Сумма
Сумма
…
В отчет будут заходить суммы из документов, даты которых попадают в данный период.
отчет, очевидно, выходит также довольно оторванным от жизни, мне просто хотелось показать с его помощью доп способности MIDAS.
Блокировки
Так как работать с БД будут сходу несколько юзеров, принципиально заблокировать документ на время редактирования документа юзером. “Знатная обязанность” синхронизации работы юзеров возлагается в этом случае на приложений. В принципе, все это можно воплотить и средствами SQL сервера, но, во-1-х, для сервера Interbase блокировки записей достаточно противоестественны, во-2-х, как будет видно ниже, приложений дозволяет просто перекрыть сходу весь документ как единый объект.
Структура БД
Требования к приложению описаны, и сейчас можно приступить к последующему шагу – созданию БД. Нам необходимы три таблицы (волшебное число три выбрано только для простоты): для документа, содержимого документа и справочника клиентов. Документ содержит две ссылки на справочник клиентов – “Поставщик” и «Получатель», а содержимое документа имеет ссылку на документ. На рисунке 1 представлена ER-диаграмма данной БД.
Набросок 1. ER-модель БД.
В данной базе целостность данных обеспечивается по последующим правилам:
При упоминании в каком-либо документе поставщика либо получателя удаление данной строчки из справочника клиентов не допускается.
При удалении документа удаляются и все связанные с ним строчки из таблицы «содержимое документа».
Вставка в таблицу «содержимое документа» допускается лишь при условии, что поле ID ссылается на имеющийся документ.
Как видно, в таблице «Заголовок документа» есть поле «Сумма». Это поле обязано содержать полную сумму документа (сумму полей «Сумма» содержимого документа). При изменении содержимого документа приходится пересчитывать документов расчет суммы всякого из их наращивает нагрузку на БД. Выслеживать актуальность этого поля можно на триггерах СУБД, но раз уж мы создаем приложений, почему бы не возложить на него эту задачку? К тому же, это обеспечивает некую независимость от особенностей функционирования сервера БД, что может оказаться полезным, к примеру, при переходе на иной .
Ниже приведен скрипт, создающий подопытную БД:
/* Определены последующие типы данных:
имя клиента
Количество для содержимого документа
Стоимость
Сумма
*/
create Domain DName varchar(180);
create domain DCount numeric(15,4) default 1 not null;
create Domain DCurrency numeric(15,2) default 0 not null;
create Domain DSum numeric(15,4) default 0 not null;
/* Справочник поставщиков и получателей */
create table CLIENT
(
CLIENT_ID integer not null,
NAME DName not null, /* имя */
PHONE varchar(40), /* Телефон */
constraint PK_CLIENT primary key (CLIENT_ID)
);
/*Заголовокдокумента*/
create table DOC_TITLE
(
DOC_ID integer not null, /* ID */
DOC_NUM varchar(40) not null, /* Номер */
DOC_DATE date not null, /* Дата */
FROM_ID integer default 0 not null, /* Поставщик */
TO_ID integer default 0 not null, /* Получатель */
DOC_SUM DSum, /* Сумма */
constraint PK_DOC_TITLE primary key (DOC_ID),
constraint FK_DOC_FROM_CLIENT foreign key (FROM_ID)
references Client (CLIENT_ID)
on update cascade,
constraint FK_DOC_TO_CLIENT foreign key (TO_ID)
references Client (CLIENT_ID)
on update cascade
);
/*содержимое*/
create table DOC_BODY
(
DOC_ID integer not null, /* сcылканазаголовок */
LINE_NUM integer not null, /* Номерп/п */
CONTENT varchar(250) not null, /* Наименование */
COUNT_NUM DCount, /* количество */
PRICE DCurrency, /* Стоимость */
constraint PK_DOC_BODY primary key (DOC_ID, LINE_NUM),
constraint FK_DOC_BODY_TITLE foreign key (DOC_ID)
references DOC_TITLE (DOC_ID)
on delete cascade
on update cascade
);
скрипт делает три таблицы: CLIENT (поставщики/получатели), DOC_TITLE (документ), DOC_BODY (содержимое документа).
Последующий шаг – формирование перечня документов. В заголовке документа содержится лишь ссылка на поставщика и получателя. Вывод перечня комфортно организовать отдельным запросом, а в данном случае – хранимой процедурой. Пусть для удобства имя клиента в перечне показывается в виде «имя (Телефон)». Для этого создадим функцию CLIENT_FULL_NAME, которая извлекает эту строчку, и будем вызывать ее из процедуры выдачи перечня LIST_DOC. Эта же процедура понадобится для отображения имени поставщика и получателя на форме редактирования документа:
create procedure CLIENT_FULL_NAME(ID integer)
returns (FULL_NAME varchar(224))
as
declare variable NAME varchar(180);
declare variable PHONE varchar(180);
begin
select NAME ,PHONE
from client
where CLIENT_ID = :ID
into :NAME, :PHONE;
FULL_NAME = »;
if (NAME is not NULL) then
FULL_NAME = NAME;
if (PHONE is not NULL) then
FULL_NAME = FULL_NAME || ‘ (‘ || PHONE || ‘)’;
end
create procedure LIST_DOC (FROM_DATE date, TO_DATE date)
returns (DOC_ID integer, DOC_NUM varchar(40), DOC_DATE date, FROM_ID integer,
TO_ID integer, FROM_NAME varchar(224), TO_NAME varchar(224),
DOC_SUM numeric(15,4))
as
begin
for select DOC_ID, DOC_NUM, DOC_DATE, FROM_ID, TO_ID, DOC_SUM
from DOC_TITLE
where DOC_DATE >= :FROM_DATE and DOC_DATE <= :TO_DATE
into :DOC_ID, :DOC_NUM, :DOC_DATE, :FROM_ID, :TO_ID, :DOC_SUM
do begin
FROM_NAME = NULL;
TO_NAME = NULL;
execute procedure CLIENT_FULL_NAME (:FROM_ID)
returning_values :FROM_NAME;
execute procedure CLIENT_FULL_NAME (:TO_ID)
returning_values :TO_NAME;
suspend;
end
end
Осталась процедура для отчета:
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))
as
begin
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;
end
end
Процедура выдает то, что необходимо для отчета, но, к огорчению, не в виде перекрестного отчета, а по строчкам:
От кого
Кому
На сумму
<Поставщик>
<Получатель>
Сумма …
…
Приводить к нормальному виду все это будет приложений.
Все готово для написания сервера приложений. Приступим.
приложений
Создаваемый нами приложений будет отдельным исполняемым модулем. Этот модуль потом можно будет расположить на отдельном компе, который сумеет создавать расчеты для нескольких клиентов и синхронизировать их работу.
Сервер приложений должен обеспечивать обработку документа как одного объекта, потому разумным будет выделить работу с ним в отдельный класс, в этом случае потомок 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:projectsdocmidasdata и именуется 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), юзеру выдается сообщение вида «Запись изменена остальным юзером», вне зависимости от предпосылки.
Остается одно
Если уж сотворен неизменный перечень полей, можно установить характеристики их отображения на клиентской части, а именно, 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
Как видно, процедура просто выдает последующее части обеспечивается способом сервера, о этом мало ниже.
2-ая хранимая процедура, 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, для удаления документа совсем необязательно его открывать, и исходя из убеждений интерфейса юзера комфортно созодать это прямо из перечня документов. На 1-ый взор, внедрение отдельного запроса для удаления кажется лишним, для этого обычно довольно объявить в обработчике 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 этих провайдеров оставлено работать с 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, клиентская часть у модуля данных одна, и остальные значения совсем ни к чему.
Сейчас составляющие для хранения и обработки данных подготовлены, осталось написать сами способы работы с ними.
Давайте разберемся, что конкретно требуется. Модуль rdmDoc предназначен как для сотворения новейшего документа, так и для редактирования имеющегося. Этот модуль можен находиться в одном из 3-х состояний, обрисованных в перечислении TObjState:
osInactive: данных нет, документ не редактируется,
osInsert: сотворен новейший документ и
osUpdate – происходит изменение имеющегося документа.
состояние хранится в переменной Fstate, находящейся снутри модуля. сходу опосля сотворения и опосля окончания обработки документа модуль данных должен находиться в неактивном состоянии.
Переход из 1-го состояния в другое должен обеспечиваться надлежащими способами. Я именовал эти способы DoInactiveState (перевод в неактивное состояние), DoOpen (открыть имеющийся документ) и DoCreateNew (создание новейшего документа). При редактировании либо добавлении документа необходимо знать его неповторимый номер, записываемый в поле DOC_ID. Для этого довольно объявить в секции private переменную FDocID: integer, которая и будет его хранить.
В библиотеке типов необходимо воплотить способы, которые будут создавать документ либо открывать имеющийся, также сохранять конфигурации. Не считая этого, пригодится свойство, позволяющее получить в хоть какой момент сумму по документу. Сумма каждой строчки содержимого пусть рассчитывается на клиентской части.
Итак, приступим. Поначалу описываются способы перехода меж состояниями, они предусмотрены для внутреннего использования, и потому их объявления содержатся в секции private:
procedure DoInactiveState;
procedure DoCreateNew;
procedure DoOpen(DocID: integer);
Разглядим их по порядку.
procedure TrdmDoc.DoInactiveState;
begin
UnregisterDoc(FDocID);
FDocID := 0;
cdsTitle.Active := False;
cdsBody.Active := False;
ibtDoc.Active := False;
FState := osInactive;
end;
Процедура DoInactiveState удаляет документ из перечня редактируемых, закрывает все клиентские наборы данных, также производит откат транзакции (если она была активна).
procedure TrdmDoc.DoOpen(DocID: Integer);
begin
if DocID = 0 then Exit;
try
if not RegisterDoc(DocID) then
raise Exception.Create(‘Документредактируется’);
FDocID := DocID; // итолькоздесь, по другому DoInactiveState удалитдокумент
ibdDocs.Connected := True;
ibtDoc.StartTransaction;
with cdsTitle do
begin
params.paramByName(‘DocID’).AsInteger := FDocID;
Active := True;
if BOF and EOF then
raise Exception.Create(‘Документненайден’);
end;
with cdsBody do
begin
params.paramByName(‘DocID’).AsInteger := FDocID;
Active := True;
end;
FState := osUpdate;
ibtDoc.Commit;
except
DoInactiveState;
raise;
end;
end;
DoOpen создана для открытия имеющегося документа, идентификатор DOC_ID которого равен входному параметру DocID. Сперва при помощи RegisterDoc делается проверка того, что документ на этот момент не редактируется. Потом идентификатор документа запоминается, и в клиентские наборы данных загружаются данные документа. В случае ошибки состояние документа переводится в osInactive.
procedure TrdmDoc.DoCreateNew;
var
NewDocID: Integer;
begin
try
NewDocID := NewID;
if not RegisterDoc(NewDocID) then
raise Exception.Create(‘Документредактируется’);
FDocID := NewDocID;
ibdDocs.Connected := True;
ibtDoc.StartTransaction;
with cdsTitle do
begin
params.paramByName(‘DocID’).AsInteger := FDocID;
Active := True;
Append;
Post;
end;
with cdsBody do
begin
params.paramByName(‘DocID’).AsInteger := FDocID;
Active := True;
end;
ibtDoc.Commit;
FState := osInsert;
except
DoInactiveState;
raise;
end;
end;
Процедура DoCreateNew создана для сотворения новейшего документа. Она фактически подобна предшествующей, кроме того, что идентификатор документа выходит от сервера БД при помощи процедуры NewID, которая обращается к хранимой процедуре на сервере. Реализация процедуры DoCreateNew весьма похожа на аналогичную реализацию в rdmCommon.
Для того, чтоб вставка новейшей записи в документ происходила правильно, довольно написать обработчик cdsTitle.OnNewRecord, задающий изначальное
procedure TrdmDoc.cdsTitleNewRecord(DataSet: TDataSet);
var
Day, Month, Year: Word;
begin
DecodeDate(Date, Year, Month, Day);
with cdsTitle do
begin
FieldByName(‘DOC_ID’).AsInteger := FDocID;
FieldByName(‘DOC_NUM’).AsString := IntToStr(FDocID)
+ ‘/’ + IntToStr(Year);
FieldByName(‘DOC_DATE’).asDateTime := Date;
FieldByName(‘DOC_SUM’).asCurrency := 0;
FieldByName(‘FROM_ID’).AsInteger := 0;
FieldByName(‘TO_ID’).AsInteger := 0;
end;
end;
procedure TrdmDoc.cdsBodyNewRecord(DataSet: TDataSet);
begin
cdsBody.FieldByName(‘DOC_ID’).AsInteger := FDocID;
end;
В дополнение ко всему нужна еще одна процедура в секции private, для подсчета суммы документа:
function TrdmDoc.CalcSum: Currency;
begin
Result := 0;
if not cdsBody.Active then Exit;
with cdsBody do
begin
First;
while not EOF do
begin
Result := Result
+ FieldByName(‘COUNT_NUM’).asCurrency
* FieldByName(‘PRICE’).asCurrency;
Next;
end;
end;
end;
В функции CalcSum просматривается содержимое документа и рассчитывается общая сумма, которая ворачивается в качестве результата.
сейчас нужно позаботиться о клиентской части, другими словами сделать нужные наружные способы сервера в библиотеке типов. Описание этих способов, сделанное редактором библиотек типов, смотрится последующим образом:
protected
function ApplyChanges: WideString; safecall;
function Get_DocID: Integer; safecall;
procedure CreateNewDoc; safecall;
procedure Set_DocID(Value: Integer); safecall;
function Get_DocSum: Currency; safecall;
Функциональность этих способов такая:
ApplyChanges – сохраняет текущий документ в БД.
DocID – свойство, доступное на запись и чтение При чтении выдается текущий ID документа (FDocID). При изменении значения характеристики документ раскрывается для редактирования с ID, равным новенькому значению. Если состояние.
CreateNewDoc – делает новейший документ (вызывает способы DoInactiveState и DoCreateNew).
DocSum – выдается текущая сумма документа, итог работы способа CalcSum.
Реализация этих способов достаточно ординарна, все главные процедуры уже есть, сложность представляет лишь функция ApplyChanges:
function TrdmDoc.ApplyChanges: WideString;
begin
lock;
try
FLastUpdateErrors := »;
if FState = osInactive then
raise Exception.Create(‘Нет новейшего либо открытого документа’);
// Вычисляем итоговую сумму документа
with cdsTitle do
begin
Edit;
FieldByName(‘DOC_SUM’).asCurrency := CalcSum;
Post;
end;
RenumLines; // перенумерация содержимого
// Сохранение в БД…
ibtDoc.StartTransaction;
// При вставке поначалу сохраняем конфигурации в cdsTitle…
if FState = osInsert then
begin
if cdsTitle.ChangeCount > 0 then
cdsTitle.ApplyUpdates(0);
if cdsBody.ChangeCount > 0 then
cdsBody.ApplyUpdates(-1);
end;
// …а при изменении – в cdsBody.
if FState = osUpdate then
begin
if cdsBody.ChangeCount > 0 then
cdsBody.ApplyUpdates(-1);
if cdsTitle.ChangeCount > 0 then
cdsTitle.ApplyUpdates(0);
end;
// FLastUpdateErrors заполняетсяна OnReconcileError.
Result := FLastUpdateErrors;
if Result = » then
ibtDoc.Commit
else
begin
ibtDoc.Rollback;
end;
finally
ibtDoc.Active := False;
unlock;
end;
end;
Дело в том, что изменение данных в БД происходит не в способе провайдера, а в способе модуля, и клиентские наборы данных ничего о этом не знают. Потому функция ApplyChanges возвращает перечень ошибок, появившихся при обновлении данных. Перечень скапливается в переменной FLastUpdateErrors, описанной в секции private как FLastUpdateErrors: String;. Перед сохранением конфигураций рассчитывается сумма документа. Процедура RenumLines нумерует строчки содержимого по порядку. Это просто доп сервис. Потом ClientDataSet-ы пробуют сохранить конфигурации в БД. При появлении ошибки заполняется поле FLastUpdateErrors:
procedure TrdmDoc.cdsTitleReconcileError(DataSet: TClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
begin
Action := raCancel;
FLastUpdateErrors := FLastUpdateErrors + ‘Заголовок: ‘ + E.Message + #13#10;
end;
procedure TrdmDoc.cdsBodyReconcileError(DataSet: TClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
begin
Action := raCancel;
FLastUpdateErrors := FLastUpdateErrors + ‘содержимое: ‘
+ E.Message + #13#10;
end;
При всем этом происходит откат транзакции. Сообщения о ошибке записываются в строчку. В случае появления ошибки клиент должен вывести сообщение и обновить клиентские наборы данных. Как будет видно ниже, в данном случае все проверки можно создать заблаговременно, и фактически вероятны лишь ошибки, связанные с неожиданными обстоятельствами (к примеру, нежданный разрыв соединения с сервером БД).
Процедура RenumLines перенумерует строчки содержимого документа так, чтоб номера шли по порядку, при этом все номера поначалу делаются отрицательными, по другому при попытке уяснить вторую запись с этим же ключем сходу генерируется исключение Key violation, что, очевидно, совсем не необходимо (Дело в том, что провайдер потрясающе понимает, какие поля составляют первичный ключ, вот и контролирует – у ClientDataSet создается контроль первичного ключа. Исключение генерируется сходу, при попытке вставки (до записи в БД)):
procedure TrdmDoc.RenumLines;
var
Num: Integer;
begin
cdsBody.IndexFieldNames := ‘DOC_ID;LINE_NUM’;
// Чтоб избежать Key violation при перенумерации, делаем все номера < 0
// На клиенте нужна проверка LINE_NUM >= 0
cdsBody.Last;
with cdsBody do
while FieldByName(‘LINE_NUM’).AsInteger > 0 do
begin
Edit;
Num := FieldByName(‘LINE_NUM’).AsInteger;
FieldByName(‘LINE_NUM’).AsInteger := -num;
Post;
Last;
end;
// перенумерация…
Num := cdsBody.RecordCount;
cdsBody.First;
with cdsBody do
while FieldByName(‘LINE_NUM’).AsInteger <= 0 do
begin
Edit;
FieldByName(‘LINE_NUM’).AsInteger := num;
Post;
Dec(Num);
First;
end;
end;
Очевидно, и вычисление суммы документа, и перенумерацию содержимого можно создать на клиентской части, но этот пример создавался конкретно чтоб показать перенос вычислений на . При наиболее сложных вычислениях это еще прибыльнее, к примеру, если в расчетах употребляются данные из доп таблиц.
Остается крайний модуль данных сервера, rdmReport, созданный для сотворения отчета. По сопоставлению с прошлыми модулями он достаточно прост (набросок 4.).
Набросок 4.
тут находится всего один компонент транзакции ibtInOut и один компонент запроса ibqInOut, обращающийся к процедуре отчета:
select * from REP_INOUT(:FromDate, :ToDate) order by TO_NAME
При всем этом нужно учесть, что данные из данной процедуры получаются совсем не в том виде, который нужен, и нуждаются в доборной обработке. Такую доп обработку лучше производить на стороне клиента, потому что это потенциально дозволяет передавать данные в наиболее малогабаритном виде, ну и само работу серверной стороны. Потому обработку данных мы будем создавать на сервере. cdsInOut – это компонент ClientDataSet, в каком формируется отчет в том виде, в каком он должен быть отображен клиенту. К этому компоненту подсоединен провайдер dspInOut с установленным флагом poIncFieldProps. Его свойство Exported равно false. От провайдера требуется лишь генерация пакета данных. И, как обычно, ResolveToDataSet = true. cdsInOut не соединен ни с каким провайдером (свойство ProviderName пустое), и должен создаваться очевидно вызовом собственного способа CreateDataSet. Для того, чтоб набор данных содержал поля, их описания должны содержаться в свойстве FieldDefs. Но по той причине, что в отчете-шахматке количество полей в записи заблаговременно непонятно, их описания приходится создавать динамически при обработке результата запроса. Для этого комфортно сделать отдельный способ, CollectInOutData:
function TrdmReport.CollectInOutData: OleVariant;
const
FieldPrefix = ‘Receiver_’;
var
ReceiverFieldName: string;
RecsOut: Integer;
ProvOptions: TGetRecordOptions;
begin
cdsInOut.Active := False;
try
with cdsInOut.FieldDefs do
begin
Clear;
// 1-ые две колонки — поставщик
with AddFieldDef do
begin
Name := ‘SenderID’;
DataType := ftInteger;
Required := True;
end;
with AddFieldDef do
begin
Name := ‘SenderName’;
DataType := ftString;
Size := 180;
end;
// сейчас набор полей — получатели
ibqInOut.First;
while not ibqInOut.EOF do
begin
ReceiverFieldName :=
FieldPrefix + ibqInOut.FieldByName(‘TO_ID’).AsString;
if IndexOf(ReceiverFieldName) = -1 then
with AddFieldDef do
begin
Name := ReceiverFieldName;
DataType := ftCurrency;
end;
ibqInOut.Next;
end;
end;
// 2-ой проход — наполнение суммами
cdsInOut.IndexFieldNames := ‘SenderID’;
cdsInOut.CreateDataSet;
with cdsInOut do
begin
ibqInOut.First;
while not ibqInOut.EOF do
begin
if FindKey([ibqInOut.FieldByName(‘FROM_ID’).AsInteger]) then
Edit
else
Insert;
ReceiverFieldName :=
FieldPrefix + ibqInOut.FieldByName(‘TO_ID’).asString;
if State = dsInsert then
FieldByName(‘SenderID’).AsInteger :=
ibqInOut.FieldByName(‘FROM_ID’).AsInteger;
FieldByName(‘SenderName’).AsString :=
ibqInOut.FieldByName(‘FROM_NAME’).AsString;
with (FieldByName(ReceiverFieldName) as TFloatField) do
begin
asCurrency :=
ibqInOut.FieldByName(‘FULL_SUM’).AsCurrency;
// пока характеристики заголовка не установлены
if DisplayFormat = » then
// установимих
begin
DisplayLabel :=
ibqInOut.FieldByName(‘TO_NAME’).AsString;
DisplayWidth := 10;
Currency := False;
DisplayFormat := ‘# ##0.00’;
end;
end;
Post;
ibqInOut.Next;
end;
// названиепервойколонки
with FieldByName(‘SenderName’) do
begin
DisplayLabel := ‘Поставщики’;
DisplayWidth := 30;
end;
FieldByName(‘SenderID’).Visible := false;
end;
// Пусть провайдер позаботится о формировании пакета.
ProvOptions := [grMetadata, grReset];
Result := dspInOut.GetRecords(-1,RecsOut,Byte(ProvOptions));
finally
cdsInOut.Active := False;
end;
end;
Хотя эта функция смотрится длинноватой и сложной, делается весьма мало: организуется два прохода по ibqInOut, который к этому времени должен содержать итог выполнения хранимой процедуры. За ранее создается два неотклонимых поля — SenderID и SenderName (ID и наименование поставщика). Во время первого прохода у cdsInOut создается перечень колонок (в FieldDefs) с именами вида ‘Receiver_NN’. Потом создается набор данных командой CreateDataSet и организуется 2-ой проход, в каком ячейки заполняются значениями сумм. При всем этом делается поиск поставщика по SenderID (с внедрением индекса), если такового поставщика еще нет – добавляется запись. Потом ячейке таблицы (с подходящим Receiver_ID) присваивается сумма, приобретенная из хранимой процедуры. Попутно инсталлируются зрительные характеристики полей. Опосля прохода по результату запросу выставляются зрительные характеристики первых 2-ух колонок. В конце концов, функция dspInOut.GetRecords возвращает ClientDataSet (вкупе со качествами полей), содержащий готовыйй отчет. Провайдер dspInOut нужен лишь чтоб в пакет были включены зрительные характеристики полей. Для этого употребляется флаг grMetadata, а данные получаются прямым вызовом способа GetRecords. Опосля получения пакета клиентский набор данных можно благополучно закрыть, что, фактически, и делается.
Для передачи содержимого отчета на клиентскую часть в библиотеке типов создается один способ, объявленный как:
function InOutData(FromDate, ToDate: TDateTime): OleVariant; safecall;
Этот способ воспринимает характеристики отчета, и выдает весь отчет, запакованный в OleVariant:
function TrdmReport.InOutData(FromDate, ToDate: TDateTime): OleVariant;
begin
lock;
try
ibdReport.Connected := True;
ibtInOut.StartTransaction;
try
with ibqInOut do
begin
ParamByName(‘FromDate’).asDateTime := FromDate;
ParamByName(‘ToDate’).asDateTime := ToDate;
Active := True;
Result := CollectInOutData;
Active := False;
end;
ibtInOut.Commit;
finally
ibtInOut.Active := False;
end;
finally
unlock;
end;
end;
Функция InOutData устанавливает характеристики запроса и делает его, опосля чего же вызывает функцию CollectInOutData, которая делает основную работу.
На этом шаге сервер приложений на сто процентов закончен, и можно, запустив его один раз для регистрации в реестре как СОМ-сервера, приступать к созданию клиентской части.
Клиент
Задачка клиентского приложения – вести взаимодействие с юзером и показывать подходящую ему информацию.
Интерфейс клиента быть может каким угодно, потому остановлюсь лишь на особенностях работы с данным сервером приложений.
В прилагаемых начальных текстах имеется клиентское приложение, содержащее три модуля данных (TdataModule), dmCommon, dmDoc и dmReport. Любой из их предназначен для соединения с подходящим удаленным модулем данных.
Я не буду тут останавливаться тщательно на описаниях реализации клиентской части, но некие индивидуальности нужно разглядеть.
Для использования сервера приложений его библиотека типов импортирована в клиентское приложение.
ПРИМЕЧАНИЕ
Дело в том, что для соединения клиентского приложения с сервером в данном случае употребляется TSocketConnection (scDoc). При воззвании к интерфейсу удаленного модуля как к variant (через свойство AppServer) вызовы способов сервера в неких вариантах вызывают сбой (Access violation). Потому все вызовы я произвожу через dispinterface, имя которого различается от имени начального интерфейса суффиксом Disp. Импорт библиотеки типов как раз и дозволяет обращаться к этому интерфейсу.
Не считая того, при воззвании к серверу с импортированной библиотекой типов все характеристики процедур проверяются на шаге компиляции, и вызов GetDispIDsOfNames не делается, что ускоряет вызовы способов.
Для импорта нужно избрать пункты меню Project -> Import Type Library, ивыбратьвсписке DocServer library. Не забудьте, что при всем этом должен быть зарегистрирован в реестре. Опосля этого остается отключить опцию Generate Component Wrapper и надавить Create Unit, так как компонент в данном случае не нужен, довольно лишь объявлений.
Работа с поставщиками и получателями
Свойство DMCommon.ClientName обеспечивает воззвание к способу сервера:
property ClientName[ID: integer]: string read GetClientName;
function TDMCommon.GetClientName(ID: integer): string;
var
AServer: IrdmCommonDisp;
begin
Result := »;
if ID = 0 then Exit;
AServer := IrdmCommonDisp(scCommon.GetServer);
Result := AServer.ClientName[ID];
AServer := nil;
end;
Компонент scCommon: TSocketConnection опосля соединения с сервером приложений выдает в качестве результата способа GetServer ссылку на интерфейс удаленного модуля данных, остается просто конвертировать ее к подходящему типу.
Получение новейшего идентификатора для поставщика и получателя делается в обработчике действия OnNewRecord:
procedure TDMCommon.cdsClientNewRecord(DataSet: TDataSet);
var
AServer: IrdmCommonDisp;
begin
AServer := IrdmCommonDisp(scCommon.GetServer);
cdsClient.FieldByName(‘CLIENT_ID’).AsInteger := AServer.NewClientID;
AServer := nil;
end;
Работа с документами
Удаление документа происходит прямо из перечня. Это делается в обработчике действия компонента TAction. А вот редактирование и добавление новейшего документа делается в отдельном модуле DMDoc, привязанном к rdmDoc:
procedure TDMCommon.actDelDocExecute(Sender: TObject);
begin
with cdsDocList do
begin
Delete;
ApplyUpdates(0);
end;
end;
function TDMDoc.ProcessDoc(DocID: Integer; NewDoc: Boolean): boolean;
var
AServer: IrdmDocDisp;
begin
AServer := IrdmDocDisp(scDoc.GetServer); // scDoc: TSocketConnection
if NewDoc then
AServer.CreateNewDoc
else
AServer.DocID := DocID;
try
cdsTitle.Active := True;
cdsBody.Active := True;
RecalcDocSum;
Result := ShowEditForm;
cdsTitle.Active := false;
cdsBody.Active := false;
finally
AServer.DocID := 0; // Отмена регистрации документа
end;
end;
Как уже говорилось, если DocID становится равным 0, закрывает документ.
Сумма документа запрашивается с сервера:
procedure TDMDoc.RecalcDocSum;
begin
with cdsBody do // Свежайшие конфигурации посылаются на
if ChangeCount > 0 then
ApplyUpdates(-1);
with cdsTitle do
begin
if not (State in [dsEdit, dsInsert]) then
Edit;
FieldByName(‘Summa’).asCurrency := GetDocSum;
end;
end;
function TDMDoc.GetDocSum: Currency;
var
AServer: IrdmDocDisp;
begin
AServer := IrdmDocDisp(scDoc.GetServer);
Result := AServer.DocSum;
end;
Поле Summa в клиентском наборе данных – вычисляемое, при всем этом его тип (свойство FieldKind) установлен в fkInternalCalc, что дозволяет работать с сиим полем, как с обыденным полем данных, используя способы Edit и Post. метод неплохим не назовешь, управление VCL советует применять OnCalcFields, принципных различий нет, internalCalc-поля рассчитываются лишь при вызове Post, однократно. 2-ой метод сотворения поля — создать calculated Fields на сервере, и установить у их ProviderFlags = []; в этом случае поля на клиенте будут иметь тип fkData (данные записи), и с ними также можно работать, как с обыкновенными полями данных.
Для показа значения поля «Поставщик» комфортно пользоваться процедурой из модуля DMCommon:
procedure TDMDoc.SetSenderName(Value: integer);
begin
with cdsTitle do
begin
if not (State in [dsEdit, dsInsert]) then
Edit;
if Value <> 0 then
FieldByName(‘FROM_ID’).AsInteger := Value
else
FieldByName(‘FROM_ID’).Clear;
FieldByName(‘FromName’).AsString :=
DMCommon.ClientName[FieldByName(‘FROM_ID’).AsInteger];
//и оставляем в режиме редактирования
end;
end;
Поле, содержащее имя поставщика (FromName), также вычисляемое (InternalCalc). В поле FROM_ID содержится ID поставщика, а в поле FromName – его полное наименование. Подобная процедура заполняет поля для Получателя.
Опосля редактирования документа конфигурации должны отсылаться на для сохранения в базе данных:
function TDMDoc.ApplyDoc: boolean;
var
AServer: IrdmDocDisp;
ErrorLog: string;
begin
with cdsTitle do
begin
if State in [dsEdit, dsInsert] then
Post;
if ChangeCount > 0 then
ApplyUpdates(0);
end;
with cdsBody do
begin
if State in [dsEdit, dsInsert] then
Post;
if ChangeCount > 0 then
ApplyUpdates(-1);
end;
AServer := IrdmDocDisp(scDoc.GetServer);
ErrorLog := AServer.ApplyChanges;
if ErrorLog <> » then
begin
MessageDlg(‘Произошли последующие ошибки:’#13#10 + ErrorLog,
mtError, [mbOK], 0);
//обновляем
cdsBody.Active := False;
cdsTitle.Active := False;
cdsTitle.Active := True;
cdsBody.Active := True;
end;
Result := ErrorLog = »;
end;
Поначалу все изготовленные конфигурации отсылаются на вызовом способов клиентских наборов данных ApplyUpdates. Потом вызывается способ сервера ApplyChanges, который сохраняет конфигурации в БД. При наличии ошибок их перечень помещается в переменную ErrorLog и отображается юзеру.
отчет
Нужно получить от сервера содержимое отчета и присвоить итог запроса свойству TClientDataSet.Data с пустым набором полей. автоматом будет сотворен перечень полей с необходимыми заголовками:
procedure TDMReport.RefreshInOut(FromDate, ToDate: TDateTime);
begin
scReport.Connected := True;
with cdsInOutRes do
begin
Active := False;
Data := FServer.InOutData(int(FromDate), int(ToDate));
Active := True;
end;
end;
тут scReport – компонент TSocketConnection, настроенный на соединение с удаленным модулем rdmReport, а компонент cdsInOutRes предназначен для получения результата. Поля этого набора данных создаются динамически на базе приобретенного от сервера пакета.
Переменная FServer получает свое
procedure TDMReport.scReportAfterConnect(Sender: TObject);
begin
FServer := IrdmReportDisp(scReport.GetServer)
end;
В итоге на клиенте выходит ClientDataSet, на сто процентов заполненный данными отчета, которые можно показать в DBGrid. Примерный итог можно узреть на рисунке 5.
Набросок 5. Наружный вид отчета.
Заключение
Технологию MIDAS стоит использовать в средних и огромных приложениях, база данных которых насчитывает 10-ки и сотки таблиц. Конкретно в этом случае применение MIDAS дает значимые достоинства в разработке и сопровождении приложения. Какие же достоинства дает внедрение MIDAS:
Очевидно, возникает доп структурирование приложения. приложений работает с базой данных. На нем лежит ответственность за сохранение целостности данных и за синхронизацию работы юзеров. На клиентской части остаются средства представления данных юзеру и контроль ввода данных.
При разработке приложения можно поначалу сделать модули сервера приложений, провести испытания на работоспособность, и лишь позже написать клиентскую часть.
Независимость от определенной СУБД. Вправду, на рабочих станциях не необходимо устанавливать клиентскую часть сервера баз данных, соединение с сервером приложений обеспечивается одной библиотекой midas.dll. При переходе на иной БД довольно переписать лишь приложений, не затрагивая клиентскую часть. Может оказаться полезным добавление еще 1-го слоя абстракции данных меж сервером приложений и сервером БД, обеспечивающего просто доступ к элементам базы. В этом случае при переходе на иной сервер БД необходимо переписать лишь отдельные модули, не затрагивая приложений в целом. нужно также учесть, что при групповой разработке приложений обычно часть программистов практикуется на базе данных, иная часть – на интерфейсе юзера. При всем этом любая группа программистов может работать над собственной частью приложения, не отвлекаясь на индивидуальности работы иной группы.
Существенно упрощается синхронизация работы юзеров, которую сейчас обеспечивает сервер приложений.
Это то, что касается разработки приложения. приложений можно оформить в виде DLL. Это дозволит применять его как удаленно (через COM+, MTS либо свой суррогатный процесс), так и в виде внутрипроцессного сервера. Крайний вариант дозволяет сделать версию приложения, не нуждающуюся в установке и конфигурации сервера приложений.
При эксплуатации также появляются некие достоинства:
Миниатюризируется поток данных меж клиентским приложением и серверной частью. Дело в том, что время от времени приходится при расчетах обращаться к доп таблицам БД. В случае трехуровнего приложения эти таблицы можно обрабатывать на сервере приложений, который нередко установлен на одном компе с сервером БД либо связан с ним каналом большенный пропускной возможности. Также нужно учесть, что, как правило, кроме конкретно данных, сервер БД и использующее его приложение обмениваются значимым количеством служебной инфы, к примеру, о типах полей таблиц. Выбор отдельной строчки результата запроса (курсора) происходит отдельной командой (fetch), что наращивает сетевой обмен. В пакеты MIDAS врубаются лишь нужные данные, передаваемые единым массивом, что приметно уменьшает нагрузку на сеть.
Так как основная обработка данных происходит на сервере приложений, который обычно устанавливается на более массивном компе, клиентская часть просит существенно меньше ресурсов на рабочих станциях.
Увеличивается защищенность приложения, так как на рабочих станциях отсутствует прямой доступ к серверу БД.
Как видно, преимуществ достаточно много, и на практике они с припасом окупают необходимость написания «доп» кода.
сейчас мне хотелось бы сказать несколько слов о переходе на наиболее новейшие версии Delphi. В Delphi 6 разработка MIDAS поменяла свое заглавие, и сейчас именуется DataSnap. По-видимому, в связи с сиим произошли некие конфигурации в составе компонент. А именно, в том, что касается данного проекта, при переводе его на Delphi 6 я столкнулся с необходимостью конфигурации объявлений неких обработчиков событий TClientDataSet и TDataSetProvider. Это соединено с тем, что в Delphi 6 возник компонент TCustomClientDataSet, который поменял TClientDataSet. к примеру, объявление
procedure TrdmDoc.cdsTitleReconcileError(DataSet: TClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
сейчас обязано смотреться так:
procedure TrdmDoc.cdsTitleReconcileError(DataSet: TCustomClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
Аналогичным образом нужно поменять другие объявления.
В остальном проект конфигураций не востребовал. думаю, на наиболее новейших версиях Delphi никаких конфигураций также не будет нужно, или они будут незначимыми.
Приложения
Очевидно, выше описан не полный текст сервера приложений, а тем наиболее, клиента. В начальных текстах содержатся доп процедуры, предназначенные, а именно, для наиболее полного контроля за ошибками.
При установке сервера приложений рекомендуется просто запустить его, основная форма сервера дает возможность избрать положение базы данных. Диалог для этого применен обычный, входящий в поставку IBX. При всем этом нужно учесть, что если в пути к базе не обозначено имя сервера, что предполагает, что сервер Interbase и база находятся на одном компе с сервером приложений, рекомендуется выбирать удаленный доступ (указать путь вида localhost:<ПутьКБазе>). Дело в том, что Interbase делает все запросы локальных соединений в одном потоке, в итоге один клиент будет ожидать выполнения запроса другого клиента.
На клиенте в главной форме есть возможность упорядочить перечень документов по хоть какому полю, просто щелкнув мышкой на заголовке колонки. Обеспечен полный контроль ошибок с выдачей сообщений юзеру.
Некие замечания.
Начальные тексты сделаны в Delphi 5. Для правильной работы сервера приложений нужно создавать компиляцию с внедрением IBX версии 4.52, начальные тексты данной версии можно взять на HTTP://codecentral.borland.com/codecentral/ccweb.exe/author?authorid=102. Перед установкой этого обновления должен быть установлен Delphi5 Update pack 1.
клиент соединяется с сервером с помощью TSocketConnection, потому для работы приложения должен быть запущен Borland Socket Server. Исполнимый файл обычно находится в каталоге ($Delphi)bin и именуется scktsrvr.exe.
Для работы приложения MIDAS употребляют библиотеку midas.dll, которая при установленном Delphi 5 находится в системном каталоге Windows. Этот модуль нужен как для работы клиента, так и сервера приложений.
Для Delphi 6 нужна установка Update pack 2 и пакета обновления компонент IBX6.xx. Для компиляции проекта я употреблял, а именно, IBX 6.04.
Вот вроде и все.
Большущее спасибо Александру Капустину, Павлу Шмакову и остальным, кто помогал мне советами и критикой.
]]>