Подставляемость и преобразование объектных типов в иерархии

Стивен Фернстайн
(Substituting and Converting Object Types in a Hierarchy, by Steven Feuerstein)
Oracle Magazine RE - Июнь 2002
Источник: Oracle Professional: March 2002

В этой статье Стивен Фернстайн рассматривает преимущества и гибкость иерархий объектных типов, исследуя подставляемость и преобразование типов.

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

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

Что такое подставляемость?

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

В этой статье мы рассмотрим очень простую иерархию типов, показанную на Рисунке 1 (и объявленную в файле food.ot, который находится в прилагаемом Download файле).

В этой иерархии тип еда (food), food_t, является корнем. Тип десерт (dessert), dessert_t, является подтипом еды, а пирожное (cake), описанное как cake_t, является в свою очередь подтипом dessert_t. Ниже приведены объявления этих типов (показаны только атрибуты, без связанных с ними PL/SQL методов):

CREATE TYPE food_t AS OBJECT (
    name VARCHAR2(100),
    food_group                    VARCHAR2 (100),
    grown_in                      VARCHAR2 (100)
    )
    NOT FINAL
    ;
 /
 CREATE TYPE dessert_t UNDER food_t (
    contains_chocolate    CHAR(1),
    year_created          NUMBER(4)
    )
    NOT FINAL
    ;
 /
 CREATE TYPE cake_t UNDER dessert_t (
    diameter      NUMBER,
    inscription   VARCHAR2(200)
    )
    ;
 /

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

DECLARE
    my_favorite   cake_t
    := cake_t (
          'Marzepan Delight',
          'CARBOHYDRATE',
          'Swedish Bakery',
          'N',
          1634,
          8,
          'Happy Birthday!'
       );
 BEGIN
    DBMS_OUTPUT.put_line (my_favorite.NAME);
    DBMS_OUTPUT.put_line (my_favorite.inscription);
 END;
 /

Обратите внимание, что я отображаю и ссылаюсь на атрибуты как базового типа еда, так и подтипа пирожное. Они все одинаково доступны мне в объекте типа пирожное.

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

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

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

Допустим, я создаю таблицу объектов типа food_t:

CREATE TABLE sustenance OF food_t;

Я могу теперь вставить строки в эту таблицу следующим образом:

BEGIN
    INSERT INTO sustenance
         VALUES (food_t (
                   'Brussel Sprouts',
                   'VEGETABLE',
                   'farm'
                )
                );
    INSERT INTO sustenance
         VALUES (dessert_t (
                   'Jello',
                   'PROTEIN',
                   'bowl',
                   'N',
                   1887
                )
                );
    INSERT INTO sustenance
         VALUES (cake_t (
                   'Marzepan Delight',
                   'CARBOHYDRATE',
                   'bakery',
                   'N',
                   1634,
                   8,
                   'Happy Birthday!'
                )
                );
 END;
 /

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

Я могу выполнить запрос к этой таблице в SQL*Plus и он покажет мне все атрибуты типа еда (и только) для трех строк.


SQL> SELECT * FROM sustenance;
 NAME                      FOOD_GROUP         GROWN_IN
 ------------------------- ------------------ -------------
 Brussel Sprouts           VEGETABLE          farm
 Jello                     PROTEIN            bowl
 Marzepan Delight          CARBOHYDRATE       bakery

Я могу также использовать преимущества подставляемости в PL/SQL блоках. В следующем коде, я объявляю еду, но инициализирую ее десертом, более конкретным типом еды.

DECLARE
    mmm_good food_t :=
       dessert_t (
          'Super Brownie',
          'CARBOHYDRATE',
          'my oven', 'Y', 1994);
 BEGIN
    DBMS_OUTPUT.PUT_LINE (
       mmm_good.name);
 END;
 /

А вот пример подставляемости в PL/SQL коллекциях:

DECLARE
    TYPE foodstuffs_nt IS TABLE OF food_t;
    fridge_contents foodstuffs_nt := (
       food_t (
         'Eggs benedict', 'PROTEIN', 'Farm'),
       dessert_t (
         'Strawberries and cream',
         'FRUIT', 'Backyard', 'N', 2001),
       cake_t (
         'Chocolate Supreme', 'CARBOHYDRATE',
         'Kitchen', 'Y', 2001,
          8, 'Happy Birthday, Veva'));
 BEGIN
    FOR indx IN
       fridge_contents.FIRST ..
 fridge_contents.LAST
    LOOP
       DBMS_OUTPUT.PUT_LINE (
          fridge_contents(indx).name);
    END LOOP;
 END;
 /

