Вы находитесь на страницах старой версии сайта.
Переходите на новую версию Interface.Ru

Объектно-ориентированный PL/SQL: проблемы и методы их решения

© Игорь Мельников
Статья была опубликована в ORACLE MAGAZINE Русское издание

Введение

Многие из нас с появлением Oracle9i Database и далее Oracle10g Database начали активно разрабатывать приложения с помощью объектно-ориентированного PL/SQL. Однако вскоре выяснилось, что корпорация Oracle не полностью реализовала возможности присущие объектно-ориентированным языкам. В результате многие разработчики приложений на Oracle Database "охладели" к объектным возможностям PL/SQL.

В предлагаемой вашему вниманию статье предлагается ряд решений проблем, с которыми сталкиваются разработчики. Я уверен, что Oracle9i PL/SQL позволяет реализовывать развитую объектную модель, и, надеюсь, мое мнение разделит читатель.

Каждый раздел статьи сопровождается исходными текстами скриптов, демонстрирующими сответствующий подход. Все скрипты запускались и проверялись с помощью последней доступной на текущий момент версии Oracle10g Database - 10.1.0.2 Скрипты тестировались на следующей версии: Oracle 10.1.0.2 Enterprise Edition for linux x86 (Intel Xeon)

Вызов переопределенного метода в типе-потомке

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

Например: пусть у нас есть класс t_ParentType в котором определен метод getName:

------------------------------------------------------------
--спецификация объектного типа t_ParentType:               -
------------------------------------------------------------
create or replace type t_ParentType as object
(
  v_Field1 varchar2(32),
  member function getName return varchar2
)
not final;
------------------------------------------------------------
--тело объектного типа t_ParentType:                       -
------------------------------------------------------------
create or replace type body t_ParentType as
  member function getName return varchar2 is
  begin
    return self.v_Field1;
  end;
end;

Теперь мы определяем объектный тип t_ChildType, который является наследником t_ParentType. В типе t_ChildType метод getName является виртуальным и переопределен. Для этого использовано ключевое слово OVERRIDING:

------------------------------------------------------------
--Спецификация объектного типа t_ChildType,                -
--который является наследником : t_ParentType              -
--Внимание: метод getName переопределен                    -
------------------------------------------------------------
create or replace type t_ChildType under t_ParentType
(
  v_Field2 varchar2(64),
  
  overriding member function getName return varchar2
)
not final;

В реализации метода getName попытаемся вызвать унаследованный метод getName (объектного типа t_ParentType)

------------------------------------------------------------
--Тело объектного типа t_ChildType,                        -
--в методе getName необходимо вызвать унаследованный метод -
------------------------------------------------------------
create or replace type body t_ChildType is
  overriding member function getName return varchar2 is
  begin
    return (???) getName || ' ' || v_Field2; -- как вызвать
                                         -- метод предка ???
  end;
end;

Таким образом, выясняется, что в PL/SQL нет синтаксической конструкции, для того чтобы сослаться на метод типа-предка.

В объектно-ориентированных языках для этого существуют специальные языковые конструкции. В Java это ключевое слово super (супер-класс), в Object Pascal - Inherited. Данный механизм обеспечивает доступ к унаследованной логике и устраняет избыточность кода. Документация по языку PL/SQL (Oracle10g Rel.1 PL/SQL User's Guide and Reference, Oracle10g Rel.1 Application Developer's Guide - Object-Relational Features) хранит по этому поводу молчание.

Для решения этой проблемы предлагается следующий алгоритм:

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

------------------------------------------------------------
--спецификация объектного типа t_ParentType,               -
--добавлен копирующий конструктор                          -
------------------------------------------------------------
create or replace type t_ParentType as object
(
  v_Field1 varchar2(32),
--копирующий конструктор:
  constructor function t_ParentType(v_pObject in out nocopy t_ParentType)
  return self as result,
  member      function getName(self in out nocopy t_ParentType)
  return varchar2
)
not final;
------------------------------------------------------------
--тело объектного типа t_ParentType                        -
------------------------------------------------------------
create or replace type body t_ParentType is
  constructor function t_ParentType(v_pObject in out nocopy t_ParentType)
  return self as result is
  begin
    self.v_Field1 := v_pObject.v_Field1;
    return;
  end;
  member function getName(self in out nocopy t_ParentType)
  return varchar2 is
  begin
    return self.v_Field1;
  end;
end;

В типе-потомке нам также будет необходим метод присваивания, который будет копировать все поля переменной экземпляра типа в текущий экземпляр, - назовем его assign. Далее добавим функцию inherited_getName, которая будет реализовывать алгоритм вызова функции getName родительского типа t_ParentType. Фактически метод inherited_getName представляет собой оболочку для метода getName типа-предка t_ParentType.

