Статьи

Методы сравнения для типов объектов, часть 5

Существуют специальные методы-члены — методы  map или order — которые мы используем, чтобы указать Oracle Database, как сравнивать два объекта одного и того же типа данных. Эта возможность очень важна, когда мы хотим выполнить тест на равенство в PL / SQL или при сортировке объектов в SQL.


Вам также может понравиться: 
Объектно-ориентированный JavaScript

Нет способа сделать это по умолчанию. Другими словами, если я создаю простой тип объекта, добавляю его в виде столбца в таблицу и пытаюсь сравнить или отсортировать, все, что я получаю, — это ошибки. Давайте взглянем. Сначала я создам таблицу с типом объекта в виде столбца и добавлю пару строк.

CREATE TYPE food_ot AS OBJECT
(
   name VARCHAR2 (100),
   food_group VARCHAR2 (50),
   grown_in VARCHAR2 (100)
)
   NOT FINAL
/

CREATE TABLE meals
(
   served_on     DATE,
   main_course   food_ot
);
/

BEGIN
   INSERT INTO meals (served_on, main_course)
        VALUES (SYSDATE, food_ot ('Shrimp cocktail', 'PROTEIN', 'Ocean'));

   INSERT INTO meals (served_on, main_course)
        VALUES (SYSDATE + 1, food_ot ('House Salad', 'VEGETABLE', 'Farm'));

   COMMIT;
END;
/

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

  SELECT m.main_course.name name
    FROM meals m
ORDER BY m.main_course.name
/

NAME
---------------
House salad
Shrimp Cocktail

SELECT m.main_course.name name
  FROM meals m, meals m2
 WHERE m.main_course = m2.main_course
 ORDER BY m.main_course.name
/

NAME
---------------
House salad
Shrimp Cocktail

Такое сравнение на равенство по умолчанию выполняет сравнение по атрибутам — и это работает только в том случае, если у вас нет столбцов LOB или пользовательских типов. В этих случаях вы увидите ошибку:

CREATE TYPE food_with_clob_ot AS OBJECT
(
   name VARCHAR2 (100),
   grown_in CLOB
)
   NOT FINAL
/

CREATE TABLE meals_with_clobs
(
   served_on     DATE,
   main_course   food_with_clob_ot
);
/

SELECT m.main_course.name name
  FROM meals_with_clobs m, meals_with_clobs m2
 WHERE m.main_course = m2.main_course
 ORDER BY m.main_course.name
/

ORA-22901: cannot compare VARRAY or LOB attributes of an object type

Теперь давайте попробуем (а) отсортировать строки с помощью ORDER BY и (б) сделать сравнение на неравенство. Это «не ходи». Поведение по умолчанию больше не доступно для удовлетворения этих запросов.

  SELECT m.main_course.name name
    FROM meals m
ORDER BY m.main_course
/

ORA-22950: cannot ORDER objects without MAP or ORDER method

  SELECT m.main_course.name name
    FROM meals m, meals m2
   WHERE m.main_course > m2.main_course
/

ORA-22950: cannot ORDER objects without MAP or ORDER method

Что касается сравнения на равенство, то оно работает по умолчанию только в SQL, а не в PL / SQL.

DECLARE
   m1   food_ot := food_ot ('Shrimp cocktail', 'PROTEIN', 'Ocean');
   m2   food_ot := food_ot ('House Salad', 'VEGETABLE', 'Farm');
BEGIN
   IF m1 = m1
   THEN
      DBMS_OUTPUT.put_line ('Equal');
   END IF;

   IF m1 <> m2
   THEN
      DBMS_OUTPUT.put_line ('Unequal');
   END IF;
END;
/

PLS-00526: A MAP or ORDER function is required for comparing objects in PL/SQL.

Так что же делать разработчику? Прочитайте эти сообщения об ошибках и приступайте к работе!

Если вы «не можете ЗАКАЗАТЬ объекты без метода MAP или ORDER», возможно, вам следует создать метод MAP или ORDER. ?

Метод MAP — это метод-член (прикрепленный к экземпляру типа), который возвращает «отображение» значения объекта на тип данных, который Oracle Database уже знает, как сравнивать, например, число или строку.

Метод члена ORDER сравнивает два разных экземпляра типа и возвращает значение флага, которое указывает их относительное упорядочение.

