Август/Сентябрь 2003


Тема номера: Объектно-реляционная технология Oracle9i


Чак Кизер

Предмет желаний
(The Object of Your Desire,
by Chuck Kueser)

Источник: журнал “Oracle Professional”, март, 2003, http://www.pinnaclepublising.com/op/OPmag.nsf/WebIndexByIssueSecure/268b0ff29259898585256ce100677bd7

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

Объектно-ориентированная технология появилась в Oracle версии 8, а в следующих релизах расширялась и развивалась. Однако она казалась незаметной и недоработанной возможностью. Основной причиной этого являются несовместимость с существующими системами и методологический дискомфорт у разработчиков. Мы столкнулись с этим фактом недавно, когда нас попросили помочь спроектировать новую схему. Это было приложение для ведения Главной Книги (GL), которое казалось весьма подходящим для объектно-ориентированного (OO) проектирования. Мы провели предварительное ОО-проектирование и были поражены негативными результатами. Разработчикам было очень неудобно работать с ОО и его SQL. В этой статье я помогу перевести упрощенное GL-приложение из традиционной реляционной методологии в объектно-ориентированную.

Поговорим об отношениях

Наше приложение для ведения главной книги будет состоять из четырех таблиц:

  • Таблица acct_lookup, которая содержит все системные ID и соответствующий текст с описанием.
  • Таблица gl_master с системными ID активных субсчетов и различными датами.
  • Таблица acct_pay, которая связана с таблицей gl_master по системному ID и содержит ID активных субсчетов.
  • Таблица acct_money, которая связана с таблицей acct_pay по ID субсчета и содержит суммы, связанные с этим ID и признак “по дебету/по кредиту”.

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

Из схемы видно, что это более-менее третья нормальная форма с отношением один-ко-многим между таблицами acct_pay и acct_money, и между таблицами acct_lookup и gl_master. Есть также отношение один-к-одному между таблицами acct_pay и gl_master. Эти таблицы создаются и управляются с помощью обычных DDL (data definition language – языка определения данных) и DML (data manipulation language – языка манипулирования данными) команд. В схеме может потребоваться также логика, чтобы гарантировать, что в таблицу acct_lookup записан корректный sysid до того, как он распространится на таблицы gl_master и acct_pay. Похожая логика может потребоваться для столбца acct в таблицах acct_pay и acct_money. Так как все суммы денег хранятся в виде абсолютных значений, то поле db_cr обозначает знак. Чтобы определить знак некоторого значения суммы, необходимо использовать следующий запрос:

select decode(db_cr,'c',(-1)*money,'d',money) 
     from acct_money
     where acct='account id' and money='money value';

Сумму денег на заданном субсчете и отчет по ID субсчетов с суммами для каждого sysid можно получить с помощью следующих запросов:

select sum(decode(db_cr,'c',(-1)*money,'d',money)) 
     from acct_money 
     where acct='account_id';
 
 select a.sysid,entry_date,settle_date,call_date,b.acct,
     decode(db_cr,'c',(-1)*money,'d',money) 
         from gl_master a,acct_pay b,acct_money c
             where a.sysid=b.sysid and 
                b.acct=c.acct;

Перейдем к объектной версии

Объектно-ориентированная версия нашей ERD показана на Рис. 2.

Как видно из рисунка, остались те же четыре таблицы, но с некоторыми изменениями в структуре. Таблица acct_money вложена в таблицу acct_pay, а отношение между таблицами acct_lookup и gl_master реализовано с использованием ref-указателя при описании столбца sysid таблицы gl_master. Отношение один-к-одному между gl_master и acct_pay необходимо поддерживать, так как требуется доступ к таблице acct_pay без таблицы gl_master, а такой дизайн упрощает SQL. Другое отличие – это методы db_cr_cnvrt и total_money. Более подробно рассмотрим их позднее, а сейчас нашей целью является освобождение от функции decode, требовавшейся ранее, и упрощение отчета. Листинг 1 содержит DDL для таблицы acct_lookup и таблицы gl_master.

Листинг 1. DDL для таблиц acct_lookup и gl_master.

create or replace type acct_type as object (
 sysid varchar2(4),
 acct_text varchar2(20))
 /

 create table acct_lookup of acct_type
 /
 create table gl_master ( 
 sysid ref acct_type scope is acct_lookup,
 entry_date date,
 settle_date date,
 call_date date)
 /

