СУБД Oracle

Приложение Б. Секреты мастерства

 
 

В этом приложении "рассматриваются три проблемы, связанные с проектированием, и предлагаются наши хитрые методы их решения. Это — мутирующие таблицы, проблема тысячелетия (проблема 2000-го года) и расширенный SQL.

 

Проблема мутирующих таблиц

Что мы имеем в виду, когда говорим о мутирующей таблицей? Этот термин вызывает в воображении часть базы данных, трансформирующуюся в страшного монстра, который выскакивает из экрана и нападает на вас. В действительности все еще хуже! Мутирующая таблица — это просто таблица, которая изменяется текущим предложением. С ней связан серьезный недостаток в функциональных возможностях триггеров Oracle.

Мы можем определить табличные триггеры для различных событий в базе данных. Эти события происходят или на уровне строки, или уровне предложения. Вспомним, что одно предложение может обрабатывать много строк. Например, при помощи предложения DELETE FROM MY_TAB можно обработать все строки таблицы MY_TAB. Следовательно, триггер BEFORE DELETE сработает один раз, а триггер BEFORE DELETE...FOR EACH ROW — один раз для каждой строки таблицы. Проблема мутирующих таблиц возникает, когда в триггере уровня строки производится попытка обратиться к таблице, которая изменяется или может изменяться тем же предложением.

Рассмотрим простой пример, в котором применяется вездесущая таблица EMP. Предположим, вы хотите ввести следующее правило: если начальник удаляется, то для всех его подчиненных значение столбца MANAGER устанавливается в NULL. Это отношение между служащим и его начальником является рефлекторным. Мы не можем ввести это отношение декларативно (ограничением PRIMARY KEY), потому что Oracle7 поддерживает только режимы RESTRICT (по умолчанию) и ON DELETE CASCADE. Поэтому мы разрабатываем третий режим, ON DELETE NULLIFY, который некоторые другие реляционные СУБД поддерживают как стандартный. Учтите, что каждый раз, когда мы не задаем (или не можем задать) ограничения для определения отношений, мы должны писать собственный код, вводящий все правила.

Давайте сначала возьмем очевидное решение и посмотрим, почему оно не работает. Пример, иллюстрирующий это решение, приведен ниже. Триггер emp_bdr не сработал из-за ошибки, связанной с мутирующей таблицей: он не может получить доступ к таблице, вызвавшей срабатывание триггера (в данном случае это таблица ЕМР).

CREATE TABLE emp ( empno  NUMBER(4) NOT NULL
                 , deptno NUMBER(4) NOT NULL
                 , ename  VARCHAR2(10)
                 , job    VARCHAR2(9)
                 , mgr    NUMBER (4)
                 , CONSTRAINT emp_pk PRIMARY KEY (empno));

INSERT INTO emp VALUES(111, 10, 'Big Boss', 'Manager' , NULL);
INSERT INTO emp VALUES(123, 11, 'Little Guy', 'Janitor', 111);

CREATE OR REPLACE TRIGGER emp_bdr BEFOR DELETE ON emp
FOR EACH ROW
BEGIN
  UPDATE emp SET mgr = NULL WHERE mgr = :old.empno;
END;
/

DELETE emp WHERE empno = 111;

/*Возникает следующая ошибка:
ERROR at line 1:
ORA-04091:таблица SCOTT.EMP мутирует, триггер/функция может ее не увидеть
ORA-06512:в строке 2
ORA-04088:ошибка во время выполнения триггера 'SCOTT.EMP_BDR'*/

Каковы же причины появления ошибки? У оператора SET и у операторов, которые работают со множеством строк, есть общее, причем довольно теоретическое, свойство. SQL не гарантирует порядок обработки строк, указанных в таком операторе. Из этого следует, что если известно, что таблица мутирует во время выполнения оператора SET, то любая ссылка на эту таблицу в триггере может даваться до или после того, как фактически произойдет изменение таблицы. Поскольку порядок, в котором вносятся изменения, не гарантирован, то не обязательно, что при каждом выполнении идентичных операторов с идентичными данными вы получите одни и те же результаты.

Как же преодолеть это ограничение и добиться того, что вы собрались сделать? Мы видели, как эту проблему пытались решить самыми разными способами — например, с помощью зеркальных таблиц или выделенного серверного процесса, который ждет сигнала на выполнение обновления, приводящего к ошибке. Эти решения очень сложны и опасны, поэтому мы предлагаем вам два простых метода выхода из данной ситуации.

Сначала рассмотрим метод, который крайне неэффективен и, следовательно, не рекомендован, но работает. Вы ждете, пока будет выполнено удаление, а затем просматриваете всю таблицу на предмет наличия строк, в которых столбец MGR содержит ссылку на несуществующего служащего. если такие строки есть, вы обнуляете в них значение столбца MGR. Как это реализовать, показано в ниже.

DROP TRIGGER emp_bdr;
CREATE OR REPLACE TRIGGER emp_ad AFTER DELETE ON emp
BEGIN
  UPDATE emp
  SET    mgr = NULL
  WHERE NOT EXISTS (SELECT NULL
        FROM emp emp2
       WHERE emp2.empno = emp.mgr);
END;
/

DELETE emp WHERE empno = 111;
SELECT * FROM emp;
>
>    EMPNO  DEPTNO ENAME     JOB           MGR
> -------- ------- --------- -------- --------
>      123      11 Litle Guy Janitor
>

ROLLBACK;

Несмотря на некоторую неэффективность, это решение обладает одним интересным свойством: оно устраняет предыдущие ошибочные ссылки, которые могли появиться вследствие выполнения программы в течение нескольких часов (или недель) со случайно отключенным триггером. При первом же удалении любого служащего запустится триггер, который обнулит качения столбца MGR, соответствующие несуществующему начальнику, для всех служащих, а не только для тех, начальники которых удалены в текущем операторе.

Такое "самоисправление" данных, на первый взгляд, может показаться очень хорошим делом, но оно влечет за собой нежелательные последствия. Каждый раз, когда удаляется один служащий, вся таблица EMP просматривается на предмет поиска служащих, работающих у несуществующего начальника. Для взятой в качестве примера таблицы с двумя строками это не важно, но для таблицы с несколькими тысячами строк производительность будет ужасно низкой. Мы должны каким-то образом ограничить поиск только теми строками, которые содержат ссылки на удаленного начальника, не создавая при этом проблему мутирующей таблицы.

Какие же еще методы можно использовать для разрешения этой проблемы? Второй (рекомендуемый) метод предусматривает умный трюк — сохранить удаленных служащих при помощи триггеров уровня строки и отложить поиск служащих, у которых удаленный служащий был начальником, до триггера AFTER уровня предложения. Для временного хранения удаленных служащих нужно использовать глобальную таблицу PL/SQL.

Что такое глобальная таблица PL/SQL? Пакеты PL/SQL могут содержать глобальные переменные. Они объявляются либо в пакете (если должны быть доступны извне), либо в теле пакета, но вне всех процедур и функций. В последнем случае их можно видеть только из данного пакета. Переменные обоих типов создаются при первом обращении к пакету (по любой причине) и остаются в памяти до тех пор, пока сеанс Oracle не разорвет соединение. PL/SQL версии 2.3 позволяет очищать таблицы, но для любого узла, на котором используется Oracle версий 7.2 и меньше, постепенное увеличение памяти для глобальных переменных пакетов может стать проблемой, которую необходимо учитывать при проектировании. В качестве одной из мер очищаемой таблице можно присвоить пустую таблицу такого же типа.