В определении типа объекта может быть только один метод MAP или ORDER. Они не могут сосуществовать.

Метод MAP

Метод MAP выполняет вычисления атрибутов объекта, чтобы получить возвращаемое значение ny встроенных типов данных Oracle (кроме LOB и BFILE) и типов ANSI SQL, таких как CHARACTER или REAL.

Oracle Database автоматически вызывает этот метод для оценки таких сравнений как obj_1> obj_2 и сравнений, которые подразумеваются в предложениях DISTINCT, GROUP BY, UNION и ORDER BY — поскольку все они требуют сортировки по строкам в таблице.

«Автоматический» означает:

1. Вы никогда не вызываете метод карты непосредственно в своем коде.

2. Предполагая, что тип имеет метод map с именем «mapme», тогда, когда вы пишете сравнение, как это

obj_1> obj_2

он автоматически переводится (незаметно для вас) на:

obj_1.mapme ()> obj_2.mapme ()

Давайте добавим метод карты к типу еды. Я буду держать это простым и глупым. Уровень белков выше, чем жидкости, которые выше, чем углеводы, которые выше, чем овощи. Возьми это число и прибавь к длине названия еды. Затем верните этот номер для сопоставления. Наконец, добавьте несколько строк.

CREATE TYPE food_t AS OBJECT
    (name VARCHAR2 (100)
  , food_group VARCHAR2 (100)
  , grown_in VARCHAR2 (100)
  , MAP MEMBER FUNCTION food_mapping
         RETURN NUMBER
    )
    NOT FINAL;
/

CREATE OR REPLACE TYPE BODY food_t
IS
    MAP MEMBER FUNCTION food_mapping
        RETURN NUMBER
    IS
    BEGIN
        RETURN (CASE self.food_group
                      WHEN 'PROTEIN' THEN 30000
                      WHEN 'LIQUID' THEN 20000
                      WHEN 'CARBOHYDRATE' THEN 15000
                      WHEN 'VEGETABLE' THEN 10000
                  END
                  + LENGTH (self.name));
    END;
END;
/

BEGIN
   -- Populate the meal table
   INSERT INTO meals
        VALUES (SYSDATE, food_ot ('Shrimp cocktail', 'PROTEIN', 'Ocean'));

   INSERT INTO meals
        VALUES (SYSDATE + 1, food_ot ('Stir fry tofu', 'PROTEIN', 'Wok'));

   INSERT INTO meals
           VALUES (SYSDATE + 1,
                   food_ot ('Peanut Butter Sandwich',
                              'CARBOHYDRATE',
                              'Kitchen'));

   INSERT INTO meals
           VALUES (SYSDATE + 1,
                   food_ot ('Brussels Sprouts', 'VEGETABLE', 'Backyard'));

   COMMIT;
END;
/

Теперь я выполняю ORDER BY для столбца типа объекта, проверяю неравенство между строками, а также выполняю сравнения внутри PL / SQL.

  SELECT m.main_course.name name
  FROM meals m
ORDER BY main_course
/

NAME
----------------------
Brussels Sprouts
Peanut Butter Sandwich
Stir fry tofu
Shrimp cocktail

  SELECT m1.main_course.name name
    FROM (SELECT *
            FROM meals m
           WHERE m.main_course.name LIKE 'S%') m1,
         (SELECT *
            FROM meals m
           WHERE m.main_course.name NOT LIKE 'S%') m2
   WHERE m1 > m2
ORDER BY m1.main_course
/

NAME
---------------
Stir fry tofu
Stir fry tofu
Shrimp cocktail
Shrimp cocktail

DECLARE
   ot1   food_ot := food_ot ('Eggs benedict', 'PROTEIN', 'Farm');
   ot2   food_ot := food_ot ('Brussels Sprouts', 'VEGETABLE', 'Backyard');
   ot3   food_ot := food_ot ('Brussels Sprouts', 'VEGETABLE', 'Backyard');
BEGIN
   IF ot1 = ot2
   THEN
      DBMS_OUTPUT.put_line ('equal - incorrect');
   ELSE
      DBMS_OUTPUT.put_line ('not equal - correct');
   END IF;

   IF ot2 <> ot3
   THEN
      DBMS_OUTPUT.put_line ('not equal - incorrect');
   ELSE
      DBMS_OUTPUT.put_line ('equal - correct');
   END IF;