Теперь рассмотрим вставки, которые не работают. Допустим, я создал объектную таблицу десертов:

CREATE TABLE sweet_nothings OF dessert_t;
 /

Если теперь я попытаюсь вставить объект типа еда, Oracle выдаст ошибку, как показано ниже:

BEGIN
    INSERT INTO sweet_nothings
         VALUES (dessert_t (
                   'Jello',
                   'PROTEIN',
                   'bowl',
                   'N',
                   1887
                )
                );
    INSERT INTO sweet_nothings
         VALUES (food_t (
                   'Brussel Sprouts',
                   'VEGETABLE',
                   'farm'
                )
                );
 END;
 /
 PL/SQL: ORA-00932: inconsistent datatypes

Я получил эту ошибку потому, что любой десерт является едой, но не любая еда является десертом. Я не могу вставить объект типа food_t в столбец типа dessert_t.

Теперь рассмотрим аналогичную ситуацию, в PL/SQL. Я объявляю в своей программе объект типа еда и инициализирую его десертом. Обратите внимание, что я указал Y или "Yes, it sure does!" ("Да, конечно, содержит!") для атрибута contains_chocolate (содержит_шоколад). Однако, если я попытаюсь в своем коде указать этот атрибут, характерный для десерта, PL/SQL выдаст мне ошибку.

SQL> DECLARE
   2  -- Опять я подставляю, но на этот раз
   3  -- я пытаюсь получить доступ к атрибуту десерта.
   4     mmm_good food_t :=
   5        dessert_t (
   6           'Super Brownie',
   7           'CARBOHYDRATE',
   8           'my oven', 'Y', 1994);
   9  BEGIN
  10     DBMS_OUTPUT.PUT_LINE (
  11        mmm_good.contains_chocolate);
  12  END;
  13  /
       mmm_good.contains_chocolate);
                *
 ERROR at line 11:
 ORA-06550: line 11, column 16:
 PLS-00302: component 'CONTAINS_CHOCOLATE' must be declared

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

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

Как отключить подставляемость

Зачем нужно отключать или ограничивать подставляемость? Я могу захотеть, чтобы таблица содержала только объекты конкретного типа внутри иерархии, а не всякие подтипы. Для обеспечения этого требования, Oracle предоставляет возможность отключать подставляемость на любом уровне для столбца или атрибута, включая встроенные атрибуты и вложенные коллекции. Для этого используется следующее предложение:

NOT SUBSTITUTABLE AT ALL LEVELS

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

CREATE TABLE meal (
    served_on DATE,
    appetizer food_t,
    main_course food_t,
    dessert dessert_t
    )
    COLUMN appetizer NOT SUBSTITUTABLE AT ALL LEVELS
    ;

Предложение NOT SUBSTITUTABLE используется, чтобы указать, что при задании значения для столбца appetizer нельзя использовать подтип еды. Я не хочу, чтобы кто-нибудь вставил десерт в качестве закуски.

Рассмотрим теперь код в Листинге 1. Я пытаюсь вставить два различных приема пищи. В первом INSERT'е я указываю объект типа food_t в качестве закуски. Во втором insert'е я пытаюсь подсунуть десерт в качестве закуски. Результатом выполнения является следующая ошибка:

ERROR at line 1:
 ORA-00932: inconsistent datatypes

Предложение NOT SUBSTITUTABLE можно применить и к целой объектной таблице. Листинг 2 демонстрирует эту возможность. Я создал таблицу объектов food_t, с именем brunches. В нее можно успешно вставлять объекты типа food_t, но при попытке вставить десерт в таблицу возникает та же ошибка "несовместимые типы данных".

Вот что следует помнить об ограничениях подставляемости:

Ограничение подставляемости для конкретного подтипа