Код для реализации решения с помощью глобальных переменных пакета приведен в примере Б.1. Этот код состоит из пакета с тремя процедурами и трех триггеров на таблице EMP, каждый из которых запускается разным событием и вызывает одну из пакетных процедур. Одна переменная пакета является таблицей для удаленных EMPNO, а другая — индексом для нее. Триггер BEFORE DELETE уровня оператора просто вызывает процедуру, которая устанавливает этот индекс в нуль, т.е. логически очищает таблицу. Триггер BEFORE DELETE уровня строки передает первичный ключ (EMPNO) удаляемой записи, и он добавляется в конец таблицы. Триггер AFTER DELETE уровня оператора проходит по таблице и обнуляет столбец MGR для всех служащих, у которых удаленный служащий был начальником.

Примечание

В примере Б.1 используются вызовы пакетных процедур внутри триггера, чтобы глобальные переменные пакета могли оставаться локальными для процедуры. Этот же прием можно реализовать с меньшими затратами, для чего нужно просто поместить глобальные переменные пакета в сам пакет (а не в тело пакета) и манипулировать ими прямо из триггеров. Как вы, наверное, догадываетесь (если прочитали остальные главы книги), мы предпочитаем исключительно инкапсулированный подход.

Пример Б.1. Как избежать появления мутирующих таблиц путем откладывания действия до уровня операторов

DROP TRIGGER emp_ad;

CREATE OR REPLACE PACKAGE pk_emp_mgr AS
  PROCEDURE clear_count;
  PROCEDURE add_mgr (p_empno IN emp.empno%TYPE);
  PROCEDURE nullify_reporting_emps;
END pk_emp_mgr;
/
CREATE OR REPLACE PACKAGE BODY pk_emp_mgr AS
  TYPE tab empno_type IS TABLE OF emp.empno%TYPE INDEX BY BINARY_INTEGER;
  g_empno     tab_empno_type;
  g empno_ind BINARY_INTEGER;

PROCEDURE clear_count IS
  BEGIN g_empno_ind := 0;
END clear count;

PROCEDURE add_mgr(p_empno IN emp.empno%TYPE) IS
BEGIN
  g_empno_ind := g_empno_ind + 1;
  g_empno(g_empno_ind) := p_empno;
END add_mgr;

PROCEDURE nullify_reporting_emps IS
l_empno emp.empno%TYPE
BEGIN
  FOR I IN 1..g_empno_ind LOOP
    l_empno := g_empno(g_empno_ind);
    UPDATE emp SET mgr = NULL WHERE mgr = l_empno;
  END LOOP;
END nullify_reporting_emps;
END pk_emp_mgr;
/

CREATE TRIGGER emp_bd BEFOR DELETE ON emp
BEGIN
  pk_emp_mgr.clear_count;
END;
/

CREATE TRIGGER emp_bdr BEFOR DELETE ON emp
FOR EACH ROW
BEGIN
  pk_emp_mgr.add_mgr(:old.empno);
END;
/

CREATE TRIGGER emp_ad AFTER DELETE ON emp
FOR EACH ROW
BEGIN
  pk_emp_mgr.nullify_reporting_emps;
END;
/

DELETE emp WHERE empno = 111;
SELECT * FROM emp;
>    EMPNO  DEPTNO ENAME     JOB           MGR
> -------- ------- --------- -------- --------
>      123      11 Litle Guy Janitor
>
ROLLBACK;

Хотя все это может показаться набором большого числа несвязанных фрагментов кода, это проверенный и эффективный метод. Однако здесь есть небольшой нюанс.

Что произойдет, если строки из таблицы EMP удаляются не в результате выдачи пользователем предложения DELETE для этой таблицы, а вследствие каскадного удаления из главной таблицы? Давайте введем в наш пример столь же вездесущую таблицу DEPT, закрепим всех служащих за соответствующими подразделениями, а затем удалим подразделение, в котором работает начальник (Big Boss). В результате Big Boss будет удален и все его подчиненные лишаться начальника. Скрипт для проверки этого механизма показан в примере Б.2.

Пример Б.2. Проблема мутирующей таблицы возникла вновь удаления из главной таблицы из-за каскадного удаления главной таблицы

CREATE TABLE dept ( deptno NUMBER(4) NOT NULL
                  , dname VARCHAR2(20) NOT NULL
                  , loc VARCHAR2(20) NOT NULL
                  , CONSTRAINT dept_pk PRIMARY KEY (deptno));

INSERT INTO dept VALUES(10, 'DIRECT SALES', 'BOSTON');
INSERT INTO dept VALUES(11, 'CLEANING', 'NEWARK');
ALTER TABLE emp ADD CONSTRAINT emp_fkl FOREIGN KEY (deptno)
REFERENCES dept(deptno) ON DELETE CASCADE;

DELETE FROM dept WHERE deptno = 10;

>ERROR at line 1:
>ORA-04091:таблица SCOTT.EMP мутирует, триггер/функция может ее не увидеть
>ORA-06512:в "SCOTT.PK_EMP_MGR", строка 19
>ORA-06512:в строке 2
>ORA-04088;ошибка во время выполнения триггера 'SCOTT.EMP_AD'

Удивительно, но проблема мутирующей таблицы возникла вновь, однако теперь о ней сообщается в триггере уровня оператора. Причина в том, что Oracle рассматривает таблицу как мутирующую (находящуюся в несогласованном состоянии) во время выполнения рекурсивного SQL. Рекурсивный SQL — это любое SQL-предложение, которое генерируется самой Oracle при выполнении другого SQL-предложения. В данном случае Oracle сгенерировала предложение DELETE для таблицы EMP при выполнении операции ON DELETE CASCADE.

Таким образом, мы выявили еще одну проблему, поэтому давайте решать и ее. Нам нужно создать для таблицы DEPT такие же триггеры уровня операторов, как и для таблицы EMP, чтобы они срабатывали и при удалении в этой таблице, а также предотвратить срабатывание кода уровня операторов для таблицы EMP, если он вызывается в результате рекурсивного SQL. Решение приведено в примере Б.3. В заголовок пакета включена глобальная переменная (in_cascade_ind), которая будет сообщать, находимся мы в режиме ON DELETE CASCADE или нет. Триггеры уровня операторов для EMP изменены таким образом, чтобы они не срабатывали, если эта переменная пакета установлена. Также определены триггеры BEFORE DELETE и AFTER DELETE для таблицы DEPT, назначение которых — установка и сброс глобальной переменной пакета, а также запуск процедуры nullify_reporting_emps. Все новые и исправленные фрагменты в примере Б.3 выделены жирным шрифтом.

Пример Б.3. Решение, в котором учтены каскадные операции

CREATE OR REPLACE PACKAGE pk_emp_mgr AS
  in_cascade_ind BOOLEAN := FALSE;
  
PROCEDURE clear_count;
  PROCEDURE add_mgr (p_empno IN emp.empno%TYPE);
  PROCEDURE nullify_reporting_emps;
END pk_emp_mgr;
/

CREATE OR REPLACE TRIGGER emp_bd BEFOR DELETE ON emp
BEGIN
  IF NOT pk_emp_mgr.in_cascade_ind THEN
  
pk_emp_mgr.clear_count;
  END IF;
END;
/

CREATE OR REPLACE TRIGGER emp_ad AFTER DELETE ON emp
BEGIN
  IF NOT pk_emp_mgr.in_cascade_ind THEN
  
pk_emp_mgr.nullify_reporting_emps;
  END IF;
END;
/

CREATE OR REPLACE TRIGGER dept_bd BEFOR DELETE ON dept
BEGIN
  pk_emp_mgr.in_cascade_ind := TRUE;
  pk_emp_mgr.clear_count;
END;
/

CREATE OR REPLACE TRIGGER dept_ad AFTER DELETE ON dept
BEGIN
  pk_emp_mgr.nullify_reporting_emps ;
  pk_emp_mgr.in_cascade_ind := FALSE;
END;

DELETE FROM dept WHERE deptno = 10;
SELECT * FROM emp;