END;
/

not equal - correct
equal - correct

Обратите внимание, что в запросе, объединяющем m1 и m2, «Stir fry tofu» предшествует «Коктейлю из креветок», потому что в нем меньше символов и, следовательно, небольшое число, возвращаемое функцией map.

Метод ЗАКАЗА

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

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

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

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

Давайте построим метод заказа для типа продуктов питания. Давайте начнем со спецификации для типа продуктов, а также создадим иерархию типов:

CREATE TYPE food_ot AS OBJECT
(
   name VARCHAR2 (100),
   food_group VARCHAR2 (100),
   ORDER MEMBER FUNCTION food_ordering (other_food_in IN food_ot)
      RETURN INTEGER
)
   NOT FINAL;
/

CREATE TYPE dessert_ot UNDER food_ot (
   contains_chocolate   CHAR (1)
 , year_created         NUMBER (4)
)
NOT FINAL;
/

CREATE TYPE cake_ot UNDER dessert_ot (
   diameter      NUMBER
 , inscription   VARCHAR2 (200)
);
/

Обратите внимание, что я использую ключевое слово ORDER, передаю экземпляр типа, с которым сравнивается SELF. Я возвращаю целое число: либо -1, 0 или 1. А теперь реализация. Вот некоторые примечания, учитывая их сложность:

  • Поскольку экземпляром может быть еда, десерт или торт, первое правило заключается в том, что супертип всегда больше, чем подтип. Я использую синтаксис self IS OF (ONLY my_type) для определения типа экземпляра.
  • Я использую коллекцию с индексами строк l_order_by_food_group, чтобы установить иерархию упорядочения по группам продуктов. (Мне действительно нравятся коллекции с индексами строк!)
  • If after checking for supertype/subtype ordering, I know that other_food_in is of the same type as SELF, then I set return value according to food group.
CREATE OR REPLACE TYPE BODY food_ot
IS
   ORDER MEMBER FUNCTION food_ordering (other_food_in IN food_ot)
      RETURN INTEGER
   /*
   Subtypes are always less. Food > Dessert > Cake

   If of the same type, same rule AS for MAP:
      Vegetable < Carbohydrate < Liquid < Protein
   */
   IS
      TYPE order_by_food_group_t IS TABLE OF PLS_INTEGER
         INDEX BY VARCHAR2 (100);

      l_order_by_food_group   order_by_food_group_t;
      c_self_eq_of   CONSTANT PLS_INTEGER := 0;
      c_self_gt_of   CONSTANT PLS_INTEGER := 1;
      c_of_gt_self   CONSTANT PLS_INTEGER := -1;
      l_ordering              PLS_INTEGER := c_self_eq_of;

      PROCEDURE initialize
      IS
      BEGIN
         l_order_by_food_group ('PROTEIN') := 1000;
         l_order_by_food_group ('LIQUID') := 100;
         l_order_by_food_group ('CARBOHYDRATE') := 10;
         l_order_by_food_group ('VEGETABLE') := 1;
      END initialize;
   BEGIN
      initialize;

      IF self IS OF (ONLY food_ot)
      THEN
         l_ordering :=
            CASE
               WHEN other_food_in IS OF (ONLY food_ot) THEN c_self_eq_of
               ELSE c_self_gt_of
            END;
      ELSIF self IS OF (ONLY dessert_t)
      THEN
         l_ordering :=
            CASE
               WHEN other_food_in IS OF (ONLY dessert_t) THEN c_self_eq_of
               WHEN other_food_in IS OF (ONLY food_ot) THEN c_of_gt_self
               ELSE c_self_gt_of
            END;
      ELSE
         /* It is cake. */
         l_ordering :=
            CASE
               WHEN other_food_in IS OF (ONLY cake_t) THEN c_self_eq_of
               ELSE c_of_gt_self
            END;
      END IF;

      IF l_ordering = c_self_eq_of
      THEN
         /*
         Further analysis is needed.
         */
         l_ordering :=
            CASE
               WHEN l_order_by_food_group (self.food_group) =
                       l_order_by_food_group (other_food_in.food_group)
               THEN
                  c_self_eq_of
               WHEN l_order_by_food_group (self.food_group) >
                       l_order_by_food_group (other_food_in.food_group)
               THEN
                  c_self_gt_of
               WHEN l_order_by_food_group (self.food_group) <
                       l_order_by_food_group (other_food_in.food_group)
               THEN
                  c_of_gt_self
            END;
      END IF;

      RETURN l_ordering;
   END;