Итак, я могу отключить все уровни подставляемости, но что если необходимо отключить всю подставляемость кроме конкретного подтипа? Предположим, например, что я хочу создать PL/SQL коллекцию десертов, которая может содержать только пирожные. Или я хочу ввести правило в моей таблице meals, что все десерты должны быть пирожными. Oracle предоставляет предложение IS OF для этой цели. Вот новое объявление таблицы meals, в которой существует два различных типа ограничения подставляемости:

CREATE TABLE meal (
    served_on DATE,
    appetizer food_t,
    main_course food_t,
    dessert dessert_t
    )
    COLUMN appetizer NOT SUBSTITUTABLE AT ALL LEVELS,
    COLUMN dessert IS OF (ONLY cake_t)
    ;

И теперь я смогу добавлять только такие приемы пищи, в которых десерты объявлены как пирожные. Поэтому следующий INSERT отвергается:

SQL> BEGIN
   2    -- This will no longer work.
   3    INSERT INTO meal VALUES (
   4    SYSDATE,
   5    food_t ('Shrimp cocktail',
   6       'PROTEIN', 'Ocean'),
   7    food_t ('Eggs benedict',
   8       'PROTEIN', 'Farm'),
   9    dessert_t ('Strawberries and cream',
  10       'FRUIT', 'Backyard', 'N', 2001));
  11  END;
  12  /
 BEGIN
 *
 ERROR at line 1:
 ORA-00932: inconsistent datatypes

Оператор IS OF type можно использовать только для того, чтобы ограничить объекты строки и столбца для одного подтипа, не для нескольких. Необходимо также использовать ключевое слово ONLY, даже если это единственная альтернатива, доступная сейчас. Вы можете использовать либо IS OF type, либо NOT SUBSTITUTABLE AT ALL LEVELS для ограничения объектного столбца, но нельзя использовать и то и другое для одного и того же столбца. Очевидно, что эти ограничения можно применять к различным столбцам, как показано раньше.

Расширение и сужение объектных типов

Рассмотрим две концепции: расширение и сужение, очень полезные при работе с объектами в иерархии типов, особенно при присваивании переменной или столбцу одного типа объекта другого типа в иерархии. Вот их определение:

Расширение является фактически "родным" для иерархий объектных типов Oracle и их свойства подставляемости. Любое пирожное является также десертом и едой. Следовательно, до тех пор, пока вы явно не ограничите подставляемость, подтип может трактоваться, храниться и обрабатываться как любой из его супертипов. Вы уже видели несколько примеров этой обработки в статье.

Давайте рассмотрим, как выполняется более сложный шаг - сужение - в SQL и PL/SQL в Oracle9i.

Сужение с TREAT

Oracle предоставляет специальную функцию TREAT, которая позволяет выполнять операцию сужения. Функция TREAT явно изменяет объявленный тип источника в присваивании на более специализированный тип или подтип в иерархии места назначения.

Для успешного сужения нужно использовать TREAT; без использования этой функции, вы не сможете сослаться на специфичные для подтипа атрибуты и методы.

Вот общий синтаксис этой функции:

TREAT (<object instance> AS <object type>)

где <object instance> - это значение столбца или строки коллекции данного конкретного супертипа в объектной иерархии, а <object type> - это подтип в этой иерархии.

Давайте рассмотрим несколько примеров использования TREAT. Предположим, что я вставил три строки в таблицу meal, как показано в Листинге 3. Обратите внимание, что в третьей строке я передал десерт в качестве главного блюда, одно из любимых занятий моего сына во время еды! Я смог сделать это, поскольку не ограничил подставляемость столбца main_course.

Предположим, что я хочу видеть список всех приемов пищи, в которых десерт указан в качестве главного блюда. Я использую оператор TREAT в предложении WHERE:

Функция TREAT - которую, кстати, в Oracle9i версии 1 можно использовать только в операторах SQL (но не в PL/SQL блоках) - возвращает объектный тип NULL для любых главных блюд, не являющихся десертами.

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

SQL> SELECT main_course.contains_chocolate
   2    FROM meal
   3   WHERE TREAT (main_course AS dessert_t)
   4         IS NOT NULL;
 SELECT main_course.contains_chocolate
                    *
 ERROR at line 1:
 ORA-00904: invalid column name