>    EMPNO  DEPTNO ENAME     JOB           MGR
> -------- ------- --------- -------- --------
>      123      11 Litle Guy Janitor
>
ROLLBACK;

DELETE emp WHERE empno = 111;
SELECT * FROM emp;
/* мы должны получить тот же результат */
ROLLBACK;

Мы победили мутирующую таблицу! Теперь у нас есть всеобъемлющее решение, позволяющее обойти эту проблему. Наш пример относится к операции DELETE, но мы можем, конечно, распространить продемонстрированный в нем метод и на операцию UPDATE. Этот метод дает надежное и эффективное решение проблемы мутирующих таблиц, не оказывая при этом существенного влияния на производительность.

К сведению

Вы также можете столкнуться с недостатком триггеров в Oracle, который связан с ограничивающими таблицами. Ограничивающая таблица — это мутирующая таблица наоборот.

При обработке оператора делается ссылка из подчиненной таблицы на главную, позволяющая соблюсти ограничение ссылочной целостности, и теперь SQL-оператор в триггере пытается модифицировать главную таблицу. Решение этой проблемы такое же, как проблемы мутирующих таблиц. Сохраните в глобальных переменных PL/SQL необходимые изменения, а затем внесите их при помощи триггера AFTER уровня оператора.

Рекурсивный SQL

Несмотря на то, что это более важно для управления производительностью Oracle, чем для понимания процесса проектирования, опытные администраторы БД Oracle должны обратить внимание на то, что с появлением Oracle7 определение рекурсивного SQL очень изменилось. В предыдущих версиях, где не было триггеров и хранимых процедур, появление рекурсивного SQL всегда было результатом того, что сама Oracle выдавала SQL, например, для выполнения операций по управлению пространством, когда таблице требовался другой экстент.

В Oracle7 есть три совершенно разных типа рекурсивного SQL:

• SQL-операторы традиционного типа, которые Oracle выдает для собственных управленческих целей, они всегда разбираются как принадлежащие пользователю SYS;

• пользовательский SQL, выполняемый из PL/SQL;

• SQL, который Oracle генерирует для поддержки декларативных ограничений. Этот тип встречается на удивление редко, так как для реализации таких ограничений в большинстве случаев производятся прямые вызовы требуемых функций внутри сервера. Исключение — ON DELETE CASCADE.

 

Проблема тысячелетия: перспектива для Oracle

Сегодня в эксплуатации находится очень много компьютерных систем, которые представляют собой бомбы с часовым механизмом, ожидающие сигнала на взрыв. Многие из этих бомб взорвутся, когда человечество будет вовсю праздновать наступление нового века. Фактически большинство компьютеров начнет сбоить гораздо раньше — так скоро, что ни о каком спокойствии не может быть и речи. Мы говорим о так называемой проблеме тысячелетия, или проблеме 2000-го года.

 

Предыстория

Что такое проблема тысячелетия? Когда проектировались и писались старые системы, никто не побеспокоился о том, чтобы для представления года использовалось минимум три (а лучше четыре) значащие цифры. Чтобы проиллюстрировать этот вопрос, давайте рассмотрим, как чаще всего представляют дату. Обычно мы пишем дату в таком виде: 01/01/98, 01 JAN 98, 1st January 98 и т.п. Будем ли мы писать дату как 01/01/00 или 01/01/000, когда наступит 2000-й год? Может быть, но выглядеть это будет странно, поэтому, скорее всего, мы будем писать дату как 01/01/2000. Во многих компьютерных системах в полях даты на экранах век не отображается, хуже того, они не позволяют вводить век при вводе дат. К тому же, как правило, в них подразумевается, что все даты относятся к двадцатому веку, и когда пользователь вводит 01/01/98, система неявно считает, что он имеет в виду 01/01/1998. Чтобы довести наши рассуждения до логического конца, скажем, что когда пользователь вводит 01/01/00, система считает, что он имеет в виду 01/01/1900 — а как раз это он (почти во всех случаях) и не имеет в виду. Вот в чем суть проблемы.

Одновременный выход из строя всех компьютеров в мире 1 января 2000 года — это хорошая тема для научной фантастики, но это все-таки, скорее, фантастика, а не реалии будущего. Многим системам уже сейчас приходится обрабатывать такие понятия, как срок действия, и хранить даты, отстоящие от сегодняшней на несколько лет вперед. Например, в приложениях, которые обрабатывают закладные на недвижимость или пенсии, уже приходилось учитывать этот момент. Проблема 2000-го года страшна для тех, кто еще о ней не задумывался и ничего не предпринимал, причем некоторые пострадают раньше других.

Ранее мы описали проблемы, которые могут возникнуть в прикладном ПО. Кроме них, несомненно, будут и проблемы с аппаратными средствами, операционными системами и системным ПО (включая большинство систем управления базами данных).

У всех компьютеров есть внутренние часы, выдающие текущую дату и время. Эти данные хранятся в аппаратных средствах обычно во внутреннем формате. В более старых машинах, страдающих от дефицита оперативной памяти, дата и время могут храниться в максимально компактной форме. Один из методов сжатия этих данных предполагал исключение века. Если вы работаете на старой аппаратуре, единственный выход — поговорить с ее поставщиком или провести эксперимент, установив какую-нибудь дату после 2000-го года.

В большинстве операционных систем есть вызовы функций, которые позволяют приложениям извлекать системную дату из аппаратуры. Операционная система должна преобразовать эту дату из внутреннего формата в формат, приемлемый для приложения. Это еще один потенциальный источник ошибок, особенно в старых операционных системах.

Системы управления базами данных хранят даты во внутренних структурах, чтобы пользователи могли обращаться к ним или строить на них запросы. Опять-таки для хранения обычно применяется внутренний формат, и между БД и пользовательским приложением производится преобразование, контролируемое СУБД. Нет сомнения в том, что некоторые из этих внутренних форматов и процессов преобразования дадут сбой, когда дело дойдет до поддержки дат в следующем столетии.

Каковы последствия для пользователей таких систем, сидящих на этих бомбах замедленного действия? Они могут быть катастрофическими и очень дорогостоящими! Давайте рассмотрим в качестве простого примера систему начисления оплаты за время работы консультантов. В конце периода система определяет, сколько дней провел консультант у клиента, и умножает это число на дневную ставку. Предположим, что консультант Смит работал у клиента "Джонс Индастриз" с 29 декабря 1999 г. по 2 февраля 2000 г. Система начисления оплаты неверно истолковывает вторую дату, введенную как 02-02-00, и считает, что это 2 февраля 1900 г. В процессе расчета она вычитает первую дату из второй, получает большое отрицательное число и умножает его на ставку консультанта. В итоге выходит еще большее отрицательное число.

В результате вместо отправки скромного счета за работу консультанта (консультанты никогда не посылают маленькие счета) мы шлем запрос на большой кредит, которого, без сомнения, заслуживает наша маленькая консультационная фирма. Вы считаете этот пример надуманным? Может быть, но он ясно показывает, какие неприятности могут произойти.

Теперь, когда понятно, какие проблемы могут возникнуть, если проблемой тысячелетия не заниматься, рассмотрим последствия для систем, в которых используется база данных Oracle. Как мы говорили в главе 7, в Oracle есть тип данных DATE, предназначенный для хранения даты и времени, причем время в этом случае хранится с точностью до секунды. Этот тип поддерживает даты в диапазоне с 1 января 4712 г. до н.э. до 31 декабря 4712 г. н.э. Во внутренних структурах Oracle хранит дату в упакованном формате как семибайтное целое, где на век, год, месяц, день, час, минуту и секунду выделяется по одному байту. Имея этот внутренний формат, Oracle неявно записывает век во всех датах, хранящихся в столбцах типа DATE.