END;
/

Now I will add rows of various types.

BEGIN
   -- Populate the meal table
   INSERT INTO meals
        VALUES (SYSDATE, food_ot ('Shrimp cocktail', 'PROTEIN'));

   INSERT INTO meals
        VALUES (SYSDATE + 1, food_ot ('Stir fry tofu', 'PROTEIN'));

   INSERT INTO meals
        VALUES (SYSDATE + 1,
                dessert_ot ('Peanut Butter Sandwich',
                            'CARBOHYDRATE',
                            'N',
                            1700));

   INSERT INTO meals
        VALUES (SYSDATE + 1, food_ot ('Brussels Sprouts', 'VEGETABLE'));

   INSERT INTO meals
        VALUES (SYSDATE + 1,
                cake_ot ('Carrot Cake',
                         'VEGETABLE',
                         'N',
                         1550,
                         12,
                         'Happy Birthday!'));

   COMMIT;
END;
/

All right, then, let’s have some fun! Ordering rows works. SQL comparisons work.

  SELECT m.main_course.name name
  FROM meals m
ORDER BY main_course
/

NAME
----------------------
Carrot Cake
Peanut Butter Sandwich
Brussels Sprouts
Shrimp cocktail
Stir fry tofu

  SELECT m1.main_course.name name
    FROM (SELECT *
            FROM meals m
           WHERE m.main_course.name LIKE 'S%') m1,
         (SELECT *
            FROM meals m
           WHERE m.main_course.name NOT LIKE 'S%') m2
   WHERE m1.main_course > m2.main_course
ORDER BY m1.main_course
/

NAME
---------------
Shrimp cocktail
Shrimp cocktail
Shrimp cocktail
Stir fry tofu
Stir fry tofu
Stir fry tofu

And how about in PL/SQL?

DECLARE
   ot1   food_ot := food_ot ('Eggs benedict', 'PROTEIN');
   ot2   food_ot := food_ot ('Brussels Sprouts', 'VEGETABLE');
   ot3   food_ot := dessert_ot ('Brownie', 'SUGAR', 'Y', 1943);
   ot4   food_ot := cake_ot (
      'Carrot Cake', 'VEGETABLE', 'N', 1550, 12, 'Happy Birthday!');
BEGIN
   IF ot1 = ot1
   THEN
      DBMS_OUTPUT.put_line ('equal - correct');
   ELSE
      DBMS_OUTPUT.put_line ('not equal - incorrect');
   END IF;

   IF ot1 = ot2
   THEN
      DBMS_OUTPUT.put_line ('equal - incorrect');
   ELSE
      DBMS_OUTPUT.put_line ('not equal - correct');
   END IF;

   IF ot2 <> ot3
   THEN
      DBMS_OUTPUT.put_line ('not equal - correct');
   ELSE
      DBMS_OUTPUT.put_line ('equal - incorrect');
   END IF;

   IF ot2 > ot3
   THEN
      DBMS_OUTPUT.put_line ('food > dessert - correct');
   ELSE
      DBMS_OUTPUT.put_line ('food < dessert - incorrect');
   END IF;

   IF ot3 > ot4
   THEN
      DBMS_OUTPUT.put_line ('dessert > cake - correct');
   ELSE
      DBMS_OUTPUT.put_line ('dessert < cake - incorrect');
   END IF;

   IF ot3 < ot4
   THEN
      DBMS_OUTPUT.put_line ('dessert < cake - incorrect');
   ELSE
      DBMS_OUTPUT.put_line ('dessert > cake - correct');
   END IF;
END;
/

equal - correct
not equal - correct
not equal - correct
food > dessert - correct
dessert > cake - correct
dessert > cake - correct

All good!

P.S. Don’t forget that if you’d like to try out all this code for yourself, all you have to do is run this LiveSQL script.

The Series:

Further Reading

A Guide to High-Performance PL/SQL

Use PL/SQL to Build and Access Document Stores