Как можно заметить, первый шаг – это создание объектного типа с атрибутами, присущими соответствующей таблице. Это тип данных, определенный пользователем, который представляет собой шаблон. Он может использоваться как основа для многих или нескольких таблиц, в которых требуются эти же атрибуты. Первое предложение create table – это просто создание таблицы соответствующего объектного типа. Обратите внимание на оператор ref в таблице gl_master. Этот оператор устанавливает неявное отношение между таблицей gl_master и объектом acct_type. Оператор scope ограничивает эту ссылку только таблицей acct_lookup. Его по возможности следует указывать всегда. Использование этого оператора уменьшает размер требуемой памяти и позволяет оптимизатору более эффективно управлять запросами, связанные по ref. Ссылки по своей сути являются отношениями, как и внешние ключи, за исключением того, что их существование не требуется. Про ref-ссылку без соответствующей строки говорят, что она “висящая”. Предикат "is dangling" ("висящая") предусмотрен для проверки этого условия и используется, в основном, так же как предикат "is null".

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

Таблицы acct_pay и acct_money

Следующие объекты, которые мы рассмотрим, это таблицы acct_pay и acct_money. Как видно из рис.2, таблица acct_money вложена в таблицу acct_pay. Такая структура применена для поддержки отношения один-ко-многим, основанного на поле acct. Кроме того, таблицы отличаются наличием методов. Метод – это часть кода, который реализует логику, связанную с объектом. При рассмотрении метода db_cr_cnvrt вспомните, что наши данные хранятся без знака. Признак db_cr показывает знак. В традиционном подходе это приводит к необходимости формирования предложения select с функцией decode. Оно выполняется, однако неудобно при кодировании и поддержке. Наш метод, который будет кратко продемонстрирован, позволяет сформировать простой select. Листинг 2 содержит DDL для создания этих двух таблиц.

Листинг 2. DDL для создания двух объектных таблиц.

create or replace type money_type as object (
 acct varchar2(4),
 db_cr char(1) ,
 money number(8,2),
 member function db_cr_cnvrt return number)
/
create or replace type money_tab as table of money_type
/
create or replace type acct_pay_type as object (
 sysid varchar2(4),
 acct_money money_tab,

 member function total_money return number)
/
create table acct_pay of acct_pay_type (
 primary key (sysid))
 object id primary key
 nested table acct_money store as acct_money_tab
 return as locator
/

Первым предложением Листинга 2 просто создается объектный тип, как было рассмотрено выше, содержащий соответствующий метод. Вторым создается тип, который представляет собой таблицу предыдущего типа. Затем обратите внимание на поле acct_money. Оно имеет тип данных money_tab, который является только что описанным типом данных. Четвертое предложение – весьма интересно. С его помощью создается таблица acct_pay_type и первичный ключ, object_id описывается как этот ключ, вложенная таблица именуется как acct_money_tab и сообщается ядру о необходимости возвратить locator. ID создаются и индексируются автоматически для всех объектов. Представьте, что это псевдо-столбцы, которые работают также как первичный ключ, занимающий 16 байтов. Идентифицируя первичный ключ как OID, можно сохранить дополнительную память и уменьшить издержки производительности.

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

Методы

Методы похожи на пакеты базы данных и состоят из двух частей: спецификации и тела. Спецификации созданы в предыдущем DDL и описывают тип метода и возвращаемые значения. Сам PL/SQL-код описывается в теле, как показано на Листинге 3.

Листинг 3. Тело методов.

create or replace type body money_type as
 member function db_cr_cnvrt return number is
 mny number(10,2);
 begin
  if (self.db_cr = 'C') then
    mny:=self.money;
  else
    mny:=self.money*(-1);
  end if;
  return mny;
 end;
 end;
 
create or replace type body acct_pay_type as
 member function total_money return number is

--
 total_sum number(10,2) :=0;
 x int;
--
 begin
  if (utl_coll.is_locator(acct_money)) then
   select sum(a.db_cr_cnvrt()) into total_sum
       from table(cast(acct_money as money_tab)) a;
  else
  for x in 1..acct_money.count loop
    total_sum:=total_sum+self.acct_money(x).db_cr_cnvrt();
   end loop;
  end if;
  return total_sum;
 end;
 end;

Первый метод добавляет знак к соответствующей денежной сумме. Местоимение "self" такого же типа, что и местоимения old и new, используемые в триггерах базы данных. Так как метод ассоциируется с типом, а не с таблицей, self позволяет гибко использовать одно и то же тело для нескольких таблиц без изменений.