В символьном или числовом формате даты в таблицах Oracle хранятся крайне редко, поскольку нет никаких веских причин избегать формат DATE. Единственное исключение из этого правила — ситуация, когда записывается только год (например, год регистрации автомобиля). Этот год может храниться в числовом или символьном столбце с двумя или четырьмя значащими цифрами.

Главная проблема с ПО, написанным для базы данных Oracle, состоит в конвертировании дат из внутреннего формата во внешний (отображаемый) и наоборот. В Oracle имеется механизм масок, позволяющий программисту задавать требуемый внешний формат даты. Это достигается путем "склеивания" набора компонентов. Вот некоторые примеры масок; DD Month YYYY, DD-MON-YY, DD/MM/YY, MM/DD/YY. Проблемы непременно возникнут, если в компоненте, обозначающем год, будет пропущен век, потому что в этом случае он считается равным 19. Специалисты Oracle были достаточно прозорливы, чтобы предвидеть эту проблему еще в 1993 г., когда была впервые выпущена Огас1е7, и ввели маску RR. RR предполагает двадцатый век, если год больше или равен 50, и двадцать первый век, если год меньше 50. Так, TO_DATE('04-JAN-05', 'DD-MON-RR') дает дату в 2005-м году, а TO_DATE('01-MAR-68'. 'DD-MON-RR') - дату в 1968-м году.

К сожалению, проектировщики и программисты не особенно быстро проникаются важностью этого новшества и использование YY в масках дат продолжает процветать. Вероятно, 2000-й год все еще кажется им далеким будущим, особенно с точки зрения компьютерной индустрии. Даже в тех организациях, которые были достаточно дисциплинированы и узаконили использование маски RR, где это возможно, или ввели стандарт YYYY, этой практики обычно придерживаются только в новых разработках или при переработке существующего кода. В эксплуатируемых Oracle-системах все еще остается много строк кода, где для дат применяется маска YY или вообще никаких масок нет.

Руководители только сейчас начинают осознавать всю глубину проблемы тысячелетия и потенциальную стоимость ее решения. Задача обнаружения всего исходного кода (в каком бы то ни было формате) и проверки каждой строки на предмет наличия возможных проблем колоссальна с точки зрения логистики и требует огромных трудозатрат. Некоторый исходный код неизбежно окажется не в нужном месте, а единственным выходом будет его переписывание.

Что нужно делать, если ваш начальник поставит перед вами, проектировщиком, задачу проанализировать проблему и обеспечить плавный переход в следующее тысячелетие? Прежде всего, не следует недооценивать масштабность этой задачи. Но как уменьшить потенциально колоссальные затраты на поддержку работоспособности существующих Oracle-систем при подходе к рубежу тысячелетий? Вот некоторые мысли на этот счет.

 

Можно ли решить проблему непосредственно в базе данных?

Ответ на этот вопрос таков: решить эту проблему в базе данных без корректировки прикладного кода можно лишь в очень ограниченной степени.

Представим экранную форму, в которую пользователь вводит дату в типичное для Oracle девятисимвольное поле (без века). Возьмем эту экранную форму из промышленной системы, перенесем на тестовую арену и поэкспериментируем с ней. Вводим в поле дату 31-DEC-00, выполняем фиксацию и запрашиваем эту запись — нам выдается 31-DEC-00. Но если мы воспользуемся SQL*Plus и запросим поле даты с веком, то увидим 31-DEC-1900. В результате мы приходим к выводу, что в этой экранной форме для века не используется маска RR.

Допустим, что вводимая дата — срок действия. Мы можем предположить, что она никогда не будет в прошлом. Учитывая это, можно написать триггер, который проверяет дату и, если она в прошлом, конвертирует ее в дату в 21-м веке. Вот код этого триггера:

CREATE OR REPLACE TRIGGER policies_biur
  BEFOR INSERT OR UPDATE OF expiry_d ON policies
  FOR EACH ROW
BEGIN
  IF :new.expiry_d < TRUNC(SYSDATE) AND :new:expiry_d IS NOT NULL
  THEN
    :new.expiry_d := add_months(new.expiry_d, 1200);
  END IF;
END;
/

Работает такой триггер нормально, но этот подход имеет ряд недостатков:

• Правильно ли предположение о том, что срок действия полиса никогда не будет датой в прошлом? Загружаются ли когда-нибудь сведения о старых полисах? Если да, то они вновь могут стать действительными и мы попадем в неловкое положение, начисляя премии давно умершим владельцам полисов.

• Изменив значение срока действия между экранной формой и базой данных, мы создали между ними противоречие. Экранная форма считает, что срок действия полиса истекает 31-DEC-1900, тогда как база данных полагает, что он заканчивается 31-DEC-2000. Если мы изменим в экранной форме другое поле, то это вызовет обновление без предварительного повторного запроса и, вероятно, мы получим уведомление от приложения о том, что эту запись изменил другой пользователь. В действительности никакого изменения не было, но экранная форма выявила противоречие и предположила, что в этом виноват какой-то другой пользователь.

• Мы смогли написать триггер-"регулятор" для этого столбца, поскольку у нас было строгое правило, на котором мы могли построить свою логику (срок действия не может находиться в прошлом). Однако во многих других случаях подобные допущения делать нельзя. Возьмем, например, дату рождения. В первом году столетия дата 01-DEC-00 может относиться или к очень молодому, или к очень старому человеку. Вероятно, мы могли бы просмотреть другие поля, например поле номера социального страхования, и определить, что у ребенка его еще нет, но это далеко не лучший подход.

• Данные, вводимые пользователем, — это лишь часть проблемы. В коде экранной формы, скорее всего, имеется условная логика, построенная на значениях дат. Могут быть даты, жестко закодированные в этой логике, но это встречается редко и вряд ли приведет к неоднозначности при определении века. В большинстве случаев вы встретите нечто вроде:

IF :creation_date > TO_DATE('0l-DEC-90', 'DD-MON-YY')
   THEN process_new_policy;
   ELSE process_old_policy;
END IF;

Более вероятно, что программа содержит локальные переменные, в которых хранятся представления значений дат, выбираемых из базы данных. Пример Б.4 содержит фрагменты кода из действующей системы, в которых может возникнуть проблема тысячелетия.

Пример Б.4. Проблемные фрагменты кода

инициализация
:b1.s_start_time := TO_DATE(TO_CHAR(SYSDATE, 'DD-MON-YY HH:MI:SS'),
                                             'DD-MON-YY HH:MI:SS');
:global.stage_date := TO_CHAR(SYSDATE, 'DD-MON-YY');
:b1.generation := TO_NUMBER(TO_CHAR(:b1.application_date, 'YY'))

условная логика
IF :org2.deleted_date > TO_DATE(TO_CHAR(SYSDATE, 'DD-MON-YY')) THEN

предикаты
... AND TO_NUMBER(TO_CHAR(csh_date_started, 'YY')) < 85
... ORDER BY TO_DATE (hci_month, 'MON-YY' ) DESC
... AND hist_date < TRUNC(TO_DATE(TO_CHAR(SYSDATE, 'DD-MON-YY')), 'YEAR')

в курсорах
CURSOR count_dates IS
SELECT COUNT(DISTINCT(TO_CHAR(hom_offer_of_cover_date, 'DD-MON-YY'))),
       COUNT(DISTINCT(TO_CHAR(hom_date_issued, 'DD-MON-YY'))),
       COUNT(DISTINCT(TO_CHAR(hom_date_cp_notice, 'DD-MON-YY'))),
       COUNT(DISTINCT(TO_CHAR(hom_date_final_cert, 'DD-MON-YY')))
  FROM homes
 WHERE (:b1.service_type = 'HB3'
   AND hom_hb3_service_request_no = :b1.service_request_no);

проблемы с пересечением границ
ELSIF TO_CHAR(launch_datetime, 'DD-MON-YY HH24:MI:SS') <
      TO_CHAR(SYSDATE, 'DD-MON-YY HH24:MI:SS') THEN

