Учебная работа. Реферат: Перенос приложений MIDAS с одной СУБД на другую
Александр Капустин
Введение
В данной статье рассматриваются препядствия, связанные с миграцией приложения MIDAS с одной СУБД на другую. Разглядим это на примере переноса приложения, описанного в статье Романа Игнатьева «MIDAS: практика внедрения». приложение написано под Interbase 5.6 и употребляет составляющие IBX на сервере приложений для доступа к СУБД. Перепишем его таковым образом, чтоб приложение сумело работать под управлением MSSQL Server 7.0 и MSSQL Server 2000 (с помощью маленьких переделок скрипта можно достигнуть работы приложения под Sybase ASE 12.0). Следует также увидеть, что переделке подвергнутся лишь скрипт СУБД и сервер приложений. Клиентская часть остается нетронутой, т.к. при использовании многозвенной архитектуры она полностью изолирована от деталей реализации серверной части.
Некие замечания к содержанию статьи.
Предполагается, что читателю уже знакомы (хотя бы в исходной стадии) синтаксис SQL (в приложении или к Interbase, или к MSSQL, также общие принципы работы с БД из Delphi (в статье употребляются IBX&ADO, но это не единственно вероятное решение).
процесс переноса происходит уже опосля того, как приложение отлажено и размеренно работает (т.к. параллельная разработка для нескольких СУБД – мало иной вариант, и его мы разглядывать не будем).
хотелось бы сказать несколько слов о том, для чего это совершенно необходимо, что мы получаем, и что теряем (мало теории).
Приобретается в главном понижение цены программного продукта, ибо если у клиента уже установлена хотя бы одна из поддерживаемых вами СУБД, нет необходимости растрачивать огромные средства на закупку СУБД, новейшего сервера и настройку всего этого. Улучшаются способности интеграции с существующими системами.
Но при всем этом мы перестаем применять на 100% способности какой-нибудь отдельной СУБД. Следует различать два варианта. 1-ый вариант, когда поддерживается сопоставимость со старенькыми версиями данной нам же СУБД (к примеру, поддерживается линейка MSSQL 6.5 – MSSQL2000). В этом случае мы ограничены способностями самой слабенькой версии, и не можем применять нововведения. 2-ой вариант, еще наиболее тяжкий и вкупе с тем увлекательный для рассмотрения, когда планируется сопоставимость меж разными СУБД, к примеру меж MSSQL и Interbase. должен сходу обмолвиться, что этот вариант встречается еще пореже, но если вы при проектировании приложения не будете учесть эту возможность, то переход вызовет еще больше сложностей.
Необходимо подчеркнуть, что при переносе двухуровневого приложения заморочек возникнет еще больше, т.к. большая часть бизнес-логики находится на сервере, и если синтаксис СУБД очень различается, способности переноса очень ограничиваются. В случае же трехуровневого приложения большая часть задач, связанных с логикой, решает приложений (охото увидеть, что это справедливо лишь для верно спроектированного приложения).
Модификация структуры БД
К огорчению, перенос структуры БД «один-в-один» меж разными СУБД фактически неосуществим. тут приведено описание неких заморочек, с которыми придется столкнуться при переносе, также вероятные пути их решения. лучше всего, если б эти вещи были учтены вначале при проектировании начального приложения, т.к. в этом случае размер работ при переносе приложения сокращается.
Добавление записей в таблицу
Для абстрагирования способа генерации неповторимых идентификаторов для каждой записи можно вынести его в хранимую функцию, которая будет возвращать ID новейшей записи. Это дозволяет просто добавлять записи в подчиненную таблицу, не производя никаких доп манипуляций. В предстоящем вы сможете возложить на эту функцию, к примеру, генерацию идентификаторов в данном спектре, либо обеспечить сквозную нумерацию. Для хранения идентификаторов проще всего иметь отдельную таблицу приблизительно последующего вида:
create table Seeds (
TableName varchar(30), —имятаблицы
ID int, —ID крайней вставленной записи в данную таблицу
LowOffset int —нижняя граница спектра
)
go
При добавлении пользовательских таблиц нужно не забывать вставлять в эту таблицу надлежащие записи. Ниже приведен пример SQL-запроса, делающего это:
insert into Seeds(TableName, ID, LowOffset, HiOffset)
values(‘MyCoolTable’, 0, 0, 1000000)
go
текст процедуры в простом случае будет смотреться так:
create procedure CLIENT_ID
@TableName varchar(30),
@ID int output
as
update Seeds
set ID = ID + 1,
@ID = ID + LowOffset
where TableName = @TableName
go
При обновлении (update) таблицы накладывается блокировка конфигурации, которая не дозволит другому клиенту выполнить эту же функцию сразу с первым. Кроме перечисленного выше таковой подход упрощает жизнь по мере необходимости репликации данных меж филиалами. Тогда в любом филиале настраивается собственный спектр, и первичные ключи гарантированно не будут пересекаться.
Контроль целостности данных
При проектировании базы нужно учитывать последующие ограничения:
Каскадное изменение по foreign key возникло лишь в MSSQL2000. Так что если задаться целью сохранить сопоставимость с прошлыми версиями (также с Sybase), каскадные конфигурации нужно создавать с помощью хранимых процедур (почему не применять триггеры, сказано ниже).
Триггеры, отрабатывающие не опосля проверки всех ограничений целостности, а заместо деяния, на которое их вызвали, также возникли лишь в MSSQL2000. В наиболее ранешних версиях они просто не смогли бы отработать каскадное изменение при наличии foreign key. Также при написании триггеров следует учитывать индивидуальности реализации для каждой СУБД. Так, к примеру, в Interbase триггер отрабатывает на каждую запись, а в MSSQL – на изменение, вставку либо удаление записи. Как вариант, можно отрешиться от поддержки целостности, основанной на foreign key, и воплотить ее на сто процентов на триггерах.
Перенос скрипта
тут приведены главные трудности, с которыми можно столкнуться при переносе скрипта Interbase на MSSQL (должен снова повториться, что статья не претендует на полный и детализированный разбор различий меж этими СУБД, да таковой анализ и не быть может на сто процентов корректным).
Соответствие интегрированных типов
Главные различия, которые следует учесть при переносе скрипта:
IB
MSSQL
Комментарий
char
char
-в MSSQL – не наиболее 8000, в IB — не наиболее 32767 char
varchar
varchar
-в MSSQL — не наиболее 8000, в IB — не наиболее 32767 char
blob
text, image
date
datetime, smalldatetime
(крайний обрезает время до минут)
money, smallmoney
— в IB нет аналогов
bit
— в IB нет аналогов
В MSSQL нет типов, представляющих лишь дату либо лишь время, имеющихся в IB6.
Домены
В IB для сотворения доменов употребляется последующая система:
create Domain DCount numeric(15,4) default 1 not null;
Для MSSQL это будет смотреться последующим образом:
create default ONE as 1
go
exec sp_addtype ‘DCount’, ‘numeric(15,4)’, ‘NOT NULL’
go
exec sp_bindefault ‘ONE’, ‘DCount’
go
Таблицы
Переносятся без заморочек, следует лишь направить внимание на замечания по контролю целостности данных (к слову, оборотное преобразование будет затруднено, если вы будете применять специальные для MSSQL типы (в особенности для MSSQL2000)).
Хранимые процедуры
Перенос хранимых процедур – это более трудозатратный процесс, т.к. придется переписывать все полностью. Но в верно спроектированном трехзвенном приложении роль ХП обязана быть сведена к минимуму. Главные трудности появляются при переводе ХП, возвращающих результирующий набор. часть из их (не содержащие сложной бизнес-логики) быть может переведена в разряд представлений (view). Для других можно или создавать временные таблицы на уровне соединения с СУБД, или создавать неизменные таблицы и разграничивать данные в их по идентификатору подключения (SPID) (но тогда не запамятовывайте их чистить :)). Если же вы решите ограничиться лишь MSSQL2000, то сможете применять тип «таблица» для возврата набора значений из процедуры. Разглядим несколько примеров перевода ХП. Процедура отчета о взаиморасчетах меж клиентами:
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
^
Преобразуется в функцию последующего вида:
create procedure rep_inout @from_date smalldatetime, @to_date smalldatetime
as
select dt.from_id, dt.to_id, isnull(sum(dt.doc_sum), 0) as full_sum,
c.name as from_name, c1.name as to_name
from doc_title dt,
client c,
client c1
where dt.doc_date >= @from_date
and dt.doc_date <= @to_date
and c.client_id = dt.from_id
and c1.client_id = dt.to_id
group by dt.from_id, c.name, dt.to_id, c1.name
go
Последующий пример. Процедура выводит перечень документов и полные имена клиентов:
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 table pDoc_List
(
SPID int, —идентификаторподключения
doc_id int,
doc_num varchar(40),
doc_date smalldatetime,
from_id int,
to_id int,
doc_sum DSum,
from_name varchar(224),
to_name varchar(224)
)
go
В данной нам временной таблице мы будем хранить данные, отвечающие нашим аспектам поиска. Для того, чтоб можно было отличить, какому клиенту предусмотрены данные, вводится столбец SPID, в каком мы будем хранить неповторимый идентификатор подключения к БД (@@spid).
Опосля того, как данные скопированы в эту таблицу, мы можем тихо, никому не мешая, обрабатывать их так, как нам захочется. А клиенту (поточнее, серверу приложений) остается лишь их избрать из данной таблицы.
ПРИМЕЧАНИЕ
нужно отметить, что данный метод применим лишь в тех вариантах, где некритично, что меж перечитыванием данных клиентом они могут поменяться
Вот код процедуры, заполняющей эту таблицу:
create proc list_doc @from_date datetime, @to_date datetime
as
declare @from_name varchar(224)
declare @to_name varchar(224)
declare @from_id int
declare @to_id int
declare @doc_id int
declare @doc_num varchar(40)
declare @doc_date datetime
declare @doc_sum dsum
delete from pDoc_List
where SPID = @@spid —очищаем временную таблицу от прошлых данных
—вставляемнужныезаписи
insert into pDoc_List(SPID,
doc_id,
doc_num,
doc_date,
from_id,
to_id,
doc_sum,
from_name,
to_name
)
select @@spid,
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
—создаем более резвый курсор для обработки записей
declare list_docs insensitive cursor for
select doc_id, from_id, to_id
from pDoc_List
where SPID = @@spid
for read only
open list_docs
fetch next from list_docs
into @doc_id, @from_id, @to_id
while @@fetch_status = 0
begin
select @from_name = », @to_name = »
exec client_full_name @from_id, @from_name output
exec client_full_name @to_id, @to_name output
—заполняем поля, которых нет в главный таблице
update pDoc_List
set from_name = @from_name,
to_name = @to_name
where SPID = @@spid
and doc_id = @doc_id
fetch next from list_docs
into @doc_id, @from_id, @to_id
end
close list_docs
deallocate list_docs
go
SQL-запрос, исполняемый на сервере приложений для передачи данных клиенту:
exec list_doc @from_date = :from_date, @to_date = :to_date
select * from pDoc_List where SPID = @@spid
ПРИМЕЧАНИЕ
При написании ХП следует направить особенное внимание на одновременную работу нескольких юзеров. нужно минимизировать воздействие «томных» (отчетных) процедур на работу клиентов (один из вариантов был показан выше).
Триггеры
здесь ничего сложного нет, нужно лишь держать в голове, что триггеры в MSSQL запускаются лишь опосля деяния (еще есть заместо (instead of), но это лишь в MSSQL2000).
Пример: предотвращение удаления клиента, если есть документы с его ролью (чтоб таковой триггер не конфликтовал с ограничением ссылочной целостности, в MSSQL нужно убрать foreign key с таблицы doc_title на client)
create trigger CLIENT_BEFORE_DELETE for CLIENT
before delete
as
begin
if (exists (select * from DOC_TITLE
where FROM_ID = OLD.CLIENT_ID))
then
exception EX_CLIENT_IN_DOC;
if (exists (select * from DOC_TITLE
where TO_ID = OLD.CLIENT_ID))
then
exception EX_CLIENT_IN_DOC;
end
^
Преобразуется в
create trigger CLIENT_AFTER_DELETE on CLIENT
for delete
as
if (exists (select d.CLIENT_ID from
DOC_TITLE dt, deleted d
where dt.FROM_ID = d.CLIENT_ID))
begin
—чтобы сообщение было видно на клиенте
raiserror (‘Существует запись в документе’, 16, 1)
—необходимо ручками откатить транзакцию
rollback transaction
end
if (exists (select d.CLIENT_ID from
DOC_TITLE dt, deleted d
where dt.TO_ID = d.CLIENT_ID))
begin
raiserror (‘Существует запись в документе’, 16, 1)
rollback transaction
end
go
Модификация сервера приложений.
тут основная часть переработки связана с переходом от IBX (InterBase Express) к ADO (activeX Data Object). Главные вещи, на которые следует направить внимание:
Реализация транзакций на клиенте – в IBX это отдельный компонент, в ADO таковая функциональность предоставляется способами TADOConnection. Еще маленькая рекомендация – аккуратненько подходите к выбору уровня изоляции транзакций (чем меньше уровень изоляции, тем резвее будет работать приложение).
Чтоб клиент работал без переделки с различными источниками данных, нужно, чтоб типы данных полей совпадали. к примеру, в IBX для numeric(15, 4) по дефлоту подставляется TFloatField, а в ADO – TBCDField. Это единственное отличие, которое мне повстречалось при переносе (но это не означает, что их совершенно нет). Неувязка отважилась ручной установкой данного типа поля в TCurrencyField.
Перевод sql-выражений из синтаксиса IB в MSSQL.
Отличия, связанные с различиями структуры БД. к примеру, если будет реализовано каскадное удаление при помощи ХП, то придется реализовывать эту логику снутри сервера, чтоб бросить клиента нетронутым.
В качестве примера приведем перевод одной из процедур сервера приложений:
//Описание того, что делает данная процедура, читайте в статье Игнатьева.
//Код, работающийс IBX
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;
ibtDoc.StartTransaction; //ibtDoc – компоненттранзакции
if FState = osInsert then
begin
if cdsTitle.ChangeCount > 0 then
cdsTitle.ApplyUpdates(-1);
if cdsBody.ChangeCount > 0 then
cdsBody.ApplyUpdates(-1);
end;
if FState = osUpdate then
begin
if cdsBody.ChangeCount > 0 then
cdsBody.ApplyUpdates(-1);
if cdsTitle.ChangeCount > 0 then
cdsTitle.ApplyUpdates(-1);
end;
Result := FLastUpdateErrors;
if Result = » then
ibtDoc.Commit
else
begin
ibtDoc.Rollback;
end;
finally
ibtDoc.Active := False; //DefaultAction = Rollback
unlock;
end;
end;
//Код, работающийс ADO
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;
adcDocs.BeginTrans; //очевидные транзакции задаются на уровне соединения
if FState = osInsert then //а не отдельным компонентом
begin
if cdsTitle.ChangeCount > 0 then
cdsTitle.ApplyUpdates(-1);
if cdsBody.ChangeCount > 0 then
cdsBody.ApplyUpdates(-1);
end;
if FState = osUpdate then
begin
if cdsBody.ChangeCount > 0 then
cdsBody.ApplyUpdates(-1);
if cdsTitle.ChangeCount > 0 then
cdsTitle.ApplyUpdates(-1);
end;
Result := FLastUpdateErrors;
if Result = » then
adcDocs.CommitTrans
else
begin
adcDocs.RollbackTrans;
end;
finally
unlock;
end;
end;
Заключение
Это всего только пример. В настоящих приложениях следует наиболее кропотливо продумывать перенос приложений. к примеру, переписывать лучше не весь сервер приложений, а лишь зависимый от источника данных код, вынося его в отдельные модули.
Данная статья не претендует на полноту освещения данного вопросца, также и создатель при изложении подходов для решения заморочек не претендует на роль «правды в крайней инстанции». тут был изложен только минимум сведений, нужный для решения поставленной задачки, также некие размышления, которые могут посодействовать при решении похожих заморочек.
Все вопросцы, замечания, исправления, дополнения направляйте на kapusto@mail.ru
Желаю выразить признательность Игнатьеву Роману, Павлу Шмакову за советы, критику и напористость.
]]>