Суть метода total_sum (ошибка в статье – не total_sum, а total_money - прим. переводчика) – возвратить итоговую сумму для system ID, он иллюстрирует использование локаторов. Локатор возвращает указатель на объект, а не на значение(я). Это позволяет при обработке данных использовать теорию множеств, а не последовательные методы. Посмотрите на код после предложения else. Это цикл, в котором по очереди просматриваются все записи с заданным system ID, конвертируется знак и суммируются значения. В коде до else тот же результат достигается с использованием локатора. Как видно из этого примера, локатор позволяет использовать функцию sum для суммирования всего набора, используемого в циклическом механизме.

Предложение cast – выражение преобразования, которое возвращает acct_money как тип money_tab. Предложение table – операнд, который делает запросы не вложенными, поэтому они могут обрабатываться реляционным способом, с использованием реляционных операторов – в данном случае функции Sum. Заметьте, что вызывается метод db_cr_cnvrt, который прикреплен к вложенной таблице. Это пример инкапсуляции логики внутри объекта. Логика вычисления итоговых сумм инкапсулирована в объект acct_pay_type, а логика db_cr_cnvrt инкапсулирована в объект money_type.

Манипулирование

На данный момент у нас есть объектные таблицы с инкапсулированной логикой. А что теперь? Далее используется SQL для манипулирования данными. Сначала загрузим данные, а затем выполним несколько select-, update- и последующих delete- предложений. Загрузка записей в таблицу acct_lookup состоит из стандартных операций insert, которые я не буду рассматривать подробно. Однако загрузка таблицы gl_master уже более интересна. Чтобы загрузить одну запись в таблицу gl_master, которая ссылается на запись в таблице acct_lookup, которая имеет sysid, равный 1234, используйте код, представленный на Листинге 4.

Листинг 4. Загрузка одной записи в таблицу gl_master.

insert into gl_master select ref(a),sysdate+540,sysdate+360
       from acct_lookup a where a.sysid='1234'
 
 insert into acct_pay values('7890',
   money_tab(money_type('4321','D',1.92),money_type ('2468','C',4.75)));
 
 insert into acct_pay values('1234',
   money_tab(money_type('4321','C',10.52),money_type ('0864','D',6.25)));
 
 insert into table(select ACCT_MONEY from acct_pay 

 where sysid='1234')
   values('1357','D',12.92);

Первые два предложения добавят записи верхнего уровня '7890' и '1234' с вложенными записями под ними. Последнее вставляет вложенную запись в существующую запись верхнего уровня. Снова можно увидеть использование оператора table для повышения уровня вложенных конструкций. Остальные insert-предложения аналогичны.

Теперь в таблицах есть данные. Что же дальше? На них, конечно, хочется посмотреть. Результат простого select из таблицы acct_pay показан на Листинге 5.

Листинг 5. Просмотр данных таблицы acct_pay.

SYSI ACCT_MONEY(ACCT, DB_CR, MONEY)
 ----- --------------------------------------------------
 7890 MONEY_TAB(MONEY_TYPE('4321', 'D', 1.92), MONEY_TYPE
 => ('2468', 'C', 4.75) )
 
 1234 MONEY_TAB(MONEY_TYPE('4321', 'C', 10.52), MONEY_TYPE
 => ('0864', 'D',6.25) , MONEY_TYPE('1357', 'D', 12.92))

Чтобы извлечь суммарное количество денег или вызвать метод db_cr_cnvrt для заданного значения sysid, просто выполните следующую команду:

select a.total_money() from acct_pay a 
     where a.sysid='1234';
 
 A.TOTAL_MONEY()

 ---------------
           -8.65
 
 select a.acct,a.db_cr_cnvrt(),a.money,a.db_cr
  from table(select acct_money from acct_pay 
  =>where sysid='1234') a;
 
 ACCT A.DB_CR_CNVRT()     MONEY  D
 ---- --------------- ---------- -
 4321           10.52     10.52  C
 0864           -6.25      6.25  D
 1357          -12.92     12.92  D

Соединение таблиц gl_master и acct_lookup дает следующий результат:

select a.sysid.sysid,a.sysid.acct_text,
     a.entry_date,a.settle_date
     from gl_master a where a.sysid is not dangling
 
 SYSI SYSID.ACCT_TEXT   ENTRY_DAT    SETTLE_DA
 ---- ----------------  ---------    ---------

 1234 Deadbeat Ltd      03-MAY-00    25-OCT-01

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

update table(select ACCT_MONEY 
   from acct_pay where sysid='1234')
     set money=15.18 where money=12.92;

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

delete table(select ACCT_MONEY 
   from acct_pay where sysid='1234')
     where money=15.18;

Перемена мышления, которая того стоит

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

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

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

E-mail this page