использование в ключах -- проблемы при упорядочении
... ORDER BY claim_no; -- claim_no -- YY/nnnnn

Эта проблема касается не только экранно-ориентированных программ, и следует учитывать ее влияние на отчеты, пакетные программы и код, резидентный в базе данных. Отчеты не должны создавать слишком серьезную проблему, потому что они обычно не обновляют базу данных. Кроме того, если они показывают только двузначный год, пользователь, скорее всего, сможет истолковать его правильно в контексте отчета. Однако некоторые регулярно запускающиеся отчеты могут обновлять таблицу, в которой регистрируется последнее время запуска отчета, а оно наверняка будет включать поле даты. Кроме того, может оказаться, что столбец даты, помещенный из базы данных в поле отчета, не содержащее век, используется в условии последующего запроса. В отчетах следует также внимательно следить за тем, как осуществляется обработка параметров, которые являются датами.

В пакетных программах проблемы обычно возникают там, где значения столбцов типа DATA помещаются в символьные переменные без века, а затем записываются опять в столбцы типа DATA базы данных или используются в условиях SQL-предложений. Увеличить размер этих переменных на два символа и заменить маски YY на YYYY довольно просто.

Таким образом, вряд ли возможно избежать необходимости добывать старый прикладной код Oracle-систем и копаться в нем. Конечно, может быть, кто-то только этого и ждет, чтобы переписать массу этого кода (и модифицировать его, например, для применения ГПИ). Однако можно ли назвать разумным предложение переписать все существующие эксплуатируемые системы? К сожалению, у нас может не быть иного выбора, если мы не найдем весь исходный код.

Однако оставим пока вопрос о тотальном переписывании и посмотрим, как с наименьшими затратами выполнить поиск и исправление проблемных фрагментов кода.

 

Поиск начинается

Как найти сегменты кода, которые в новом столетии могут преподнести проблемы? Прежде всего, необходимо поместить файлы исходного кода в общий каталог (или создать отдельный каталог для файлов каждого типа). Затем требуется выбрать средство поиска. В ОС Unix есть богатый набор средств и утилит для поиска последовательностей символов в файлах (назовем лишь три из них — grep, sed и awk).

Что же мы ищем? Сначала можно провести поиск всех строк, содержащих символы "YY". Число ненужных совпадений можно сократить, если искать символы "Y" только в строках, заключенных в одинарные кавычки (но при этом в некоторых комментариях может остаться Y). Поиск всех экземпляров "YY", не входящих в строку "YYYY", отсеет еще некоторое число ненужных совпадений, но не проскользнут ли в эту сеть нужные нам экземпляры? Можно расширить критерии поиска, включив экземпляр типа .-...-.., где точка является метасимволом, обозначающим один любой символ. К сожалению, в SQL очень слабая типизация данных, и он позволяет конвертировать тип DATE в другие типы и наоборот, не задавая ни маску, ни конвертирующую функцию. Такие случаи найти простыми средствами поиска трудно.

В некоторых элементах кода могут возникать проблемы с датами, не имеющие связи с Oracle. Например, мы можем искать файл операционной системы по дате его создания. Часть нашего приложения может быть shell-скриптом Unix, в котором используется утилита date. В этом случае нам придется искать строки, содержащие последовательность "%у". Таким образом, без глубокого понимания того, как работает приложение, мы никогда не найдем все эти вещи до тех пор, пока что-нибудь не начнет действовать неправильно. Это одна из причин, по которой тщательное тестирование имеет очень важное значение.

Если весь исходный код хранится в текстовых файлах, то в результате поиска будут найдены отдельные строки кода. Их можно проанализировать и некоторые проблемы устранить на месте. Поиск в двоичном файле вряд ли даст понятные результаты, но он все равно полезен, поскольку даст возможность установить, содержит ли SQL, встроенный в приложение, проблемные участки, о которых мы говорили. В конце поиска у нас будет список "рисковых" файлов исходного кода, требующих внимания, и мы получим общее представление о масштабе проблемы. После этого нужно лишь закатать рукава и устранить ее!

 

Устраняем проблему

К сожалению, каждый случай придется рассматривать отдельно. Мы не можем слепо заменить все экземпляры YY на RR. Сначала следует убедиться в том, правильно ли предположение, что любая дата, в которой год меньше 50, относится к двадцать первому веку. Конечно, нельзя заменять все экземпляры YY на YYYY, не проанализировав последствия от увеличения размеров для операций присваивания значений переменным или полям экранной формы. Если размер всех полей дат в экранной форме увеличится, то, возможно, придется полностью ее переделать.

 

Автоматизация процесса исправления кода

Если масштабы проблемы значительны, то следует подумать об автоматизации, особенно если в коде встречаются повторяющиеся структуры. Вновь упомянем утилиты Unix, подобные awk, идеально подходящие для этой цели. Если в операционной системе утилит для манипулирования строками нет, можно написать программу на 3GL (например, на С или Коболе), которая читает файл или набор файлов, выполняет ряд четко определенных изменений и записывает файл. Вот некоторые правила, которых необходимо придерживаться, выполняя автоматизацию процесса исправления кода:

• Перед началом работы сделайте резервную копию всего исходного кода.

• Перед проведением операций над всем каталогом с исходным кодом протестируйте свои действия на одном файле.

• В первую очередь займитесь повторяющимися проблемами, которые относительно легко устранить.

• Периодически прогоняйте утилиту поиска, чтобы закрепить результаты.

• Убедитесь в том, что любой автоматизированный метод предупреждает вас о потенциальных опасных последствиях и обязательно следуйте предупреждениям. Например, если в модуле Oracle Forms мы заменяем строчку

global.cur_date := TO_CHAR(SYSDATE, 'DD-MON-YY')

на

global.cur_date := TO_CHAR(SYSDATE, 'DD-MOM-YYYY')

то должны проверить, где еще используется переменная globat.cur_date. Причем эта проверка не обязательно будет ограничена одной формой, и придется провести поиск и в других исходных файлах. Если обнаружится, что эта переменная используется, например, в таком присваивании:

:b1.cur_date := :global.cur_date

то необходимо посмотреть, как объявлена переменная :b1.cur_date, и обеспечить, чтобы в нее можно было поместить более длинное значение. После этого мы должны проверить формы на предмет наличия ссылок на :b1.cur_date и т.д.

 

Тестирование

Хорошая стратегия тестирования — ключ к успеху всего мероприятия. Если вам удастся убедиться, что все будет по высшему классу, то в новогоднюю ночь 1999 г. вы сможете спать (или веселиться) в полной уверенности, что ничего не произойдет. Протестируйте все модули, выявленные на этапе поиска, с использованием даты после 1 января 2000 г., чтобы посмотреть, что происходит, если код не изменять. Это даст возможность контролировать последствия проблемы и уверенность при повторном тестировании после исправления в том, что исправление было необходимым и эффективным.

Для каждого изменяемого исходного модуля выполните полное автономное тестирование. При этом следуйте плану автономного тестирования модуля (если он есть) и, конечно, добавьте к этому плану специальные тесты для проверки дат в 2000 г.

Затем перезапустите систему и набор интеграционных тестов и также добавьте тесты для проверки дат.

Настройте специальный экземпляр Oracle для тестирования проблемы тысячелетия. Использование отдельного экземпляра позволит для выбираемых с сервера данных установить фиксированное значение даты (с помощью параметра инициализации Oracle FIX_DATE; он будет влиять только на дату Oracle — SYSDATE), Приложения клиент/сервер могут применять локальные часы на клиенте, и для получения даты вам придется проверять вызовы операционной системы. В Oracle Forms существует различие между переменными $$DATE$$ и $$DBDATE$$: первая — это локальная дата операционной системы, а вторая — дата базы данных сервера.