------------------------------------------------------------
--Спецификация объектного типа t_ChildType,                -
--который является наследником : t_ParentType              -
--Добавлен метод присваивания - assign                     -
------------------------------------------------------------
create or replace type t_ChildType under t_ParentType
(
  v_Field2          varchar2(64),
  constructor function t_ChildType(v_pField1 varchar2,
                                   v_pField2 varchar2)
                                   return self as result,
--метод для вызова унаследованного метода getName:
  member function inherited_getName(self in out nocopy t_ChildType)
  return varchar2,
--метод присваивания:
  member procedure assign(self      in out nocopy t_ChildType,
                          v_pObject in out nocopy t_ChildType),
  
  overriding member function getName(self in out nocopy t_ChildType)
  return varchar2
)
not final;
------------------------------------------------------------
--Тело объектного типа t_ChildType                         -
------------------------------------------------------------
create or replace type body t_ChildType is
  constructor function t_ChildType(v_pField1 varchar2,
                                   v_pField2 varchar2)
                                   return self as result is
  begin
    self.v_Field1 := v_pField1; 
    self.v_Field2 := v_pField2; 
    return;
  end;
  member function inherited_getName(self in out nocopy t_ChildType)
  return varchar2 is
    v_xInheritedObject t_ParentType;     --экземпляр объекта-предка
    v_xRes             varchar2(32);
  begin
-- создаем экземпляр предка с помощью копирующего конструктора
    v_xInheritedObject := new t_ParentType(self);     
-- вызываем метод getName класса-предка
    v_xRes             := v_xInheritedObject.getName; 
-- в общем случае вызов метода предка мог изменить поля
    self.assign(v_xInheritedObject); 
-- поэтому необходимо обратно скопировать их в текущий объект (self)
    return v_xRes;
  end;
  ----------------------------------------------------------
  -- метод присваивания:                                   -
  -- просто копируем все поля-объекта источника в текущий  -
  -- экземпляр (self)                                      -
  ----------------------------------------------------------
  member procedure assign(v_pObject in out nocopy t_ChildType) is
  begin
    self.v_Field1 := v_pObject.v_Field1;
    self.v_Field2 := v_pObject.v_Field2;
  end;                               
  ----------------------------------------------------------
  -- переопределенный метод getName:                       -
  -- через вызов inherited_getName происходит обращение к  -
  -- унаследованному методу getName                        -
  ---------------------------------------------------------- 
  overriding member function getName(self in out nocopy t_ChildType)
  return varchar2 is
  begin
    return inherited_getName || '-' || v_Field2;
  end;
end;

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

Например: в следующей иерархии классов: t_ParentType -> t_ChildType -> t_SubChildType для вызова метода произвольного типа-предка можно использовать следующие правило: к имени метода добавляется цифра - номер уровня в иерархии. В этом случае имена методов-оболочек соответственно будут выглядеть следующим образом: getName0->getName1->getName2

Наследование конструкторов

Очередная трудность связана с тем, что в PL/SQL не поддерживает прямой вызов унаследованного конструктора. (Проще говоря, конструкторы базового типа не наследуются!). Например: пусть у нас есть класс t_ParentType в котором определен пользовательский (user-defined) конструктор:

----------------------------------------------------------------
--спецификация объектного типа t_ParentType:                   -
----------------------------------------------------------------
create or replace type t_ParentType as object
(
  v_Field1 varchar2(32),
  constructor function t_ParentType(v_pName varchar2)
  return self as result
)
not final;
----------------------------------------------------------------
--тело объектного типа t_ParentType:                           -
----------------------------------------------------------------
create or replace type body t_ParentType as
  constructor function t_ParentType(v_pName varchar2)
  return self as result is
  begin
    self.v_Field1 := v_pName;    
    return;
  end;
end;

Теперь мы определяем объектный тип t_ChildType, который является наследником t_ParentType. В типе t_ChildType также определен пользовательский конструктор:

----------------------------------------------------------------
--Спецификация объектного типа t_ChildType,                    -
--который является наследником : t_ParentType                  -
----------------------------------------------------------------
create or replace type t_ChildType under t_ParentType
(
  v_Field2 varchar2(64),
  constructor function t_ChildType(v_pName        varchar2,
                                   v_pDescription varchar2)
                                   return self as result
);

В реализации конструктора типа t_ChildType попытаемся вызвать унаследованный конструктор:

----------------------------------------------------------------
--Тело объектного типа t_ChildType                             -
--в конструкторе необходимо вызвать унаследованный конструктор -
----------------------------------------------------------------
create or replace type body t_ChildType is
  constructor function t_ChildType(v_pName        varchar2,
                                   v_pDescription varchar2)
                                   return self as result is
  begin
    t_ParentType(v_pName => v_pName);
    self.v_Field2 := v_pDescription; 
    return;
  end;
end;

Выясняется, что сделать это не удается:

liNE/COL ERROR
-------- -----------------------------------------------------------------
6/5      PLS-00306: wrong number or types of arguments in call to
         'T_PARENTTYPE'

Итак: как же вызвать конструктор родительского типа, чтобы не дублирвать уже реализованный в нем код ?

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

