Перенесення програм MIDAS з однієї СУБД на іншу h2>
Олександр Капустін p>
Введення
h2>
У даній статті розглядаються проблеми, пов'язані з
міграцією програми MIDAS з однієї СУБД на іншу. Розглянемо це на прикладі
перенесення програми, описаного в статті Романа Ігнатьєва "MIDAS: практика
застосування ". Додаток написано під Interbase 5.6 і використовує компоненти
IBX на сервері додатків для доступу до СУБД. Перепишемо його таким чином, щоб
додаток зміг працювати під керуванням MSSQL Server 7.0 і MSSQL Server 2000
(за допомогою невеликих переробок скрипта можна домогтися роботи програми під
Sybase ASE 12.0). Слід також зауважити, що переробці піддадуться тільки
скрипт СУБД і сервер додатків. Клієнтська частина залишається недоторканою, тому що при
використанні багатоланкової архітектури вона абсолютно ізольована від деталей
реалізації серверної частини. p>
Деякі зауваження до змісту статті. p>
Передбачається, що читачеві вже знайомі (хоча б у
початковій стадії) синтаксис SQL (в додатку або до Interbase, або до MSSQL, а
також загальні принципи роботи з БД з Delphi (у статті використовуються IBX & ADO,
але це не єдино можливе рішення). p>
Процес перенесення відбувається вже після того, як додаток
злагоджено і стабільно працює (тому що паралельно розробляється для кількох СУБД
- Трохи інший випадок, і його ми розглядати не будемо). p>
Хотілося б сказати кілька слів про те, навіщо це
взагалі потрібно, що ми набуваємо, і що втрачаємо (трохи теорії). p>
купують в основному зниження вартості
програмного продукту, бо якщо у клієнта вже встановлена хоча б одна з
підтримуваних вами СУБД, немає необхідності витрачати великі гроші на закупівлю
СУБД, нового сервера й настроювання всього цього. Поліпшуються можливості інтеграції
з існуючими системами. p>
Але при цьому ми перестаємо використовувати на 100%
можливості будь-якої окремої СУБД. Слід розрізняти два випадки. Перший
випадок, коли підтримується сумісність зі старими версіями цієї ж СУБД
(наприклад, підтримується лінійка MSSQL 6.5 - MSSQL2000). У цьому випадку ми
обмежені можливостями найслабкішої версії, і не можемо використовувати
нововведення. Другий випадок, набагато більш важкий і разом з тим цікавий
для розгляду, коли планується сумісність між різними СУБД,
наприклад між MSSQL і Interbase. Повинен відразу зауважити, що цей випадок
зустрічається набагато рідше, але якщо ви при проектуванні програми не будете
враховувати цю можливість, то перехід викличе набагато більше складнощів. p>
Слід зазначити, що при перенесенні дворівневого
програми проблем виникне набагато більше, тому що більша частина бізнес-логіки
знаходиться на сервері, і якщо синтаксис СУБД сильно відрізняється, можливості
перенесення сильно обмежуються. У випадку ж трирівневого програми
більшість завдань, пов'язаних з логікою, вирішує сервер додатків (хочеться
помітити, що це справедливо тільки для правильно спроектованого
додатки). p>
Модифікація структури БД
p>
На жаль, перенесення структури БД
"один-в-один" між різними СУБД практично неможливий. Тут
наведено опис деяких проблем, з якими доведеться зіткнутися при
перенесення, а також можливі шляхи їх вирішення. Найкраще, якби ці речі були
враховано спочатку при проектуванні вихідного програми, тому що в цьому випадку
обсяг робіт при переносі програми скорочується. p>
Додавання записів до таблиці
p>
Для абстрагування методу створення унікальних
ідентифікаторів для кожного запису можна винести його в збережену процедуру,
яка буде повертати ID нового запису. Це дозволяє легко додавати записи
в підлеглу таблицю, не роблячи ніяких додаткових маніпуляцій. У
Надалі ви можете покласти на цю процедуру, наприклад, генерацію
ідентифікаторів в заданому діапазоні, або забезпечити наскрізну нумерацію. Для
зберігання ідентифікаторів простіше всього мати окрему таблицю приблизно
наступного виду: p>
create table Seeds ( p>
TableName varchar (30), - ім'я таблиці p>
ID int, - ID останньої
вставленою записи в дану таблицю p>
LowOffset int - нижня
межа діапазону p>
) p>
go p>
При додаванні користувача таблиць необхідно не
забувати вставляти в цю таблицю відповідні записи. Нижче наведений приклад
SQL-запиту, що робить це: p>
insert
into Seeds (TableName, ID, LowOffset, HiOffset) p>
values ( 'MyCoolTable',
0, 0, 1000000) p>
go p>
Текст процедури в простому випадку буде виглядати
так: p>
create procedure CLIENT_ID p>
@ TableName varchar (30), p>
@ ID int output p>
as p>
update Seeds p>
set ID = ID + 1, p>
@ ID = ID + LowOffset p>
where TableName = @ TableName p>
go p>
проведення процедури оновлення (update) таблиці накладається
блокування зміни, яка не дозволить іншому клієнту виконати цю ж
процедуру одночасно з першим. Крім перерахованого вище такий підхід
спрощує життя при необхідності реплікації даних між філіями. Тоді в
кожній філії настроюється свій діапазон, і первинні ключі гарантовано не
будуть перетинатися. p>
Контроль цілісності даних
p>
При проектуванні бази необхідно врахувати наступні
Обмеження: p>
Каскадне зміна по foreign key з'явилося тільки в
MSSQL2000. Так що якщо поставити собі за мету зберегти сумісність з попередніми
версіями (а також з Sybase), каскадні зміни необхідно проводити при
допомоги збережених процедур (чому не використовувати тригери, сказано нижче). p>
тригери, відпрацьовують не після перевірки всіх
обмежень цілісності, а замість дії, на яку їх викликали, також
з'явилися тільки в MSSQL2000. У більш ранніх версіях вони просто не змогли б
відпрацювати каскадне зміна при наявності foreign key. Також при написанні
тригерів слід врахувати особливості реалізації для кожної СУБД. Так, наприклад,
в Interbase тригер відпрацьовує на кожен запис, а в MSSQL - на зміну,
вставку або видалення запису. Як варіант, можна відмовитися від підтримки
цілісності, заснованої на foreign key, і реалізувати її повністю на
тригерах. p>
Перенесення скрипта
p>
Тут наведені основні труднощі, з якими можна
зіткнутися при перенесенні скрипта Interbase на MSSQL (повинен ще раз
повторитися, що стаття не претендує на повний і детальний розбір відмінностей
між цими СУБД, та такий аналіз і не може бути повністю коректним). p>
Відповідність вбудованих типів
p>
Основні відмінності, які слід враховувати при
перенесенні скрипта: p>
IB p>
MSSQL p>
Коментар p>
char p>
char p>
-в MSSQL - не більше 8000, в
IB - не більше 32767 char p>
varchar p>
varchar p>
-в MSSQL - не більше 8000, в
IB - не більше 32767 char p>
blob p>
text, image p>
date p>
datetime, smalldatetime p>
(останній обрізає час
до хвилин) p>
money, smallmoney p>
- в IB немає аналогів p>
bit p>
- в IB немає аналогів p>
У MSSQL немає типів, що представляють лише дату або
тільки час, що є в IB6. p>
Домени
p>
У IB для створення доменів використовується наступна
конструкція: p>
create
domain DCount numeric (15,4) default 1 not null; p>
Для MSSQL це буде виглядати наступним чином: p>
create
default ONE as 1 p>
go p>
exec
sp_addtype 'DCount', 'numeric (15,4)', 'NOT NULL' p>
go p>
exec
sp_bindefault 'ONE', 'DCount' p>
go p>
Таблиці
p>
переносяться без проблем, варто лише звернути
увагу на зауваження з контролю цілісності даних (до речі, зворотне
перетворення буде ускладнений, якщо ви будете використовувати специфічні для
MSSQL типи (особливо для MSSQL2000 )). p>
Збережені процедури
p>
Перенесення збережених процедур - це найбільш трудомісткий
процес, тому що доведеться переписувати всі цілком. Але в правильно
спроектованому триланкового додатку роль ХП повинна бути зведена до мінімуму.
Основні труднощі виникають при перекладі ХП, повертають результуючий
набір. Частина з них (що не містять складної бізнес-логіки) може бути переведена
в розряд уявлень (view). Для інших можна або створювати тимчасові
таблиці на рівні з'єднання з СУБД, або створювати постійні таблиці та
розмежовувати дані в них за ідентифікатором підключення (SPID) (але тоді не
забувайте їх чистити:)). Якщо ж ви вирішите обмежитися тільки MSSQL2000, то
можете використовувати тип "таблиця" для повернення набору значень з
процедури. Розглянемо кілька прикладів перекладу ХП. Процедура звіту про
взаєморозрахунках між клієнтами: p>
create procedure REP_INOUT (FROM_DATE date, TO_DATE date) p>
returns (FROM_ID integer,
FROM_NAME varchar (180), TO_ID integer, p>
TO_NAME varchar (180), 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>
create procedure rep_inout @ from_date smalldatetime, @ to_date
smalldatetime p>
as p>
select dt.from_id, dt.to_id,
isnull (sum (dt.doc_sum), 0) as full_sum, p>
c.name as from_name, c1.name
as to_name p>
from doc_title dt, p>
client c, p>
client c1 p>
where dt.doc_date> =
@ from_date p>
and dt.doc_date <=
@ to_date p>
and c.client_id = dt.from_id p>
and c1.client_id = dt.to_id
p>
group by dt.from_id, c.name,
dt.to_id, c1.name p>
go p>
Наступний приклад. Процедура виводить список документів
і повні імена клієнта: p>
create procedure LIST_DOC (FROM_DATE date, TO_DATE date) p>
returns (DOC_ID integer,
DOC_NUM varchar (40), DOC_DATE date, p>
FROM_ID integer, TO_ID
integer, FROM_NAME varchar (224), p>
TO_NAME varchar (224), 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>
На прикладі перекладу даної процедури покажемо один з
варіантів того, як можна звести до мінімуму кількість блокувань на часто
використовуваної таблиці. p>
Створимо для початку допоміжну таблицю наступного
виду: p>
create table pDoc_List p>
( p>
SPID int, - ідентифікатор підключення p>
doc_id int, p>
doc_num varchar (40), p>
doc_date smalldatetime, p>
from_id int, p>
to_id int, p>
doc_sum DSum, p>
from_name varchar (224), p>
to_name varchar (224) p>
) p>
go p>
У цієї тимчасової таблиці ми будемо зберігати дані,
відповідають нашим критеріям пошуку. Для того, щоб можна було відрізнити, якому
клієнтові призначені дані, вводиться стовпець SPID, в якому ми будемо зберігати
унікальний ідентифікатор підключення до БД (@ @ spid). p>
Після того, як дані скопійовані в цю таблицю, ми
можемо спокійно, нікому не заважаючи, обробляти їх так, як нам заманеться. А
клієнтові (точніше, сервера додатків) залишається тільки їх вибрати з даної
таблиці. p>
ПРИМІТКА p>
Треба зазначити, що даний алгоритм
застосовується лише в тих випадках, де некритично, що між перечитуванням
даних клієнтом вони можуть змінитися p>
Ось код процедури, що заповнює цю таблицю: p>
create proc list_doc @ from_date datetime, @ to_date datetime p>
as p>
declare @ from_name varchar (224) p>
declare @ to_name varchar (224) p>
declare @ from_id int p>
declare @ to_id int p>
declare @ doc_id int p>
declare @ doc_num varchar (40) p>
declare @ doc_date datetime p>
declare @ doc_sum dsum p>
delete from pDoc_List p>
where SPID = @ @ spid - очищаємо
тимчасову таблицю від попередніх даних p>
- вставляємо потрібні записи p>
insert into
pDoc_List (SPID, p>
doc_id, p>
doc_num, p>
doc_date, p>
from_id, p>
to_id, p>
doc_sum, p>
from_name, p>
to_name p>
) p>
select @ @ spid, p>
doc_id, p>
doc_num, p>
doc_date, p>
from_id, p>
to_id, p>
doc_sum, p>
'', p>
'' p>
from doc_title p>
where doc_date> =
@ from_date and doc_date <= @ to_date p>
p>
- створюємо найбільш швидкий курсор для обробки записів p>
declare list_docs insensitive cursor for p>
select doc_id, from_id,
to_id p>
from pDoc_List p>
where SPID = @ @ spid p>
for read only p>
open list_docs p>
fetch next from list_docs p>
into @ doc_id, @ from_id, @ to_id p>
while @ @ fetch_status = 0 p>
begin p>
select @ from_name ='',
@ to_name ='' p>
exec client_full_name
@ from_id, @ from_name output p>
exec client_full_name @ to_id,
@ to_name output p>
- заповнюємо поля, яких немає в основній таблиці p>
update pDoc_List p>
set from_name = @ from_name, p>
to_name = @ to_name p>
where SPID = @ @ spid p>
and doc_id = @ doc_id p>
p>
fetch next from list_docs p>
into @ doc_id, @ from_id,
@ to_id p>
end p>
close list_docs p>
deallocate list_docs p>
go p>
SQL-запит, що виконується на сервері додатків для
передачі даних клієнтові: p>
exec
list_doc @ from_date =: from_date, @ to_date =: to_date p>
select
* From pDoc_List where SPID = @ @ spid p>
ПРИМІТКА p>
При написанні ХП слід звернути
особливу увагу на одночасну роботу декількох користувачів. Необхідно
мінімізувати вплив "важких" (звітних) процедур на роботу
клієнтів (один з варіантів було показано вище). p>
Тригери
p>
Тут нічого складного немає, необхідно тільки пам'ятати,
що тригери в MSSQL запускаються тільки після дії (є ще замість
(instead of), але це тільки в MSSQL2000). p>
Приклад: запобігання видалення клієнта, якщо
існують документи з його участю (щоб такий тригер не конфліктував з
обмеженням посилальної цілісності, в MSSQL необхідно прибрати foreign key з таблиці
doc_title на client) p>
create trigger CLIENT_BEFORE_DELETE for CLIENT p>
before delete p>
as p>
begin p>
if (exists (select * from
DOC_TITLE p>
where FROM_ID =
OLD.CLIENT_ID)) p>
then p>
exception EX_CLIENT_IN_DOC; p>
if (exists (select * from
DOC_TITLE p>
where TO_ID =
OLD.CLIENT_ID)) p>
then p>
exception EX_CLIENT_IN_DOC; p>
end p>
^ p>
Перетвориться в p>
create trigger CLIENT_AFTER_DELETE on CLIENT p>
for delete p>
as p>
if (exists (select d.CLIENT_ID
from p>
DOC_TITLE dt, deleted d p>
where dt.FROM_ID =
d.CLIENT_ID)) p>
begin p>
- щоб повідомлення було видно на клієнті p>
raiserror ( 'Існує запис у документі',
16, 1) p>
- необхідно ручками відкотити транзакцію p>
rollback transaction p>
end p>
if (exists (select d.CLIENT_ID
from p>
DOC_TITLE dt, deleted d p>
where dt.TO_ID =
d.CLIENT_ID)) p>
begin p>
raiserror ( 'Існує запис у документі',
16, 1) p>
rollback transaction p>
end p>
go p>
Модифікація сервера додатків.
p>
Тут основна частина переробки пов'язана з переходом
від IBX (InterBase Express) до ADO (ActiveX Data Object). Основні речі, на
які слід звернути увагу: p>
Реалізація транзакцій на клієнта - в IBX це окремий
компонент, в ADO така функціональність надається методами
TADOConnection. Ще невелика рекомендація - акуратно підходите до вибору
рівня ізоляції транзакцій (чим менше рівень ізоляції, тим швидше буде
працювати додаток). p>
Щоб клієнт працював без переробки з різними
джерелами даних, необхідно, щоб типи даних полів збігалися. Наприклад, в
IBX для numeric (15, 4) за замовчуванням підставляється TFloatField, а в ADO --
TBCDField. Це єдина відмінність, яку мені зустрілося при перенесенні (але
це не означає, що їх взагалі немає). Проблема зважилася ручної установкою даного
типу поля в TCurrencyField. p>
Переклад sql-виразів з синтаксису IB в MSSQL. p>
Відмінності, пов'язані з відмінностями структури БД.
Наприклад, якщо буде реалізовано каскадне видалення за допомогою ХП, то доведеться
реалізовувати цю логіку всередині сервера, щоб залишити клієнта недоторканим. p>
Як приклад наведемо переклад однієї з процедур
сервера додатків: p>
// Опис того, що робить
дана процедура, читайте у статті Ігнатьєва. p>
// Код, що працює з IBX p>
function TrdmDoc.ApplyChanges: WideString; p>
begin p>
lock; p>
try p>
FLastUpdateErrors: =''; p>
if FState = osInactive then p>
raise Exception.Create ( 'Документ не був створений або відкритий'); p>
with cdsTitle do p>
begin p>
Edit; p>
FieldByName ( 'DOC_SUM'). AsCurrency: = CalcSum; p>
Post; p>
end; p>
ibtDoc.StartTransaction;
//ibtDoc - компонент транзакції p>
if FState = osInsert then p>
begin p>
if cdsTitle.ChangeCount>
0 then p>
cdsTitle.ApplyUpdates (-1); p>
if cdsBody.ChangeCount> 0
then p>
cdsBody.ApplyUpdates (-1); p>
end; p>
if FState = osUpdate then p>
begin p>
if cdsBody.ChangeCount> 0
then p>
cdsBody.ApplyUpdates (-1); p>
if cdsTitle.ChangeCount>
0 then p>
cdsTitle.ApplyUpdates (-1); p>
end; p>
Result: = FLastUpdateErrors; p>
if Result =''then p>
ibtDoc.Commit p>
else p>
begin p>
ibtDoc.Rollback; p>
end; p>
finally p>
ibtDoc.Active: = False;
//DefaultAction = Rollback p>
unlock; p>
end; p>
end; p>
// Код, що працює з ADO p>
function TrdmDoc.ApplyChanges: WideString;
begin p>
lock; p>
try p>
FLastUpdateErrors: =''; p>
if FState = osInactive then p>
raise Exception.Create ( 'Документ не був створений або відкритий'); p>
with cdsTitle do p>
begin p>
Edit; p>
FieldByName ( 'DOC_SUM'). AsCurrency: = CalcSum; p>
Post; p>
end; p>
adcDocs.BeginTrans;// явні транзакції задаються на рівні
з'єднання p>
if FState = osInsert then// а не окремим
компонентом p>
begin p>
if cdsTitle.ChangeCount>
0 then p>
cdsTitle.ApplyUpdates (-1); p>
if cdsBody.ChangeCount> 0
then p>
cdsBody.ApplyUpdates (-1); p>
end; p>
if FState = osUpdate then p>
begin p>
if cdsBody.ChangeCount> 0
then p>
cdsBody.ApplyUpdates (-1); p>
if cdsTitle.ChangeCount>
0 then p>
cdsTitle.ApplyUpdates (-1); p>
end; p>
Result: = FLastUpdateErrors; p>
if Result =''then p>
adcDocs.CommitTrans p>
else p>
begin p>
adcDocs.RollbackTrans; p>
end; p>
finally p>
unlock; p>
end; p>
end; p>
Висновок
h2>
Це всього лише приклад. У реальних додатках варто
більш ретельно продумувати перенесення додатків. Наприклад, переписувати краще не
весь сервер додатків, а тільки залежний від джерела даних код, виносячи його
в окремі модулі. p>
Дана стаття не претендує на повноту висвітлення
даного питання, а також і автор при викладі підходів для вирішення проблем не
претендує на роль "істини в останній інстанції". Тут був викладений
лише мінімум відомостей, необхідний для вирішення поставленого завдання, а також деякі
міркування, які можуть допомогти при вирішенні подібних проблем. p>
Всі питання, зауваження, виправлення, доповнення
направляйте на [email protected] p>
Хочу висловити вдячність Ігнатьєву Роману, Павлу
Шмакова за поради, критику і наполегливість. P>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>