Как мы упоминали выше, в некоторых элементах кода могут использоваться даты, не относящиеся к сфере действия Oracle (например, даты создания файлов). Самый безопасный механизм тестирования этих дат — использовать выделенную машину с переустановленной датой, хотя это не всегда удобно.

 

Универсальное решение

Было бы прекрасно найти действительно универсальное решение, но, скорее всего, это несбыточная мечта. Если вы сможете изобрести такое решение, то наверняка заработаете кучу денег, когда начнется паника, а начальство высунет голову из песка и поймет, что, сколько бы тел оно не бросило на эту амбразуру, момент будет вот-вот упущен! *

Сейчас на рынке появляются средства, которые якобы позволяют автоматизировать процесс адаптации к 2000-му году. Однако у нас имеются сомнения на этот счет. Кроме того, необходимо еще узнать, рассчитаны ли какие-либо из них на Oracle.

 

Выводы

Никогда не рано начинать планирование и исследование. Попробуйте оценить масштабы проблемы в своих приложениях и, если у вас используются приобретенные продукты, поговорите с их поставщиками. Работайте методично, выявляйте проблемные участки, исправляйте их и тестируйте. Проверьте, как работает исправленный код, чтобы обеспечить себе спокойный сон в следующем столетии.

 

Обеспечение расширяемости

Как мы уже неоднократно говорили, проектирование заключается в поиске допустимого способа удовлетворения функциональных требований имеющимися средствами. Мы решили поговорить о расширенном SQL, поскольку видели ряд проектов, в которых использовались очень дорогостоящие методы удовлетворения "простого" требования о том, что пользователь должны иметь возможность как-то управлять действиями своей системы. Ясно, что такие возможности необходимо проектировать заранее. Наш опыт говорит, что попытки добавить их потом могут быть исключительно трудными в реализации. Главная сложность здесь — найти все зависимости.

 

Когда рассматривать возможность расширяемости

Многие требования по настраиваемости системы под требования пользователей легко реализовать с помощью обычных методов проектирования и компоновки. Если цены могут изменяться, то мы сохраняем их в таблице PRICES. Если уровни скидок могут изменяться, то мы заносим их в таблицу DISCOUNT_LEVELS, и т.д. Как мы отмечали в главе 3, выявление таких случаев должно быть частью аналитической работы, а ее результаты должны отражаться в модели "сущность-отношение".

А что если отдел маркетинга хочет, во-первых, иметь возможность автоматически применять скидки, а, во-вторых, изменять установленные правила с уведомлением за сутки? Условие об уведомлении за сутки в действительности будет результатом неимоверных усилий с вашей стороны — ведь сначала пользователи захотят, чтобы правила можно было изменять по требованию, т.е. моментально.

Всякий раз, когда вам говорят, что изменению подлежит не правило, а значение элемента в правиле, можно рассматривать возможность расширяемости. Если затем вам скажут, что о будущем правиле в данный момент ничего не известно, то вы, конечно, не сможете реализовать его прямо сейчас. Вы либо обеспечиваете расширяемость, либо должны изъять это требование. Таким образом, мы пришли к самой сути проблемы: необходимо определить, не является ли данное требование настолько неопределенным, что вы не можете гарантировать его выполнение.

Представьте, что вы потратили два-три года на проектирование расширяемости системы скидок и в итоге обнаружили, что достаточную расширяемость не обеспечили — например, потому, что не могли предполагать, что кто-то захочет использовать первые три-четыре цифры номера телефона покупателя в правиле установления скидки. Разве может что-либо принести большее разочарование?

Для простоты изложения мы проигнорируем целый ряд вопросов, которые нужно решать в реальной жизни (и которые часто игнорируются и там), например:

• Кто отвечает за проверку влияния предлагаемого изменения на систему?

• Кто отвечает за тестирование изменения на предмет того, работает ли оно так, как ожидалось?

• Кто отвечает за устранение последствий ошибочно внесенных изменений?

Этой теме можно посвятить целую книгу и, вероятно, когда-нибудь мы так и сделаем, но на данный момент мы лишь хотим завершить свою книгу небольшим разделом о методах, позволяющих обеспечить свойство расширяемости в Oracle-приложениях, и рассказать о некоторых связанных с этим проблемах.

 

Типы расширяемости

Когда мы имеем дело с просьбами обеспечить расширяемость, то различаем два типа расширяемости (пользователи, а во многих случаях и обслуживающий систему персонал часто такого различия не проводят). Это:

• алгоритмическая расширяемость, где необходимы новые правила обработки, дополняющие или заменяющие существующие правила;

• схемная расширяемость, где требуются новые атрибуты для существующих сущностей или, что сложнее, совершенно новые сущности, связанные отношениями с существующими сущностями.

Схемная расширяемость всегда сопровождается алгоритмической, поскольку существующий код вряд ли будет ссылаться на объекты, которые отсутствовали в момент его создания, и нет никакого смысла вводить новые объекты данных, если никакая логика не будет на них ссылаться. Кроме того, схемная расширяемость всегда управляется данными, т.е. ее основой является хранение в базе данных определений новых элементов данных, которые затем выбираются и используются алгоритмами, генерирующими динамический SQL.

В среде Oracle существует три основных подхода к предоставлению пользователям возможности алгоритмической расширяемости: управляемый данными, управляемый представлениями и управляемый процедурами или функциями. В следующих разделах мы рассмотрим эти методы и сравним их с традиционным подходом.

 

Расширяемость, управляемая данными

В большинстве знакомых нам проектов, где делалась попытка обеспечить алгоритмическую расширяемость, использовался метод, управляемый, данными. В примере Б.5 показано определение таблицы, в котором реализовано некоторое правило, управляемое данными.

Пример Б.5. Определение таблицы, позволяющее реализовать части правила или формулы для расчета

