MIDAS. Практичне застосування h2>
Роман Ігнатьєв p>
Введення p>
Необхідні знання: Перед прочитанням рекомендується
ознайомитися з технологією MIDAS хоча б на рівні демонстраційних програм,
що поставляються з Delphi. p>
Технологія MIDAS (Multi-tier Distributed Application
Services Suite, Сервіс для створення багаторівневих додатків)
була запропонована фірмою Borland вже досить давно, перший додаток з її
використанням я написав ще в 98 році, на Delphi 4. І з тих пір практично
всі програми для роботи з базами даних створюються мною саме на основі
MIDAS. Про переваги, думаю, говорити не треба - навіть просте розділення
програми на дві частини, одна з яких працює з базою даних (сервер
додатків), а інша забезпечує інтерфейс користувача, створює значні
зручності як при розробці програми, так і при його використанні. p>
Зараз існує безліч статей і книг з
технології створення багатоланкових додатків на Delphi, але, на жаль, мені не
вдалося знайти літератури, в якій би розглядалися деякі питання, що цікавлять
мене питання. Справа в тому, що у всіх прикладах створюється триланкового
додаток, в якому сервер додатків просто з'єднує сервер БД з клієнтською
частиною, просто абстрагуючись роботу з базою даних. p>
З одного боку, це дає деяку перевагу при
переході з дворівневої технології (клієнт-сервер) на трирівневу, для чого
компоненти доступу до бази даних з клієнтської частини переносяться в сервер
додатків. З іншого боку, хочеться більшого, а саме перенесення на сервер не
тільки роботи з таблицями бази даних, але й основний логіки програми,
залишаючи клієнтської частини тільки завдання взаємодії з користувачем. p>
Нижче розглядається дуже простий додаток для
роботи з сервером БД. База даних, з якою працює це додаток, що містить
всього 3 таблиці. З його допомогою мені хотілося б показати деякі способи
створення повноцінного сервера додатків, що забезпечує повну обробку
даних. p>
Все, що написано нижче, відноситься до Delphi 5, в
Як сервер обраний Interbase 5.6. Саме ці продукти я використовував у
більшості проектів. Проте база даних працює і на більш старших версіях
Interbase, я перевіряв її працездатність, зокрема, на IB6, а вихідні
тексти додатки з мінімальними змінами можна компілювати на старших
версіях Delphi. На жаль, деякі зміни робити все ж таки доведеться, так
як MIDAS постійно розвивається. Але, як правило, такі зміни мають
косметичний характер, і зробити їх нескладно. Як змінити проект для
компіляції на Delphi 6, буде розказано в заключній частині. Незважаючи на
те, що в сервері додатків використовуються компоненти прямого доступу Interbase
Express (IBX), неважко перейти на інший сервер БД, просто замінивши компоненти
доступу і трохи змінивши текст методів. p>
Виходячи з практичних міркувань нижче описано
створення трирівневого додатки, включаючи розробку структури бази даних.
Мені не хотілося користуватися готовими прикладами, що входять в поставку Interbase
і Delphi, щоб не дотримуватися шаблонів, які нав'язує ними. p>
Постановка завдання p>
Я збираюся на простому прикладі показати можливості
технології, тому програма буде працювати лише з одним логічним об'єктом
- Деяким абстрактним документом, що складається з заголовка і вмісту. Така
структура була обрана з-за того, що зв'язок "майстер-таблиця - підлегла
таблиця "(master-detail) дуже часто зустрічається на практиці, і також часто в
додатках зустрічається обробка різних документів. p>
"Документ" тут розуміється як деяка абстракція,
нагадує реальні документи, такі, як накладна або рахунок. p>
Зрозуміло, необхідно забезпечити одночасну роботу
з документами декількох користувачів. p>
Опис документа p>
Заголовок документа простий, і складається з наступних
полів: p>
Номер - номер документа. p>
Дата - дата виписки, за замовчуванням поточна. p>
Постачальник - ім'я, телефон. p>
Одержувач - також ім'я і телефон. p>
Сума - загальна сума по документу. p>
Постачальник і одержувач будуть вибиратися з єдиного
довідника клієнтів, що містить лише два поля: ім'я і телефон. p>
Документ містить записи з відомостями про переміщення
деяких товарів. Вміст документа - це таблиця у базі даних, записи якої
складаються з наступних полів: p>
Порядковий номер p>
Номер документа, до якого належить. p>
Найменування - найменування товару. p>
Кількість. p>
Ціна. p>
Сума. p>
Результат може виглядати наступним чином: p>
номер п/п p>
найменування p>
кількість p>
ціна p>
сума p>
1 p>
Щось p>
1 p>
30.00 p>
30.00 p>
2 p>
Что-то еще p>
2 p>
10.50 p>
21.00 p>
... p>
Користувачеві повинен видаватися перелік документів з
зазначенням реквізитів документа і його підсумкової суми. p>
Сума документа буде розраховуватися сервером
додатків на основі його вмісту. p>
Крім цього, створимо загальний звіт --
"шахову" відомість, де по одній осі розташовані постачальники, а по
другий - одержувачі: p>
За період з <дата> по
<дата> (за датою документа) p>
Від кого p>
Кому p>
Получатель1 (ім'я) p>
Получатель2 (ім'я) p>
... p>
Поставщік1 (ім'я) p>
Сума p>
Сума p>
Поставщік2 (ім'я) p>
Сума p>
Сума p>
... p>
До звіту будуть входити суми з документів, дати яких
потрапляють у заданий період. p>
Звіт, зрозуміло, виходить також досить
відірваним від життя, мені просто хотілося показати з його допомогою додаткові
можливості MIDAS. p>
Блокування
p>
Оскільки працювати з БД будуть відразу кілька
користувачів, важливо заблокувати документ на час редагування документа
користувачем. "Почесна обов'язок" синхронізацію роботи користувачів
покладається у даному випадку на сервер додатків. У принципі, все це можна
реалізувати і засобами SQL сервера, але, по-перше, для сервера Interbase
блокування записів досить неприродні, по-друге, як буде видно нижче,
сервер додатків дозволяє легко блокувати відразу весь документ як єдиний
об'єкт. p>
Структура БД
p>
Вимоги до додатка описані, і тепер можна
приступити до наступного етапу - створення БД. Нам потрібні три таблиці (магічне
число три вибрано виключно для простоти): для документа, вмісту
документа і довідника клієнтів. Документ містить два посилання на довідник
клієнтів - "Постачальник" і "Одержувач", а вміст документа має
посилання на документ. На малюнку 1 представлена ER-діаграма цієї БД. P>
p>
Малюнок 1. ER-модель БД. p>
У даній базі цілісність даних забезпечується за
наступних правил: p>
При згадуванні в будь-якому документі постачальника або
одержувача видалення цього рядка з довідника клієнтів не допускається. p>
Після видалення документа видаляються і всі пов'язані з ним
рядки з таблиці "Вміст документа". p>
Вставка до таблиці "Вміст документа"
допускається тільки за умови, що поле ID посилається на існуючий документ.
p>
Як видно, в таблиці "Заголовок документа"
є поле "Сума". Це поле повинно містити повну суму документа
(суму полів "Сума" вмісту документа). При зміні
вмісту документа доводиться перераховувати значення цього поля. Наявність
такого поля, є порушенням принципів нормалізації. Цю суму можна завжди
порахувати в SQL-запиті при виведенні даних користувачеві. Але при видачі списку
документів розрахунок суми кожного з них збільшує навантаження на сервер БД.
Відстежувати актуальність цього поля можна на тригерах СУБД, але раз вже ми
створюємо сервер додатків, чому б не покласти на нього це завдання? До того
ж, це забезпечує певну незалежність від особливостей функціонування
сервера БД, що може виявитися корисним, наприклад, при переході на інший
сервер. p>
Нижче наведено скрипт, який створює піддослідних БД: p>
/* Визначено такі типи
даних: p>
Ім'я клієнта p>
Кількість для вмісту документа p>
Ціна p>
Сума p>
*/ p>
create domain DName varchar (180); p>
create domain DCount numeric (15,4) default 1 not null; p>
create domain DCurrency numeric (15,2) default 0 not null; p>
create domain DSum numeric (15,4) default 0 not null; p>
/* Довідник постачальників і
одержувачів */ p>
create table CLIENT p>
( p>
CLIENT_ID integer not null, p>
NAME DName not null,/* Ім'я */ p>
PHONE varchar (40),/* Телефон */ p>
constraint PK_CLIENT primary
key (CLIENT_ID) p>
); p>
/* Заголовок документа */ p>
create table DOC_TITLE p>
( p>
DOC_ID integer not null,/* ID */ p>
DOC_NUM varchar (40) not
null,/* Номер */ p>
DOC_DATE date not null,/* Дата */ p>
FROM_ID integer default 0 not
null,/* Постачальник */ p>
TO_ID integer default 0 not
null,/* Одержувач */ p>
DOC_SUM DSum,/* Сума */ p>
constraint PK_DOC_TITLE primary
key (DOC_ID), p>
constraint FK_DOC_FROM_CLIENT
foreign key (FROM_ID) p>
references Client (CLIENT_ID) p>
on update cascade, p>
constraint FK_DOC_TO_CLIENT
foreign key (TO_ID) p>
references Client (CLIENT_ID) p>
on update cascade p>
); p>
/* Вміст */ p>
create table DOC_BODY p>
( p>
DOC_ID integer not null,/* сcилка на заголовок */ p>
LINE_NUM integer not null,/* Номер п/п */ p>
CONTENT varchar (250) not null,
/ * Найменування */ p>
COUNT_NUM DCount,/* Кількість */ p>
PRICE DCurrency,/* Ціна */ p>
constraint PK_DOC_BODY primary
key (DOC_ID, LINE_NUM), p>
constraint FK_DOC_BODY_TITLE
foreign key (DOC_ID) p>
references DOC_TITLE (DOC_ID) p>
on delete cascade p>
on update cascade p>
); p>
Скрипт створює три таблиці: CLIENT
(постачальники/одержувачі), DOC_TITLE (документ), DOC_BODY (зміст документа). p>
Наступний етап - формування списку документів. У
заголовку документа міститься тільки посилання на постачальника і одержувача. Висновок
списку зручно організувати окремим запитом, а в даному випадку - що зберігається
процедурою. Нехай для зручності ім'я клієнта в списку відображається у вигляді
"Ім'я (Телефон)". Для цього зробимо процедуру CLIENT_FULL_NAME,
яка витягує цей рядок, і будемо викликати її з процедури видачі списку
LIST_DOC. Ця ж процедура стане в нагоді для відображення імені постачальника і
одержувача на формі редагування документа: p>
create procedure CLIENT_FULL_NAME (ID integer) p>
returns (FULL_NAME varchar (224)) p>
as p>
declare variable NAME
varchar (180); p>
declare variable PHONE
varchar (180); p>
begin p>
select NAME, PHONE p>
from client p>
where CLIENT_ID =: ID p>
into: NAME,: PHONE; p>
FULL_NAME =''; p>
if (NAME is not NULL) then p>
FULL_NAME = NAME; p>
if (PHONE is not NULL) then p>
FULL_NAME = FULL_NAME | | '('
| | PHONE | |')'; p>
end p>
create procedure LIST_DOC (FROM_DATE date, TO_DATE date) p>
returns (DOC_ID integer, DOC_NUM varchar (40), DOC_DATE date, FROM_ID
integer, p>
TO_ID integer, FROM_NAME
varchar (224), TO_NAME varchar (224), p>
DOC_SUM numeric (15,4)) p>
as p>
begin p>
for select DOC_ID, DOC_NUM,
DOC_DATE, FROM_ID, TO_ID, DOC_SUM p>
from DOC_TITLE p>
where DOC_DATE> =
: FROM_DATE and DOC_DATE <=: TO_DATE p>
into: DOC_ID,: DOC_NUM,
: DOC_DATE,: FROM_ID,: TO_ID,: DOC_SUM p>
do begin p>
FROM_NAME = NULL; p>
TO_NAME = NULL; p>
execute procedure
CLIENT_FULL_NAME (: FROM_ID) p>
returning_values: FROM_NAME; p>
execute procedure
CLIENT_FULL_NAME (: TO_ID) p>
returning_values: TO_NAME; p>
suspend; p>
end p>
end p>
Залишилася процедура для звіту: p>
create procedure REP_INOUT (FROM_DATE date, TO_DATE date) p>
returns (FROM_ID integer, FROM_NAME varchar (180), TO_ID integer,
TO_NAME varchar (180), p>
FULL_SUM numeric (15,4)) p>
as p>
begin p>
for select FROM_ID, TO_ID,
sum (DOC_SUM) p>
from DOC_TITLE p>
where DOC_DATE> =
: FROM_DATE and DOC_DATE <=: TO_DATE p>
group by FROM_ID, TO_ID p>
into: FROM_ID,: TO_ID,
: FULL_SUM p>
do begin p>
FROM_NAME = NULL; p>
TO_NAME = NULL; p>
select NAME p>
from client p>
where CLIENT_ID =: FROM_ID p>
into: FROM_NAME; p>
select NAME p>
from client p>
where CLIENT_ID =: TO_ID p>
into: TO_NAME; p>
if (FULL_SUM is NULL) then p>
FULL_SUM = 0; p>
suspend; p>
end p>
end p>
Процедура видає те, що потрібно для звіту, але, на
жаль, не у вигляді перехресного звіту, а за рядками: p>
Від кого p>
Кому p>
На суму p>
<Постачальник> p>
<Одержувач> p>
Сума ... p>
... p>
Приводити до нормального вигляду все це буде сервер
додатків. p>
Все готово для написання сервера додатків.
Розпочнемо. p>
Сервер додатків
p>
Створюваний нами сервер додатків буде окремим
виконуваним модулем. Цей модуль потім можна буде розташувати на окремому
комп'ютері, який зможе проводити розрахунки для декількох клієнтів і
синхронізувати їх роботу. p>
Сервер додатків повинен забезпечувати обробку
документа як єдиного об'єкта, тому розумним буде виділити роботу з ним в
окремий клас, в даному випадку нащадок TRemoteDataModule. Нам також
знадобиться модуль даних для роботи з довідником постачальників і одержувачів,
і видачі списку документів. Звіт я вирішив також виділити в окремий модуль. У
підсумку на сервер необхідно створити три нащадка TRemoteDataModule: rdmCommon
(загальний модуль зі списками постачальників/одержувачів та документів), rdmDoc і
rdmReport - відповідно для документа і звіту. p>
Майстер створення віддаленого модуля даних пропонує по
замовчуванням політику завантаження виконуваного модуля Multiple instance і модель
потоків Apartment. Це саме те, що нам треба! Дійсно, Instancing =
Internal призведе до створення серверного компонента в клієнтському процесі (це
поширюється тільки на сервер, який створюється у вигляді DLL). При Single instance
кожна клієнтська частина буде з'єднуватися зі своїм власним примірником
сервера додатків, а синхронізацію простіше зробити, якщо всі клієнти
приєднуються до одному примірнику сервера додатків. Вибір моделі потоків
Apartment дозволить уникнути ручної синхронізації доступу до даних компонента. p>
Тепер залишається створити три (знову три, це теж
випадково) нащадка TRemoteDataModule, розташувати на них компоненти доступу до
даними і написати код для обробки даних. p>
При цьому необхідно враховувати, що при використанні
моделі потоків Apartment кожен модуль даних працює у своєму потоці, і
тому в кожному модулі повинен знаходиться окремий компонент TIBDatabase. p>
При прямому доступі провайдера до бази (властивість
ResolveToDataset = false) MIDAS також вимагає наявності окремої копії об'єкта
TIBTransaction для кожного компонента доступу до даних, тобто у кожного
провайдера повинна бути своя транзакція. Компонент TIBTransaction специфічний для
компонентів прямого доступу до Interbase, звичайно робота з транзакціями покладено
на компонент з'єднання з базою даних. p>
ПРИМІТКА p>
При використанні сервера Interbase для
доступу до даних за технологією MIDAS логічно використовувати IBX, провайдери
даних чудово працюють з цими компонентами. Єдине зауваження --
Borland сертифікувала на момент написання статті версію IBX 4.52. Більше
пізні версії працюють у складі MIDAS трохи інакше, ніж раніше. У
Зокрема, транзакції тепер не закриваються автоматично після вибірки
даних. p>
Розглянемо віддалені модулі даних по порядку, і
почнемо з модуля довідників (rdmCommon) (малюнок 2). p>
p>
Малюнок 2. Загальний модуль rdmCommon. p>
Компонент ibqDocs має тип TIBDatabase і забезпечує
підключення модуля з сервером БД. У мене БД знаходиться в каталозі
d: projectsdocmidasdata і називається doc.gdb. У що додається до статті
проект сервер додатків дозволяє вказати довільне місцезнаходження
сервера БД і файлу бази даних. p>
Для того, щоб при кожному з'єднанні сервер
додатків не запитував ім'я користувача і пароль, вони просто вказані в
параметрах з'єднання. Ім'я користувача SYSDBA та пароль masterkey є
установками за замовчуванням при інсталяції сервера Interbase. p>
Перерахуємо компоненти модуля. До компоненту транзакції
ibtClient приєднаний запит ibqClient (компонент TIBQuery), до якого, у свою
чергу, приєднаний провайдер dspClient. Відповідно, у транзакції і
запиту зазначено з'єднання з БД ibdDocs. Залишається тільки встановити тип
транзакції read committed (зручніше за все це зробити, двічі клацнувши на
відповідному компоненті, і вибравши його тип), і у властивості SQL-запиту
записати "select * from client". Тепер провайдер може надавати
клієнтської частини можливість працювати з довідником клієнтів. Але для
підвищення комфорту потрібно додати можливість декільком користувачам змінювати
одночасно різні поля в одній і тій же записи в таблиці (їх два: Name і
Phone). Робиться це досить просто, в редакторі полів (Fields Editor)
ibqClient потрібно створити постійна список всіх полів запиту, і в поля
CLIENT_ID в його властивість ProviderFlags додати опцію pfInKey. Потім у
провайдера dspClient встановити властивість UpdateMode в upWhereChanged. У цьому
випадку, якщо різні клієнтські частини змінять різні поля о?? ної записи в таблиці
CLIENT, сервер додатків прийме ці зміни. У випадку, якщо будуть змінені
одні й ті ж поля запису, клієнтської частини буде видане повідомлення виду
«Запис змінена іншим користувачем». P>
ПРИМІТКА p>
Тут мені хотілося б зупинитися на
властивості TField.ProviderFlags і TDataSetProvider.UpdateMode. Справа в тому, що
мене часто запитують, що залежить від значень цих властивостей, а залежить від них
досить багато. У довідці за VCL ці властивості описані, на мій погляд,
недостатньо докладно, а зв'язок між ними досить тісний. Отже, нехай
є компонент TQuery, TIBQuery або якийсь інший (запит), з'єднаний
з сервером БД, і до нього приєднаний TDataSetProvider. У цьому випадку на логіку
роботи впливають саме значення властивості ProviderFlags полів цього
запиту, аналогічні властивості полів на стороні клієнта ніякого впливу не
роблять. Комбінація значень цих властивостей повністю визначає, як будуть
проводитися операції оновлення даних на сервері БД. Розглянемо оновлення
даних в таблиці. Додавання та видалення запису відбувається аналогічно. P>
Провайдер з встановленим властивістю
ResolveToDataset = false при оновленні запису формує SQL-запит виду
UPDATE
SET = , ... WHERE
= AND ..., у повній відповідності до стандарту
SQL (при ResolveToDataset = True проводиться пошук і оновлення прямо в таблиці). P>
Ім'я таблиці
береться з
Dataset (провайдер чудово розуміє запити SQL виду Select from ...),
або задається в обробнику OnGetTableName. Значення NewValue і OldValue для
кожного поля беруться з пакета оновлення, що посилається провайдера. Імена
полів у висловах SET і FROM формуються автоматично, саме на основі
властивостей ProviderFlags і UpdateMode того набору даних, через який
провайдер працює з базою. Алгоритм наступний: p>
У пропозиція SET входять тільки ті
поля, у яких встановлено прапор pfUpdate у властивості ProviderFlags (потрібно
оновлювати в базі даних) і OldValue <> NewValue (значення поля було
змінено). p>
Пропозиція WHERE формується наступним
так: p>
Беруться всі поля, у який встановлені
[pfInKey, pfInWhere], фактично це первинний ключ. При UpdateMode = upWhereKeyOnly
більше ніяких полів не береться. p>
При UpdateMode = upWhereChanged до полів
первинного ключа додаються ті поля, у яких OldValue <> NewValue і
pfWhere in ProviderFlags, що дозволяє робити перевірку на зміну тих же
полів іншим користувачем. p>
При UpdateMode = upWhereAll до списку
полів WHERE входять всі поля запису, у яких pfWhere in ProviderFlags. p>
У випадку, якщо запис в таблиці на
сервері не знайдена (немає записів, що задовольняють умові WHERE) користувачеві
видається повідомлення виду "Запис змінена іншим користувачем", поза
залежно від причини. p>
Залишається одне значення прапора, pfHidden.
Поля з цим прапором не передаються клієнтського додатку, і не приймаються від
нього, прапор вказує, що ці поля - тільки для використання на стороні сервера. p>
Якщо вже створено постійний список полів, можна
встановити параметри їх відображення на клієнтської частини, зокрема,
DisplayLabel, DisplayWidth і Visible, а у провайдера - прапори poIncFieldProps.
При цьому на клієнтської частини можна не турбуватися про список полів - значення,
отримані з сервера додатків, перевизначають задані на клієнті в будь-якому
випадку. Разом з тим у провайдера треба встановити опцію poMultiRecordUpdates, щоб
на клієнтської частини можна було змінювати відразу кілька записів у довіднику
до відправки змін на сервер. p>
Поле CLIENT_ID в довіднику постачальників та одержувачів
є первинним ключем, а отже, в ньому мають міститися унікальні
значення. Для отримання унікальних значень зручно використовувати
автоінкрементальние поля (autoincrement field). У IB власне
автоінкрементним полів немає, наростаючі значення одержують від генератора з
допомогою функції Gen_ID, і як правило, привласнюють це значення полю в
тригері. Мені подобається ситуація, коли нове унікальне значення з'являється на
клієнтської частини відразу після додавання запису. Тому замість присвоєння
значення, отриманого від генератора, в тригері, використовується збережена
процедура, результатом роботи якої і є це значення. Для цього в
віддаленому модулі даних розташований компонент spNewID: TIBStoredProc,
приєднаний до компоненту транзакції ibtDefault, який надає доступ
до процедури, що зберігаються на сервері БД. Процедура описана в базі даних таким
так: p>
create
procedure CLIENT_ID p>
returns
(ID integer) p>
as p>
begin p>
ID = Gen_ID (CLIENT_ID_GEN, 1); p>
end p>
Як видно, процедура просто видає таке значення
генератора. Це гарантує, що при послідовних запитах до неї це
значення повторюватися не буде. Отримання значення на клієнтської частини
забезпечується методом сервера, про це трохи нижче. p>
Друга збережена процедура, spClientFullName,
приєднана до компоненту транзакції ibtClient і призначена для видачі імені
і телефону постачальника або одержувача у вигляді рядка «Ім'я (телефон)»,
що підлягає поверненню процедурою сервера БД CLIENT_FULL_NAME. Цей рядок також
передається на клієнтську частину через метод сервера. p>
Група компонентів ibtDocList, ibqDocList, dspDocList
і ibqDelDoc призначена для роботи зі списком документів. У IbtDocList,
компонента транзакції, встановлено режим read committed, а в компоненті
ibqDocList міститься SQL-запит «select * from List_doc (: FromDate,: ToDate)».
Весь список документів відразу виводити досить безглуздо, їх може бути
багато. Тому запит вибирає список документів, дати яких лежать в
проміжку від FromDate до ToDate. Провайдер dspDocList видає цей список
клієнтської частини. p>
Додатковий компонент, ibqDelDoc, як, думаю, видно
з його назви, призначений для видалення документа, в його властивості SQL варто
запит «delete from DOC_TITLE where DOC_ID =: DOC_ID». Незважаючи на те, що для
створення і зміни документа планується використовувати окремий модуль,
rdmDoc, для видалення документа зовсім необов'язково його відкривати, і з точки
зору інтерфейсу користувача зручно робити це прямо з списку документів. На
перший погляд, використання окремого запиту для видалення здається зайвим,
для цього звичайно досить оголосити в обробнику dspDocList.OnGetTableName
ім'я таблиці (DOC_TITLE), а відступ буде автоматично забезпечено. Однак у
постановці завдання стоїть умова, що відкритий в одній клієнтської частини документ
повинен бути недоступний для зміни (а значить, і видалення) з інших
клієнтських частин. Тому доводиться робити це в обробнику події
dspDocList.OnBeforeUpdateRecord наступним чином: p>
procedure TrdmCommon.dspDocListBeforeUpdateRecord (Sender: TObject; p>
SourceDS: TDataSet; DeltaDS:
TClientDataSet; UpdateKind: TUpdateKind; p>
var Applied: Boolean); p>
var p>
DocID: Integer; p>
begin p>
if UpdateKind = ukDelete then
//Тільки якщо запис видаляється p>
begin p>
DocID: =
DeltaDS.FieldByName ( 'DOC_ID'). AsInteger; p>
try p>
if not RegisterDoc (DocID)
then// Намагаємося зареєструвати p>
raise Exception.Create ( 'Документ редагується'); p>
with ibqDelDoc do// Видаляємо p>
begin p>
paramByName ( 'DocID'). AsInteger: = DocID; p>
ExecSQL; p>
end; p>
Applied: = True; p>
finally p>
UnregisterDoc (DocID);// Зміна закінчено, видалили p>
end; p>
end; p>
end; p>
Якщо видаляється документ, спробуємо його
зареєструвати в списку редагованих функцією RegisterDoc, потім, якщо це
вийшло, видаляємо його з допомогою запиту ibqDelDoc і видаляємо зі списку
редагування (UnregisterDoc). Встановлюємо Applied: = true, щоб сказати
провайдеру, що все вже зроблено. p>
Звичайно, одночасно може редагуватися
(вилучатися для того, додаватися) досить багато документів, тому потрібний єдиний список
цих документів, до якого будуть звертатися процедури RegisterDoc і
UnregisterDoc. Оскільки звернення до нього буде проводитися з модулів
даних, що працюють в різних потоках, то найкраще для цього підходить
TThreadList (потокобезопасний клас списку). Список документів має бути
єдиним для всіх клієнтських частин, тому його потрібно розташувати в окремому
модулі, наприклад, в модулі головної форми сервера. На ній потім можна вивести,
наприклад, список редагованих на даний момент документів. Так і зробимо. P>
У модулі головної форми сервера в розділі
implementation оголосимо змінну DocList: TThreadList; Цей список краще
ініціалізувати відразу при запуску сервера і знищувати при виході: p>
initialization p>
DocList: = TThreadList.Create; p>
finalization p>
if Assigned (DocList) then p>
begin p>
DocList.Free; p>
DocList: = nil; p>
end; p>
end. p>
З цим списком працюють дві функції: RegisterDoc і
UnregisterDoc: p>
function RegisterDoc (DocID: integer): boolean; p>
begin p>
Result: = False; p>
if DocID = 0 then Exit; p>
with DocList.LockList do p>
try p>
if IndexOf (Pointer (DocID))
<0 then p>
begin p>
Add (Pointer (DocID )); p>
Result: = True; p>
end; p>
finally p>
DocList.UnlockList; p>
end; p>
end; p>
function UnregisterDoc (DocID: integer): boolean; p>
begin p>
Result: = False; p>
if DocID = 0 then Exit; p>
with DocList.LockList do p>
try p>
if IndexOf (Pointer (DocID))
> = 0 then p>
begin p>
Remove (Pointer (DocID )); p>
Result: = True; p>
end; p>
finally p>
DocList.UnlockList; p>
end; p>
end; p>
У списку зберігаються ідентифікатори документів. Але
TThreadList призначений для зберігання покажчиків. Тому для зберігання в цьому
списку ідентифікатора, що має тип Integer, доведеться привести його до типу
pointer. Звичайно, якщо буде потрібно зберігати додаткову інформацію про
документі, наприклад, його номер, доведеться організувати в списку посилання на
запису, з виділенням пам'яті під цей запис і знищенням непотрібних записів. При
цьому зовнішній вигляд функцій не зміниться, просто ускладниться робота зі списком, і
може знадобитися звернення до БД для отримання додаткової інформації. p>
Тепер все просто: всі модулі даних, які працюють
з документами, що використовують ці дві функції, і якщо RegisterDoc повертає false
(а це станеться тільки в тому випадку, якщо номер уже є в списку), то
користувачеві видається повідомлення, що з документом вже працюють. Функція
UnregisterDoc просто видаляє номер зі списку. P>
На клієнті знадобиться, окрім доступу до двох
провайдерам, ще пара функцій - отримання нового значення для CLIENT_ID
довідника клієнтів і отримання повного імені клієнта. Для цього необхідно
створити опис цих функцій в бібліотеці типів. p>
Залежно від того, який синтаксис використовується в
редакторі бібліотеки типів (IDL або Pascal), оголошення цих функцій виглядає
по-різному, нижче наведено їх опису в protected-секції модуля даних: p>
protected p>
class procedure UpdateRegistry (Register:
Boolean; const ClassID, ProgID: string); p>
override; p>
function NewClientID: Integer; safecall; p>
function Get_ClientName (ClientID: Integer):
WideString; safecall; p>
На IDL це виглядає так: p>
[id (0x00000001)] p>
HRESULT
_stdcall NewClientID ([out, retval] long * Result); p>
[propget,
id (0x00000004)] p>
HRESULT
_stdcall ClientName ([in] long ClientID, [out, retval] BSTR * Value); p>
Реалізація цих функцій досить проста. Треба викликати
процедури, що зберігаються, і видати повертається ними значення в якості результату: p>
function TrdmCommon.NewClientID: Integer; p>
begin p>
lock; p>
with spNewID do p>
try p>
ExecProc; p>
Result: =
paramByName ( 'ID'). AsInteger; p>
finally p>
unlock; p>
end; p>
end; p>
function TrdmCommon.Get_ClientName (ClientID: Integer): WideString; p>
begin p>
lock; p>
try p>
with spClientFullName do p>
begin p>
paramByName ( 'ID'). AsInteger
: = ClientID; p>
ExecProc; p>
Result: =
paramByName ( 'FULL_NAME'). AsString; p>
end; p>
finally p>
unlock; p>
end; p>
end; p>
Тепер основний модуль готовий, і можна перейти до
написання наступного модуля даних, призначеного для роботи з документом. p>
p>
Малюнок 3. p>
Тут вже все трохи складніше. Зрозуміло, тут теж
є з'єднання з базою ibdDoc, налаштована на сервер БД. Збережені процедури
spNewID видає на цей раз номер для нового документа, використовуючи процедуру
DOC_TITLE_ID, аналогічну процедуру CLIENT_ID. P>
На цей раз в модулі даних, крім компонентів
запитів до сервера, присутні два компоненти TСlientDataSet і два провайдера
даних. Ці додаткові компоненти призначені саме для організації
розрахунків на сервері. Оскільки, як ми домовилися, на сервері додатків
повинна розраховуватися сума документа, то на ньому має бути відомо
вміст документа до того, як воно буде збережено в БД. Зрозуміло, це
можна здійснити, використовуючи події провайдера для попередньої обробки
пакету змін, що надійшов від клієнтської частини, але мені хотілося показати
можливість організації роботи з документом як з повноцінним об'єктом. p>
Ідея проста: нехай весь видалений модуль даних
працює з одним документом. У такому випадку цей модуль буде виглядати для
клієнтської частини як повноцінний об'єкт, що володіє всіма даними документа і
надає клієнтської частини всі необхідні властивості. Зрозуміло, від
пакетів даних ніхто не відмовляється. p>
Таким чином, організовується такий алгоритм роботи:
Клієнтська частина створює на сервері або новий документ, або відкриває
існуючий (видалення документів вже реалізовано). Сервер додатків створює
модуль даних і, якщо необхідно, закачує в нього вміст документа з
сервера БД. Після цього клієнтська частина і видалений модуль даних спільно
обробляють ці дані, займаючись кожен своєю справою: клієнтська частина
надає засоби для зміни цих даних користувачем, а віддалений
модуль робить всі необхідні розрахунки. p>
До одного компоненту транзакції ibtDoc приєднано на
Цього разу два запити ibqTitle і ibqBody, відповідно вибирають один рядок
заголовка документа (select * from DOC_TITLE where DOC_ID =: DocID) і все
рядки цього документа (select * from DOC_BODY where DOC_ID =: DocID). p>
ПРИМІТКА p>
Хоча MIDAS вимагає наявності своєї
IBTransaction для кожної пари компонентів "IBQuery-провайдер", в
даному випадку це не є обов'язковим. Провайдери не будуть починати і завершувати
транзакції, відкриватися і закриватися транзакція буде явно, у
відповідних методах. p>
До цих запитах приєднані провайдери dspTitleInner
і dspBodyInner, призначення яких - отримати дані з сервера БД і передати їх
у відповідні ClientDataSet. Властивість Exported у цих провайдерів
встановлено в false, вони потрібні тільки всередині сервера додатків, і бачити їх на
клієнтської частини нема чого. Відповідно, клієнтський набір даних cdsTitle
(компонент TClientDataSet) отримує один рядок заголовка з dspTitle і
cdsBody, вміст документа з dspBody. p>
Для того, щоб клієнтська частина могла одержувати і
змінювати дані документа, до клієнтських розділами даних cdsTitle і cdsBody
приєднані провайдери даних, dspTitle і dspBody, відповідно. Властивості
Exported цих провайдерів залишено значення за замовчуванням, True, зате властивість
ResolveToDataSet встановлено в True, для того, щоб ці провайдери не намагалися
працювати з ClientDataSet за допомогою запитів. Таким чином, клієнтська частина
може отримувати та змінювати дані не з TIBQuery, але з TClientDataSet, причому
абсолютно про це не здогадуючись. За командою з клієнтської частини зміни,
передаються серверу додатків, який і зберігає їх у БД. p>
Тепер подивимося, що нам потрібно для подібної
реалізації. Функції синхронізації обробки документів RegisterDoc і
UnregisterDoc вже є, треба їх тільки використовувати. З їхньою допомогою
гарантується, що одночасно один і той же документ редагуватися НЕ
буде, тому у провайдерів даних dspTitleInner і dspTitleBody достатньо
встановити UpdateMode = upWhereKeyOnly, і визначити ключові поля у запитів.
Вміст документа може складатися з декількох рядків, тому у dspBodyInner
і dspBody потрібно встановити прапор poAllowMultiRecordUpdates. Тепер потрібно
розібратися з полями клієнтських наборів даних, встановивши в них відповідні
властивості. Я тут зупинюся лише на властивості ProviderFlags. Оскільки поле
«Посилання на документ» (DOC_ID) на клієнтської частини не потрібно, йому можна задати
прапор pfHidden. Зрозуміло, у всіх ключових полів (DOC_ID і LINE_NUM) і в наборі
даних заголовка, і у вмісті документа треба вказати прапор pfInKey. У
провайдерів dspTitle і dspBody потрібно встановити політику оновлення UpdateMode
= UpWhereKeyOnly, клієнтська частина у модуля даних одна, і інші значення
абсолютно ні до чого. p>
Тепер компоненти для зберігання і обробки даних
підготовлені, залишилося написати самі методи роботи з ними. p>
Давайте розберемося, що саме потрібно. Модуль
rdmDoc призначений як для створення нового документу, так і для редагування
існуючого. Цей модуль можен знаходитися в одному з трьох станів,
описаних в перерахуванні TObjState: p>
osInactive: даних немає, документ не редагується, p>
osInsert: створений новий документ і p>
osUpdate - відбувається зміна існуючого
документа. p>
Стан зберігається у змінній Fstate, що знаходиться
всередині модуля. Відразу після створення і після закінчення обробки документа
модуль даних повинен знаходитися в неактивному стані. p>
Перехід з одного стану в інший повинен
забезпечуватися відповідними методами. Я назвав ці методи DoInactiveState
(перевод в неактивний стан), DoOpen (відкрити існуючий документ) і
DoCreateNew (створення нового документа). Під час редагування чи додавання
документа потрібно знати його унікальний номер, записується в полі DOC_ID. Для
цього достатньо оголосити в секції private змінну FDocID: integer, яка
і буде його зберігати. p>
У бібліотеці типів потрібно реалізувати методи, які
будуть створювати документ або відкривати існуючий, а також зберігати
зміни. Крім цього, знадобиться властивість, що дозволяє отримати в будь-якій
момент суму по документу. Сума кожного рядка вмісту нехай розраховується
на клієнтської частини. p>
Отже, приступимо. Спочатку описуються методи переходу
між станами, вони призначені для внутрішнього використання, і тому
їх оголошення містяться в секції private: p>
procedure DoInactiveState; p>
procedure DoCreateNew; p>
procedure DoOpen (DocID: integer); p>
Розглянемо їх по порядку. p>
procedure
TrdmDoc.DoInactiveState; p>
begin p>
UnregisterDoc (FDocID); p>
FDocID: = 0; p>
cdsTitle.Active: = False; p>
cdsBody.Active: = False; p>
ibtDoc.Active: = False; p>
FState: = osInactive; p>
end; p>
Процедура DoInactiveState видаляє документ зі списку
редагованих, закриває усі клієнтські набори даних, а також виробляє відкат
транзакції (якщо вона була активна). p>
procedure TrdmDoc.DoOpen (DocID: Integer); p>
begin p>
if DocID = 0 then Exit; p>
try p>
if not RegisterDoc (DocID) then p>
raise Exception.Create ( 'Документ редагується'); p>
FDocID: = DocID;// і тільки тут, інакше DoInactiveState видалить документ p>
ibdDocs.Connected: = True; p>
ibtDoc.StartTransaction; p>
with cdsTitle do p>
begin p>
params.paramByName ( 'DocID'). AsInteger: = FDocID; p>
Active: = True; p>
if BOF and EOF then p>
raise Exception.Create ( 'Документ не знайдений'); p>
end; p>
with cdsBody do p>
begin p>
params.paramByName ( 'DocID'). AsInteger: = FDocID; p>
Active: = True; p>
end; p>
FState: = osUpdate; p>
ibtDoc.Commit; p>
except p>
DoInactiveState; p>
raise; p>
end; p>
end; p>
DoOpen призначена для відкриття існуючого
документа, ідентифікатор DOC_ID якого дорівнює вхідному параметру DocID. Першим
справою за допомогою RegisterDoc проводиться перевірка того, що документ у даний
момент не редагується. Потім ідентифікатор документа запам'ятовується, і в
клієнтські набори даних завантажуються дані документа. У разі помилки
стан документа переводиться в osInactive. p>
procedure TrdmDoc.DoCreateNew; p>
var p>
NewDocID: Integer; p>
begin p>
try p>
NewDocID: = NewID; p>
if not RegisterDoc (NewDocID)
then p>
raise Exception.Create ( 'Документ редагується'); p>
FDocID: = NewDocID; p>
ibdDocs.Connected: = True; p>
ibtDoc.StartTransaction; p>
with cdsTitle do p>
begin p>
params.paramByName ( 'DocID'). AsInteger: = FDocID; p>
Active: = True; p>
Append; p>
Post; p>
end; p>
with cdsBody do p>
begin p>
params.paramByName ( 'DocID'). AsInteger: = FDocID; p>
Active: = True; p>
end; p>
ibtDoc.Commit; p>
FState: = osInsert; p>
except p>
DoInactiveState; p>
raise; p>
end; p>
end; p>
Процедура DoCreateNew призначена для створення нового
документа. Вона практично аналогічна попередньої, за винятком того, що
ідентифікатор документа виходить від сервера БД за допомогою процедури NewID,
яка звертається до збереженої процедури на сервері.
|
|
|
|
|