----------------------------------------------------------------
--Спецификация объектного типа t_ChildType,                    -
--который является наследником : t_ParentType                  -
--добавлен метод присваивания assign                           -
----------------------------------------------------------------
create or replace type t_ChildType under t_ParentType
(
  v_Field2          varchar2(64),
  constructor function t_ChildType(v_pName        varchar2,
                                   v_pDescription varchar2)
                                   return self as result,
  member procedure assign(self in out nocopy t_ChildType,
  v_pObject in out nocopy t_ParentType),
  member function getName
  return varchar2
);
----------------------------------------------------------------
--Тело объектного типа t_ChildType                             -
--в конструкторе вызывается конструктор базового типа          -
----------------------------------------------------------------
create or replace type body t_ChildType is
  constructor function t_ChildType(v_pName        varchar2,
                       v_pDescription varchar2)
                       return self as result is
--экземпляр объекта-предка
    v_xInheritedObject t_ParentType;                            
  begin
--вызов конструктора базового типа
    v_xInheritedObject := new t_ParentType(v_pName => v_pName); 
-передача данных текущему экземпляру
    self.assign(v_xInheritedObject);    
                        -
    self.v_Field2 := v_pDescription; 
    return;
  end;
  --------------------------------------------------------------
  --метод присваивания экземпляра базового типа текущему       -
  --оьъекту (self)                                             -
  --------------------------------------------------------------
  member procedure assign(self      in out nocopy t_ChildType,
                          v_pObject in out nocopy t_ParentType) is
  begin
    self.v_Field1 := v_pObject.v_Field1;
  end;                               
  member function getName
  return varchar2 is
  begin
    return self.v_Field1 || ' - ' || self.v_Field2; 
  end;
end;

Вышеописанная методика демонстрируется в данном примере.

Реализация констант-атрибутов типа

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

create or replace type t_ParentType as object
  (
    v_Name  varchar2(50),
    static v_Const varchar2(32) := 'Scott Tiger'
  );

Увы, мы получаем ошибку:

ORA-06545: PL/SQL: compilation error - compilation aborted
ORA-06550: line 5, column 12:
PLS-00103: Encountered the symbol "V_CONST" when expecting
one of the following:
function procedure

Для реализации таких атрибутов можно использовать статический метод, который бы возвращал требуемое значение. Если значение атрибута также имеет объектный тип, то в качестве места хранения значения такого атрибута можно использовать вспомогательный пакет. Для защиты переменной от модификации необходимо поместить её объявление в тело пакета.

Следующий листинг реализует данный подход:

--------------------------------------------------------------
  --Значение данного типа должен иметь атрибут объектного типа -
  --------------------------------------------------------------
  create or replace type t_DictConst as object                
  (
    v_Id          number(9),
    v_Name        varchar2(50),
    v_Code        varchar2(15),
    v_Description varchar2(250)
  );
  --------------------------------------------------------------
  --Спецификация вспомогательного пакета для типа t_ParentType:-
  --функция getConst возвращает объект типа t_DictConst        -
  --------------------------------------------------------------
  create or replace package serv$ParentType is
    function getConst return t_DictConst;
  end;
  --------------------------------------------------------------
  --Тело пакета: объект-константа формируется в процедуре init -
  --------------------------------------------------------------
  create or replace package body serv$ParentType is 
  
    v_gDictConst t_DictConst;
    function getConst return t_DictConst is
    begin
      return v_gDictConst;
    end;
    procedure init is
    begin
      v_gDictConst := new t_DictConst(1,'Scott Tiger',
      '01','Scott Tiger - Oracle demo-user'); 
    end;
  begin
    init;
  end;

Следующий объектный тип реализует статический метод, который возвращает объект-константу:

  --------------------------------------------------------------
  --Спецификация объектного типа t_ParentType                  -
  --Статический метод возвращает константу                     -
  --------------------------------------------------------------
  create or replace type t_ParentType as object
  (
    v_Name varchar2(50),
    
    static function getConst return t_DictConst
  );
  create or replace type body t_ParentType is
  
    static function getConst return t_DictConst is
    begin
      return serv$ParentType.getConst;
    end;
  end;

Вышеописанная методика демонстрируется в данном примере.

Заключение

Мы рассмотрели методы решения наиболее часто встречающихся проблем при использовании объектно-ориентированных возможностей PL/SQL. Конечно, многие проблемы могут быть решены только самими разработчика корпорации Oracle. Например, отсутствие защищенных полей объектного типа (так называемых private-полей), отсутствие поддержки интерфейсов и т.д.

Будем надеяться, что в следующих версиях Oracle Database эти недоработки будут устранены.

Дополнительная информация

За дополнительной информацией обращайтесь в компанию Interface Ltd.

Обсудить на форуме Oracle

Рекомендовать страницу

INTERFACE Ltd.
Телефон/Факс: +7 (495) 925-0049
Отправить E-Mail
http://www.interface.ru
Rambler's Top100
Ваши замечания и предложения отправляйте редактору
По техническим вопросам обращайтесь к вебмастеру
Дата публикации: 20.03.06