CREATE TABLE rule_elements
  ( rule# NUMBER
    CONSTRAINT rule_element_rule# REFERENCES rules
  , step# NUMBER NOT NULL
  , op   CHAR(l) NOT NULL
    CONSTRAINT rule_element_op CHECK (op IN ('+', '-', '*', '/', '%'))
  , type CHAR(l) NOT NULL
    CONSTRAINT rule_element_type CHECK (type IN ('literal', 'column'))
  , literal NUMBER
    CONSTRAINT rule_element_literal CHECK
        ((type = 'literal' AND literal IS NOT NULL)
          OR (type <> 'literal' AND literal IS NULL))
  , colname VARCHAR2(30)
    CONSTRAINT rule_element_colname CHECK
        ((type = 'column' AND colname IS NOT NULL)
          OR (type <> 'column' AND colname IS NULL))
  , CONSTRAINT rule_element_pk PRIMARY KEY (rule#, step#)
);

Проведя анализ этого кода, мы можем предсказать, что, хотя с его помощью можно обрабатывать простые правила, в которых задействуются только столбцы одной строки, вы столкнетесь с проблемами, когда понадобится сослаться на другие таблицы. Кроме того, кажется, этот код не способен обрабатывать круглые скобки и арифметические функции, подобные ROUND(). Приложив некоторые усилия, можно предложить схему, которая реализует эти возможности, а, может быть, и позволяет выполнять операции IF. В этом случае нам нужно найти не только способы реализации таблицы и средств ее сопровождения, но и правил, которые эта таблица описывает.

Такую реализацию обычно осуществляют с помощью простого, написанного специально под конкретный проект генератора кода, выдающего динамический SQL или динамический PL/SQL. Скорее всего, интерпретатор PL/SQL использует центральный процессор менее интенсивно, чем динамический PL/SQL. Однако, если в большинстве случаев будет генерироваться один и тот же анонимный блок, то вам окажет услугу разделяемая SQL-область. Это вряд ли будет иметь место, если значения столбцов вставляются в сгенерированный код буквально (как литеральная константа), и, к сожалению, очень немногие специальные генераторы кода поддерживают связанные переменные.

Гораздо более серьезной проблемой при таком подходе является то, что пользователей приходится учить, как сопровождать таблицу RULE_ELEMENTS, поскольку они, скорее всего, хотят выразить свое требование так, как это делается в электронной таблице. Возможно, легче научить их писать на SQL и PL/SQL, чем объяснить, как разлагать решение на элементы. Если вы и впрямь хотите потратить деньги впустую, можете также написать компилятор, который будет брать выражения, записанные в какой-то изобретенной вами нотации, и преобразовывать их в таблицу RULE_ELEMENTS. Однако не забывайте, что вам уже пришлось написать программу преобразования элементов RULE_ELEMENTS в SQL.

Надеемся, что этот материал ясно иллюстрирует невозможность использования такого метода на уровне проектов. И стоимость, и время реализации заказа слишком велики, а гарантия, что пользователи смогут реализовать каждое расширение, которое может потребоваться, отсутствует.

Существует более "прямой" метод, который может обеспечить большую гибкость. Таблицы, представленные в табл. Б.1, содержат не отдельные элементы формул, а сами формулы (на PL/SQL) и предложения, при помощи которых получают значения задействованных в них операндов (на SQL).

Таблица Б.1. Хранимый PL/SQL и SQL для реализации правил скидок

Formula

NAME

RETURNS

FORMULA

CREDLIM

 
 
 

CREDLIM2

BOOLEAN

 
 
 

BOOLEAN

IF (cur_cred >1000)
THEN
  RETURN (&cur_cred + :order_value) < 500;
ELSE
  RETURN (&cur_cred + :order_value) < 250;
END IF;

RETURN (&cur_cred + :order_value)< &orders_last_year * 0.125;

 

Formula_Params

NAME

S_SQL

cur_cred

order_last_year

SELECT SUM(ord.orderprice)
  FROM orders ord
 WHERE ord.status NOT IN ('PAID', 'CANC')
   AND ord.cus_id = :customer_id'

SELECT SUM(otd.order_price)
  FROM orders ord
 WHERE ord.status = 'PAID'
   AND ord.order_d BETWEEN SYSDATE AND SYSDATE - 365
   AND ord.cus_id = :customer_id

Конечно, при этом возникает целый ряд вопросов реализации, а общая производительность зависит от того, насколько хорошо осуществляется управление кэшами, позволяющее избежать постоянного повторного посещения одних и тех же строк. По этой причине, может, лучше определить при помощи таблицы FORMULA_PARAMS двухуровневую структуру SQL-предложений, способную возвращать набор значений, чтобы при однократном обращении к базе данных из всех посещаемых строк получать все необходимые данные.

Нам импонирует этот "прямой" подход, при котором предложения на SQL или PL/SQL хранятся в таблицах правил, а для их вставки в требуемый контекст в процессе обработки используется динамический SQL. Однако здесь также могут возникнуть проблемы с эффективностью, связанные с объемом работы по синтаксическому анализу. Кроме того, этот метод будет полезным только при условии, что какой-то пользователь сможет аккуратно выполнить все этапы разработки этого мини-проекта, реализующего новое правило. Конечно, это:

• анализ;

• проектирование;

• реализация;

• тестирование;

• внедрение.

Как правило, выполняют только этап реализации (кодирования). В результате в руки пользователей попадает мощное и дорогое оружие, при помощи которого они начинают зверски уничтожать свои данные.

Анализ ситуации: проектирование экранных форм

Метод проектирования, управляемый данными, не является панацеей от всех бед. Им не следует увлекаться, иначе можно спроектировать нечто настолько абстрактное, что его реализация окажется не под силу простым смертным (вроде нас с вами) или полученный код будет очень неэффективным. Возможно и сочетание обоих результатов.

Одному из авторов недавно достался в наследство код, который вполне можно считать примером "программной перегрузки". Каждое поле экранной формы размещалось в одном и том же месте и имело один и тот же размер. Войдя в редактор экранных форм, он сразу же воскликнул: "Как странно выглядит эта экранная форма!" После долгих поисков в коде стало очевидно, что размер и положение каждого поля изменялись динамически. Размер определялся размером соответствующего столбца базы данных, а код, выполняющий позиционирование полей, обеспечивал плотное их размещение. Название поля в экранной форме образовывалось из его имени. Этот код работал и давал определенное преимущество: можно было изменить размер столбца в базе данных или добавить в экранную форму новый столбец, не беспокоясь о размещении полей. Тем не менее, он работал очень медленно, и его невозможно было сопровождать. Структура и последовательность меню в это системе хранились в таблицах базы данных, а все меню создавались динамически, даже несмотря на то, что в самом инструментальном средстве имелась программа сопровождения меню. Настоящее расточительство!

Некоторые проектировщики в своих моделях данных тяготеют к еще большей абстракции. У них наблюдается тенденция к созданию метамоделей, которые совершенно невозможно заставить работать эффективно. Как выявить таких опасных проектировщиков? Нужно обращать особое внимание на столбцы, содержащие имя другой таблицы или столбца. Например, большая дуга в концептуальной информационной модели может быть реализована как один внешний ключ и столбец, содержащий имя таблицы, на которую этот ключ ссылается. Проблема решения, в котором применяется имя таблицы, на которую ссылается внешний ключ, состоит в том, что единственный способ выполнения запроса к такой структуре заключается в выборе каждой строки из главной таблицы и в использовании отдельного запроса для выборки подчиненных строк. Это неэффективно и трудно, особенно из такого инструмента, как Oracle Forms или PowerBuilder.

Анализ ситуации: Oracle Payroll

Oracle Payroll входит в комплект приложений Oracle Financials. Исходная спецификация продукта требовала, чтобы программа могла вычислять заработную плату для любой организации, позволяя (требуя) вводить формулы расчета зарплаты в процессе локальной адаптации без изменения кода базового продукта. Кроме того, необходимо было обеспечить возможность быстрого изменения правил в случае, когда клиент сталкивался с изменением в налоговом законодательстве или коллективных трудовых соглашениях.

Решение, рекомендованное одним из авторов этой книги и принятое группой проектировщиков, заключалось в хранении правил расчета в виде PL/SQL-выражений в пользовательских таблицах. Необходимо также было реализовать метасловарь внутри приложения, чтобы выражение "возраст служащего" или "уровень квалификации" в формуле, можно было преобразовать в имя таблицы, имя столбца и первичный ключ. Во время выполнения соответствующая формула выбиралась и просматривалась на предмет того, какие переменные для нее необходимы. Эти переменные затем выбирались из базы данных. Формулу затем можно было выполнять как анонимный блок PL/SQL. В организации со сложными правилами начисления зарплаты для одного служащего пришлось бы выполнять более чем 100 формул, и в Oracle версии 6 единственный способ, которым можно было выполнять этот PL/SQL, заключался в выполнении его как анонимного блока.

С функциональной точки зрения данный подход был почти идеальным. Мы говорим почти, так как из-за его сложности конечные пользователи испытывали серьезные трудности с настройкой и ее часто приходилось выполнять силами собственных специалистов или с привлечением консультантов по зарплате. Кроме того, на ранних стадиях эксплуатации возникали большие проблемы с производительностью. В главе 14 мы описываем некоторые приемы, которые позволили достичь в данном случае приемлемой производительности.

 

Расширяемость, управляемая представлениями

Не углубляясь в дебри реляционной теории, отметим, что средствами SQL всегда можно выбрать любой четко упорядоченный набор данных из любого набора таблиц. Поэтому при помощи представлений всегда можно (по крайней мере, теоретически) расширить правила для запросов путем внесения дополнений в представления, используемые для поиска, т.е. в представления, применяемые в триггерах и процедурах, которые выполняют обновление.

К сожалению, при этом существуют весьма реальные проблемы. Задаче определения необходимых представлений может быть трудной, а во многих случаях создать эффективное представление вообще невозможно. Чтобы дополнить представление, пользователи не только должны обладать хорошими знаниями SQL и структур данных, но и иметь как минимум некоторые DDL-привилегии. Наконец, даже при наличии этих расширений в последних версиях Oracle очень немногие представления, содержащие соединения, поддаются обновлению.

Проблему не решает и использование генераторов, так как очень немногие из них (если такие вообще существуют) могут выдавать определения представлений, требуемые для выполнения сложных расчетов с условиями.

 

Расширяемость, управляемая процедурами и функциями

Предположим, что для реализации новых функциональных возможностей нам придется применять традиционные приемы программирования. В данном случае мы можем попытаться ограничить количество изменений в приложении, сделав так, чтобы каждое атомарное действие, выполняемое приложением, было реализовано в исходном коде только один раз. Это означает, что все правила реализации процессов (в отличие от правил отображения) будут храниться в словаре данных как хранимые процедуры (или функции).

Это не что иное, как еще один призыв к инкапсуляции. Если впасть в крайности, то такой подход в результате даст приложение с очень больший числом очень маленьких сегментов кода. А это, в свою очередь, вызовет проблемы с производительностью и управлением проектом. Тем не менее, в определенных случаях затраты на создание множества маленьких функций могут хорошо окупиться, поскольку простые изменения и расширения функциональных возможностей можно делать в одном месте (оперативном словаре данных), причем с гарантией того, что они немедленно вступят в силу в масштабах всей системы.

Таким образом, значения, область действия которых ограничена приложением (например, максимальная длина кода счета), могут быть реализованы как функции в пакете, относящемся конкретно к данному приложению. Например:

CREATE PACKAGE acc_values AS
CREATE PACKAGE acc_values AS
...
FUNCTION max_acno_len RETURN NUMBER;
...
END; -- acc_values

CREATE PACKAGE BODY acc_values AS
...
  FUNCTION max_acno_len RETURN NUMBER IS
  BEGIN
    IF (SYSDATE >= '0l-JAN-98')
    THEN RETURN 10;
    ELSE RETURN 8;
    END IF;
  END;
...
END; -- acc_values body

С синтаксической точки зрения функции без аргументов ничем не отличаются от глобальных переменных пакета. В данном примере значение можно получить с помощью глобальной переменной пакета и конструктора в этом пакете (полагая, что в 0 часов 1 января 1998 года к базе данных не подключено ни одно приложение). Однако функциональный подход имеет более общий характер, так как позволяет заменять константу значением, требующим поиска в базе данных, несмотря на то, что во многих случаях поиск в базе данных потребует наличия параметров. Они всегда должны передаваться как часть вызова функции или выбираться внутри функции путем вызова других функции. Например, acc_values.curr_acno возвращает номер счета, который в данный момент обрабатывает приложение.

Несмотря на мощь, такие приемы предполагают изменение исходного кода проекта, и поэтому могут рассматриваться просто как четко ограниченная форма традиционного расширения приложения, которую мы рассматриваем ниже. Они могут также стать очень сложными, если явно простые функции приложения будут зависеть от взаимодействия нескольких сотен специальных функций.

 

Традиционный подход

Традиционный подход заключается в том, чтобы просто попросить отдел вычислительной техники или поставщика приложения внести изменения, необходимые для поддержки нового требования. Этот метод хорошо известен отделам работы с заказчиками и воспринимается с большим недоверием, так как процесс выполнения заказов часто растягивается на годы. Пользователи излагают свои просьбы, ведутся нескончаемые переговоры, в конце концов создается и отправляется что-то, что может удовлетворить, а может и не удовлетворить их потребности. Именно по этим причинам пользователи требуют обеспечения гибкости, которая позволит им самим изменять правила.

 

Расширяемость: заключение

Если невозможно обеспечить полную расширяемость алгоритмов, то мы рекомендуем использовать процедуры и функции PL/SQL, несмотря на потенциальные проблемы с производительностью.

Мы работали над приложениями, в которых задействовались и "самодокументированные" данные и правила, управляемые данными. Эти приложения работали (в установленных пределах), но, честно говоря, мы не можем рекомендовать никакой другой метод, кроме управления всеми действительно новыми требованиями в рамках сопровождения приложений. В процессе проектирования мы можем (чтобы продвинуть пользователей еще хотя бы на шаг вперед) лишь четко сформулировать, чего можно достичь в проектировании с помощью методов, управляемых данными. Вместе со сравнительными оценками затрат на обеспечение ограниченной и расширенной гибкости этого может оказаться достаточно для того, чтобы убедить спонсоров проекта согласится на менее претенциозную спецификацию.

Мы хотели бы завершить книгу утверждением, которое советуем вспомнить в случае, когда ваша задача как проектировщика покажется вам неразрешимой, — оно отражает истинную причину проблемы:

Трудно спроектировать код, который полностью удовлетворил бы абсолютно неизвестному требованию.

 
 

*  Мы хотели дать здесь небольшой материал о том, что увеличение числа работников, решающих ту или иную проблему, не обязательно приведет к быстрейшему ее решению, но подумали, что раз вы начали читать эту книгу, то уже знаете это.

СУБД Oracle

Знаете ли Вы, что примитивные конструкторы - это операции, которые создают объекты, соответствующего им типа, не используя никаких объектов в качестве аргументов. Примером такой операции является создание пустого списка.

НОВОСТИ ФОРУМА

Форум Рыцари теории эфира


Рыцари теории эфира
 10.11.2021 - 12:37: ПЕРСОНАЛИИ - Personalias -> WHO IS WHO - КТО ЕСТЬ КТО - Карим_Хайдаров.
10.11.2021 - 12:36: СОВЕСТЬ - Conscience -> РАСЧЕЛОВЕЧИВАНИЕ ЧЕЛОВЕКА. КОМУ ЭТО НАДО? - Карим_Хайдаров.
10.11.2021 - 12:36: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ - Upbringing, Inlightening, Education -> Просвещение от д.м.н. Александра Алексеевича Редько - Карим_Хайдаров.
10.11.2021 - 12:35: ЭКОЛОГИЯ - Ecology -> Биологическая безопасность населения - Карим_Хайдаров.
10.11.2021 - 12:34: ВОЙНА, ПОЛИТИКА И НАУКА - War, Politics and Science -> Проблема государственного терроризма - Карим_Хайдаров.
10.11.2021 - 12:34: ВОЙНА, ПОЛИТИКА И НАУКА - War, Politics and Science -> ПРАВОСУДИЯ.НЕТ - Карим_Хайдаров.
10.11.2021 - 12:34: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ - Upbringing, Inlightening, Education -> Просвещение от Вадима Глогера, США - Карим_Хайдаров.
10.11.2021 - 09:18: НОВЫЕ ТЕХНОЛОГИИ - New Technologies -> Волновая генетика Петра Гаряева, 5G-контроль и управление - Карим_Хайдаров.
10.11.2021 - 09:18: ЭКОЛОГИЯ - Ecology -> ЭКОЛОГИЯ ДЛЯ ВСЕХ - Карим_Хайдаров.
10.11.2021 - 09:16: ЭКОЛОГИЯ - Ecology -> ПРОБЛЕМЫ МЕДИЦИНЫ - Карим_Хайдаров.
10.11.2021 - 09:15: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ - Upbringing, Inlightening, Education -> Просвещение от Екатерины Коваленко - Карим_Хайдаров.
10.11.2021 - 09:13: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ - Upbringing, Inlightening, Education -> Просвещение от Вильгельма Варкентина - Карим_Хайдаров.
Bourabai Research - Технологии XXI века Bourabai Research Institution