Даже если все выбранные главные блюда являются десертами, у Oracle нет возможности узнать это; столбец main_course объявлен как тип food_t. Итак, что я должен делать? Использовать функцию TREAT в списке SELECT также как в запросе! Этот запрос и результаты представлены в Листинге 4.

Я могу также использовать TREAT в DML операциях, таких как INSERT'ы и UPDATE'ы. Предположим, например, что я не хочу разрешать вставлять в таблицу meal такие строки, в которых десерт является основным блюдом. Я могу добавить ограничение на таблицу, чтобы предотвратить это, но я также могу удалить все такие строки, используя UPDATE вместе с TREAT.

Только помните, что мы пока не можем использовать TREAT вне SQL оператора, напрямую в родном PL/SQL коде. Возможно, мы получим PL/SQL TREAT в Oracle9i версии 2.

Программный компромисс относительно иерархий типов

Реализация наследования в Oracle без сомнения значительно улучшает полезность и мощность объектных типов в языке PL/SQL. Означает ли это, что многие и многие разработчики PL/SQL будут теперь использовать преимущества объектных типов и, в частности, эти замечательные новые возможности? У меня есть сомнения на этот счет, и на это - две причины:

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

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

Итак, прежде всего, рассмотрите, что вам могут дать иерархии объектных типов. Возможно, вы обнаружите, что объектные типы прекрасно вам подходят и имеют важные преимущества. В таком случае, глубоко изучите эти возможности и обеспечьте их использование во всей их мощности. Если вы не видите в этом ничего хорошего, не расстраивайтесь. Просто оставайтесь со своими реляционными таблицами и более простыми структурами данных. Они работали на протяжении 25 лет (юбилей, который Oracle сейчас отмечает); и, вероятно, будут хороши в течение еще долгого времени.


Листинг 1. Попытка указать два приема пищи.

BEGIN
   INSERT INTO meal VALUES (
      SYSDATE,
      food_t ('Shrimp cocktail', 'PROTEIN', 'Ocean'),
      food_t ('Eggs benedict', 'PROTEIN', 'Farm'),
      dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001));
   INSERT INTO meal VALUES (
      SYSDATE + 1,
      dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001),
      food_t ('Eggs benedict', 'PROTEIN', 'Farm'),
      cake_t ('Apple Pie', 'FRUIT', 'Baker''s Square', 'N', 2001, 8, NULL));
END;

Листинг 2. Ограничение подставляемости в объектной таблице

SQL> CREATE TABLE brunches OF food_t NOT SUBSTITUTABLE AT ALL LEVELS;
Table created.
SQL>
SQL> INSERT INTO brunches VALUES (
  2     food_t ('Eggs benedict', 'PROTEIN', 'Farm'));
1 row created.
SQL> INSERT INTO brunches VALUES (
  2     dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001));
   dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001))
   *
ERROR at line 2:
ORA-00932: inconsistent datatypes

Листинг 3. Вставка записей в таблицу meal.

BEGIN
   -- Заполнение таблицы meal
   INSERT INTO meal VALUES (
      SYSDATE,
      food_t ('Shrimp cocktail', 'PROTEIN', 'Ocean'),
      food_t ('Eggs benedict', 'PROTEIN', 'Farm'),
      dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001));
   INSERT INTO meal VALUES (
      SYSDATE + 1,
      food_t ('Shrimp cocktail', 'PROTEIN', 'Ocean'),
      food_t ('Stir fry tofu', 'PROTEIN', 'Vat'),
      cake_t ('Apple Pie', 'FRUIT', 'Baker''s Square', 'N', 2001, 8, NULL));
   INSERT INTO meal VALUES (
      SYSDATE + 1,
      food_t ('Fried Calamari', 'PROTEIN', 'Ocean'),
      -- Butter cookies for dinner? Yikes!
      dessert_t ('Butter cookie', 'CARBOHYDRATE', 'Oven', 'N', 2001),
      cake_t ('French Silk Pie', 'CARBOHYDRATE',
         'Baker''s Square', 'Y', 2001, 6, 'To My Favorite Frenchman'));
END;
/

Листинг 4. Использование оператора TREAT.

SQL> SELECT TREAT (main_course AS dessert_t).contains_chocolate chocolatey
  2    FROM meal
  3   WHERE TREAT (main_course AS dessert_t) IS NOT NULL;
CHOCOLATEY
---------------
N