Модели и смыслы данных в Cache и Oracle

Объектные модели данных

Разбить на страницы
Показывать лекцию целиком

Далее будет рассмотрено расширение модели данных за счёт включения в неё части метаданных. Это позволит работать со структурами данных, которые заранее не определены. Подобные модели принято называть универсальными. Обратим внимание на некоторые их особенности и способы реализации.

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

К сожалению, доступные нам инструментальные средства позволяют получить скрипты только для изолированных классов базы данных. Поэтому примеров создания баз данных с помощью UML не будет.

Объёмистые второй и третий параграфы лекции представляют объектную модель ODMG на примере Cache и объектно-реляционную модель на примере Oracle. Замечательная особенность Cache в том, что связи между иерархической, реляционной и объектной моделями в ней встроены, и потому отслеживаются естественным образом, без каких-либо дополнительных средств.

10.1 Модели данных

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

По крайней мере, некоторые данные должны обладать свойством перси-стентности — способностью сохраняться на диске.

Различия роли типов в программах и базах данных перечислены в таблице 10.1

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

Задать тип данных — это значит определить:

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

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

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

    Для задания модели данных необходимо определить:

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

    Бегло охарактеризуем с этой точки зрения модель "сущность-связь":

  • основные компоненты: сущность, связь, атрибут;
  • сущности бывают сильные и слабые,
  • связи соединяют сущности; связи имеют кратности $$1 : n$$ и $$m : n$$ и бывают идентифицирующие и не идентифицирующие; не идентифицирующие связи делятся на обязательные и не обязательные;
  • агрегаты сущностей и связей не предусматриваются;
  • атрибуты входят в состав сущностей и связей; в связях можно выделить атрибуты привязки и эмерджентные атрибуты;
  • обычно определяются типы данных число, строка, дата;
  • допустимые операции над данными не уточняются, так как модель предназначена только для представления структур данных, но не манипулирования ими;
  • допустимые ограничения целостности — первичный, уникальный и внешний ключи.
  • Иногда полезно поговорить о некоторой модели на языке описания другой модели. Например, описывая объектным языком реляционную модель данных, следовало бы указывать, что типы столбцов могут быть любыми, в том числе, векторными, но внутренняя структура данных всех типов в рамках реляционной модели не подлежит разбору. Таблицы представляют собой векторные типы. Конструкторы этих типов — команды CREATE TABLE и ALTER TABLE. Деструктор — команда DROP TABLE. Строки таблицы это экземпляры типов таблиц. Их конструкторы — команды INSERT, UPDATE, DELETE и TRUNCATE.

    10.1.1 Схемы Джекобса

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

    На множестве имен $$М$$ вводится понятие "R-правило", определенное как выражение вида:

    $$R_j=(R_{j_1},\dots,R_{j_m})$$

    где $$R_{j_1},\dots,R_{j_m}$$ — имена из $$M$$.

    Имя $$R$$ в левой части правила имеет высший порядок, имена $$R_{j_1},\dots,R_{j_m} \in~ M$$ попарно различны, причём одно из них может совпадать с $$R_j$$. Нулевой порядок могут иметь имена стоящие только в правых частях правил.

    Схема базы данных (или просто схема) это конечный набор

    $$S=\{R_j=(R_{j_1},\dots,R_{j_m})\}$$

    R-правил обладающих свойствами:

    левые части попарно различны;

    множества имен нулевого порядка и высших порядков не пересекаются;

    имена в правой части каждого правила уникальны.

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

    Пример схемы:

    Sch={
    Вуз  = (Факультет),
    Факультет    =   (Кафедра, Связь1),
    Кафедра      =   (Название, Сотрудник),
    Сотрудник   =   (ФИО, Должность, Связь2),
    Связь1        = (Кафедра),
    Связь2  = (Сотрудник),
    

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

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

    Правило схемы $$S=\{R_j=(R_{j_1},\dots,R_{j_m})\}$$ определяет правило подстановки $$R_j\to R_{j_1}R_{j_2}\dots R_{j_m}$$..

    Выпишем свойства, определяющие реляционную, иерархическую и сетевую модели данных.

    Характеристическое свойство реляционной модели

    Определение (1). Схема будет реляционной, если в правых частях всех её $$R$$-правил стоят только имена нулевого порядка.

    Установим взаимно однозначное соответствие между $$R$$-правилом и предикатом

    $$R_j(R_{j_1}R_{j_2}\dots R_{j_m})$$

    Тогда определение реляционности схемы можно перефразировать так:

    Определение (2). В реляционной схеме имена отношений не могут совпадать с именами атрибутов.

    Характеристическое свойство иерархической модели

    Определение (1). Схема будет иерархической, если любое имя может встречаться в правой части Д-правила только один раз и в грамматике, определённой схемой, не существует последовательности выводов такой, что $$R_i$$ выводимо из $$R_i$$.

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

    Характеристическое свойство сетевой модели

    Определение (1). Схема будет сетевой, если нет правила, в котором имя высшего порядка встречается одновременно и в правой и в левой части.

    Определение (2). В сетевой модели имена отношений не могут совпадать с именами атрибутов в рамках одного предиката.

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

    Дальнейшее рассмотрение схем Джекобса связано с введением формализма многосортной логики, который оказывается связанным с тремя перечисленными моделями данных. Но поскольку требуемый уровень изложения существенно выше принятого в книге, которую вы читаете, мы ограничимся указанием на то, что соответствующая теория была построена и отсылаем Вас к первоисточникам или к изложению этого вопроса в книге М.Ш. Цаленко [см. раздел "Что читать?"].

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

    10.1.2 UML как обобщённая объектная модель

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

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

    В объектно-реляционных СУБД за основу берётся реляционная модель, которую расширяют системой объектных типов данных, конструируемых пользователем. Язык запросов представляет собой расширение SQL.

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

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

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

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

    Рассмотрим язык UML (Unified Modeling Language) обладающий более широкими возможностями. Отметим, что термин "унифицированный" не означает претензий на универсальность. Просто UML был создан путем объединения как минимум трех языков концептуального моделирования.

    Для построения реляционных, объектных и объектно-реляционных баз данных из определённых в UML-2 тринадцати видов диаграмм нам достаточно диаграмм классов. Это структурные диаграммы, определяющие наборы классов, их атрибуты, операторы и связи между ними.

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

    Возможно четыре варианта обозначения класса, приведенные на рисунке 10.1.

    (рис 10.1) Изображения класса

    В таблице 10.1 приведен пример класса.

    Пример класса
    Счёт №...
    сальдо (остаток)
    проверить()
    зачислисть()
    овердрафтНаСумму()

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

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

    В частности, с точки зрения объектно-реляционной модели рассматриваемой ниже (в разделе 10.3), реляционная таблица рассматривается как класс без операций. Вызов операции может обеспечить доступ к скрытым атрибутам (вспомните методы get() и set()) и изменять его состояние.

    Вспомним, что активным называют объект способный создать управляющее воздействие. Четвёртая секция (рисунок 10.1), используемая в активных классах, представляет сигналы, управляющие поведением класса. В частности, сигналы — это исключительные ситуации.

    Определение. Диаграмма классов — это набор классов и cвязей между ними.

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

    Вернёмся к описанию классов.

    Для записи атрибутов используется следующий синтаксис:

    квантор_видимости имя_атрибута [кратность_атрибута]: тип_атрибута=нач_значение {строка_свойство}.
    

    Области видимости атрибутов обозначаются знаками:

  • + общедоступный (public),
  • # защищенный (protected),
  • - закрытый (private).
  • Область видимости protected может иметь разный смысл в различных языках. Например, защищённый атрибут может быть виден только внутри пакета.

    Кратность атрибута характеризует наличие ни одного (0) или нескольких атрибутов данного типа. Например:

    [0..1] — иногда есть, иногда нет

    [5, 7..9]— любое из перечисленных чисел 5,7,8,9.

    * — любая кратность

    Примеры записей атрибутов: +получитьАдрес : Address #Цена : {$200}

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

    квантор_видимости имя_операции(список_параметров): тип_возвращаемого_значения {строка_свойство}
    

    Квантор видимости принимает те же значения + , — и #.

    Строка-свойство в этом случае обозначает особые свойства операции. Например, {query} означает, что операция не может изменять данные, то есть является запросом.

    Список параметров:

    вид_параметра имя_параметра тип_параметра = значение_по_умолчанию
    вид_параметра: {in, out, inout} — входной, выходной, входной и выходной
    

    Для атрибутов и операций можно задавать область действия. Для свойств существует два варианта:

  • instance — каждый экземпляр имеет своё значение свойства;
  • static — для всех экземпляров одно значение. В UML различают следующие виды связей:
  • зависимости (dependency relationship);
  • ассоциации (association relationship);
  • обобщения (generation relationship), это взгляд на наследование снизу вверх;
  • реализации (realization relationship);
  • агрегации (aggregation relationship);
  • композиции (composition relationship).
  • Кратко рассмотрим связи за исключением реализации.

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

    При проектировании реляционных баз связи-зависимости реализовать невозможно.

    Зависимости изображаются штриховой линией со стрелкой, направленной к классу-источнику зависимости (рисунок 10.2).

    (рис 10.2) Пример связи-зависимости

    Определение. Связи-ассоциации классифицируются по числу соединяемых классов. Обозначаются они сплошными линиями. Выделяют бинарные связи (соединяют два класса), тернарные (три класса) и N-арные. Для бинарных связей может быть указан порядок соединяемых классов. Так, в примере на рисунке 10.3 связь читается так: "Мама моет раму".

    (рис 10.3) Пример связи-ассоциации

    Концы связи-ассоциации можно помечать именами ролей, для которых могут быть указаны кратности связи. Назначения ролей такие же, как в ER-диаграммах (см. раздел 2.2.6). Кратности роли указывают, сколько объектов с этой ролью могут участвовать в ассоциации. Так, кратность 1 указывает на обязательность связи, кратность 0..1 означает её необязательность. Задание диапазона 1..* говорит о том, что все объекты должны участвовать в каком-нибудь экземпляре ассоциации и что число объектов, участвующих в одном экземпляре ассоциации не ограничено.

    В примере на рисунке 10.4 в показано, что работник обязательно работает равно в одном отделе, а отдел может существовать вообще без работников, но может иметь до 15 человек.

    (рис 10.4) Пример связи-ассоциации с кратностью

    Как вы уже поняли, ассоциации реализуются в реляционной модели.

    Определение. Связью-обобщением называется связь между более общим классом, называемым родителем или предком, и более специализированным классом, называемым потомком или подклассом.

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

    (рис 10.5) Пример связи-обобщения

    Множественное наследование порождает проблему именования атрибутов и операций в подклассе. При совпадении имён атрибутов и операций у родительских классов, необходимо уточнить их семантику и либо переименовать одно из имён, либо запретить наследование от одного из родительских классов, либо унаследовать всё, а затем в потомке произвести переименование.

    В Cache допускается множественное наследование. При совпадении имён элементов классов-предков действует определение элемента последнего класса в списке предков. Исключение составляют ключевые слова класса, наследуемые от первого предка в списке.

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

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

    (рис 10.6) Пример связи-агрегации

    В нашем примере предполагается, что столы и стулья могут существовать независимо от аудитории. Свои функции они могут исполнять, например, в коридоре.

    Агрегатов в реляционной модели нет.

    Определение. Связи-композиции — это агрегации, у которых связь компонентов и агрегата настолько сильна, что уничтожение агрегата приводит к уничтожению его составных частей (рисунок 10.7)

    (рис 10.7) Пример связи-композиции

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

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

    В объектных и объектно-реляционных моделях данных использование инструментария UML даёт значительно больший эффект.

    10.2 Объектная модель данных

    В настоящее время имеется два основных претендента на ведущую роль в применении объектов в базах данных. Это объектно-реляционные базы, стандартизованные в рамках SQL3 и объектные базы в стандартах консорциума ODMG (Object Data Management Group).

    Объектную модель ODMG можно считать расширением языков ООП на персистентные классы, объекты которых могут сохраняться в базе данных. Естественно, в СУБД, реализующей такую модель, необходимо создать свою систему хранения объектов.

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

    10.2.1 Особенности архитектуры Cache. Система классов

    Объектная система Cache построена на объектном расширении языка ObjectScript, который изначально был персистентным. Уникальная особенность системы в том, что она позволяет работать с данными одновременно в объектной, реляционной и иерархической моделях, не заботясь ни о каких отображениях (mappings). Заметим, что сама фирма Intersystems, скорее, не согласится с такой трактовкой её детища. Для неё Cache, в первую очередь, объектная СУБД. Однако, легко получаемые в Cache дедуктивная, полуструктурированная и другие модели, включая так называемые NoSQL, позволяют считать Cache, по крайней мере, потенциально полимодельной СУБД.

    Универсальная архитектура Cache

    В универсальной архитектуре Cache (рисунок 10.8, таблица 10.2) и объекты и таблицы отображаются в многомерные структуры, хранением которых заведует механизм многомерной памяти. Унифицированные структуры данных доступны и серверу объектов и SQL-серверу. Программное обеспечение промежуточного уровня может обращаться к одному из этих серверов. Шлюз SQL позволяет обмениваться данными с другими базами реляционного типа.

    (рис 10.8) Универсальная архитектуря Cache
    Универсальная архитектура Cache
    Middleware/Application Промежуточное ПО/Приложение
    SQL Database База данных SQL
    Objects Объекты
    Unified Data Arhitecture Унифицированная архитектура данных
    Multidimensional Storage Engine Средство многомерного хранения

    SQL-доступ в Cache допускает использование интерфейсов ODBC, JDBC и ADO.Net. Поддерживается связывание данных с объектно-ориентированными языками, включая Java, C# и C++.

    Система классов

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

    Можно считать понятия класса и типа синонимами. Как и в естественных языках, объёмы понятий-синонимов перекрываются, но не совпадают. Предопределённые типы данных инкапсулированы, то есть их определения не доступны для изменения. Однако, пользовательские типы открыты. Обычно классы выстраиваются в иерархию наследования. У типов этого нет. Типы данных не могут содержать свойств. Для типов данных невозможно создавать экземпляры.

    В Cache принята следующая терминология. Классы вообще подразделяются на классы типов данных и классы объектов (рисунок 10.9). Классы типов данных определяют допустимые значения констант (литералов) и позволяют их контролировать. Классы типов данных содержат предопределённые наборы методов проверки и приведения значений атрибутов к другим типам.

    (рис 10.9) Таксономия классов Cache

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

    Незарегистрированные классы (Non-Registered Class) предназначены для создания пользовательской объектной системы и не имеют предопределенного поведения. Объектные ссылки для них не формируются. Разработка функций незарегистрированного класса —обязанность разработчика.

    Объекты зарегистрированных классов и их наследники имеют объектную ссылку OREF (object reference), идентифицирующую объект, находящийся в памяти компьютера.

    Хранимые объекты имеют вторую ссылку OID (object ID), идентифицирующую объект на физическом носителе.

    Зарегистрированные классы это временные классы, обладающие предопределенным поведением. Реализуется оно набором встроенных функций, наследуемых из системного класса %RegisteredObject и отвечающих, в частности, за создание новых объектов и за управление размещением объектов в памяти. Заметим, что знак процента в имени означает, что класс или метод системные.

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

    Зарегистрированные классы могут быть ещё хранимыми и встраиваемыми. Первые хранятся независимо и потому имеют уникальную не изменяемую объектную ссылку OID, по которой объект может быть найден на диске, и ссылку OREF.

    Встраиваемые классы могут попасть на диск только в составе хранимого класса и потому OID не имеют, но при выгрузке в память для них создаётся соответствующая ссылка OREF. Встраиваемые классы наследуют свое поведение от класса %Serial.

    Хранимые классы наследуют свое поведение от класса %Persistent, используя обширный набор его методов, включающий создание объекта, подкачку объекта из базы в память, удаление объекта и т.д. Каждый экземпляр хранимого класса имеет два уникальных идентификатора — OID и OREF.

    Более подробно методы хранимых классов будут рассмотрены ниже.

    10.2.2 Работа с классами

    Классы

    Для того, чтобы определить класс (создать спецификацию класса) необходимо определить элементы его спецификации, перечисленные ниже:

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

    Уникальное имя класса, свойства (атрибуты) и методы понимаются в обычном для ООП смысле. Другие элементы:

  • Параметры. Позволяют изменить возможности класса во время его компиляции. При этом обычно используют генераторы методов.
  • Запросы — это операции с объектами класса, играющие роль фильтров.
  • Индексы. Эти не обычные для ООП элементы используются, как и в реляционной модели, для ускорения доступа.
  • Триггеры. Определяются в рамках объектной модели, так как в действительности и для таблиц, и для классов создаётся единственная структура. Используются триггеры только в реляционной модели, так как активность в объектной модели реализуется методами классов.
  • Свойства

    Свойства представляют собой константы предопределенных типов, ссылки на объекты, встроенные объекты, потоки данных (BLOB, CLOB), коллекции, древесные значения и отношения. Коллекции могут быть массивами и списками.

    Несколько примеров задания свойств:

  • Переменная строчного типа

    Property Name As %String(MAXLEN = 20);
    
  • Ссылка на объект. Пусть имеется хранимый класс Address. Тогда свойство Address можно описать так
    Property Address As Address;
    
  • Встроенный объект. Пусть имеется встраиваемый класс Address. Свойство Address записывается точно так же как в предыдущем варианте

    Property Address As Address;
    

    но речь идет не о ссылке, а о встраивании объекта.

  • Свойства-отношения используются и в расширенной реляционной модели, и в объектной. Для независимых объектов поддерживается тип один-ко-многим, а для зависимых родитель-потомок (потомок зависит от родителя). Отношение подобно свойству содержащему две объектные ссылки. При этом обеспечены ссылочная целостность. В результате, при удалении объекта не может появиться висячая ссылка на другой стороне отношения. На одной стороне отношения (многие или потомок) помещается как бы простое свойство, а на другой стороне коллекция, наследующая системному классу %RelationshipObject.

    В определении класса Cache можно задать вариант хранения свойств, указывая одно из ключевых слов transient, calculated, multidimensional. Задание по умолчанию определяет свойство, которое имеется в памяти и хранится в базе данных.

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

    Если свойство объявлено как вычисляемое (calculated), то его значения будут вычисляться каждый раз при выполнении. Для свойств этого вида разработчик должен сам создать метод Get() производящий необходимые вычисления.

    Ключевое слово multidimensional означает, что свойство представляет многомерный массив.

    Коллекции можно рассматривать как отношения $$1 : n$$.

    Методы

    Как всегда в ООП, различают метод класса и метод экземпляра (объекта). Метод класса не имеет доступа к свойствам или методам экземпляров, однако имеет доступ к параметрам классов. Типичный пример метода класса — конструктор %New(), который не применим к экземпляру класса, но порождает его.

    В коде метода можно определить язык, на котором метод написан. Тексты на языках ObjectScript и Cache Basic транслируются в исполняемый код. Методы написанные на Java выполняются в среде виртуальной машины Java.

    Методы могут возвращать данные любого допустимого типа.

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

    Cache поддерживает четыре типа методов:

  • Методы-коды (Code Methods);
  • Методы-выражения (Expression Methods);
  • Методы-вызовы (Call Methods);
  • Методы-генераторы (Method Generators).
  • Метод-код представляет собой программу, написанную на языках Object-Script, Cache Basic или Java. При использовании первых двух языков в код метода можно включать макросы, встроенный SQL, встроенные HTML и JavaScript.

    Метод-выражение содержит одно вычисляемое выражение, после своего вызова вычисляет его и возвращает результат.

    Метод-вызов обращается к внешней для класса программе.

    Метод-генератор содержит код, предназначенный для порождения кода на языке ObjectScript. Используется только при компиляции класса для генерации версии времени исполнения.

    Создаём класс

    Создадим простейший класс с единственным атрибутом Name. Запускаем Cache Studio. Выбираем в меню пункт Файл-Создать (рисунок 10.10).

    (рис 10.10) Создание класса

    Затем выделяем иконку "Класс Cache" и нажимаем кнопку "OK". Появляется первый экран мастера создания класса (рисунок 10.11).

    (рис 10.11) Задание имени и комментария

    Пакет, который предлагается указать, служит для объединения некоторой совокупности родственных классов. Пакеты %SYS, DOCBOOK, SAMPLES и USER создаются СУБД при инсталляции.

    Оставляем имя пакета по умолчанию (User) и указываем имя класса. Можно дополнить описание класса комментарием, который как всегда будет проигнорирован компилятором. Кнопкой "Далее >" переходим к следующему шагу (рисунок 10.12).

    (рис 10.12) Выбор типа класса

    Здесь мастер предлагает нам выбрать тип объекта (таблица 10.3).

    Выбор типа объекта
    Persistent Тип объектов хранимых в базе
    Serial встраиваемые объекты; экземпляры классов этого типа могут сохраняться в базе только в случае включения в хранимые объекты
    Registered тип объектов, которые могут существовать только в оперативной памяти без возможности их сохранения на диске
    Abstract тип объектов у которых не может быть экземпляров
    Data Type (тип данных) указывает на то, что данный класс будет типом данных
    CSP класс типа CacheServerPage, предназначенный для построения серверных страниц Cache
    Extends (расширяет) указывает на то, что данный класс является наследником некоторого уже существующего типа или типов; организовать множественное наследование можно перечислив через запятую имена классов родителей

    Выбираем тип Persistent и кнопкой "Далее >" продолжаем создание класса (рисунок 10.13).

    (рис 10.13) Дополнительные характеристики класса

    В последнем окне, изображенном на рисунке 10.15, мастер предлагает указать дополнительные характеристики. Их значения станут частично понятными из дальнейшего изложения, а пока давайте оставим их пустыми и завершим создание кнопкой "Готово".

    Пополним созданный класс свойством Name. Для этого выбираем пункт меню Класс > Добавить > Новое свойство для запуска мастера (рисунок 10.14).

    Указываем имя свойства Name и, если есть желание, добавляем комментарий.

    (рис 10.14) Задание имени свойства и его описания

    Идём далее (рисунок 10.15).

    (рис 10.15) Тип свойства

    Выбираем тип создаваемого свойства.

    В Cache поддерживаются несколько типов свойств:

  • Единичное значения типа. Простые типы данных (%String, %Integer, %Date и т.д.) наследуют свое поведение от класса типов данных.
  • Коллекция типа. Объектные базы, в отличие от реляционных, могут хранить множество значений в одном свойстве. В Cache поддерживаются два типа коллекций — массив и список. Элементы массива имеют индекс, уникально характеризующий элемент в массиве. Элемент списка определяется номером его позиции в списке.
  • Отношение. Связи представляют собой двунаправленные зависимости между хранимыми объектами. В Cache реализовано два типа связей — Parent - Child (родитель - потомок) и One - Many (один-ко-многим). Первая связь зависимая, то есть при удалении предка, автоматически удаляются все наследники. Связь One-Many независимая связь, поэтому удаление предка при существовании наследников приводит к ошибке.
  • Логично предположить, что свойство Name является строкой. Поэтому не меняя типа переходим к следующему шагу (рисунок 10.16).

    (рис 10.16) Характеристики свойства

    Здесь необходимо небольшое пояснение. Первый пункт аналогичен заданию свойства NOT NULL для полей SQL-таблиц, второй — создает индекс для определяемого свойства, третий — аналогичен ограничению UNIQUE , четвертый указывает на то, что данное свойство является вычислимым, то есть его значение вычисляется во время обращения к нему. Ниже будет установлено, что при создании класса определяется соответствующая ему таблица. Её имя может отличаться от имени класса. Если необходимо, чтобы имена свойств и соответствующих им столбцов таблиц не совпадали, укажите имя SQL-столбца.

    Характеристики свойства по умолчанию оставим без изменения. Остаётся ещё раз нажать кнопку "Далее >". Появится список параметров созданного свойства (рисунок 10.17).

    (рис 10.17) Параметры свойства

    Изменим в нём значение параметра MAXLEN "20". На последнем шаге мастер предлагает переопределить методы доступа к свойствам Get и Set. Мы не станем этого делать и завершим создание свойства Name нажатием на кнопку "Готово" (рисунок 10.18).

    (рис 10.18) Переопределение методов Set() и Get()

    Теперь в окне Studio мы получим следующий текст

    Class User.A Extends %Persistent {
    Property Name As %String(MAXLEN = 20); }
    

    Чтобы класс стал доступным для использования, его нужно откомпилировать (позиция меню "Собрать > Компилировать").

    Выясним, как создаются экземпляры класса (объекты) и где они хранятся.

    Запустим портал управления системой, выбрав пункт " Утилиты > Портал управления системой" в главном меню Studio (рисунок 10.19). В управлении данными выбираем раздел Глобалы.

    (рис 10.19) Раздел "Глобалы"

    Затем переходим в область User. Проверим, не существует ли глобал с именем ^User.AD. Его имя составляется из имени класса с приписанным суффиксом "D" (данные). Смысл этого таинственного действия в том, что любая хранимая структура образует глобал, и экземпляры класса A будут храниться именно в этом глобале. Скорее всего, глобал ^User.AD там не обнаружится. Точнее, наши предыдущие действия не создавали такого гло-бала. Но, если ^User.AD существует, удалим его.

    После этого запускаем терминал. В нём создаем экземпляр класса с помощью метода-конструктора %New(): |uSER>s ss=##class(User.A).%New()

    Макроподстановка ##class создает объектную ссылку OREF. Что же представляет собой эта ссылка?

    USER>w ss 
    1@User.A
    

    Итак, OREF состоит из двух частей имени класса "User.A" и идентификатора объекта "1".

    Можно убедиться, что глобал ^User.AD ещё не появился.

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

    USER>s ss.Name="John"  // параметру Name объекта №1 присвоено значение.
    USER>d ss.%Save()  // объект №1 сохранен на диске.
    USER>d ss.%Close()  // объект №1 закрыт, то есть удален из памяти.
    

    С помощью портала управления системой обнаруживаем созданный глобал ^User.AD (рисунок 10.20). Забегая вперёд, отметим, что при использовании индексов может быть создан ещё глобал индекса с именем ^User.AI.

    (рис 10.20) Область USER

    Нажав на Просмотр, получаем подробную информацию о глобале:

    ^UserTD=1
    ^User.TD(1)=$lb.("","John")
    

    Обратите внимание, что после закрытия объекта значение переменной хранящей OREF не меняется.

    USER>W ss 
    1@User.A
    

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

    Просмотреть OID объекта можно с помощью метода %Oid():

    USER>W ss.%Oid() 
    User.A
    

    В действительности OID представляет собой список, состоящий из ID объекта и имени класса. Можем в этом убедиться, выполнив в терминале следующую команду:

    USER>f i=1:1:$ll(ss.%Oid())  {w !,$li(ss.%Oid(),i)}
    
    1
    User.A
    

    Теперь создаем второй объект:

    USER>s ss=##class(User.A).%New() 
    USER>s ss.Name="Peter"  // параметру Name объекта №2 присвоено значение. 
    USER>d ss.%Save()      // объект №2 сохранен на диске.
    USER>d ss.%Close()  // объект №2 закрыт, то есть удален из памяти
    

    Остановимся на минутку, чтобы представить в общих чертах структуру класса A. Мы создавали персистентный класс, наследующий системному классу %Persistent. От родителя все такие классы получают следующие методы внешнего интерфейса:

  • %New(). Конструктор объекта. Его задача — создать экземпляр класса и присвоить ему OREF.
  • %Save() . Сохраняет экземпляр класса на диске и присваивает ему
  • OID.
  • %Close(). В старых версиях уменьшал значение счетчика OREF на единицу и уничтожал версию объекта в памяти. Начиная с версии 5.0, этот метод ничего не делает.
  • %Open(). Метод класса. В качестве первого аргумента получает OID объекта (или ID заключенный в оператор построения списка LB(). Если он находит объект существующий в базе данных, то создает в памяти его копию, содержащую значения всех свойств, и возвращает объект. Если объект уже загружен в память, просто возвращается OREF. Вообще у метода три аргумента. Второй аргумент Concurrency определяет особенности параллельной работы и принимает значения 0, 1, 2, 3, 4. По умолчанию установлен в "1", что означает создание разделяемой блокировки при загрузке объекта в память.
  • %OpenId(). Отличается от предыдущего тем, что в качестве аргумента получает не OID, а ID.
  • %Delete(). Удаляет версию объекта, хранящуюся на диске, копия в памяти при этом остается. В качестве единственного параметра получает OID, этот идентификатор больше не используется впоследствии (это касается классов с внутренней системой хранения Cache, в противном случае ответственность за повторное использование OID-ов полностью ложится на плечи разработчика).
  • %DeleteId(). Отличается от предыдущего тем, что в качестве аргумента получает не OID, а ID.
  • %IsModified(). Возвращает "истинно" (1), если значения свойств объекта были изменены, в противном случае — 0.
  • 10.2.3 Классы, таблицы, объекты и деревья

    Таблиц не бывает

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

    В предыдущем разделе мы создали класс А и один объект этого класса. Неожиданность в том, что в разделе SQL портала управления системой обнаруживается таблица с именем SQLUser.A и тем же атрибутом Name, что в созданном классе (таблица 10.4).

    Таблица SQLUser.A, соответствующая классу A
    Столбец Тип данных Столбец # Обязательный Уникальный Сортировка Скрыто MaxLen BLOB Контейнер Селективность Тип xDBC
    ID %Library.Integer 1 Yes Yes No No 1 INTEGER
    name %Library.String 2 No No SQLUPPER No 20 No VARCHAR
    x__classname %Library.CacheString 3 No No Yes No VARCHAR

    Обратите внимание на то, что Cache сама создала столбцы ID и

    x classname. Их можно прочитать запросами SQL, но запрос типа

    SELECT * выдает в результате только заданные в определении класса столбцы, ID объектов и номера строк (таблица 10.5).

    Результат запроса SELECT * FROM A
    # ID Name
    1 1 John
    Завершено

    Столбцы "ID" и "#" (номер строки в результате запроса) — это не одно и то же. Проиллюстрируем это, добавив в таблицу SQLUser.A еще два объекта с помощью инструкции INSERT:

    INSERT INTO A VALUES('James') 
    INSERT INTO A VALUES('Michael')
    

    Выполнив запрос SELECT * FROM A, увидим следующий результат (листинг 10.1).

    SELECT * FROM A
    #  ID  Name 
    1  1  John 
    2  2   James
    3  3  Michael
    

    Теперь удалим строку с ID=2 и снова выполним запрос SELECT * FROM A (листинг 10.2):

    SELECT * FROM A
    # ID Name
    1 1 John
    2 3 Michael
    

    Понятно, что левая колонка с именем "#" — это номер строки результата, а колонка "ID" — это идентификатор объекта (строки таблицы). То есть ID — это суррогатный ключ, созданный автоматически.

    Попробуем перейти в обратном направлении, от таблиц к классам.

    Построим несложную таблицу. В SQL-менеджере наберём команду:

    CREATE TABLE T   (c1 NUMBER(2),   c2 CHAR(3))
    

    но не будем её исполнять. Посмотрим сначала в портале, не существует ли класса с именем "T". Для этого в разделе Классы перейдем в область User. Поскольку создаваемые классы привязываются к области имён, перед именем класса следует ожидать появления префикса User. Значит, ищем имя User.T. Если оно найдётся, удалим этот класс. Теперь исполним команду создания таблицы T. В портале появится класс User.T. Если вы этого не увидели, нажмите на кнопку Обновить для обновления содержимого страницы браузера. Класс User.T обязательно появится. Нажав на ссылку Документация рядом с именем класса, можем посмотреть его содержимое. Чтобы увидеть описание класса, необходимо в студии в навигаторе зайти в папку Классы/User и щёлкнуть два раза левой кнопкой мыши по имени класса T. Появится описание этого класса на языке CDL (Class Define Language):

    Class User.T Extends %Persistent  [ ClassType = persistent, DdlAllowed,  Owner = UnknownUser, 
    ProcedureBlock, SqlRowIdPrivate, SqlTableName = T,  StorageStrategy = Default]
    {
    Property c1
    As %Library.Numeric(MAXVAL = 99,  MINVAL = -99,  SCALE = 0) [  SqlColumnNumber = 2 ]; Property c2
     
    As %Library.String(MAXLEN = 3)   [ SqlColumnNumber = 3 ];
    }
    

    Несмотря на незнание языка CDL, понимаем, что в первой строке записано имя класса User.T, потомка класса %Persistent, а свойства c1 и c2 соответствуют именам столбцов c1 и c2. Ширина столбцов c1 и c2 соответственно 2 и 3, но в определении числовое свойство задается минимальным и максимальным значениями (—99 и 99), а для строчных данных указывается максимальное число символов (MAXLEN=3). Обратите внимание, что для созданных свойств SqlColumnNumber равно 2 и 3 соответственно. Происходит это потому, что столбцу ID всегда назначается номер 1.

    И ещё одна любопытная подробность. Создайте сами класс без единого атрибута. В реляционной ипостаси в него можно внести сколь угодно много строк (INSERT INTO имя VALUES(NULL)). Такое возможно благодаря тому, что СУБД сама создаёт столбцы ID и #.

    Итак, создание класса вызывает появление таблицы, а таблица генерирует класс. В студии посмотрим, что у нас хранится ("Файл >Открыть") на самом деле. Обнаруживаются файлы с расширением .cls, хранящие исходные тексты классов. Описаний таблиц (скриптов CREATE TABLE ...) не существует.

    Значит, таблиц в Cache действительно не существует, есть возможность смотреть на классы как на таблицы.

    Вставляли строку, а создали или пополнили дерево

    Проверим на всякий случай, не существует ли глобал с именем T, после которого приписана буква D. Если глобал ^User.TD существует, удалите его. Теперь в SQL-менеджере введём в таблицу T одну строку:

    INSERT INTO T VALUES  (22, 'QQ')
    

    С помощью команды SELECT * FROM T убеждаемся, что строчка действительно записана (листинг 10.3).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    

    Переходим в раздел "Глобалы" и обнаруживаем глобал AUser.TD. Если он не появился, нажмите на кнопку F5. Интересно, как выглядит вновь созданный глобал. Щёлкаем дважды левой кнопкой мыши по строчке AUser.TD и получаем его структуру (листинг 10.4)

    ^UserTD=1
    ^User.TD(1)=$lb.("",22,"QQ")
    
    

    В узле глобала ^User.TD(1) находится построенный список из пустого элемента и введённых нами значений $LB("","22","QQ"), соответствующий строке таблицы SQLUser.T. Корню дерева ^User.TD присвоено значение 1. Если добавить ещё одну строку, например (1, 'A'), выполнив команду

    INSERT INTO T VALUES   (1, 'A')
    

    то дерево изменится так (листинг 10.5).

    ^UserTD=2
    ^User.TD(1)=$lb.("",22,"QQ")
    ^User.TD(1)=$lb.("",1,"A")
    
    

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

    Глобалы рассматриваемого вида можно изменять в языке ObjectScript, используя функции для работы со списками. Попробуем изменить таблицу не командой SQL вида

    UPDATE T SET c1=11 WHERE c1=1
    

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

    s ^User.TD(2)=$LB("","11","A")
    

    Прочитаем результат в SQL-менеджере, как обычно, командой SELECT * FROM T (листинг 10.6).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    

    Попробуем теперь добавить с терминала еще одну строчку:

    s ^User.TD(3)=$LB("","7","Z")
    

    Проверим результат командой SELECT. Вроде бы все получилось (листинг 10.7).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    3 7 Z
    

    Вставим ещё одну строку командой INSERT в SQL-менеджере. Однако, новая строка не была введена (листинг 10.8), а заместила старую третью строку (7,'Z'). Произошло это потому, что корень дерева ^User.TD хранит число строк в таблице, а мы это значение не изменили.

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    3 33 HH
    

    Теперь вставим строку правильно:

    USER>s ^User.TD=4
    USER>s
    ^AUser.TD(4)=$LB("","9","99")
    

    Командой SELECT убеждаемся, что строка действительно вставлена (листинг 10.9).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    3 33 HH
    4 9 99
    

    И теперь при добавлении новой строки, например (55, 'UU'), с помощью SQL-менеджера, она не будет замещать последнюю строку, добавленную нами через терминал (листинг 10.10).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    3 33 HH
    4 9 99
    5 55 UU
    

    Обратимся к словарю. В Cache все классы словаря хранятся в пакете %Dictionary. Чтобы его просмотреть, в портале выберем "System Explorer > Классы" перейдем в область USER. После нажатия на ссылку Документация около имени любого класса, появится страница описания класса.

    Убедимся, что не все атрибуты класса отображаются в столбцы таблицы. Для свойств класса можно установить значение видимости Private. Такое свойство не будет передаваться в таблицу.

    Создадим класс Z, у которого второе свойство имеет значение видимости Private:

    Class User.Z Extends %Persistent {
    Property P1 As %String;
    Property P2 As %String [ Private ];
    }
    

    Запрос SELECT * FROM Z показывает, что столбца P2 в таблице нет. Попутно мы, подобно Журдену у Мольера, в сорок лет узнавшему, что он всю жизнь говорил прозой, убедились, что в реляционных базах данных все столбцы общедоступны, то есть имеют видимость Public.

    В Cache любая заполненная таблица порождает простое сбалансированное дерево уровня 1. Однако, не каждое дерево может быть отображено одной таблицей.

    Упражнение 1

    Установите, будет ли уничтожен глобал, если связанная с ним таблица будет удалена командой DROP.

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

    Каждому объекту некоторого класса соответствует строка таблицы и узел в дереве глобала. Естественно, что вне СУБД Cache можно предложить другие отображения между деревьями, таблицами, классами и объектами.

    (рис 10.21) Отображение классов в таблицы и таблицы в дерево

    Наследование

    Одно из существенных отличий объектной модели от реляционной в том, что классы могут быть наследниками других классов. Разберемся с некоторыми особенностями наследования в Cache.

    Создадим класс Human следующей структуры:

    Class User.Human Extends %Persistent
    {
    Property Pass As %String(MAXLEN = 11);
    // серия и номер паспорта
    Property Name As %String(MAXLEN = 20);
    //имя
    }
    

    Класс-наследник Student расширяет базовый класс свойством NZach — номер зачетной книжки

    Class User.Student Extends User.Human
    {
    Property NZach As %String(MAXLEN = 10);
    }
    

    В SQL-представлении появляются две таблицы Human и Student, причем поля Name и Pass в таблице Student будут виртуальными. То есть, при создании новой записи в таблице Student значения этих полей сохраняются в таблице Human и информация о них извлекается по внутренней ссылке.

    Создать новый экземпляр класса Student можно двумя способами: sql-запросом или средствами ObjectScript.

    В классе Human создадим объект Петр ("Петр", "0305 855637"), а в классе Student — объект Иван ("Иван", "0305 163788", "8765").

    Данные обоих классов (родителя и наследника) помещены в один глобал ^User.HumanD (лиситинг 10.11).

    ^User.HumanD=2
    ^User.HumanD(1)=$lb("","0305 855637","Петр")
    ^User.HumanD(2)=$lb("~Student","0305 163788","Иван")
    ^User.HumanD(2,"Student")=$lb("8765")
    

    А вот содержимое таблиц Human и Student, полученное запросами вида SELECT * выглядит неожиданно с точки зрения реляционного народа (таблицы 10.6 и 10.7). Приходится признать, что в реляционной ипостаси Cache наследование реализуется.

    Содержимое таблицы Human
    # ID Name Pass
    1 1 Петр 0305 855637
    2 2 Иван 0305 163788
    Завершено
    Содержимое таблицы Student
    # ID NZnach Name Pass
    1 2 8765 Иван 0305 163788
    Завершено

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

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

    Сериализуемые объекты

    На первый взгляд кажется, что сериализуемые объекты в реляционном представлении приводят к появлению двухуровневых шапок таблиц, которые, как будет показано в разделе 10.3, характерны для объектно-реляционной модели. Покажем, что в объектной модели это не так. Создадим встроенный класс Address:

    Class User.Address Extends %SerialObject {
    Property City As %String; Property State As %String; 
    }
    

    Создадим класс Person в виде:

    Class User.Person Extends %Persistent
    {
    Property Name As %String; Property YearOB As %Integer; Property Home As Address;
    }
    

    Создадим объект класса Person:

    S p=##class(User.Person).%New()
    S p.Name="Nick", p.YearOB=1984, p.Home.City="NewYork" S p.Home.State="NY" D p.%Save()
    

    Образовался глобал:

    ^User.PersonD=1
    ^User.PersonD(1)=$LB("","Nick","1984",$LB("NewYork","NY"))
    

    Видим, что объект сериализуемого класса в глобале представляется списком

    $LB("NewYork","NY")
    

    В SQL-проекции инструкцией SELECT * ... выберем всё содержимое таблицы Person (таблица 10.8). Обращаем внимание на появление столбцов с именами Home_City и Home_State, образованными соединениями имени атрибута Home из класса Person и имен атрибутов City и State из класса Address. Это позволяет избежать появления двухуровневой шапки в таблице и, тем самым, не выходить за рамки реляционных таблиц.

    Результат с синтезированными именами столбцов
    # ID Name YearOB Home_City Home_State
    1 1 Nick 1984 New York NY
    Завершено

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

    SELECT Name, Home_City FROM Person
    

    Индексы в объектной модели

    Индексы в объектной модели Cache можно создать тремя способами:

  • SQL-командой CREATE INDEX в реляционном представлении класса;
  • непосредственно в коде определения класса;
  • с помощью мастера создания индексов.
  • В реляционной модели инструкция создания индекса:

    CREATE   [UNIQUE   |   BITMAP   |   BITSLICE  ]   INDEX index-name ON [TABLE] [имя_схемы.]имя_таблицы (имя_поля, ...)
    

    Индексы bitslice ускоряют работу с диапазонами, в том числе вычисления групповых функций.

    Синтаксис строки определяющей индекс в описании класса:

    INDEX имя_индекса
    ON список_атрибутов [список_ключевых_слов];
    

    Например, в созданном нами классе Person древесный индекс на свойство Name в теле класса может быть записан строкой

    Index NameIDX On Name;
    

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

     Index NamelDX On Name [type=standard];
    

    Для побитового индекса type= bitmap, третий вариант bitslice.

    Уникальный индекс определяется ключевым словом unique. Например, индекс для ИНН, который имеют не все люди, может быть определён фразой:

    Index INNIDX on INN [Unique];
    

    В стандартном индексе можно хранить не только идентификаторы объектов, но и данные. Достаточно в описание индекса включить секцию Data:

    Index NameIDX On Name [Data=(YearOB)];
    

    Теперь запросы к столбцам Name и YearOB потребуют только данных из файла Personl. Индексами можно управлять и непосредственно из программы используя метод %BuildIndices() класса %Persistent. Например, для перестроения всех индексов класса Person, у которого уже созданы объекты, следует набрать

    Set ss= ##class(USER.Person).%BuildIndices()
    

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

    Для создания индекса с помощью мастера в студии нажмите кнопку "Индекс" со знаком молнии, либо наберите в главном меню "Класс > Добавить > Индекс", затем задайте имя индекса и в следующем окне выберите тип индекса (рисунок 10.22).

    (рис 10.22) Задание типа индекса

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

    Опция IDKEY задаёт свою идентификацию объекта.

    Вариант Extent обеспечивает создание индекса ускоряющего выборки всех объектов экстента (в Cache под экстентом понимается набор объектов класса и всех его подклассов).

    Далее для древесного индекса задаётся список свойств, на которых он строится. Затем необходимо выбрать способ сортировки каждого выбранного свойства, определяемый параметром COLLATION. Перечень вариантов сортировки приведен в документации Cache.

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

    Запросы в описании класса

    Выбираем кнопку "Новый запрос" либо в основном меню проходим путь "Класс > Добавить > Запрос". Затем называем имя запроса и выбираем способ его реализации (SQL или пользовательский код). Если запрос строится не на SQL, придется самим написать три метода класса, определяющие его работу. Это QueryExecute(), QueryFetch() и QueryClose(), где Query —имя запроса.

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

    В панели "Колонки" определите порядок следования столбцов таблицы, включаемых в результат запроса.

    Остаётся перечислить критерии отбора объектов и указать порядок сортировки результата.

    Триггеры в объектной модели

    Поскольку триггеры в Cache это элементы класса, то кроме создания их в SQL, описанного в разделе 8.7, можно определить их прямым написанием текста в Cache Studio, либо воспользоваться мастером. Для вызова мастера создания триггера, необходимо выбрать пункт меню Класс > Добавить > Новый триггер SQL.

    Вспомним, что тип триггера определяется событием, которое инициирует его выполнение (INSERT, UPDATE, DELETE), и моментом времени относительно этого события (BEFORE или AFTER). Ещё одно событие — UPDATE OF — возникает при изменении значений определенных столбцов. Но его можно использовать, только если код триггера написан на языке SQL.

    Можно также задавать триггеры, срабатывающие сразу на несколько событий: INSERT/UPDATE, UPDATE/DELETE, INSERT/UPDATE/DELETE. Это означает, что триггер будет выполняться при возникновении любого из указанных событий.

    Методы callback —это методы СУБД, которые определяют реакцию системы на некоторые события. В Cache существуют предопределённые callback-методы, и имеется возможность их переопределения. Это позволяет придерживаться принципа разделения системного и прикладного программного обеспечения, но одновременно дает возможность переопределения реакции системы.

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

  • BEFORE INSERT ~ %OnBeforeSave
  • AFTER INSERT ~ %OnAfterSave
  • BEFORE UPDATE — %OnBeforeSave
  • AFTER UPDATE — %OnAfterSave
  • BEFORE DELETE — %OnDelete
  • Однако это не значит, что, например, триггеры BEFORE INSERT и BEFORE UPDATE есть по сути одно и то же. Адекватный метод %OnBeforeSave выглядит следующим образом:

    Method %OnBeforeSave(insert As %Boolean) As %Status [Private,ServerOnly=1]
    {
    if insert {
    //код триггера на BEFORE INSERT
    }
    else {
    //код триггера на BEFORE UPDATE
    }
    Quit $$$OK
    }
    

    Если возникает событие, ассоциированное с триггером, то вызывается выполнение кода этого триггера.

    Рассмотрим подробнее создание триггера в Studio. Для примера создадим триггер на добавление строки в таблицу SQLUser.Person, который будет вставлять ID добавленного объекта в специальную таблицу LogTable, содержащую текстовый столбец TableName и числовой столбец IDValue.

    При вызове мастера создания триггера появляется окно, как показано на рисунке 10.23.

    (рис 10.23) Мастер создания триггера

    Введем название триггера "LogEvent" и нажав кнопку "Далее" перейдем к следующему окну. Здесь нужно указать событие и время срабатывания триггера. Выбрать можно только одно из событий INSERT, UPDATE, DELETE. Если необходимо указать несколько событий, придётся редактировать текст триггера вручную.

    В определении класса событие задается с помощью параметра Event. Например:

    Trigger NewTrigger1  [ Event = INSERT ]

    Несколько событий можно указать через "/", например:

    Trigger NewTrigger1  [ Event = INSERT/UPDATE ]

    Выберем в мастере событие INSERT и время срабатывания AFTER (рисунок 10.24) и нажмем кнопку "Далее".

    (рис 10.24) Указание времени срабатывания триггера

    Последний шаг при создании триггера с помощью мастера — собственно написание кода.

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

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

  • {имя_поля*О} — старое значение поля обновляемой строки;
  • {имя_поля*N} — новое значение поля обновляемой строки;
  • {имя^толя*C} —равно 1, если значение поля в обновляемой строке было изменено, иначе 0.
  • Добавим в текстовую область код, как показано на рисунке 10.25, и нажмем кнопку "Готово".

    (рис 10.25) Код триггера

    В определении класса появилось следующее понятное теперь описание:

    Trigger LogEvent  [ Event = INSERT,  Time = AFTER ] {
    // извлекаем ID добавляемой строки
    Set id = {ID}
    // вставляем значение в таблицу LogTable sql(INSERT INTO LogTable  (TableName, IDValue) VALUES  ('SQLUser.Person', :id))
    

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

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

    10.2.4 Класс %ResultSet

    Класс %ResultSet позволяет использовать результаты запросов классов в программах, написанных на ObjectScript, Java или ActiveX. Есть два варианта обращения к %ResultSet. Первый способ состоит в создании экземпляра %ResultSet, методу %New() которого передаётся в качестве аргумента строка, содержащая разделённые двоеточием имя класса и имя запроса:

    Set result=##class(%ResultSet).%New("Person:ByName") 
    Второй способ заключается в явном установлении свойств ClassName и QueryName 
    объекта %ResultSet. Set result=##class(%ResultSet).%New() 
    Set result.ClassName="Person" Set result.QueryName="ByName"
    

    После того, как создан объект %ResultSet, необходимо выполнить запрос и затем проанализировать результат.

    Объект %ResultSet располагает следующими основными методами:

  • Execute() выполняет запрос, с которым связан объект %ResultSet;
  • Next() осуществляет переход на следующую строку выборки;
  • Close() закрывает %ResultSet после завершения его использования.
  • Для доступа к полям текущей записи класс %ResultSet предоставляет несколько способов:

  • использование метода Get(), которому передаётся в качестве аргумента имя нужного столбца;
  • использование свойства Data, которое также предоставляет доступ к значению столбца текущей записи по имени столбца; этот способ быстрее предыдущего;
  • использование метода GetData(), которому передаётся в качестве аргумента номер столбца в запросе.
  • Ниже приведен пример использования класса %ResultSet с запросом ByName класса Person. Данный запрос возвращает данные о сотрудниках, упорядоченные по их именам в алфавитном порядке. В результате выводятся первые двадцать имен.

    //создание экземпляра %ResultSet
    Set result=##class(%ResultSet).%New("Person:ByName") //выполнение запроса Set sc=result.Execute() //обработка результатов в цикле For i=1:1:20 {
    If result.Next(.sc) {
    Write result.Data("Name"),! } Else { Quit
    }
    }
    

    Класс %ResultSet может исполнять динамические SQL-запросы. В этом случае объект %ResultSet создается с указанием "%DynamicQue-ry:SQL":

    S result=##class(%ResultSet).%New("%DynamicQuery:SQL")
    

    Затем с помощью метода Prepare необходимо подготовить запрос, например:

    S q="SELECT ID,Name,Salary FROM Employee WHERE Salary>90000" S sc=result.Prepare(q)
    

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

    Класс %ResultSet позволяет производить только последовательный обход записей в прямом порядке, от первой к последней. Чтобы вернуться назад и просмотреть предыдущие строки результата, нужно заново исполнять запрос (методом Execute()) и некоторое количество раз вызывать метод Next(), чтобы добраться до нужной записи.

    Более удобный доступ к результатам исполнения запроса обеспечивает класс %ScrollableResultSet. Он позволяет обходить строки результата как в прямом порядке, используя метод Next() , так и в обратном, используя метод Previous(). Кроме того, у него имеется свойство CurrRow, в котором хранится номер текущей строки. Путем установления CurrRow в нужное значение, можно получать доступ к любой строке выборки по ее номеру (имеется в виду последовательный номер в выборке, но не ID).

    При использовании %ScrollableResultSet запрос исполняется в момент первого обращения, а результат исполнения запроса сохраняется в глобальной переменной.

    Приведём пример использования %ScrollableResultSet с тем же запросом ByName класса Person:

    Set q="Person:ByName" //создание экземпляра
    Set result=##class(%ScrollableResultSet).%New(q)
    //выполнение
    Set sc=result.Execute()
    //просмотр первой записи
    Do result.Next(.sc)
    Write "1-я запись: ",result.Data("Name"),! //просмотр 10-й записи Set result.CurrRow=10
    Write "10-я запись: ",result.Data("Name"),! //просмотр записей с 9-й по 2-ю в обратном порядке 
    Write "Записи с 9-й по 2-ю в обратном порядке:",! For i=9:-1:2 {
    If result.Previous(.sc) {
    Write result.Data("Name"),! } Else { Quit
    }
    }
    

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

    10.2.5 Способы хранения класса

    Используем созданные ранее классы Address и Person, имеющие следующую структуру:

    Class User.Address Extends %SerialObject
    {
    Property City As %String; Property State As %String;
    }
    
    Class User.Person Extends %Persistent
    {
    Property Name As %String; Property YearOB As %Integer; Property Home As Address;
    }
    

    Как известно, по умолчанию объект класса Person сохраняется в глобале ^Temp.PersonD следующим образом:

    ^User.PersonD(2)=$lb("","Nick",1984,$lb("NewYork","NY"))
    

    Чтобы изменить привычный способ хранения экземпляров класса Person, необходимо создать новый способ хранения (Storage). Это можно сделать при помощи кнопки на панели инструментов "Новый способ хранения". Появится мастер создания способа хранения (рисунок 10.26).

    (рис 10.26) Мастер создания способа хранения

    Выбираем способ хранения типа "Хранение Cache". Если нажмем "Далее", появится форма для ввода названий глобалов, в которых будет храниться информация об объектах вместо используемых по умолчанию ^User.PersonD и ^User.PersonI, однако мы их изменять не будем, поэтому просто нажимаем на кнопку "Готово".

    Новый способ хранения создан. Обратимся к инспектору класса. В левом выпадающем списке выберем "Storage" и увидим имеющиеся в классе способы хранения (рисунок 10.27).

    (рис 10.27) Список способов хранения в Инспекторе

    Дважды кликнув на "NewStorage1", перейдем в инспекторе к описанию данного способа хранения.

    Изменим хранение свойств таким образом, чтобы домашний адрес человека располагался не в узле глобала ^User.PersonD(i) как вложенный список, а в отдельном узле под индексом ^User.PersonD(i, "Home").

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

    (рис 10.28) Свойства способа хранения

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

    (рис 10.29) Редактирование способа хранения

    Мы должны создать два узла:

  • В первом узле в виде списка будут храниться все свойства объекта, кроме домашнего адреса.
  • Во втором под индексом "Home" будет храниться значение домашнего адреса.
  • Для создания первого узла нажмем кнопку "Добавить" и заполним появившуюся форму так, как показано на рисунке 10.30.

    (рис 10.30) Новый узел в способе хранения

    Поле Имя содержит имя узла данных внутри способа хранения. Узел PersonDefaultData содержится в способе хранения, создаваемом в классе по умолчанию. Мы создаем узел с таким же именем, в котором также в виде списка будут храниться значения всех свойств, кроме свойства Home.

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

    Все свойства, имеющиеся в классе должны быть отражены в способе хранения, включая %%CLASSNAME. Если какие-то из них пропущены, то при компиляции автоматически создается узел с именем PersonDefaultData, значением которого является список значений этих свойств. Если узел PersonDefaultData уже существует, то создается узел PersonDefaultData1, который хранит пропущенные данные в виде списка значений в узле глобала вида ^User.PersonD(i, 1).

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

    Далее расположены 3 радио-кнопки с выбором:

  • Множественные свойства. Выбор данного пункта означает, что в узле будет храниться список, содержащий указанные свойства в указанном порядке
  • Одно свойство. Выбор данного пункта означает, что в узле будет храниться значение единственного указанного свойства.
  • Свойство массив. Выбор данного пункта позволяет задать хранение свойств типа Список или Массив.
  • В нижней части формы показано, какой вид будет иметь глобальная ссылка для этого узла. В данном случае под индексом со значением ID будет храниться список вида $LB(%% CLASSNAME, Name, YearOB).

    Теперь вновь перейдем на NewStorage1 и добавим еще второй узел, заполнив поля формы следующим образом (рисунок 10.31).

    (рис 10.31) Хранения свойства Home

    Нажмем "ОК" и откомпилируем класс.

    Чтобы посмотреть, как изменилась структура хранения экземпляров класса Person, необходимо добавить в базу новый объект, так как изменение способа хранения для класса не перестраивает узлы, соответствующие ранее созданным объектам. Для этого выполним в терминале следующие команды:

    USER>S p=##class(User.Person).%New()
    USER>S p.Name="Alex", p.YearOB=1989
    USER>S p.Home.State="USA",p.Home.City="Washington"
     
    USER>D p.%Save()
    

    С помощью портала управления системой увидим, что в глобале ^User.Person появилась ветка для этого объекта, имеющая следующий вид:

    ^User.PersonD(5)= $lb("","Alex",1989) ^User.PersonD(5,"Home")= $lb("Washington","USA")
    

    Посмотрим, как влияет изменение способа хранения на SQL-проекцию класса Person. Выполним в портале запрос SELECT * FROM Person (таблица 10.9).

    SQL-проекция класса Person
    # ID Name YearOB HomeCity HomeState
    1 1 Nick 1
    2 2 Nick
    3 3 Alex 1989 Washington USA
    Завершено

    Мы видим, что SQL-проекция опирается на действующий в момент исполнения запроса способ хранения. Однако это не означает, что информация о хранении ранее созданных объектов утеряна. В классе сохраняется как новый, так и старый способ хранения. Действующий способ хранения указывается с помощью параметра класса StorageStrategy:

    Class User.Person
    Extends %Persistent [ StorageStrategy = NewStorage1 ]
    

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

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

    Поскольку структуру хранения можно прописать только у персистентно-го класса, то расположение узлов-свойств объекта типа Address в глобале ^User.PersonD придется описывать в классе Person, используя для указания необходимого свойства точечный синтаксис, например Home.City.

    Изменение способа хранения может использоваться для повышения быстродействия.

    10.2.6 Что мы узнали о реализации объектной модели в Cache, о связях моделей данных и какие ещё отображения могут быть интересны

    Как и следовало ожидать, в Cache реализованы все основные особенности объектной модели данных. Оригинальные черты объектов в Cache определены двумя особенностями этой СУБД. Это:

  • единая архитектура Cache, позволяющая "взглянуть" на одни и те же данные с точки зрения трёх моделей —иерархической, реляционной и объектной;
  • возможность управлять способом хранения данных.
  • Единая архитектура определяет отображения между помянутыми тремя моделями данных, с которыми вы уже познакомились. Таблица представляется как класс без методов, а иерархии используются для хранения экземпляров классов, они же кортежи отношений или строки таблиц. Наследование оказывается возможным и в расширенном варианте реляционной модели.

    С другой стороны, от реляционной модели в классы Cache перешли запросы, индексы и триггеры. В результате, запросы могут использоваться как фильтры набора объектов. Индексы, как и в таблицах, могут ускорить доступ к данным. Триггеры в объектной модели, использующей по умолчанию способ хранения %CacheStorage, не работают. Однако, при выборе другой модели хранения %CacheSQLStorage методы %Save() и %Delete() могут вызывать триггеры, если события для которых триггеры созданы наступили.

    Для организации активности в объектной модели Cache пользуются методами обратной связи (Callback Methods), позволяющими создать свою обработку событий. Например, метод обратной связи %OnOpen() вызываемый методом %Open() , позволяет для хранимых и сериализуемых классов написать свои обработчики события открытия объекта.

    Мы совсем не рассматривали те зарегистрированные классы, которые не являются ни хранимыми, ни сериализуемыми. Это аналоги классов языков общего назначения, таких как C++, Java и др.

    Уже упоминалось (в разделе 10.1.4), что для интерфейсов пользователя, выполняющихся обычно на объектно-ориентированных языках, в том числе на специфичных для Cache технологиях CSP и Zen, выполняются отображения данных из базы в интерфейс и обратно. Мы их рассматривать не можем, однако, обратим внимание на интереснейшую возможность отображения классов Cache в классы Java.

    10.3 Объектно-реляционная модель данных Oracle

    Изучая объектную модель данных, мы уже заметили, что методы были написаны на основном для Cache не объектно-ориентированном языке ObjectScript. В объектно-реляционной модели данных, которая будет рассмотрена на примере СУБД Oracle, ситуация с реализацией методов та же. Только методы создаются на встроенном процедурном языке, который называется PL/SQL.

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

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

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

    Если вы хотите изучить объектно-реляционные базы полнее, скачайте персональный вариант Oracle 11g и используйте многочисленные учебные материалы Oracle.

    Инсталляция этой СУБД и дополнительные сведения о ней приведены на сайте книги.

    10.3.1 Введение в процедурный язык PL/SQL

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

    Сразу обратим внимание на то, что в Oracle любая инструкция SQL и PL/SQL заканчивается точкой с запятой.

    Блоки

    Программы PL/SQL структурируются анонимными (не имеющими имени) блоками. Структура такого блока:

    [DECLARE
    объявления
    ]
    BEGIN
    выполняемые операторы [EXCEPTION
    обработка исключительных ситуаций
    ]
    END;
    

    Блок обязательно заканчивается точкой с запятой.

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

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

    Объявление переменных и констант можно выполнить так:

    имя_переменной тип_данных
    [[NOT NULL]   := выражение_по_умолчанию]; имя_переменной тип_данных CONSTANT := выражение;
    

    Присваивание обозначается знаком :=.

    Разберём несложный пример анонимного блока (листинг 10.11). Набрав текст нажимаем Run.

    DECLARE
    х  NUMBER(5,2);
    у  NUMBER(5,2):=2;
    result NUMBER(5,2); BEGIN
    — Зто однострочный комментарий result  := 12.02; x := result / y;
    DBMSOUTPDT.PUT_LINE(1 хранен 1   || x);
    END;
    
    Results:
    x равен 6,01 
    Statement processed.
    0,00 seconds
    

    Язык PL/SQL, в отличие от SQL, "молчаливый" язык. Вывод на экран в нём необходимо прописать явно. Процедура PUT_LINE из пакета DBMS_OUTPUT позволяет вывести на экран строку, сформированную как конкатенация (обозначенная знаком ||) из двух строк —текста 'x равен ' и строки, представляющей значение x. Числовой тип x здесь неявно приводится к текстовому типу. Теперь строка DBMS_OUTPUT.PUT_LINE('x равен ' || x); понятна.

    Запомните, что анонимный блок может быть подставлен вместо любого оператора PL/SQL или SQL.

    SQL внутри PL/SQL

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

    Создадим таблицу и введём в неё одну строку:

    CREATE TABLE qq(c1 CHAR(10),   c2 INTEGER); INSERT INTO qq VALUES('QWERTY', 1);
    

    Извлечём значение первого столбца с помощью запроса типа SELECT .. INTO:

    DECLARE
    result char(10); BEGIN
    SELECT cl INTO result FROM qq WHERE c2=1; DBMS_OUTPUT.PUT_LINE(result);
    END;
    

    Удаление слов INTO result вызовет появление ошибки.

    Разветвления и циклы

    Синтаксис команды разветвления в общем обычный:

    IF условие THEN последовательность_операторов_1 ELSIF условие2 THEN 
    последовательность_операторов_2 ELSE последовательность_операторов_3 END IF;
    

    Обратите внимание на то, что вместо обычного ELSEIF почему-то пишется ELSIF. Циклы строятся на основе так называемого простого цикла:

    [<<имя_цикла>>] LOOP
    последовательность_операторов_1 EXIT имя_цикла WHEN условие_выхода
    последовательность_операторов_2 END LOOP;
    

    Без инструкций останова EXIT или EXIT WHEN он зацикливается. Фраза в двойных угловых скобках "имя_цикла" это метка имени цикла. Пример простого цикла с меткой, которая в нём не используется, приведён в листинге 10.12:

    DECLARE
    v_i INTEGER  := 1; 
    BEGIN
    "1QQP_1"
    LOOP
    DBHSOUTPUT.PUTLINE{'В цикле    v_i =  '   || v_i) EXIT loop_l WHEN v_i > 2; 
    v_i  := v_i + 1; 
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('Вышли из цикла')
    
    Results:
    В цикле v_i = 1 
    В цикле v_i = 2 
    В цикле v_i = 3 
    Вышли из цикла
    
    

    Пример цикла WHILE с предусловием:

    DECLARE
    i INTEGER:=1;
    BEGIN
    WHILE i < 6 LOOP
    DBMS_OUTPUT.PUT_LINE(i); i  :=i +1;
    END LOOP;
    END;
    

    Пример цикла FOR:

    BEGIN
    FOR i IN 1   ..   5 LOOP
    DBMS_OUTPUT.PUT_LINE(i);
    END LOOP;
    END;
    

    Обратите внимание на то, что переменная i не была объявлена явно, и цикл сам создал её с типом INTEGER. Слово REVERSE во фразе FOR i IN REVERSE 1 .. 5 LOOP задало бы обратное направление перебора значений.

    Процедуры и функции

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

    Упрощенный синтаксис инструкции создания процедуры:

    CREATE [OR REPLACE] PROCEDURE имя_процедуры
    [(имя_параметра [IN | OUT | INOUT] тип [, ...])]
    IS | AS
    BEGIN
    тело_процедуры
    END имя_процедуры;
    

    Упрощенный синтаксис создания функции:

    CREATE   [OR REPLACE]   FUNCTION имя_функции [(имя_параметра [IN  | OUT  |  INOUT] 
    тип [, ...])] RETURN тип_возвращаемого значения IS  | AS BEGIN
    тело_функции END имя_функции;
    

    Тело функции или процедуры это последовательность операторов.

    Слова OR REPLACE добавляют, если необходимо заменить существующую функцию с таким же наименованием.

    Режим параметров указывается как IN — входной, OUT — выходной, или INOUT — входо-выходной. Входной параметр нельзя изменять в теле функции, а выходному обязательно должно быть присвоено значение.

    Пример функции, вычисляющей площадь круга по его радиусу:

    CREATE OR REPLACE
    FUNCTION circle_area (p_radius IN NUMBER) RETURN NUMBER AS
    v_pi      NUMBER := 3.1415926; v_area NUMBER; BEGIN
    v_area := v_pi * POWER(p_radius, 2); RETURN v_area; END circle_area;
    

    Слово RETURN в теле функции присутствует, по крайней мере, один раз и указывает имя переменной, которая будет возвращена.

    Обратите внимание на то, что в именованиях переменных использован префикс "v_", в имени формального параметра префикс "p_". Подобные соглашения об именованиях очень удобны, особенно если они соблюдаются широким кругом разработчиков.

    Вызовем функцию circle_area из анонимного блока:

    BEGIN
    DBMS_OUTPUT.PUT_LINE(circle_area(2));
    END;
    

    Созданная функция может быть вызвана и из SQL, например,

    SELECT circle_area(2) FROM dual;
    

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

    SELECT sysdate FROM dual;
    

    Пакеты

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

    Пакет состоит из двух частей:

  • спецификация или заголовок пакета;
  • тело пакета.
  • Заголовок пакета содержит спецификации переменных, типов данных, процедур, функций и курсоров которые предполагается сделать доступными извне. Здесь нет программного кода. Курсоры —это стандартное средство для реализации запросов SQL. Мы их не будем рассматривать.

    Спецификация может быть скомпилирована и при отсутствии тела пакета.

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

    Тело пакета успешно компилируется только вместе со спецификацией, либо если спецификация была скомпилирована раньше.

    Мы не можем заниматься пакетами подробно. Приведём лишь несколько вариантов их использования. Мы уже работали с функцией PUT_LINE пакета DBMS_OUTPUT.

    Функция GET_DDL пакета DBMS_METADATA позволяет получить тексты определения любого хранимого объекта базы. Рассмотрим примеры её применения.

  • Запрос описания функции

    SELECT dbms_metadata.get_ddl('PROCEDURE',
    'имя_таблицы', 'имя_схемы') FROM dual;
    

    Например, для только что созданной функции circle_area:

    SELECT dbms_metadata.get_ddl('FUNCTION', 'CIRCLE_AREA', 'HR')
    FROM dual;
    
  • Запрос описания таблицы

    SELECT dbms_metadata.get_ddl('TABLE',
    'имя_таблицы', 'имя_схемы') 
    FROM dual;
    

    Оказывается в только что созданной таблице qq было задано по умолчанию много параметров. Запрос

    SELECT dbms_metadata.get_ddl('TABLE','QQ','HR') 
    FROM dual;
    

    возвращает текст:

    CREATE TABLE
    "HR"."QQ"   ("C1" CHAR(10),"C2" NUMBER(*,0)) 
    PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING 
    STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0
    FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT) TABLESPACE "USERS"
    

    Здесь параметры блока PCTFREE и PCTUSED, которые станут понятны после освоения следующей лекции 11, и задание совершенно не нужного таблице qq сегмента 64 Кбайт, и указание табличного пространства USERS, в котором таблица находится и многое другое.

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

    Обратите внимание на то, что все текстовые константы в запросах переводятся в верхний регистр и помещаются в одинарные кавычки.

  • Запрос описания пользователя

    SELECT dbms_metadata.get_ddl('USER',
    'имя_пользователя') 
    FROM dual;
    
  • 10.3.2 Объектные типы данных

    Теперь мы готовы заниматься объектно-реляционными базами.

    Объекты могут быть постоянными и временными. Хранимый (persistent) объект может быть как значением столбца обычной таблицы, так и строкой таблицы (в этом случае таблицу называют объектной).

    Временный (transient) объект создается в памяти и уничтожается после окончания работы программы. Отправив такой объект в базу данных вы делаете его постоянным. А постоянный объект можно извлечь из базы данных и поместить его во временный объект такого же или совместимого типа.

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

    Выборка и манипулирование хранимыми объектами в базе данных осуществляется на языке SQL, начиная с третьей версии. Для работы с временными объектами на сервере предназначен язык PL/SQL. Язык C++ позволяет работать с объектами на стороне клиента. Используются вызовы программ на C++ из PL/SQL. В Java можно работать на всех трёх слоях клиент-серверной архитектуры.

    Можно выделить следующие разновидности объектных типов:

  • простой объектный тип, который строится на скалярных предопределённых типах данных;
  • составной объектный тип, использующий другие объектные типы;
  • ссылочный объектный тип;
  • типы коллекций двух разновидностей VARRAY и NESTED TABLES.
  • Ссылочный объектный тип REF это логический указатель, определяющий отношения между экземплярами классов. Он основывается на одноэлементных или коллекционных типах данных. Указатели REF задают ассоциации UML и заменяют внешние ключи, предоставляя прямую навигацию между объектами разных типов.

    VARRAY - это упорядоченная коллекция фиксированной длины. Хранится в сегменте таблицы, использующей такой тип.

    Вложенная таблица это неограниченная и неупорядоченная коллекция. Хранится в своём сегменте, не совпадающем с сегментом основной таблицы. Последняя фраза станет понятной после изучения следующей 11-й лекции.

    Создание пользовательского типа данных

    Создадим тип данных dept_type, описывающий таблицу dept (рисунок 10.32). Затем создадим таблицу emp_dept (то есть emp включающую в себя dept).

    (рис 10.32) Создание типа данных dept_type

    Заметьте, что в окне можно держать несколько инструкций, но исполняемая инструкция должна быть выделена как на (на рисунке 10.32.

    Ранее использованные в текущем сеансе инструкции можно вызвать, нажав на History, и в появившемся внизу окне кликнуть левой кнопкой мыши на нужной команде.

    А теперь получим информацию о созданном типе данных из представления словаря USER_TYPE_ATTRS (таблица 10.10).

    Получение информации о типе dept_type
    SELECT type_name, attr_name, attr_type_name FROM user_type_attrsWHERE    type name='DEPT TYPE';
    TYPE_HAME ATT R_ NAME ATT R_TYPE_ NAME
    DEPTTYPE DEPTNO NUMBER
    DEFTJYPE DNAME VARCHAR2
    DEPT_TYPE LOC VARCHAR2

    Можно использовать аналогичное представление dba_type_attrs. Его структура показана в таблице 10.11.

    Представление dba_type_attrs
    Столбец Описание столбца

    OWNER

    TYPENAME

    ATTRNAME

    ATTR_TYPE_MOD

    ATTR_TYPE_OWNER

    ATTR_TYPE_NAME

    LENGTH

    PRECISION

    SCALE

    CHARACTER_SET_NAME

    ATTRNO

    Владелец типа

    Имя типа

    Имя атрибута

    Модификатор атрибута типа

    Владелец атрибута типа

    Имя типа атрибута

    Длина для атрибутов CHAR/VARCHAR

    Точность для атрибутов типа number

    Диапазон для числового типа

    Имя таблицы символов

    Номер атрибута

    Изменение и удаление типов. Зависимости объектов

    В наших первых примерах типы не содержат методов. В общем случае в типе, как и в пакете, создаётся спецификация и тело метода.

    Объектные типы можно изменять оператором ALTER TYPE и удалять оператором DROP TYPE.

    Форматы некоторых команд:

    [ALTER TYPE имя_типа COMPILE [SPECIFICATION|BODY]
    

    Компилирует спецификацию или тело типа. Если не указано ни одно из слов SPECIFICATION, BODY, то компилируются оба. Последовательность компиляции такая же как для пакетов.

    ALTER TYPE имя_типа
    REPLACE AS OBJECT (спецификация_объектного_типа)
    

    Новое описание, заданное спецификацией_объектного_типа должно во всем, кроме дополнительных методов совпадать с исходным.

    После выполнения этого оператора, если тело типа существовало ранее, оно становится не действительным (INVALID).

    Удаление типа.

    DROP TYPE  [имя_схемы.]имя_типа [FORCE]
    

    Если указан параметр FORCE, то тип удалится, даже если существуют зависимые от него объекты. Естественно, все зависимые объекты становятся INVALID.

    Пример зависимости объектов. Создаем два независимых типа

    CREATE OR REPLACE TYPE Objl AS OBJECT ( Cl NUMBER, C2 VARCHAR2(5)
    );
    CREATE OR REPLACE TYPE Obj2 AS OBJECT (
    Cl CHAR(2),
    C2 VARCHAR2(5)
    );
    

    и зависящий от них Obj3

    CREATE OR REPLACE TYPE Obj3 AS OBJECT (
    al Objl, a2 Obj2
    );
    

    Тогда попытка удаления

    DROP TYPE Objl;
    

    вызовет ошибку, так как от Objl зависит Obj3.

    А вот удаление сначала Obj3, затем Objl или Obj2 ошибкой не будет.

    Конструкторы по умолчанию

    Метод конструктора, как всегда в ООП, предусматривается для любого объектного типа и возвращает новый экземпляр этого типа.

    (рис 10.33) Удаление типа Obj1

    Для демонстрации применения конструктора по умолчанию создадим тип address_type, затем на его основе объектный тип person и таблицу peoples со столбцом этого типа.

    CREATE   TYPE  address_type   as object (
    zipcode  NUMBER(5),
    country  VARCHAR2(20),
    city  VARCHAR2(30),
    street VARCHAR2(30),
    numb NUMBER(4)
    );
    
    CREATE   TYPE person AS OBJECT (
     
    name VARCHAR2(40), birthday DATE, address address_type, MEMBER FUNCTION Age
    
    — дата рождения
    спецификация метода, возвращающего возраст
     
    RETURN NUMBER  — возвращаемое значение
    );
    

    Поскольку определена спецификация типа, содержащего функцию-член типа, необходимо еще создать тело типа:

    CREATE OR REPLACE TYPE BODY person IS — задание методов
    MEMBER FUNCTION Age RETURN NUMBER IS
    BEGIN    -- вычисление возраста
    RETURN ROUND(MONTHS_BETWEEN(sysdate, birthday)/12);
    END;
    END;
    

    А теперь посмотрите сами описание нового типа, используя команду DESC person или без сокращений DESCRIBE person. Создадим таблицу типа person командой CREATE TABLE peoples OF person; Проверим ее структуру (таблица 10.12):

    Структура таблицы peoples
    DESC People
    Table Column Data Type Length Precision Scale Primary Key Nullable Default Comment
    PEOPLES NAME Varchar2 40 - - - + - -
    BIRTHDAY Date 7 - - - + - -
    ADDRESS Address_Type 1 - - - + - -
    1-3

    В объектных таблицах работают ограничения CHECK, PRIMARY KEY и UNIQUE. Например, создадим таблицу peoplesl c первичным ключом:

    CREATE TABLE peoplesl OF person (name   PRIMARY KEY);
    

    Проверим ее структуру для сравнения с peoples. В столбце Primary Key в строке NAME появится отметка первичного ключа.

    В объектных таблицах вставка строки "по-старому" не удастся. Например, вставка

    INSERT INTO peoples
    VALUES    ('Сидоров И.  П.', '22.11.80',350000,'Россия', 'Краснодар','Ставропольская',  14 9);
    

    вызывает появление ошибки.

    Теперь вставка должна производиться так:

    INSERT INTO peoples
    VALUES('Сидоров И.П.', '22.11.80', address_type (35000, 'Россия', 'Краснодар','Ставропольская',  14 9)
    );
    

    Читать как в обычных реляционных таблицах также не удастся. Так, команда

    SELECT * FROM peoples;
    

    вызывает сообщение об ошибке.

    Тот же результат дают запросы:

    SELECT name, birthday, address   FROM peoples; 
    SELECT name, birthday, address.country FROM peoples;
    

    А вот следующая команда позволяет работает нормально (таблица 10.13):

    Выборка из таблицы peoples
    SELECT name, birthday,p.address.countryFROM peoples p;
    NAME BIRTHDAY ADDRESS.COUNTRY
    Сидоров И.П. 22.11.80 Россия

    Обращение к подобъектам производится с использованием точечного синтаксиса и псевдонимов:

    SELECT p.address.country FROM peoples p;
    

    А вот без псевдонима никак нельзя. Следующий запрос вызывает появление ошибки:

    SELECT peoples.address.country FROM peoples;
    

    Как хранятся объектные таблицы

    Давайте посмотрим, что произошло при создании объектной таблицы peoples. Для этого обратимся к представлению словаря user_tab_col (таблица 10.14), которое перечисляет все столбцы указанной таблицы, в том числе, скрытые (hidden).

    Представление user_tab_cols
    SELECT column_name,  hidden_column, data_typeFROM user_tab_colsWHERE table name =  'PEOPLES';
    COLUMN_NAME HIDDEN_COLUMN DATA_TYPE
    SYS_NC_OIDS YES RAW
    SYS_NC_ROWINFO$ YES PERSON
    NAME NO NO VARCHAR2
    BIRTHDAY NO DATE
    ADDRESS YES ADDRESS_TYPE
    SYS_NC00006$ YES NUMBER
    SYS_NC00007$ YES VARCHAR2
    SYS_NC00008$ YES VARCHAR2
    SYS_NC00009$ YES VARCHAR2
    SYS NC00010$ YES NUMBER

    Итак, создана не совсем обычная реляционная таблица peoples. В ней имеются скрытые столбцы.

    Столбцы name и birthday это обычные реляционные столбцы и читаются обычным запросом. Столбец address и SYS_NC_ROWINFO$ таким запросом не читаются. Столбец SYS_NC_OID$, по-видимому, содержит объектные идентификаторы строк таблицы. Последние пять скрытых столбцов по типам подозрительно напоминают столбцы zipcode, country, city, street и numb, определённые в типе address_type. Проверьте это предположение, выполнив запрос

    SELECT SYS_NC00006$,  SYS_NC00007$, SYS_NC00008$, SYS_NC00009$, SYS_NC00010$
    FROM peoples;
    

    Вы увидите введённые вами данные.

    Из сказанного следует, что объектно-реляционная таблица это не тривиальное расширение реляционной таблицы.

    Индексы и ограничения целостности

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

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

    CREATE INDEX country_idx ON peoples (address.country);
    

    В этих же ситуациях можно определять и вводить ограничения целостности, например,

    ALTER TABLE peoples
    ADD CONSTRAINT peoples_cons1
    CHECK (address.country IS NOT NULL);
    

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

    NULL'bi в объектных таблицах

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

    Пример вставки значений NULL:

    INSERT INTO peoples
    VALUES(null, '22.11.80',
    address_type (null,  'Россия', null,null,null));
    

    Ссылочные типы

    Мы уже упоминали, что версия Oracle XE, видимо, не предназначалась для работы с объектно-реляционной моделью. Не всё, что реализуется в основных версиях СУБД, в ней удаётся. Правда, замеченные нами проблемы касаются работы оператора SELECT и процедуры PUT_LINE. В предыдущих разделах вы видели не совсем естественную работу SELECT с объектными таблицами. Здесь мы не сможем запрашивать ещё и данные ссылочного типа. С ними не работает и процедура PUT_LINE.

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

    Получение ссылки:

    DECLARE RefVar REF person; BEGIN
    SELECT ref(P)    INTO RefVar FROM peoples P WHERE P.address.zipcode=35000;
    END;
    Добавим в этот анонимный блок обращение по ссылке:
    DECLARE RefVar REF person; BEGIN
    SELECT ref(P)   INTO RefVar FROM peoples P WHERE P.address.zipcode=35000; 
    UPDATE   peoples P SET P.address.numb=166 WHERE ref(P)= RefVar;
    END;
    

    Создадим еще один тип:

    CREATE TYPE project AS OBJECT ( projno NUMBER(5), Emp_ref REF person );
    

    и объектную таблицу:

    CREATE TABLE projects OF project;
    

    Занесем один объект в таблицу projects:

    DECLARE refVar REF person; BEGIN
    SELECT ref(P) INTO refVar FROM peoples P
    WHERE P.name='C^4opoB И.П.',-
    INSERT INTO projects VALUES (1, refvar);
    END;
    

    Команда SELECT * FROM projects в других версиях Oracle даст что-нибудь вроде:

    Но, как уже говорилось, в Oracle XE она не работает.

    PROJNO EMP_REF
    -------------------------------------------------------------------------------------
    1  00022 02087 6D564 02DC6F0403E034 08002 0C944 037 6D56402DC6D
    

    Оператор deref извлекает объект по заданной для него ссылке

    Например, выборку из таблиц projects и peoples, связанных ссылками можно было бы организовать так:

    SELECT projno, deref(Emp_ref) FROM   projects P WHERE P.Emp_ref    IS NOT DANGLING;
    

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

    К сожалению, и этот запрос в Oracle XE не работает.

    Поскольку идентификатор таблицы, на которую ссылаются, содержится в ссылке, то ссылка в поле P.emp_ref может указывать на строку любой объектной таблицы типа person, а не одной таблицы peoples.

    Остается разъяснить предикаты is dangling и is not dangling. Если объект, на который указывает ref удалить, то ссылка будет указывать на несуществующий объект. Такую ссылку называют висячей (dangling). Предикат is dangling принимает истинное значение, если ссылка висячая.

    Пример: Установка значений NULL для всех висячих ссылок

    BEGIN
    UPDATE projects SET emp_ref = NULL
    WHERE emp_ref IS DANGLING;
    END;
    

    Объектные идентификаторы OID

    Пакет DBMS_ROWID позволяет узнать объектный идентификатор любой таблицы, и реляционной и объектной. Естественно, по OID восстанавливается имя таблицы (peoples в нашем примере в листинге 10.13).

    SELECT DBMS_ROWID.ROWID_Object(RowID)
    FROM peoples
    WHERE RowNum=1;
    
    
    select Object_Name
    from USER_OBJECTS
    where Object_ID=13698;
    
    Results:
    OBJECT_NAME
    PEOPLES
    
    

    10.3.3 Методы

    Различают методы-члены класса, методы конструктора, методы сравнения и статические методы.

    Методы — члены класса и методы конструкторов по умолчанию были рассмотрены в предыдущем разделе.

    Методы конструкторов создаваемых пользователем

    Построим пользовательский конструктор в типе person_typ2.

    CREATE OR REPLACE TYPE person_typ2 AS OBJECT (
    name  VARCHAR2(4 0),
    dob  DATE,  — дата рождения
    phone VARCHAR2(12),
    — спецификация метода, возвращающего возраст MEMBER FUNCTION Age
    RETURN NUMBER,        — возвращаемое значение CONSTRUCTOR FUNCTION person_typ (
    p_name VARCHAR,
    p_dob DATE )    RETURN SELF AS RESULT
    );
    

    Ключевые слова CONSTRUCTOR FUNCTION применяются для задания пользовательских конструкторов. Фраза RETURN SELF AS RESULT означает, что конструктор вернёт объект типа person_typ2.

    Создаём тело типа:

    CREATE OR REPLACE TYPE BODY person_typ2 AS
    MEMBER FUNCTION Age RETURN NUMBER IS
    BEGIN  — вычисление возраста
     
    RETURN ROUND(MONTHS_BETWEEN(sysdate, birthday)/12); END;
    CONSTRUCTOR FUNCTION person_typ ( p_name VARCHAR, p_dob DATE ) RETURN SELF AS RESULT IS BEGIN
    SELF.name := p_name; SELF.dob  := p_dob; SELF.phone  := '2-222-222';
    RETURN;
    END; END;
    

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

    SELF.name := p_name;
    

    означает, что атрибуту name создаваемого объекта передаётся значение, присвоенное формальному параметру p_name конструктора.

    Проверьте работу конструктора по умолчанию и пользовательского конструктора, позволяющего создать объект типа person_typ2 с двумя параметрами — именем и датой рождения. А вот телефон у таких объектов всегда один и тот же 2-222-222.

    Методы сравнения (MAP и ORDER)

    В предопределённых скалярных типах всегда задаются отношения эквивалентности и порядка. Именно поэтому возможно сравнение и упорядочение значений типа. В объектных типах эти отношения должен задать разработчик. Вы не сможете употреблять фразу ORDER BY в запросах, а для установления эквивалентности двух объектов придётся сравнивать какие-то поля классов.

    Методы сравнения MAP и ORDER позволяют задать эквивалентность и порядок на объектном типе данных.

    В объектной модели Oracle предусмотрены определяемые пользователем методы MAP и ORDER, позволяющие ввести отношение порядка.

    Метод MAP

    Метод MAP это функция член типа (member function) без аргументов с именем сопровождаемым ключевым словом MAP. Она может возвращать значения только следующих типов: DATE, NUMBER, CHAR, VARCHAR2 или REAL.

    В спецификации типа для задания метода MAP необходимо ввести фразу

    MAP MEMBER FUNCTION имя_функции RETURN тип_данных.
    

    В описании тела типа задаётся обычное описание функции члена типа перед которым помещается ключевое слово MAP:

    MAP MEMBER FUNCTION имя_функции RETURN тип_данных IS описание_функции;
    

    Приведём простейший пример реализации метода MAP, в котором точка характеризуется тремя параметрами, а эквивалентность точек определяется по двум из них:

    CREATE OR REPLACE TYPE point_typ AS OBJECT ( x NUMBER(3), y NUMBER(3), weight NUMBER(4,1), distance NUMBER(4,1),
    MAP MEMBER FUNCTION point_ecv RETURN NUMBER
    );
    CREATE OR REPLACE TYPE BODY point_typ AS
    MAP MEMBER FUNCTION point_ecv RETURN NUMBER IS BEGIN
    distance := x**2+y**2; RETURN distance; END;
    END;
    

    Теперь можно сравнивать объекты типа point_typ по значениям, возвращаемым функцией point_ecv, например, используя self.distance.

    Метод ORDER

    Это функция с одним аргументом объектного типа, возвращающая значение: — 1, если параметр больше SELF; 1, если параметр меньше SELF; 0, если параметр равен SELF. Для одного типа нельзя одновременно использовать и MAP и ORDER. Метод ORDER моделирует парное сравнение, используемое в психологии и социологии. Когда удаётся сравнить объекты исследования попарно, но не возможности сравнить несколько объектов сразу.

    Статические методы

    Методы-члены, методы сравнения и конструкторы задают поведение экземпляров объектного типа. Параметр SELF обеспечивает им доступ к атрибутам объектов.

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

    10.3.4 Наследование

    Используется единичное наследование, при котором, как обычно, тип-потомок наследует от предка все атрибуты и методы. Потомок обязательно расширяет тип-предок дополнительными атрибутами и, может быть, переопределяет методы.

    Фраза NOT FINAL в конце определения типа делает наследование возможным. Этот же результат достигается по умолчанию. Фраза FINAL запрещает наследование.

    Пример (заимствован из документа "Objeci-Relational Developer's Guide" B28371-03):

    Создаём тип person_typ для которого возможно наследование.

    CREATE OR REPLACE TYPE person_typ AS OBJECT (
    id NUMBER,
    name VARCHAR2(30),
    phone VARCHAR2(30),
    MEMBER FUNCTION show RETURN VARCHAR2)  NOT FINAL;
    CREATE OR REPLACE TYPE BODY person_typ AS MEMBER FUNCTION show RETURN VARCHAR2 IS BEGIN
    RETURN 'Id:  '   ||  TO_CHAR(id)   ||   ', Name:  '   || name; END; END;
    

    Заметим, что при необходимости можно запретить наследование командой

    ALTER TYPE person_typ NOT FINAL;
    

    и вновь разрешить его. Создаём подтип (наследуемый тип). Сначала определяем спецификацию типа

    CREATE OR REPLACE TYPE student_typ UNDER person_typ (
    dept_id NUMBER, major VARCHAR2(30),
    OVERRIDING MEMBER FUNCTION show RETURN VARCHAR2)
    NOT FINAL;
    

    затем тело типа

    CREATE OR REPLACE TYPE BODY student_typ AS OVERRIDING MEMBER FUNCTION show RETURN VARCHAR2 IS BEGIN
    RETURN (self AS person_typ).show || ' -- Major:  '   || major;
    END; END;
    

    Слово UNDER в спецификации типа обозначает наследование. Major это профилирующий предмет, изучаемый студентом.

    10.3.5 Коллекции

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

  • Вложенные таблицы (nested tables) - одномерные, неограниченные коллекции однородных элементов.
  • Массивы переменной длины (variable-size arrays) VARRAY представляют одномерную ограниченную коллекцию однородных элементов. В отличие от вложенных таблиц, порядок элементов при обработке и запоминании сохраняется. Количество элементов от 0 до указанного при определении максимального значения. Обращаются к элементам коллекции по индексу, как в массиве.
  • Важнейшее преимущество коллекции - возможность передать ее всю между базой и PL/SQL за одну последовательность чтений, что может существенно увеличить скорость обмена.

    Массивы переменной длины

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

    Простой пример: Пусть необходимо хранить в таблице идентификатор, фамилию, имя, отчество (одним полем) и до трех номеров телефонов. Определим тип Phones_typ как коллекцию типа VARRAY

    CREATE TYPE phones_typ AS VARRAY(3)  OF CHAR(9);
    

    Теперь определим таблицу этого типа

    CREATE TABLE clients ( id NUMBER, name VARCHAR2(50), phones Phones_typ
    );
    

    Вставим в неё строку

    INSERT INTO clients VALUES(42,'Сидоров Иван Петрович', phones_typ(  '11-22-66', '57-23-56'));
    

    Получить всю информацию о массивах VARRAY можно из представления словаря user_varrays, а обо всех доступных пользователю массивах из представления all_varrays. Достаточно выполнить запрос вроде

    SELECT * FROM user_varrays;
    

    Вложенные таблицы

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

    Рассмотрим простой пример.

    Для задания вложенной таблицы создадим объектный тип Doc_typ:

    CREATE TYPE Doc_typ AS OBJECT ( dno CHAR(5), dname CHAR(20), dtype CHAR(3)
    );
    

    Используя его, создадим тип вложенной таблицы:

    CREATE TYPE Documents_typ   AS   TABLE   OF Doc_typ;
    

    Теперь определим таблицу Document:

    CREATE TABLE Document (
    dno  CHAR(5)   PRIMARY KEY,
    dname  CHAR(20) UNIQUE,
    docum  Documents_typ
    ) NESTED   TABLE  docum   STORE   AS docum_table;
    

    С помощью команды describe обнаруживаем, что таблица doc-um_table действительно существует.

    Информация о всех вложенных таблицах получается запросом

    SELECT * FROM user_nested_tables;
    

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

    10.3.6 Объектные представления

    Существуют объектные представления (object view), которые могут создаваться над реляционными данными. Все расширения DML, предназначенные для работы с объектными таблицами, поддерживают и объектные представления. На их основе можно создавать объектно-ориентированные приложения, не модифицируя существующие реляционные схемы. Свойства объектных представлений подобны свойствам объектных таблиц.

    Создадим таблицу и тип данных:

    CREATE TABLE emp_table ( empnum NUMBER(5), ename VARCHAR2(20), salary NUMBER(9,2), job VARCHAR2(40)
    );
    

    Заполним её

    INSERT INTO emp_table
    VALUES(33,  'Иванов',  25000, 'начальник'); INSERT INTO emp_table
    VALUES(33,  'Петров',  20000, 'разработчик');
    

    Создадим тип

    CREATE OR REPLACE TYPE employee_typ AS OBJECT (
    empno NUMBER(5), ename VARCHAR2(20), salary NUMBER(9,2), job VARCHAR2(20)
    );
    

    и объектное представление

    CREATE OR REPLACE VIEW emp_view2 OF employee_t WITH OBJECT OID (empno) AS
    SELECT e.empnum, e.ename,    e.salary, e.job
    FROM emp_table e
    WHERE job = 'разработчик';
    

    Объектное представление выглядит для пользователя как объектная таблица со строками типа employee_typ. Каждая ее строка имеет уникальный объектный идентификатор.

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

    10.3.7 Сравнение полученных объектных моделей данных

    Итак, изучены две, технически достаточно различные объектные модели. Сравним их, имея в виду, что речь идёт о конкретных решениях, а не о соотношениях объектной и объектно-реляционной моделей данных вообще.

    Сначала перечислим общие особенности. Это, в первую очередь, появление персистентных классов, объекты которых хранятся в базе данных. Как следствие, использование двух объектных ссылок OID и OREF, обеспечивающих доступ к объектам в памяти и на диске.

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

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

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

    Поскольку Cache позволяет пользователю гораздо больше "самостоятельности", чем Oracle, в ней можно изменять структуры хранения и в пользовательских программах иметь доступ к промежуточным результатам работы СУБД. Но это отличия реализаций, а не моделей.

    Ещё один, наверное неожиданный, результат использования единой архитектуры Cache — "втаскивание" наследования в табличную модель данных.

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

    Страницы:

    Далее будет рассмотрено расширение модели данных за счёт включения в неё части метаданных. Это позволит работать со структурами данных, которые заранее не определены. Подобные модели принято называть универсальными. Обратим внимание на некоторые их особенности и способы реализации.

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

    К сожалению, доступные нам инструментальные средства позволяют получить скрипты только для изолированных классов базы данных. Поэтому примеров создания баз данных с помощью UML не будет.

    Объёмистые второй и третий параграфы лекции представляют объектную модель ODMG на примере Cache и объектно-реляционную модель на примере Oracle. Замечательная особенность Cache в том, что связи между иерархической, реляционной и объектной моделями в ней встроены, и потому отслеживаются естественным образом, без каких-либо дополнительных средств.

    10.1 Модели данных

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

    По крайней мере, некоторые данные должны обладать свойством перси-стентности — способностью сохраняться на диске.

    Различия роли типов в программах и базах данных перечислены в таблице 10.1

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

    Задать тип данных — это значит определить:

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

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

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

    Для задания модели данных необходимо определить:

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

    Бегло охарактеризуем с этой точки зрения модель "сущность-связь":

  • основные компоненты: сущность, связь, атрибут;
  • сущности бывают сильные и слабые,
  • связи соединяют сущности; связи имеют кратности $$1 : n$$ и $$m : n$$ и бывают идентифицирующие и не идентифицирующие; не идентифицирующие связи делятся на обязательные и не обязательные;
  • агрегаты сущностей и связей не предусматриваются;
  • атрибуты входят в состав сущностей и связей; в связях можно выделить атрибуты привязки и эмерджентные атрибуты;
  • обычно определяются типы данных число, строка, дата;
  • допустимые операции над данными не уточняются, так как модель предназначена только для представления структур данных, но не манипулирования ими;
  • допустимые ограничения целостности — первичный, уникальный и внешний ключи.
  • Иногда полезно поговорить о некоторой модели на языке описания другой модели. Например, описывая объектным языком реляционную модель данных, следовало бы указывать, что типы столбцов могут быть любыми, в том числе, векторными, но внутренняя структура данных всех типов в рамках реляционной модели не подлежит разбору. Таблицы представляют собой векторные типы. Конструкторы этих типов — команды CREATE TABLE и ALTER TABLE. Деструктор — команда DROP TABLE. Строки таблицы это экземпляры типов таблиц. Их конструкторы — команды INSERT, UPDATE, DELETE и TRUNCATE.

    10.1.1 Схемы Джекобса

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

    На множестве имен $$М$$ вводится понятие "R-правило", определенное как выражение вида:

    $$R_j=(R_{j_1},\dots,R_{j_m})$$

    где $$R_{j_1},\dots,R_{j_m}$$ — имена из $$M$$.

    Имя $$R$$ в левой части правила имеет высший порядок, имена $$R_{j_1},\dots,R_{j_m} \in~ M$$ попарно различны, причём одно из них может совпадать с $$R_j$$. Нулевой порядок могут иметь имена стоящие только в правых частях правил.

    Схема базы данных (или просто схема) это конечный набор

    $$S=\{R_j=(R_{j_1},\dots,R_{j_m})\}$$

    R-правил обладающих свойствами:

    левые части попарно различны;

    множества имен нулевого порядка и высших порядков не пересекаются;

    имена в правой части каждого правила уникальны.

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

    Пример схемы:

    Sch={
    Вуз  = (Факультет),
    Факультет    =   (Кафедра, Связь1),
    Кафедра      =   (Название, Сотрудник),
    Сотрудник   =   (ФИО, Должность, Связь2),
    Связь1        = (Кафедра),
    Связь2  = (Сотрудник),
    

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

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

    Правило схемы $$S=\{R_j=(R_{j_1},\dots,R_{j_m})\}$$ определяет правило подстановки $$R_j\to R_{j_1}R_{j_2}\dots R_{j_m}$$..

    Выпишем свойства, определяющие реляционную, иерархическую и сетевую модели данных.

    Характеристическое свойство реляционной модели

    Определение (1). Схема будет реляционной, если в правых частях всех её $$R$$-правил стоят только имена нулевого порядка.

    Установим взаимно однозначное соответствие между $$R$$-правилом и предикатом

    $$R_j(R_{j_1}R_{j_2}\dots R_{j_m})$$

    Тогда определение реляционности схемы можно перефразировать так:

    Определение (2). В реляционной схеме имена отношений не могут совпадать с именами атрибутов.

    Характеристическое свойство иерархической модели

    Определение (1). Схема будет иерархической, если любое имя может встречаться в правой части Д-правила только один раз и в грамматике, определённой схемой, не существует последовательности выводов такой, что $$R_i$$ выводимо из $$R_i$$.

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

    Характеристическое свойство сетевой модели

    Определение (1). Схема будет сетевой, если нет правила, в котором имя высшего порядка встречается одновременно и в правой и в левой части.

    Определение (2). В сетевой модели имена отношений не могут совпадать с именами атрибутов в рамках одного предиката.

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

    Дальнейшее рассмотрение схем Джекобса связано с введением формализма многосортной логики, который оказывается связанным с тремя перечисленными моделями данных. Но поскольку требуемый уровень изложения существенно выше принятого в книге, которую вы читаете, мы ограничимся указанием на то, что соответствующая теория была построена и отсылаем Вас к первоисточникам или к изложению этого вопроса в книге М.Ш. Цаленко [см. раздел "Что читать?"].

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

    10.1.2 UML как обобщённая объектная модель

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

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

    В объектно-реляционных СУБД за основу берётся реляционная модель, которую расширяют системой объектных типов данных, конструируемых пользователем. Язык запросов представляет собой расширение SQL.

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

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

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

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

    Рассмотрим язык UML (Unified Modeling Language) обладающий более широкими возможностями. Отметим, что термин "унифицированный" не означает претензий на универсальность. Просто UML был создан путем объединения как минимум трех языков концептуального моделирования.

    Для построения реляционных, объектных и объектно-реляционных баз данных из определённых в UML-2 тринадцати видов диаграмм нам достаточно диаграмм классов. Это структурные диаграммы, определяющие наборы классов, их атрибуты, операторы и связи между ними.

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

    Возможно четыре варианта обозначения класса, приведенные на рисунке 10.1.

    (рис 10.1) Изображения класса

    В таблице 10.1 приведен пример класса.

    Пример класса
    Счёт №...
    сальдо (остаток)
    проверить()
    зачислисть()
    овердрафтНаСумму()

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

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

    В частности, с точки зрения объектно-реляционной модели рассматриваемой ниже (в разделе 10.3), реляционная таблица рассматривается как класс без операций. Вызов операции может обеспечить доступ к скрытым атрибутам (вспомните методы get() и set()) и изменять его состояние.

    Вспомним, что активным называют объект способный создать управляющее воздействие. Четвёртая секция (рисунок 10.1), используемая в активных классах, представляет сигналы, управляющие поведением класса. В частности, сигналы — это исключительные ситуации.

    Определение. Диаграмма классов — это набор классов и cвязей между ними.

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

    Вернёмся к описанию классов.

    Для записи атрибутов используется следующий синтаксис:

    квантор_видимости имя_атрибута [кратность_атрибута]: тип_атрибута=нач_значение {строка_свойство}.
    

    Области видимости атрибутов обозначаются знаками:

  • + общедоступный (public),
  • # защищенный (protected),
  • - закрытый (private).
  • Область видимости protected может иметь разный смысл в различных языках. Например, защищённый атрибут может быть виден только внутри пакета.

    Кратность атрибута характеризует наличие ни одного (0) или нескольких атрибутов данного типа. Например:

    [0..1] — иногда есть, иногда нет

    [5, 7..9]— любое из перечисленных чисел 5,7,8,9.

    * — любая кратность

    Примеры записей атрибутов: +получитьАдрес : Address #Цена : {$200}

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

    квантор_видимости имя_операции(список_параметров): тип_возвращаемого_значения {строка_свойство}
    

    Квантор видимости принимает те же значения + , — и #.

    Строка-свойство в этом случае обозначает особые свойства операции. Например, {query} означает, что операция не может изменять данные, то есть является запросом.

    Список параметров:

    вид_параметра имя_параметра тип_параметра = значение_по_умолчанию
    вид_параметра: {in, out, inout} — входной, выходной, входной и выходной
    

    Для атрибутов и операций можно задавать область действия. Для свойств существует два варианта:

  • instance — каждый экземпляр имеет своё значение свойства;
  • static — для всех экземпляров одно значение. В UML различают следующие виды связей:
  • зависимости (dependency relationship);
  • ассоциации (association relationship);
  • обобщения (generation relationship), это взгляд на наследование снизу вверх;
  • реализации (realization relationship);
  • агрегации (aggregation relationship);
  • композиции (composition relationship).
  • Кратко рассмотрим связи за исключением реализации.

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

    При проектировании реляционных баз связи-зависимости реализовать невозможно.

    Зависимости изображаются штриховой линией со стрелкой, направленной к классу-источнику зависимости (рисунок 10.2).

    (рис 10.2) Пример связи-зависимости

    Определение. Связи-ассоциации классифицируются по числу соединяемых классов. Обозначаются они сплошными линиями. Выделяют бинарные связи (соединяют два класса), тернарные (три класса) и N-арные. Для бинарных связей может быть указан порядок соединяемых классов. Так, в примере на рисунке 10.3 связь читается так: "Мама моет раму".

    (рис 10.3) Пример связи-ассоциации

    Концы связи-ассоциации можно помечать именами ролей, для которых могут быть указаны кратности связи. Назначения ролей такие же, как в ER-диаграммах (см. раздел 2.2.6). Кратности роли указывают, сколько объектов с этой ролью могут участвовать в ассоциации. Так, кратность 1 указывает на обязательность связи, кратность 0..1 означает её необязательность. Задание диапазона 1..* говорит о том, что все объекты должны участвовать в каком-нибудь экземпляре ассоциации и что число объектов, участвующих в одном экземпляре ассоциации не ограничено.

    В примере на рисунке 10.4 в показано, что работник обязательно работает равно в одном отделе, а отдел может существовать вообще без работников, но может иметь до 15 человек.

    (рис 10.4) Пример связи-ассоциации с кратностью

    Как вы уже поняли, ассоциации реализуются в реляционной модели.

    Определение. Связью-обобщением называется связь между более общим классом, называемым родителем или предком, и более специализированным классом, называемым потомком или подклассом.

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

    (рис 10.5) Пример связи-обобщения

    Множественное наследование порождает проблему именования атрибутов и операций в подклассе. При совпадении имён атрибутов и операций у родительских классов, необходимо уточнить их семантику и либо переименовать одно из имён, либо запретить наследование от одного из родительских классов, либо унаследовать всё, а затем в потомке произвести переименование.

    В Cache допускается множественное наследование. При совпадении имён элементов классов-предков действует определение элемента последнего класса в списке предков. Исключение составляют ключевые слова класса, наследуемые от первого предка в списке.

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

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

    (рис 10.6) Пример связи-агрегации

    В нашем примере предполагается, что столы и стулья могут существовать независимо от аудитории. Свои функции они могут исполнять, например, в коридоре.

    Агрегатов в реляционной модели нет.

    Определение. Связи-композиции — это агрегации, у которых связь компонентов и агрегата настолько сильна, что уничтожение агрегата приводит к уничтожению его составных частей (рисунок 10.7)

    (рис 10.7) Пример связи-композиции

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

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

    В объектных и объектно-реляционных моделях данных использование инструментария UML даёт значительно больший эффект.

    10.2 Объектная модель данных

    В настоящее время имеется два основных претендента на ведущую роль в применении объектов в базах данных. Это объектно-реляционные базы, стандартизованные в рамках SQL3 и объектные базы в стандартах консорциума ODMG (Object Data Management Group).

    Объектную модель ODMG можно считать расширением языков ООП на персистентные классы, объекты которых могут сохраняться в базе данных. Естественно, в СУБД, реализующей такую модель, необходимо создать свою систему хранения объектов.

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

    10.2.1 Особенности архитектуры Cache. Система классов

    Объектная система Cache построена на объектном расширении языка ObjectScript, который изначально был персистентным. Уникальная особенность системы в том, что она позволяет работать с данными одновременно в объектной, реляционной и иерархической моделях, не заботясь ни о каких отображениях (mappings). Заметим, что сама фирма Intersystems, скорее, не согласится с такой трактовкой её детища. Для неё Cache, в первую очередь, объектная СУБД. Однако, легко получаемые в Cache дедуктивная, полуструктурированная и другие модели, включая так называемые NoSQL, позволяют считать Cache, по крайней мере, потенциально полимодельной СУБД.

    Универсальная архитектура Cache

    В универсальной архитектуре Cache (рисунок 10.8, таблица 10.2) и объекты и таблицы отображаются в многомерные структуры, хранением которых заведует механизм многомерной памяти. Унифицированные структуры данных доступны и серверу объектов и SQL-серверу. Программное обеспечение промежуточного уровня может обращаться к одному из этих серверов. Шлюз SQL позволяет обмениваться данными с другими базами реляционного типа.

    (рис 10.8) Универсальная архитектуря Cache
    Универсальная архитектура Cache
    Middleware/Application Промежуточное ПО/Приложение
    SQL Database База данных SQL
    Objects Объекты
    Unified Data Arhitecture Унифицированная архитектура данных
    Multidimensional Storage Engine Средство многомерного хранения

    SQL-доступ в Cache допускает использование интерфейсов ODBC, JDBC и ADO.Net. Поддерживается связывание данных с объектно-ориентированными языками, включая Java, C# и C++.

    Система классов

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

    Можно считать понятия класса и типа синонимами. Как и в естественных языках, объёмы понятий-синонимов перекрываются, но не совпадают. Предопределённые типы данных инкапсулированы, то есть их определения не доступны для изменения. Однако, пользовательские типы открыты. Обычно классы выстраиваются в иерархию наследования. У типов этого нет. Типы данных не могут содержать свойств. Для типов данных невозможно создавать экземпляры.

    В Cache принята следующая терминология. Классы вообще подразделяются на классы типов данных и классы объектов (рисунок 10.9). Классы типов данных определяют допустимые значения констант (литералов) и позволяют их контролировать. Классы типов данных содержат предопределённые наборы методов проверки и приведения значений атрибутов к другим типам.

    (рис 10.9) Таксономия классов Cache

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

    Незарегистрированные классы (Non-Registered Class) предназначены для создания пользовательской объектной системы и не имеют предопределенного поведения. Объектные ссылки для них не формируются. Разработка функций незарегистрированного класса —обязанность разработчика.

    Объекты зарегистрированных классов и их наследники имеют объектную ссылку OREF (object reference), идентифицирующую объект, находящийся в памяти компьютера.

    Хранимые объекты имеют вторую ссылку OID (object ID), идентифицирующую объект на физическом носителе.

    Зарегистрированные классы это временные классы, обладающие предопределенным поведением. Реализуется оно набором встроенных функций, наследуемых из системного класса %RegisteredObject и отвечающих, в частности, за создание новых объектов и за управление размещением объектов в памяти. Заметим, что знак процента в имени означает, что класс или метод системные.

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

    Зарегистрированные классы могут быть ещё хранимыми и встраиваемыми. Первые хранятся независимо и потому имеют уникальную не изменяемую объектную ссылку OID, по которой объект может быть найден на диске, и ссылку OREF.

    Встраиваемые классы могут попасть на диск только в составе хранимого класса и потому OID не имеют, но при выгрузке в память для них создаётся соответствующая ссылка OREF. Встраиваемые классы наследуют свое поведение от класса %Serial.

    Хранимые классы наследуют свое поведение от класса %Persistent, используя обширный набор его методов, включающий создание объекта, подкачку объекта из базы в память, удаление объекта и т.д. Каждый экземпляр хранимого класса имеет два уникальных идентификатора — OID и OREF.

    Более подробно методы хранимых классов будут рассмотрены ниже.

    10.2.2 Работа с классами

    Классы

    Для того, чтобы определить класс (создать спецификацию класса) необходимо определить элементы его спецификации, перечисленные ниже:

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

    Уникальное имя класса, свойства (атрибуты) и методы понимаются в обычном для ООП смысле. Другие элементы:

  • Параметры. Позволяют изменить возможности класса во время его компиляции. При этом обычно используют генераторы методов.
  • Запросы — это операции с объектами класса, играющие роль фильтров.
  • Индексы. Эти не обычные для ООП элементы используются, как и в реляционной модели, для ускорения доступа.
  • Триггеры. Определяются в рамках объектной модели, так как в действительности и для таблиц, и для классов создаётся единственная структура. Используются триггеры только в реляционной модели, так как активность в объектной модели реализуется методами классов.
  • Свойства

    Свойства представляют собой константы предопределенных типов, ссылки на объекты, встроенные объекты, потоки данных (BLOB, CLOB), коллекции, древесные значения и отношения. Коллекции могут быть массивами и списками.

    Несколько примеров задания свойств:

  • Переменная строчного типа

    Property Name As %String(MAXLEN = 20);
    
  • Ссылка на объект. Пусть имеется хранимый класс Address. Тогда свойство Address можно описать так
    Property Address As Address;
    
  • Встроенный объект. Пусть имеется встраиваемый класс Address. Свойство Address записывается точно так же как в предыдущем варианте

    Property Address As Address;
    

    но речь идет не о ссылке, а о встраивании объекта.

  • Свойства-отношения используются и в расширенной реляционной модели, и в объектной. Для независимых объектов поддерживается тип один-ко-многим, а для зависимых родитель-потомок (потомок зависит от родителя). Отношение подобно свойству содержащему две объектные ссылки. При этом обеспечены ссылочная целостность. В результате, при удалении объекта не может появиться висячая ссылка на другой стороне отношения. На одной стороне отношения (многие или потомок) помещается как бы простое свойство, а на другой стороне коллекция, наследующая системному классу %RelationshipObject.

    В определении класса Cache можно задать вариант хранения свойств, указывая одно из ключевых слов transient, calculated, multidimensional. Задание по умолчанию определяет свойство, которое имеется в памяти и хранится в базе данных.

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

    Если свойство объявлено как вычисляемое (calculated), то его значения будут вычисляться каждый раз при выполнении. Для свойств этого вида разработчик должен сам создать метод Get() производящий необходимые вычисления.

    Ключевое слово multidimensional означает, что свойство представляет многомерный массив.

    Коллекции можно рассматривать как отношения $$1 : n$$.

    Методы

    Как всегда в ООП, различают метод класса и метод экземпляра (объекта). Метод класса не имеет доступа к свойствам или методам экземпляров, однако имеет доступ к параметрам классов. Типичный пример метода класса — конструктор %New(), который не применим к экземпляру класса, но порождает его.

    В коде метода можно определить язык, на котором метод написан. Тексты на языках ObjectScript и Cache Basic транслируются в исполняемый код. Методы написанные на Java выполняются в среде виртуальной машины Java.

    Методы могут возвращать данные любого допустимого типа.

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

    Cache поддерживает четыре типа методов:

  • Методы-коды (Code Methods);
  • Методы-выражения (Expression Methods);
  • Методы-вызовы (Call Methods);
  • Методы-генераторы (Method Generators).
  • Метод-код представляет собой программу, написанную на языках Object-Script, Cache Basic или Java. При использовании первых двух языков в код метода можно включать макросы, встроенный SQL, встроенные HTML и JavaScript.

    Метод-выражение содержит одно вычисляемое выражение, после своего вызова вычисляет его и возвращает результат.

    Метод-вызов обращается к внешней для класса программе.

    Метод-генератор содержит код, предназначенный для порождения кода на языке ObjectScript. Используется только при компиляции класса для генерации версии времени исполнения.

    Создаём класс

    Создадим простейший класс с единственным атрибутом Name. Запускаем Cache Studio. Выбираем в меню пункт Файл-Создать (рисунок 10.10).

    (рис 10.10) Создание класса

    Затем выделяем иконку "Класс Cache" и нажимаем кнопку "OK". Появляется первый экран мастера создания класса (рисунок 10.11).

    (рис 10.11) Задание имени и комментария

    Пакет, который предлагается указать, служит для объединения некоторой совокупности родственных классов. Пакеты %SYS, DOCBOOK, SAMPLES и USER создаются СУБД при инсталляции.

    Оставляем имя пакета по умолчанию (User) и указываем имя класса. Можно дополнить описание класса комментарием, который как всегда будет проигнорирован компилятором. Кнопкой "Далее >" переходим к следующему шагу (рисунок 10.12).

    (рис 10.12) Выбор типа класса

    Здесь мастер предлагает нам выбрать тип объекта (таблица 10.3).

    Выбор типа объекта
    Persistent Тип объектов хранимых в базе
    Serial встраиваемые объекты; экземпляры классов этого типа могут сохраняться в базе только в случае включения в хранимые объекты
    Registered тип объектов, которые могут существовать только в оперативной памяти без возможности их сохранения на диске
    Abstract тип объектов у которых не может быть экземпляров
    Data Type (тип данных) указывает на то, что данный класс будет типом данных
    CSP класс типа CacheServerPage, предназначенный для построения серверных страниц Cache
    Extends (расширяет) указывает на то, что данный класс является наследником некоторого уже существующего типа или типов; организовать множественное наследование можно перечислив через запятую имена классов родителей

    Выбираем тип Persistent и кнопкой "Далее >" продолжаем создание класса (рисунок 10.13).

    (рис 10.13) Дополнительные характеристики класса

    В последнем окне, изображенном на рисунке 10.15, мастер предлагает указать дополнительные характеристики. Их значения станут частично понятными из дальнейшего изложения, а пока давайте оставим их пустыми и завершим создание кнопкой "Готово".

    Пополним созданный класс свойством Name. Для этого выбираем пункт меню Класс > Добавить > Новое свойство для запуска мастера (рисунок 10.14).

    Указываем имя свойства Name и, если есть желание, добавляем комментарий.

    (рис 10.14) Задание имени свойства и его описания

    Идём далее (рисунок 10.15).

    (рис 10.15) Тип свойства

    Выбираем тип создаваемого свойства.

    В Cache поддерживаются несколько типов свойств:

  • Единичное значения типа. Простые типы данных (%String, %Integer, %Date и т.д.) наследуют свое поведение от класса типов данных.
  • Коллекция типа. Объектные базы, в отличие от реляционных, могут хранить множество значений в одном свойстве. В Cache поддерживаются два типа коллекций — массив и список. Элементы массива имеют индекс, уникально характеризующий элемент в массиве. Элемент списка определяется номером его позиции в списке.
  • Отношение. Связи представляют собой двунаправленные зависимости между хранимыми объектами. В Cache реализовано два типа связей — Parent - Child (родитель - потомок) и One - Many (один-ко-многим). Первая связь зависимая, то есть при удалении предка, автоматически удаляются все наследники. Связь One-Many независимая связь, поэтому удаление предка при существовании наследников приводит к ошибке.
  • Логично предположить, что свойство Name является строкой. Поэтому не меняя типа переходим к следующему шагу (рисунок 10.16).

    (рис 10.16) Характеристики свойства

    Здесь необходимо небольшое пояснение. Первый пункт аналогичен заданию свойства NOT NULL для полей SQL-таблиц, второй — создает индекс для определяемого свойства, третий — аналогичен ограничению UNIQUE , четвертый указывает на то, что данное свойство является вычислимым, то есть его значение вычисляется во время обращения к нему. Ниже будет установлено, что при создании класса определяется соответствующая ему таблица. Её имя может отличаться от имени класса. Если необходимо, чтобы имена свойств и соответствующих им столбцов таблиц не совпадали, укажите имя SQL-столбца.

    Характеристики свойства по умолчанию оставим без изменения. Остаётся ещё раз нажать кнопку "Далее >". Появится список параметров созданного свойства (рисунок 10.17).

    (рис 10.17) Параметры свойства

    Изменим в нём значение параметра MAXLEN "20". На последнем шаге мастер предлагает переопределить методы доступа к свойствам Get и Set. Мы не станем этого делать и завершим создание свойства Name нажатием на кнопку "Готово" (рисунок 10.18).

    (рис 10.18) Переопределение методов Set() и Get()

    Теперь в окне Studio мы получим следующий текст

    Class User.A Extends %Persistent {
    Property Name As %String(MAXLEN = 20); }
    

    Чтобы класс стал доступным для использования, его нужно откомпилировать (позиция меню "Собрать > Компилировать").

    Выясним, как создаются экземпляры класса (объекты) и где они хранятся.

    Запустим портал управления системой, выбрав пункт " Утилиты > Портал управления системой" в главном меню Studio (рисунок 10.19). В управлении данными выбираем раздел Глобалы.

    (рис 10.19) Раздел "Глобалы"

    Затем переходим в область User. Проверим, не существует ли глобал с именем ^User.AD. Его имя составляется из имени класса с приписанным суффиксом "D" (данные). Смысл этого таинственного действия в том, что любая хранимая структура образует глобал, и экземпляры класса A будут храниться именно в этом глобале. Скорее всего, глобал ^User.AD там не обнаружится. Точнее, наши предыдущие действия не создавали такого гло-бала. Но, если ^User.AD существует, удалим его.

    После этого запускаем терминал. В нём создаем экземпляр класса с помощью метода-конструктора %New(): |uSER>s ss=##class(User.A).%New()

    Макроподстановка ##class создает объектную ссылку OREF. Что же представляет собой эта ссылка?

    USER>w ss 
    1@User.A
    

    Итак, OREF состоит из двух частей имени класса "User.A" и идентификатора объекта "1".

    Можно убедиться, что глобал ^User.AD ещё не появился.

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

    USER>s ss.Name="John"  // параметру Name объекта №1 присвоено значение.
    USER>d ss.%Save()  // объект №1 сохранен на диске.
    USER>d ss.%Close()  // объект №1 закрыт, то есть удален из памяти.
    

    С помощью портала управления системой обнаруживаем созданный глобал ^User.AD (рисунок 10.20). Забегая вперёд, отметим, что при использовании индексов может быть создан ещё глобал индекса с именем ^User.AI.

    (рис 10.20) Область USER

    Нажав на Просмотр, получаем подробную информацию о глобале:

    ^UserTD=1
    ^User.TD(1)=$lb.("","John")
    

    Обратите внимание, что после закрытия объекта значение переменной хранящей OREF не меняется.

    USER>W ss 
    1@User.A
    

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

    Просмотреть OID объекта можно с помощью метода %Oid():

    USER>W ss.%Oid() 
    User.A
    

    В действительности OID представляет собой список, состоящий из ID объекта и имени класса. Можем в этом убедиться, выполнив в терминале следующую команду:

    USER>f i=1:1:$ll(ss.%Oid())  {w !,$li(ss.%Oid(),i)}
    
    1
    User.A
    

    Теперь создаем второй объект:

    USER>s ss=##class(User.A).%New() 
    USER>s ss.Name="Peter"  // параметру Name объекта №2 присвоено значение. 
    USER>d ss.%Save()      // объект №2 сохранен на диске.
    USER>d ss.%Close()  // объект №2 закрыт, то есть удален из памяти
    

    Остановимся на минутку, чтобы представить в общих чертах структуру класса A. Мы создавали персистентный класс, наследующий системному классу %Persistent. От родителя все такие классы получают следующие методы внешнего интерфейса:

  • %New(). Конструктор объекта. Его задача — создать экземпляр класса и присвоить ему OREF.
  • %Save() . Сохраняет экземпляр класса на диске и присваивает ему
  • OID.
  • %Close(). В старых версиях уменьшал значение счетчика OREF на единицу и уничтожал версию объекта в памяти. Начиная с версии 5.0, этот метод ничего не делает.
  • %Open(). Метод класса. В качестве первого аргумента получает OID объекта (или ID заключенный в оператор построения списка LB(). Если он находит объект существующий в базе данных, то создает в памяти его копию, содержащую значения всех свойств, и возвращает объект. Если объект уже загружен в память, просто возвращается OREF. Вообще у метода три аргумента. Второй аргумент Concurrency определяет особенности параллельной работы и принимает значения 0, 1, 2, 3, 4. По умолчанию установлен в "1", что означает создание разделяемой блокировки при загрузке объекта в память.
  • %OpenId(). Отличается от предыдущего тем, что в качестве аргумента получает не OID, а ID.
  • %Delete(). Удаляет версию объекта, хранящуюся на диске, копия в памяти при этом остается. В качестве единственного параметра получает OID, этот идентификатор больше не используется впоследствии (это касается классов с внутренней системой хранения Cache, в противном случае ответственность за повторное использование OID-ов полностью ложится на плечи разработчика).
  • %DeleteId(). Отличается от предыдущего тем, что в качестве аргумента получает не OID, а ID.
  • %IsModified(). Возвращает "истинно" (1), если значения свойств объекта были изменены, в противном случае — 0.
  • 10.2.3 Классы, таблицы, объекты и деревья

    Таблиц не бывает

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

    В предыдущем разделе мы создали класс А и один объект этого класса. Неожиданность в том, что в разделе SQL портала управления системой обнаруживается таблица с именем SQLUser.A и тем же атрибутом Name, что в созданном классе (таблица 10.4).

    Таблица SQLUser.A, соответствующая классу A
    Столбец Тип данных Столбец # Обязательный Уникальный Сортировка Скрыто MaxLen BLOB Контейнер Селективность Тип xDBC
    ID %Library.Integer 1 Yes Yes No No 1 INTEGER
    name %Library.String 2 No No SQLUPPER No 20 No VARCHAR
    x__classname %Library.CacheString 3 No No Yes No VARCHAR

    Обратите внимание на то, что Cache сама создала столбцы ID и

    x classname. Их можно прочитать запросами SQL, но запрос типа

    SELECT * выдает в результате только заданные в определении класса столбцы, ID объектов и номера строк (таблица 10.5).

    Результат запроса SELECT * FROM A
    # ID Name
    1 1 John
    Завершено

    Столбцы "ID" и "#" (номер строки в результате запроса) — это не одно и то же. Проиллюстрируем это, добавив в таблицу SQLUser.A еще два объекта с помощью инструкции INSERT:

    INSERT INTO A VALUES('James') 
    INSERT INTO A VALUES('Michael')
    

    Выполнив запрос SELECT * FROM A, увидим следующий результат (листинг 10.1).

    SELECT * FROM A
    #  ID  Name 
    1  1  John 
    2  2   James
    3  3  Michael
    

    Теперь удалим строку с ID=2 и снова выполним запрос SELECT * FROM A (листинг 10.2):

    SELECT * FROM A
    # ID Name
    1 1 John
    2 3 Michael
    

    Понятно, что левая колонка с именем "#" — это номер строки результата, а колонка "ID" — это идентификатор объекта (строки таблицы). То есть ID — это суррогатный ключ, созданный автоматически.

    Попробуем перейти в обратном направлении, от таблиц к классам.

    Построим несложную таблицу. В SQL-менеджере наберём команду:

    CREATE TABLE T   (c1 NUMBER(2),   c2 CHAR(3))
    

    но не будем её исполнять. Посмотрим сначала в портале, не существует ли класса с именем "T". Для этого в разделе Классы перейдем в область User. Поскольку создаваемые классы привязываются к области имён, перед именем класса следует ожидать появления префикса User. Значит, ищем имя User.T. Если оно найдётся, удалим этот класс. Теперь исполним команду создания таблицы T. В портале появится класс User.T. Если вы этого не увидели, нажмите на кнопку Обновить для обновления содержимого страницы браузера. Класс User.T обязательно появится. Нажав на ссылку Документация рядом с именем класса, можем посмотреть его содержимое. Чтобы увидеть описание класса, необходимо в студии в навигаторе зайти в папку Классы/User и щёлкнуть два раза левой кнопкой мыши по имени класса T. Появится описание этого класса на языке CDL (Class Define Language):

    Class User.T Extends %Persistent  [ ClassType = persistent, DdlAllowed,  Owner = UnknownUser, 
    ProcedureBlock, SqlRowIdPrivate, SqlTableName = T,  StorageStrategy = Default]
    {
    Property c1
    As %Library.Numeric(MAXVAL = 99,  MINVAL = -99,  SCALE = 0) [  SqlColumnNumber = 2 ]; Property c2
     
    As %Library.String(MAXLEN = 3)   [ SqlColumnNumber = 3 ];
    }
    

    Несмотря на незнание языка CDL, понимаем, что в первой строке записано имя класса User.T, потомка класса %Persistent, а свойства c1 и c2 соответствуют именам столбцов c1 и c2. Ширина столбцов c1 и c2 соответственно 2 и 3, но в определении числовое свойство задается минимальным и максимальным значениями (—99 и 99), а для строчных данных указывается максимальное число символов (MAXLEN=3). Обратите внимание, что для созданных свойств SqlColumnNumber равно 2 и 3 соответственно. Происходит это потому, что столбцу ID всегда назначается номер 1.

    И ещё одна любопытная подробность. Создайте сами класс без единого атрибута. В реляционной ипостаси в него можно внести сколь угодно много строк (INSERT INTO имя VALUES(NULL)). Такое возможно благодаря тому, что СУБД сама создаёт столбцы ID и #.

    Итак, создание класса вызывает появление таблицы, а таблица генерирует класс. В студии посмотрим, что у нас хранится ("Файл >Открыть") на самом деле. Обнаруживаются файлы с расширением .cls, хранящие исходные тексты классов. Описаний таблиц (скриптов CREATE TABLE ...) не существует.

    Значит, таблиц в Cache действительно не существует, есть возможность смотреть на классы как на таблицы.

    Вставляли строку, а создали или пополнили дерево

    Проверим на всякий случай, не существует ли глобал с именем T, после которого приписана буква D. Если глобал ^User.TD существует, удалите его. Теперь в SQL-менеджере введём в таблицу T одну строку:

    INSERT INTO T VALUES  (22, 'QQ')
    

    С помощью команды SELECT * FROM T убеждаемся, что строчка действительно записана (листинг 10.3).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    

    Переходим в раздел "Глобалы" и обнаруживаем глобал AUser.TD. Если он не появился, нажмите на кнопку F5. Интересно, как выглядит вновь созданный глобал. Щёлкаем дважды левой кнопкой мыши по строчке AUser.TD и получаем его структуру (листинг 10.4)

    ^UserTD=1
    ^User.TD(1)=$lb.("",22,"QQ")
    
    

    В узле глобала ^User.TD(1) находится построенный список из пустого элемента и введённых нами значений $LB("","22","QQ"), соответствующий строке таблицы SQLUser.T. Корню дерева ^User.TD присвоено значение 1. Если добавить ещё одну строку, например (1, 'A'), выполнив команду

    INSERT INTO T VALUES   (1, 'A')
    

    то дерево изменится так (листинг 10.5).

    ^UserTD=2
    ^User.TD(1)=$lb.("",22,"QQ")
    ^User.TD(1)=$lb.("",1,"A")
    
    

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

    Глобалы рассматриваемого вида можно изменять в языке ObjectScript, используя функции для работы со списками. Попробуем изменить таблицу не командой SQL вида

    UPDATE T SET c1=11 WHERE c1=1
    

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

    s ^User.TD(2)=$LB("","11","A")
    

    Прочитаем результат в SQL-менеджере, как обычно, командой SELECT * FROM T (листинг 10.6).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    

    Попробуем теперь добавить с терминала еще одну строчку:

    s ^User.TD(3)=$LB("","7","Z")
    

    Проверим результат командой SELECT. Вроде бы все получилось (листинг 10.7).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    3 7 Z
    

    Вставим ещё одну строку командой INSERT в SQL-менеджере. Однако, новая строка не была введена (листинг 10.8), а заместила старую третью строку (7,'Z'). Произошло это потому, что корень дерева ^User.TD хранит число строк в таблице, а мы это значение не изменили.

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    3 33 HH
    

    Теперь вставим строку правильно:

    USER>s ^User.TD=4
    USER>s
    ^AUser.TD(4)=$LB("","9","99")
    

    Командой SELECT убеждаемся, что строка действительно вставлена (листинг 10.9).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    3 33 HH
    4 9 99
    

    И теперь при добавлении новой строки, например (55, 'UU'), с помощью SQL-менеджера, она не будет замещать последнюю строку, добавленную нами через терминал (листинг 10.10).

    SELECT * FROM T
    # c1 c2
    1 22 QQ
    2 11 A
    3 33 HH
    4 9 99
    5 55 UU
    

    Обратимся к словарю. В Cache все классы словаря хранятся в пакете %Dictionary. Чтобы его просмотреть, в портале выберем "System Explorer > Классы" перейдем в область USER. После нажатия на ссылку Документация около имени любого класса, появится страница описания класса.

    Убедимся, что не все атрибуты класса отображаются в столбцы таблицы. Для свойств класса можно установить значение видимости Private. Такое свойство не будет передаваться в таблицу.

    Создадим класс Z, у которого второе свойство имеет значение видимости Private:

    Class User.Z Extends %Persistent {
    Property P1 As %String;
    Property P2 As %String [ Private ];
    }
    

    Запрос SELECT * FROM Z показывает, что столбца P2 в таблице нет. Попутно мы, подобно Журдену у Мольера, в сорок лет узнавшему, что он всю жизнь говорил прозой, убедились, что в реляционных базах данных все столбцы общедоступны, то есть имеют видимость Public.

    В Cache любая заполненная таблица порождает простое сбалансированное дерево уровня 1. Однако, не каждое дерево может быть отображено одной таблицей.

    Упражнение 1

    Установите, будет ли уничтожен глобал, если связанная с ним таблица будет удалена командой DROP.

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

    Каждому объекту некоторого класса соответствует строка таблицы и узел в дереве глобала. Естественно, что вне СУБД Cache можно предложить другие отображения между деревьями, таблицами, классами и объектами.

    (рис 10.21) Отображение классов в таблицы и таблицы в дерево

    Наследование

    Одно из существенных отличий объектной модели от реляционной в том, что классы могут быть наследниками других классов. Разберемся с некоторыми особенностями наследования в Cache.

    Создадим класс Human следующей структуры:

    Class User.Human Extends %Persistent
    {
    Property Pass As %String(MAXLEN = 11);
    // серия и номер паспорта
    Property Name As %String(MAXLEN = 20);
    //имя
    }
    

    Класс-наследник Student расширяет базовый класс свойством NZach — номер зачетной книжки

    Class User.Student Extends User.Human
    {
    Property NZach As %String(MAXLEN = 10);
    }
    

    В SQL-представлении появляются две таблицы Human и Student, причем поля Name и Pass в таблице Student будут виртуальными. То есть, при создании новой записи в таблице Student значения этих полей сохраняются в таблице Human и информация о них извлекается по внутренней ссылке.

    Создать новый экземпляр класса Student можно двумя способами: sql-запросом или средствами ObjectScript.

    В классе Human создадим объект Петр ("Петр", "0305 855637"), а в классе Student — объект Иван ("Иван", "0305 163788", "8765").

    Данные обоих классов (родителя и наследника) помещены в один глобал ^User.HumanD (лиситинг 10.11).

    ^User.HumanD=2
    ^User.HumanD(1)=$lb("","0305 855637","Петр")
    ^User.HumanD(2)=$lb("~Student","0305 163788","Иван")
    ^User.HumanD(2,"Student")=$lb("8765")
    

    А вот содержимое таблиц Human и Student, полученное запросами вида SELECT * выглядит неожиданно с точки зрения реляционного народа (таблицы 10.6 и 10.7). Приходится признать, что в реляционной ипостаси Cache наследование реализуется.

    Содержимое таблицы Human
    # ID Name Pass
    1 1 Петр 0305 855637
    2 2 Иван 0305 163788
    Завершено
    Содержимое таблицы Student
    # ID NZnach Name Pass
    1 2 8765 Иван 0305 163788
    Завершено

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

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

    Сериализуемые объекты

    На первый взгляд кажется, что сериализуемые объекты в реляционном представлении приводят к появлению двухуровневых шапок таблиц, которые, как будет показано в разделе 10.3, характерны для объектно-реляционной модели. Покажем, что в объектной модели это не так. Создадим встроенный класс Address:

    Class User.Address Extends %SerialObject {
    Property City As %String; Property State As %String; 
    }
    

    Создадим класс Person в виде:

    Class User.Person Extends %Persistent
    {
    Property Name As %String; Property YearOB As %Integer; Property Home As Address;
    }
    

    Создадим объект класса Person:

    S p=##class(User.Person).%New()
    S p.Name="Nick", p.YearOB=1984, p.Home.City="NewYork" S p.Home.State="NY" D p.%Save()
    

    Образовался глобал:

    ^User.PersonD=1
    ^User.PersonD(1)=$LB("","Nick","1984",$LB("NewYork","NY"))
    

    Видим, что объект сериализуемого класса в глобале представляется списком

    $LB("NewYork","NY")
    

    В SQL-проекции инструкцией SELECT * ... выберем всё содержимое таблицы Person (таблица 10.8). Обращаем внимание на появление столбцов с именами Home_City и Home_State, образованными соединениями имени атрибута Home из класса Person и имен атрибутов City и State из класса Address. Это позволяет избежать появления двухуровневой шапки в таблице и, тем самым, не выходить за рамки реляционных таблиц.

    Результат с синтезированными именами столбцов
    # ID Name YearOB Home_City Home_State
    1 1 Nick 1984 New York NY
    Завершено

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

    SELECT Name, Home_City FROM Person
    

    Индексы в объектной модели

    Индексы в объектной модели Cache можно создать тремя способами:

  • SQL-командой CREATE INDEX в реляционном представлении класса;
  • непосредственно в коде определения класса;
  • с помощью мастера создания индексов.
  • В реляционной модели инструкция создания индекса:

    CREATE   [UNIQUE   |   BITMAP   |   BITSLICE  ]   INDEX index-name ON [TABLE] [имя_схемы.]имя_таблицы (имя_поля, ...)
    

    Индексы bitslice ускоряют работу с диапазонами, в том числе вычисления групповых функций.

    Синтаксис строки определяющей индекс в описании класса:

    INDEX имя_индекса
    ON список_атрибутов [список_ключевых_слов];
    

    Например, в созданном нами классе Person древесный индекс на свойство Name в теле класса может быть записан строкой

    Index NameIDX On Name;
    

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

     Index NamelDX On Name [type=standard];
    

    Для побитового индекса type= bitmap, третий вариант bitslice.

    Уникальный индекс определяется ключевым словом unique. Например, индекс для ИНН, который имеют не все люди, может быть определён фразой:

    Index INNIDX on INN [Unique];
    

    В стандартном индексе можно хранить не только идентификаторы объектов, но и данные. Достаточно в описание индекса включить секцию Data:

    Index NameIDX On Name [Data=(YearOB)];
    

    Теперь запросы к столбцам Name и YearOB потребуют только данных из файла Personl. Индексами можно управлять и непосредственно из программы используя метод %BuildIndices() класса %Persistent. Например, для перестроения всех индексов класса Person, у которого уже созданы объекты, следует набрать

    Set ss= ##class(USER.Person).%BuildIndices()
    

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

    Для создания индекса с помощью мастера в студии нажмите кнопку "Индекс" со знаком молнии, либо наберите в главном меню "Класс > Добавить > Индекс", затем задайте имя индекса и в следующем окне выберите тип индекса (рисунок 10.22).

    (рис 10.22) Задание типа индекса

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

    Опция IDKEY задаёт свою идентификацию объекта.

    Вариант Extent обеспечивает создание индекса ускоряющего выборки всех объектов экстента (в Cache под экстентом понимается набор объектов класса и всех его подклассов).

    Далее для древесного индекса задаётся список свойств, на которых он строится. Затем необходимо выбрать способ сортировки каждого выбранного свойства, определяемый параметром COLLATION. Перечень вариантов сортировки приведен в документации Cache.

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

    Запросы в описании класса

    Выбираем кнопку "Новый запрос" либо в основном меню проходим путь "Класс > Добавить > Запрос". Затем называем имя запроса и выбираем способ его реализации (SQL или пользовательский код). Если запрос строится не на SQL, придется самим написать три метода класса, определяющие его работу. Это QueryExecute(), QueryFetch() и QueryClose(), где Query —имя запроса.

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

    В панели "Колонки" определите порядок следования столбцов таблицы, включаемых в результат запроса.

    Остаётся перечислить критерии отбора объектов и указать порядок сортировки результата.

    Триггеры в объектной модели

    Поскольку триггеры в Cache это элементы класса, то кроме создания их в SQL, описанного в разделе 8.7, можно определить их прямым написанием текста в Cache Studio, либо воспользоваться мастером. Для вызова мастера создания триггера, необходимо выбрать пункт меню Класс > Добавить > Новый триггер SQL.

    Вспомним, что тип триггера определяется событием, которое инициирует его выполнение (INSERT, UPDATE, DELETE), и моментом времени относительно этого события (BEFORE или AFTER). Ещё одно событие — UPDATE OF — возникает при изменении значений определенных столбцов. Но его можно использовать, только если код триггера написан на языке SQL.

    Можно также задавать триггеры, срабатывающие сразу на несколько событий: INSERT/UPDATE, UPDATE/DELETE, INSERT/UPDATE/DELETE. Это означает, что триггер будет выполняться при возникновении любого из указанных событий.

    Методы callback —это методы СУБД, которые определяют реакцию системы на некоторые события. В Cache существуют предопределённые callback-методы, и имеется возможность их переопределения. Это позволяет придерживаться принципа разделения системного и прикладного программного обеспечения, но одновременно дает возможность переопределения реакции системы.

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

  • BEFORE INSERT ~ %OnBeforeSave
  • AFTER INSERT ~ %OnAfterSave
  • BEFORE UPDATE — %OnBeforeSave
  • AFTER UPDATE — %OnAfterSave
  • BEFORE DELETE — %OnDelete
  • Однако это не значит, что, например, триггеры BEFORE INSERT и BEFORE UPDATE есть по сути одно и то же. Адекватный метод %OnBeforeSave выглядит следующим образом:

    Method %OnBeforeSave(insert As %Boolean) As %Status [Private,ServerOnly=1]
    {
    if insert {
    //код триггера на BEFORE INSERT
    }
    else {
    //код триггера на BEFORE UPDATE
    }
    Quit $$$OK
    }
    

    Если возникает событие, ассоциированное с триггером, то вызывается выполнение кода этого триггера.

    Рассмотрим подробнее создание триггера в Studio. Для примера создадим триггер на добавление строки в таблицу SQLUser.Person, который будет вставлять ID добавленного объекта в специальную таблицу LogTable, содержащую текстовый столбец TableName и числовой столбец IDValue.

    При вызове мастера создания триггера появляется окно, как показано на рисунке 10.23.

    (рис 10.23) Мастер создания триггера

    Введем название триггера "LogEvent" и нажав кнопку "Далее" перейдем к следующему окну. Здесь нужно указать событие и время срабатывания триггера. Выбрать можно только одно из событий INSERT, UPDATE, DELETE. Если необходимо указать несколько событий, придётся редактировать текст триггера вручную.

    В определении класса событие задается с помощью параметра Event. Например:

    Trigger NewTrigger1  [ Event = INSERT ]

    Несколько событий можно указать через "/", например:

    Trigger NewTrigger1  [ Event = INSERT/UPDATE ]

    Выберем в мастере событие INSERT и время срабатывания AFTER (рисунок 10.24) и нажмем кнопку "Далее".

    (рис 10.24) Указание времени срабатывания триггера

    Последний шаг при создании триггера с помощью мастера — собственно написание кода.

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

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

  • {имя_поля*О} — старое значение поля обновляемой строки;
  • {имя_поля*N} — новое значение поля обновляемой строки;
  • {имя^толя*C} —равно 1, если значение поля в обновляемой строке было изменено, иначе 0.
  • Добавим в текстовую область код, как показано на рисунке 10.25, и нажмем кнопку "Готово".

    (рис 10.25) Код триггера

    В определении класса появилось следующее понятное теперь описание:

    Trigger LogEvent  [ Event = INSERT,  Time = AFTER ] {
    // извлекаем ID добавляемой строки
    Set id = {ID}
    // вставляем значение в таблицу LogTable sql(INSERT INTO LogTable  (TableName, IDValue) VALUES  ('SQLUser.Person', :id))
    

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

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

    10.2.4 Класс %ResultSet

    Класс %ResultSet позволяет использовать результаты запросов классов в программах, написанных на ObjectScript, Java или ActiveX. Есть два варианта обращения к %ResultSet. Первый способ состоит в создании экземпляра %ResultSet, методу %New() которого передаётся в качестве аргумента строка, содержащая разделённые двоеточием имя класса и имя запроса:

    Set result=##class(%ResultSet).%New("Person:ByName") 
    Второй способ заключается в явном установлении свойств ClassName и QueryName 
    объекта %ResultSet. Set result=##class(%ResultSet).%New() 
    Set result.ClassName="Person" Set result.QueryName="ByName"
    

    После того, как создан объект %ResultSet, необходимо выполнить запрос и затем проанализировать результат.

    Объект %ResultSet располагает следующими основными методами:

  • Execute() выполняет запрос, с которым связан объект %ResultSet;
  • Next() осуществляет переход на следующую строку выборки;
  • Close() закрывает %ResultSet после завершения его использования.
  • Для доступа к полям текущей записи класс %ResultSet предоставляет несколько способов:

  • использование метода Get(), которому передаётся в качестве аргумента имя нужного столбца;
  • использование свойства Data, которое также предоставляет доступ к значению столбца текущей записи по имени столбца; этот способ быстрее предыдущего;
  • использование метода GetData(), которому передаётся в качестве аргумента номер столбца в запросе.
  • Ниже приведен пример использования класса %ResultSet с запросом ByName класса Person. Данный запрос возвращает данные о сотрудниках, упорядоченные по их именам в алфавитном порядке. В результате выводятся первые двадцать имен.

    //создание экземпляра %ResultSet
    Set result=##class(%ResultSet).%New("Person:ByName") //выполнение запроса Set sc=result.Execute() //обработка результатов в цикле For i=1:1:20 {
    If result.Next(.sc) {
    Write result.Data("Name"),! } Else { Quit
    }
    }
    

    Класс %ResultSet может исполнять динамические SQL-запросы. В этом случае объект %ResultSet создается с указанием "%DynamicQue-ry:SQL":

    S result=##class(%ResultSet).%New("%DynamicQuery:SQL")
    

    Затем с помощью метода Prepare необходимо подготовить запрос, например:

    S q="SELECT ID,Name,Salary FROM Employee WHERE Salary>90000" S sc=result.Prepare(q)
    

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

    Класс %ResultSet позволяет производить только последовательный обход записей в прямом порядке, от первой к последней. Чтобы вернуться назад и просмотреть предыдущие строки результата, нужно заново исполнять запрос (методом Execute()) и некоторое количество раз вызывать метод Next(), чтобы добраться до нужной записи.

    Более удобный доступ к результатам исполнения запроса обеспечивает класс %ScrollableResultSet. Он позволяет обходить строки результата как в прямом порядке, используя метод Next() , так и в обратном, используя метод Previous(). Кроме того, у него имеется свойство CurrRow, в котором хранится номер текущей строки. Путем установления CurrRow в нужное значение, можно получать доступ к любой строке выборки по ее номеру (имеется в виду последовательный номер в выборке, но не ID).

    При использовании %ScrollableResultSet запрос исполняется в момент первого обращения, а результат исполнения запроса сохраняется в глобальной переменной.

    Приведём пример использования %ScrollableResultSet с тем же запросом ByName класса Person:

    Set q="Person:ByName" //создание экземпляра
    Set result=##class(%ScrollableResultSet).%New(q)
    //выполнение
    Set sc=result.Execute()
    //просмотр первой записи
    Do result.Next(.sc)
    Write "1-я запись: ",result.Data("Name"),! //просмотр 10-й записи Set result.CurrRow=10
    Write "10-я запись: ",result.Data("Name"),! //просмотр записей с 9-й по 2-ю в обратном порядке 
    Write "Записи с 9-й по 2-ю в обратном порядке:",! For i=9:-1:2 {
    If result.Previous(.sc) {
    Write result.Data("Name"),! } Else { Quit
    }
    }
    

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

    10.2.5 Способы хранения класса

    Используем созданные ранее классы Address и Person, имеющие следующую структуру:

    Class User.Address Extends %SerialObject
    {
    Property City As %String; Property State As %String;
    }
    
    Class User.Person Extends %Persistent
    {
    Property Name As %String; Property YearOB As %Integer; Property Home As Address;
    }
    

    Как известно, по умолчанию объект класса Person сохраняется в глобале ^Temp.PersonD следующим образом:

    ^User.PersonD(2)=$lb("","Nick",1984,$lb("NewYork","NY"))
    

    Чтобы изменить привычный способ хранения экземпляров класса Person, необходимо создать новый способ хранения (Storage). Это можно сделать при помощи кнопки на панели инструментов "Новый способ хранения". Появится мастер создания способа хранения (рисунок 10.26).

    (рис 10.26) Мастер создания способа хранения

    Выбираем способ хранения типа "Хранение Cache". Если нажмем "Далее", появится форма для ввода названий глобалов, в которых будет храниться информация об объектах вместо используемых по умолчанию ^User.PersonD и ^User.PersonI, однако мы их изменять не будем, поэтому просто нажимаем на кнопку "Готово".

    Новый способ хранения создан. Обратимся к инспектору класса. В левом выпадающем списке выберем "Storage" и увидим имеющиеся в классе способы хранения (рисунок 10.27).

    (рис 10.27) Список способов хранения в Инспекторе

    Дважды кликнув на "NewStorage1", перейдем в инспекторе к описанию данного способа хранения.

    Изменим хранение свойств таким образом, чтобы домашний адрес человека располагался не в узле глобала ^User.PersonD(i) как вложенный список, а в отдельном узле под индексом ^User.PersonD(i, "Home").

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

    (рис 10.28) Свойства способа хранения

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

    (рис 10.29) Редактирование способа хранения

    Мы должны создать два узла:

  • В первом узле в виде списка будут храниться все свойства объекта, кроме домашнего адреса.
  • Во втором под индексом "Home" будет храниться значение домашнего адреса.
  • Для создания первого узла нажмем кнопку "Добавить" и заполним появившуюся форму так, как показано на рисунке 10.30.

    (рис 10.30) Новый узел в способе хранения

    Поле Имя содержит имя узла данных внутри способа хранения. Узел PersonDefaultData содержится в способе хранения, создаваемом в классе по умолчанию. Мы создаем узел с таким же именем, в котором также в виде списка будут храниться значения всех свойств, кроме свойства Home.

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

    Все свойства, имеющиеся в классе должны быть отражены в способе хранения, включая %%CLASSNAME. Если какие-то из них пропущены, то при компиляции автоматически создается узел с именем PersonDefaultData, значением которого является список значений этих свойств. Если узел PersonDefaultData уже существует, то создается узел PersonDefaultData1, который хранит пропущенные данные в виде списка значений в узле глобала вида ^User.PersonD(i, 1).

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

    Далее расположены 3 радио-кнопки с выбором:

  • Множественные свойства. Выбор данного пункта означает, что в узле будет храниться список, содержащий указанные свойства в указанном порядке
  • Одно свойство. Выбор данного пункта означает, что в узле будет храниться значение единственного указанного свойства.
  • Свойство массив. Выбор данного пункта позволяет задать хранение свойств типа Список или Массив.
  • В нижней части формы показано, какой вид будет иметь глобальная ссылка для этого узла. В данном случае под индексом со значением ID будет храниться список вида $LB(%% CLASSNAME, Name, YearOB).

    Теперь вновь перейдем на NewStorage1 и добавим еще второй узел, заполнив поля формы следующим образом (рисунок 10.31).

    (рис 10.31) Хранения свойства Home

    Нажмем "ОК" и откомпилируем класс.

    Чтобы посмотреть, как изменилась структура хранения экземпляров класса Person, необходимо добавить в базу новый объект, так как изменение способа хранения для класса не перестраивает узлы, соответствующие ранее созданным объектам. Для этого выполним в терминале следующие команды:

    USER>S p=##class(User.Person).%New()
    USER>S p.Name="Alex", p.YearOB=1989
    USER>S p.Home.State="USA",p.Home.City="Washington"
     
    USER>D p.%Save()
    

    С помощью портала управления системой увидим, что в глобале ^User.Person появилась ветка для этого объекта, имеющая следующий вид:

    ^User.PersonD(5)= $lb("","Alex",1989) ^User.PersonD(5,"Home")= $lb("Washington","USA")
    

    Посмотрим, как влияет изменение способа хранения на SQL-проекцию класса Person. Выполним в портале запрос SELECT * FROM Person (таблица 10.9).

    SQL-проекция класса Person
    # ID Name YearOB HomeCity HomeState
    1 1 Nick 1
    2 2 Nick
    3 3 Alex 1989 Washington USA
    Завершено

    Мы видим, что SQL-проекция опирается на действующий в момент исполнения запроса способ хранения. Однако это не означает, что информация о хранении ранее созданных объектов утеряна. В классе сохраняется как новый, так и старый способ хранения. Действующий способ хранения указывается с помощью параметра класса StorageStrategy:

    Class User.Person
    Extends %Persistent [ StorageStrategy = NewStorage1 ]
    

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

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

    Поскольку структуру хранения можно прописать только у персистентно-го класса, то расположение узлов-свойств объекта типа Address в глобале ^User.PersonD придется описывать в классе Person, используя для указания необходимого свойства точечный синтаксис, например Home.City.

    Изменение способа хранения может использоваться для повышения быстродействия.

    10.2.6 Что мы узнали о реализации объектной модели в Cache, о связях моделей данных и какие ещё отображения могут быть интересны

    Как и следовало ожидать, в Cache реализованы все основные особенности объектной модели данных. Оригинальные черты объектов в Cache определены двумя особенностями этой СУБД. Это:

  • единая архитектура Cache, позволяющая "взглянуть" на одни и те же данные с точки зрения трёх моделей —иерархической, реляционной и объектной;
  • возможность управлять способом хранения данных.
  • Единая архитектура определяет отображения между помянутыми тремя моделями данных, с которыми вы уже познакомились. Таблица представляется как класс без методов, а иерархии используются для хранения экземпляров классов, они же кортежи отношений или строки таблиц. Наследование оказывается возможным и в расширенном варианте реляционной модели.

    С другой стороны, от реляционной модели в классы Cache перешли запросы, индексы и триггеры. В результате, запросы могут использоваться как фильтры набора объектов. Индексы, как и в таблицах, могут ускорить доступ к данным. Триггеры в объектной модели, использующей по умолчанию способ хранения %CacheStorage, не работают. Однако, при выборе другой модели хранения %CacheSQLStorage методы %Save() и %Delete() могут вызывать триггеры, если события для которых триггеры созданы наступили.

    Для организации активности в объектной модели Cache пользуются методами обратной связи (Callback Methods), позволяющими создать свою обработку событий. Например, метод обратной связи %OnOpen() вызываемый методом %Open() , позволяет для хранимых и сериализуемых классов написать свои обработчики события открытия объекта.

    Мы совсем не рассматривали те зарегистрированные классы, которые не являются ни хранимыми, ни сериализуемыми. Это аналоги классов языков общего назначения, таких как C++, Java и др.

    Уже упоминалось (в разделе 10.1.4), что для интерфейсов пользователя, выполняющихся обычно на объектно-ориентированных языках, в том числе на специфичных для Cache технологиях CSP и Zen, выполняются отображения данных из базы в интерфейс и обратно. Мы их рассматривать не можем, однако, обратим внимание на интереснейшую возможность отображения классов Cache в классы Java.

    10.3 Объектно-реляционная модель данных Oracle

    Изучая объектную модель данных, мы уже заметили, что методы были написаны на основном для Cache не объектно-ориентированном языке ObjectScript. В объектно-реляционной модели данных, которая будет рассмотрена на примере СУБД Oracle, ситуация с реализацией методов та же. Только методы создаются на встроенном процедурном языке, который называется PL/SQL.

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

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

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

    Если вы хотите изучить объектно-реляционные базы полнее, скачайте персональный вариант Oracle 11g и используйте многочисленные учебные материалы Oracle.

    Инсталляция этой СУБД и дополнительные сведения о ней приведены на сайте книги.

    10.3.1 Введение в процедурный язык PL/SQL

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

    Сразу обратим внимание на то, что в Oracle любая инструкция SQL и PL/SQL заканчивается точкой с запятой.

    Блоки

    Программы PL/SQL структурируются анонимными (не имеющими имени) блоками. Структура такого блока:

    [DECLARE
    объявления
    ]
    BEGIN
    выполняемые операторы [EXCEPTION
    обработка исключительных ситуаций
    ]
    END;
    

    Блок обязательно заканчивается точкой с запятой.

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

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

    Объявление переменных и констант можно выполнить так:

    имя_переменной тип_данных
    [[NOT NULL]   := выражение_по_умолчанию]; имя_переменной тип_данных CONSTANT := выражение;
    

    Присваивание обозначается знаком :=.

    Разберём несложный пример анонимного блока (листинг 10.11). Набрав текст нажимаем Run.

    DECLARE
    х  NUMBER(5,2);
    у  NUMBER(5,2):=2;
    result NUMBER(5,2); BEGIN
    — Зто однострочный комментарий result  := 12.02; x := result / y;
    DBMSOUTPDT.PUT_LINE(1 хранен 1   || x);
    END;
    
    Results:
    x равен 6,01 
    Statement processed.
    0,00 seconds
    

    Язык PL/SQL, в отличие от SQL, "молчаливый" язык. Вывод на экран в нём необходимо прописать явно. Процедура PUT_LINE из пакета DBMS_OUTPUT позволяет вывести на экран строку, сформированную как конкатенация (обозначенная знаком ||) из двух строк —текста 'x равен ' и строки, представляющей значение x. Числовой тип x здесь неявно приводится к текстовому типу. Теперь строка DBMS_OUTPUT.PUT_LINE('x равен ' || x); понятна.

    Запомните, что анонимный блок может быть подставлен вместо любого оператора PL/SQL или SQL.

    SQL внутри PL/SQL

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

    Создадим таблицу и введём в неё одну строку:

    CREATE TABLE qq(c1 CHAR(10),   c2 INTEGER); INSERT INTO qq VALUES('QWERTY', 1);
    

    Извлечём значение первого столбца с помощью запроса типа SELECT .. INTO:

    DECLARE
    result char(10); BEGIN
    SELECT cl INTO result FROM qq WHERE c2=1; DBMS_OUTPUT.PUT_LINE(result);
    END;
    

    Удаление слов INTO result вызовет появление ошибки.

    Разветвления и циклы

    Синтаксис команды разветвления в общем обычный:

    IF условие THEN последовательность_операторов_1 ELSIF условие2 THEN 
    последовательность_операторов_2 ELSE последовательность_операторов_3 END IF;
    

    Обратите внимание на то, что вместо обычного ELSEIF почему-то пишется ELSIF. Циклы строятся на основе так называемого простого цикла:

    [<<имя_цикла>>] LOOP
    последовательность_операторов_1 EXIT имя_цикла WHEN условие_выхода
    последовательность_операторов_2 END LOOP;
    

    Без инструкций останова EXIT или EXIT WHEN он зацикливается. Фраза в двойных угловых скобках "имя_цикла" это метка имени цикла. Пример простого цикла с меткой, которая в нём не используется, приведён в листинге 10.12:

    DECLARE
    v_i INTEGER  := 1; 
    BEGIN
    "1QQP_1"
    LOOP
    DBHSOUTPUT.PUTLINE{'В цикле    v_i =  '   || v_i) EXIT loop_l WHEN v_i > 2; 
    v_i  := v_i + 1; 
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('Вышли из цикла')
    
    Results:
    В цикле v_i = 1 
    В цикле v_i = 2 
    В цикле v_i = 3 
    Вышли из цикла
    
    

    Пример цикла WHILE с предусловием:

    DECLARE
    i INTEGER:=1;
    BEGIN
    WHILE i < 6 LOOP
    DBMS_OUTPUT.PUT_LINE(i); i  :=i +1;
    END LOOP;
    END;
    

    Пример цикла FOR:

    BEGIN
    FOR i IN 1   ..   5 LOOP
    DBMS_OUTPUT.PUT_LINE(i);
    END LOOP;
    END;
    

    Обратите внимание на то, что переменная i не была объявлена явно, и цикл сам создал её с типом INTEGER. Слово REVERSE во фразе FOR i IN REVERSE 1 .. 5 LOOP задало бы обратное направление перебора значений.

    Процедуры и функции

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

    Упрощенный синтаксис инструкции создания процедуры:

    CREATE [OR REPLACE] PROCEDURE имя_процедуры
    [(имя_параметра [IN | OUT | INOUT] тип [, ...])]
    IS | AS
    BEGIN
    тело_процедуры
    END имя_процедуры;
    

    Упрощенный синтаксис создания функции:

    CREATE   [OR REPLACE]   FUNCTION имя_функции [(имя_параметра [IN  | OUT  |  INOUT] 
    тип [, ...])] RETURN тип_возвращаемого значения IS  | AS BEGIN
    тело_функции END имя_функции;
    

    Тело функции или процедуры это последовательность операторов.

    Слова OR REPLACE добавляют, если необходимо заменить существующую функцию с таким же наименованием.

    Режим параметров указывается как IN — входной, OUT — выходной, или INOUT — входо-выходной. Входной параметр нельзя изменять в теле функции, а выходному обязательно должно быть присвоено значение.

    Пример функции, вычисляющей площадь круга по его радиусу:

    CREATE OR REPLACE
    FUNCTION circle_area (p_radius IN NUMBER) RETURN NUMBER AS
    v_pi      NUMBER := 3.1415926; v_area NUMBER; BEGIN
    v_area := v_pi * POWER(p_radius, 2); RETURN v_area; END circle_area;
    

    Слово RETURN в теле функции присутствует, по крайней мере, один раз и указывает имя переменной, которая будет возвращена.

    Обратите внимание на то, что в именованиях переменных использован префикс "v_", в имени формального параметра префикс "p_". Подобные соглашения об именованиях очень удобны, особенно если они соблюдаются широким кругом разработчиков.

    Вызовем функцию circle_area из анонимного блока:

    BEGIN
    DBMS_OUTPUT.PUT_LINE(circle_area(2));
    END;
    

    Созданная функция может быть вызвана и из SQL, например,

    SELECT circle_area(2) FROM dual;
    

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

    SELECT sysdate FROM dual;
    

    Пакеты

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

    Пакет состоит из двух частей:

  • спецификация или заголовок пакета;
  • тело пакета.
  • Заголовок пакета содержит спецификации переменных, типов данных, процедур, функций и курсоров которые предполагается сделать доступными извне. Здесь нет программного кода. Курсоры —это стандартное средство для реализации запросов SQL. Мы их не будем рассматривать.

    Спецификация может быть скомпилирована и при отсутствии тела пакета.

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

    Тело пакета успешно компилируется только вместе со спецификацией, либо если спецификация была скомпилирована раньше.

    Мы не можем заниматься пакетами подробно. Приведём лишь несколько вариантов их использования. Мы уже работали с функцией PUT_LINE пакета DBMS_OUTPUT.

    Функция GET_DDL пакета DBMS_METADATA позволяет получить тексты определения любого хранимого объекта базы. Рассмотрим примеры её применения.

  • Запрос описания функции

    SELECT dbms_metadata.get_ddl('PROCEDURE',
    'имя_таблицы', 'имя_схемы') FROM dual;
    

    Например, для только что созданной функции circle_area:

    SELECT dbms_metadata.get_ddl('FUNCTION', 'CIRCLE_AREA', 'HR')
    FROM dual;
    
  • Запрос описания таблицы

    SELECT dbms_metadata.get_ddl('TABLE',
    'имя_таблицы', 'имя_схемы') 
    FROM dual;
    

    Оказывается в только что созданной таблице qq было задано по умолчанию много параметров. Запрос

    SELECT dbms_metadata.get_ddl('TABLE','QQ','HR') 
    FROM dual;
    

    возвращает текст:

    CREATE TABLE
    "HR"."QQ"   ("C1" CHAR(10),"C2" NUMBER(*,0)) 
    PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING 
    STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0
    FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT) TABLESPACE "USERS"
    

    Здесь параметры блока PCTFREE и PCTUSED, которые станут понятны после освоения следующей лекции 11, и задание совершенно не нужного таблице qq сегмента 64 Кбайт, и указание табличного пространства USERS, в котором таблица находится и многое другое.

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

    Обратите внимание на то, что все текстовые константы в запросах переводятся в верхний регистр и помещаются в одинарные кавычки.

  • Запрос описания пользователя

    SELECT dbms_metadata.get_ddl('USER',
    'имя_пользователя') 
    FROM dual;
    
  • 10.3.2 Объектные типы данных

    Теперь мы готовы заниматься объектно-реляционными базами.

    Объекты могут быть постоянными и временными. Хранимый (persistent) объект может быть как значением столбца обычной таблицы, так и строкой таблицы (в этом случае таблицу называют объектной).

    Временный (transient) объект создается в памяти и уничтожается после окончания работы программы. Отправив такой объект в базу данных вы делаете его постоянным. А постоянный объект можно извлечь из базы данных и поместить его во временный объект такого же или совместимого типа.

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

    Выборка и манипулирование хранимыми объектами в базе данных осуществляется на языке SQL, начиная с третьей версии. Для работы с временными объектами на сервере предназначен язык PL/SQL. Язык C++ позволяет работать с объектами на стороне клиента. Используются вызовы программ на C++ из PL/SQL. В Java можно работать на всех трёх слоях клиент-серверной архитектуры.

    Можно выделить следующие разновидности объектных типов:

  • простой объектный тип, который строится на скалярных предопределённых типах данных;
  • составной объектный тип, использующий другие объектные типы;
  • ссылочный объектный тип;
  • типы коллекций двух разновидностей VARRAY и NESTED TABLES.
  • Ссылочный объектный тип REF это логический указатель, определяющий отношения между экземплярами классов. Он основывается на одноэлементных или коллекционных типах данных. Указатели REF задают ассоциации UML и заменяют внешние ключи, предоставляя прямую навигацию между объектами разных типов.

    VARRAY - это упорядоченная коллекция фиксированной длины. Хранится в сегменте таблицы, использующей такой тип.

    Вложенная таблица это неограниченная и неупорядоченная коллекция. Хранится в своём сегменте, не совпадающем с сегментом основной таблицы. Последняя фраза станет понятной после изучения следующей 11-й лекции.

    Создание пользовательского типа данных

    Создадим тип данных dept_type, описывающий таблицу dept (рисунок 10.32). Затем создадим таблицу emp_dept (то есть emp включающую в себя dept).

    (рис 10.32) Создание типа данных dept_type

    Заметьте, что в окне можно держать несколько инструкций, но исполняемая инструкция должна быть выделена как на (на рисунке 10.32.

    Ранее использованные в текущем сеансе инструкции можно вызвать, нажав на History, и в появившемся внизу окне кликнуть левой кнопкой мыши на нужной команде.

    А теперь получим информацию о созданном типе данных из представления словаря USER_TYPE_ATTRS (таблица 10.10).

    Получение информации о типе dept_type
    SELECT type_name, attr_name, attr_type_name FROM user_type_attrsWHERE    type name='DEPT TYPE';
    TYPE_HAME ATT R_ NAME ATT R_TYPE_ NAME
    DEPTTYPE DEPTNO NUMBER
    DEFTJYPE DNAME VARCHAR2
    DEPT_TYPE LOC VARCHAR2

    Можно использовать аналогичное представление dba_type_attrs. Его структура показана в таблице 10.11.

    Представление dba_type_attrs
    Столбец Описание столбца

    OWNER

    TYPENAME

    ATTRNAME

    ATTR_TYPE_MOD

    ATTR_TYPE_OWNER

    ATTR_TYPE_NAME

    LENGTH

    PRECISION

    SCALE

    CHARACTER_SET_NAME

    ATTRNO

    Владелец типа

    Имя типа

    Имя атрибута

    Модификатор атрибута типа

    Владелец атрибута типа

    Имя типа атрибута

    Длина для атрибутов CHAR/VARCHAR

    Точность для атрибутов типа number

    Диапазон для числового типа

    Имя таблицы символов

    Номер атрибута

    Изменение и удаление типов. Зависимости объектов

    В наших первых примерах типы не содержат методов. В общем случае в типе, как и в пакете, создаётся спецификация и тело метода.

    Объектные типы можно изменять оператором ALTER TYPE и удалять оператором DROP TYPE.

    Форматы некоторых команд:

    [ALTER TYPE имя_типа COMPILE [SPECIFICATION|BODY]
    

    Компилирует спецификацию или тело типа. Если не указано ни одно из слов SPECIFICATION, BODY, то компилируются оба. Последовательность компиляции такая же как для пакетов.

    ALTER TYPE имя_типа
    REPLACE AS OBJECT (спецификация_объектного_типа)
    

    Новое описание, заданное спецификацией_объектного_типа должно во всем, кроме дополнительных методов совпадать с исходным.

    После выполнения этого оператора, если тело типа существовало ранее, оно становится не действительным (INVALID).

    Удаление типа.

    DROP TYPE  [имя_схемы.]имя_типа [FORCE]
    

    Если указан параметр FORCE, то тип удалится, даже если существуют зависимые от него объекты. Естественно, все зависимые объекты становятся INVALID.

    Пример зависимости объектов. Создаем два независимых типа

    CREATE OR REPLACE TYPE Objl AS OBJECT ( Cl NUMBER, C2 VARCHAR2(5)
    );
    CREATE OR REPLACE TYPE Obj2 AS OBJECT (
    Cl CHAR(2),
    C2 VARCHAR2(5)
    );
    

    и зависящий от них Obj3

    CREATE OR REPLACE TYPE Obj3 AS OBJECT (
    al Objl, a2 Obj2
    );
    

    Тогда попытка удаления

    DROP TYPE Objl;
    

    вызовет ошибку, так как от Objl зависит Obj3.

    А вот удаление сначала Obj3, затем Objl или Obj2 ошибкой не будет.

    Конструкторы по умолчанию

    Метод конструктора, как всегда в ООП, предусматривается для любого объектного типа и возвращает новый экземпляр этого типа.

    (рис 10.33) Удаление типа Obj1

    Для демонстрации применения конструктора по умолчанию создадим тип address_type, затем на его основе объектный тип person и таблицу peoples со столбцом этого типа.

    CREATE   TYPE  address_type   as object (
    zipcode  NUMBER(5),
    country  VARCHAR2(20),
    city  VARCHAR2(30),
    street VARCHAR2(30),
    numb NUMBER(4)
    );
    
    CREATE   TYPE person AS OBJECT (
     
    name VARCHAR2(40), birthday DATE, address address_type, MEMBER FUNCTION Age
    
    — дата рождения
    спецификация метода, возвращающего возраст
     
    RETURN NUMBER  — возвращаемое значение
    );
    

    Поскольку определена спецификация типа, содержащего функцию-член типа, необходимо еще создать тело типа:

    CREATE OR REPLACE TYPE BODY person IS — задание методов
    MEMBER FUNCTION Age RETURN NUMBER IS
    BEGIN    -- вычисление возраста
    RETURN ROUND(MONTHS_BETWEEN(sysdate, birthday)/12);
    END;
    END;
    

    А теперь посмотрите сами описание нового типа, используя команду DESC person или без сокращений DESCRIBE person. Создадим таблицу типа person командой CREATE TABLE peoples OF person; Проверим ее структуру (таблица 10.12):

    Структура таблицы peoples
    DESC People
    Table Column Data Type Length Precision Scale Primary Key Nullable Default Comment
    PEOPLES NAME Varchar2 40 - - - + - -
    BIRTHDAY Date 7 - - - + - -
    ADDRESS Address_Type 1 - - - + - -
    1-3

    В объектных таблицах работают ограничения CHECK, PRIMARY KEY и UNIQUE. Например, создадим таблицу peoplesl c первичным ключом:

    CREATE TABLE peoplesl OF person (name   PRIMARY KEY);
    

    Проверим ее структуру для сравнения с peoples. В столбце Primary Key в строке NAME появится отметка первичного ключа.

    В объектных таблицах вставка строки "по-старому" не удастся. Например, вставка

    INSERT INTO peoples
    VALUES    ('Сидоров И.  П.', '22.11.80',350000,'Россия', 'Краснодар','Ставропольская',  14 9);
    

    вызывает появление ошибки.

    Теперь вставка должна производиться так:

    INSERT INTO peoples
    VALUES('Сидоров И.П.', '22.11.80', address_type (35000, 'Россия', 'Краснодар','Ставропольская',  14 9)
    );
    

    Читать как в обычных реляционных таблицах также не удастся. Так, команда

    SELECT * FROM peoples;
    

    вызывает сообщение об ошибке.

    Тот же результат дают запросы:

    SELECT name, birthday, address   FROM peoples; 
    SELECT name, birthday, address.country FROM peoples;
    

    А вот следующая команда позволяет работает нормально (таблица 10.13):

    Выборка из таблицы peoples
    SELECT name, birthday,p.address.countryFROM peoples p;
    NAME BIRTHDAY ADDRESS.COUNTRY
    Сидоров И.П. 22.11.80 Россия

    Обращение к подобъектам производится с использованием точечного синтаксиса и псевдонимов:

    SELECT p.address.country FROM peoples p;
    

    А вот без псевдонима никак нельзя. Следующий запрос вызывает появление ошибки:

    SELECT peoples.address.country FROM peoples;
    

    Как хранятся объектные таблицы

    Давайте посмотрим, что произошло при создании объектной таблицы peoples. Для этого обратимся к представлению словаря user_tab_col (таблица 10.14), которое перечисляет все столбцы указанной таблицы, в том числе, скрытые (hidden).

    Представление user_tab_cols
    SELECT column_name,  hidden_column, data_typeFROM user_tab_colsWHERE table name =  'PEOPLES';
    COLUMN_NAME HIDDEN_COLUMN DATA_TYPE
    SYS_NC_OIDS YES RAW
    SYS_NC_ROWINFO$ YES PERSON
    NAME NO NO VARCHAR2
    BIRTHDAY NO DATE
    ADDRESS YES ADDRESS_TYPE
    SYS_NC00006$ YES NUMBER
    SYS_NC00007$ YES VARCHAR2
    SYS_NC00008$ YES VARCHAR2
    SYS_NC00009$ YES VARCHAR2
    SYS NC00010$ YES NUMBER

    Итак, создана не совсем обычная реляционная таблица peoples. В ней имеются скрытые столбцы.

    Столбцы name и birthday это обычные реляционные столбцы и читаются обычным запросом. Столбец address и SYS_NC_ROWINFO$ таким запросом не читаются. Столбец SYS_NC_OID$, по-видимому, содержит объектные идентификаторы строк таблицы. Последние пять скрытых столбцов по типам подозрительно напоминают столбцы zipcode, country, city, street и numb, определённые в типе address_type. Проверьте это предположение, выполнив запрос

    SELECT SYS_NC00006$,  SYS_NC00007$, SYS_NC00008$, SYS_NC00009$, SYS_NC00010$
    FROM peoples;
    

    Вы увидите введённые вами данные.

    Из сказанного следует, что объектно-реляционная таблица это не тривиальное расширение реляционной таблицы.

    Индексы и ограничения целостности

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

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

    CREATE INDEX country_idx ON peoples (address.country);
    

    В этих же ситуациях можно определять и вводить ограничения целостности, например,

    ALTER TABLE peoples
    ADD CONSTRAINT peoples_cons1
    CHECK (address.country IS NOT NULL);
    

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

    NULL'bi в объектных таблицах

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

    Пример вставки значений NULL:

    INSERT INTO peoples
    VALUES(null, '22.11.80',
    address_type (null,  'Россия', null,null,null));
    

    Ссылочные типы

    Мы уже упоминали, что версия Oracle XE, видимо, не предназначалась для работы с объектно-реляционной моделью. Не всё, что реализуется в основных версиях СУБД, в ней удаётся. Правда, замеченные нами проблемы касаются работы оператора SELECT и процедуры PUT_LINE. В предыдущих разделах вы видели не совсем естественную работу SELECT с объектными таблицами. Здесь мы не сможем запрашивать ещё и данные ссылочного типа. С ними не работает и процедура PUT_LINE.

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

    Получение ссылки:

    DECLARE RefVar REF person; BEGIN
    SELECT ref(P)    INTO RefVar FROM peoples P WHERE P.address.zipcode=35000;
    END;
    Добавим в этот анонимный блок обращение по ссылке:
    DECLARE RefVar REF person; BEGIN
    SELECT ref(P)   INTO RefVar FROM peoples P WHERE P.address.zipcode=35000; 
    UPDATE   peoples P SET P.address.numb=166 WHERE ref(P)= RefVar;
    END;
    

    Создадим еще один тип:

    CREATE TYPE project AS OBJECT ( projno NUMBER(5), Emp_ref REF person );
    

    и объектную таблицу:

    CREATE TABLE projects OF project;
    

    Занесем один объект в таблицу projects:

    DECLARE refVar REF person; BEGIN
    SELECT ref(P) INTO refVar FROM peoples P
    WHERE P.name='C^4opoB И.П.',-
    INSERT INTO projects VALUES (1, refvar);
    END;
    

    Команда SELECT * FROM projects в других версиях Oracle даст что-нибудь вроде:

    Но, как уже говорилось, в Oracle XE она не работает.

    PROJNO EMP_REF
    -------------------------------------------------------------------------------------
    1  00022 02087 6D564 02DC6F0403E034 08002 0C944 037 6D56402DC6D
    

    Оператор deref извлекает объект по заданной для него ссылке

    Например, выборку из таблиц projects и peoples, связанных ссылками можно было бы организовать так:

    SELECT projno, deref(Emp_ref) FROM   projects P WHERE P.Emp_ref    IS NOT DANGLING;
    

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

    К сожалению, и этот запрос в Oracle XE не работает.

    Поскольку идентификатор таблицы, на которую ссылаются, содержится в ссылке, то ссылка в поле P.emp_ref может указывать на строку любой объектной таблицы типа person, а не одной таблицы peoples.

    Остается разъяснить предикаты is dangling и is not dangling. Если объект, на который указывает ref удалить, то ссылка будет указывать на несуществующий объект. Такую ссылку называют висячей (dangling). Предикат is dangling принимает истинное значение, если ссылка висячая.

    Пример: Установка значений NULL для всех висячих ссылок

    BEGIN
    UPDATE projects SET emp_ref = NULL
    WHERE emp_ref IS DANGLING;
    END;
    

    Объектные идентификаторы OID

    Пакет DBMS_ROWID позволяет узнать объектный идентификатор любой таблицы, и реляционной и объектной. Естественно, по OID восстанавливается имя таблицы (peoples в нашем примере в листинге 10.13).

    SELECT DBMS_ROWID.ROWID_Object(RowID)
    FROM peoples
    WHERE RowNum=1;
    
    
    select Object_Name
    from USER_OBJECTS
    where Object_ID=13698;
    
    Results:
    OBJECT_NAME
    PEOPLES
    
    

    10.3.3 Методы

    Различают методы-члены класса, методы конструктора, методы сравнения и статические методы.

    Методы — члены класса и методы конструкторов по умолчанию были рассмотрены в предыдущем разделе.

    Методы конструкторов создаваемых пользователем

    Построим пользовательский конструктор в типе person_typ2.

    CREATE OR REPLACE TYPE person_typ2 AS OBJECT (
    name  VARCHAR2(4 0),
    dob  DATE,  — дата рождения
    phone VARCHAR2(12),
    — спецификация метода, возвращающего возраст MEMBER FUNCTION Age
    RETURN NUMBER,        — возвращаемое значение CONSTRUCTOR FUNCTION person_typ (
    p_name VARCHAR,
    p_dob DATE )    RETURN SELF AS RESULT
    );
    

    Ключевые слова CONSTRUCTOR FUNCTION применяются для задания пользовательских конструкторов. Фраза RETURN SELF AS RESULT означает, что конструктор вернёт объект типа person_typ2.

    Создаём тело типа:

    CREATE OR REPLACE TYPE BODY person_typ2 AS
    MEMBER FUNCTION Age RETURN NUMBER IS
    BEGIN  — вычисление возраста
     
    RETURN ROUND(MONTHS_BETWEEN(sysdate, birthday)/12); END;
    CONSTRUCTOR FUNCTION person_typ ( p_name VARCHAR, p_dob DATE ) RETURN SELF AS RESULT IS BEGIN
    SELF.name := p_name; SELF.dob  := p_dob; SELF.phone  := '2-222-222';
    RETURN;
    END; END;
    

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

    SELF.name := p_name;
    

    означает, что атрибуту name создаваемого объекта передаётся значение, присвоенное формальному параметру p_name конструктора.

    Проверьте работу конструктора по умолчанию и пользовательского конструктора, позволяющего создать объект типа person_typ2 с двумя параметрами — именем и датой рождения. А вот телефон у таких объектов всегда один и тот же 2-222-222.

    Методы сравнения (MAP и ORDER)

    В предопределённых скалярных типах всегда задаются отношения эквивалентности и порядка. Именно поэтому возможно сравнение и упорядочение значений типа. В объектных типах эти отношения должен задать разработчик. Вы не сможете употреблять фразу ORDER BY в запросах, а для установления эквивалентности двух объектов придётся сравнивать какие-то поля классов.

    Методы сравнения MAP и ORDER позволяют задать эквивалентность и порядок на объектном типе данных.

    В объектной модели Oracle предусмотрены определяемые пользователем методы MAP и ORDER, позволяющие ввести отношение порядка.

    Метод MAP

    Метод MAP это функция член типа (member function) без аргументов с именем сопровождаемым ключевым словом MAP. Она может возвращать значения только следующих типов: DATE, NUMBER, CHAR, VARCHAR2 или REAL.

    В спецификации типа для задания метода MAP необходимо ввести фразу

    MAP MEMBER FUNCTION имя_функции RETURN тип_данных.
    

    В описании тела типа задаётся обычное описание функции члена типа перед которым помещается ключевое слово MAP:

    MAP MEMBER FUNCTION имя_функции RETURN тип_данных IS описание_функции;
    

    Приведём простейший пример реализации метода MAP, в котором точка характеризуется тремя параметрами, а эквивалентность точек определяется по двум из них:

    CREATE OR REPLACE TYPE point_typ AS OBJECT ( x NUMBER(3), y NUMBER(3), weight NUMBER(4,1), distance NUMBER(4,1),
    MAP MEMBER FUNCTION point_ecv RETURN NUMBER
    );
    CREATE OR REPLACE TYPE BODY point_typ AS
    MAP MEMBER FUNCTION point_ecv RETURN NUMBER IS BEGIN
    distance := x**2+y**2; RETURN distance; END;
    END;
    

    Теперь можно сравнивать объекты типа point_typ по значениям, возвращаемым функцией point_ecv, например, используя self.distance.

    Метод ORDER

    Это функция с одним аргументом объектного типа, возвращающая значение: — 1, если параметр больше SELF; 1, если параметр меньше SELF; 0, если параметр равен SELF. Для одного типа нельзя одновременно использовать и MAP и ORDER. Метод ORDER моделирует парное сравнение, используемое в психологии и социологии. Когда удаётся сравнить объекты исследования попарно, но не возможности сравнить несколько объектов сразу.

    Статические методы

    Методы-члены, методы сравнения и конструкторы задают поведение экземпляров объектного типа. Параметр SELF обеспечивает им доступ к атрибутам объектов.

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

    10.3.4 Наследование

    Используется единичное наследование, при котором, как обычно, тип-потомок наследует от предка все атрибуты и методы. Потомок обязательно расширяет тип-предок дополнительными атрибутами и, может быть, переопределяет методы.

    Фраза NOT FINAL в конце определения типа делает наследование возможным. Этот же результат достигается по умолчанию. Фраза FINAL запрещает наследование.

    Пример (заимствован из документа "Objeci-Relational Developer's Guide" B28371-03):

    Создаём тип person_typ для которого возможно наследование.

    CREATE OR REPLACE TYPE person_typ AS OBJECT (
    id NUMBER,
    name VARCHAR2(30),
    phone VARCHAR2(30),
    MEMBER FUNCTION show RETURN VARCHAR2)  NOT FINAL;
    CREATE OR REPLACE TYPE BODY person_typ AS MEMBER FUNCTION show RETURN VARCHAR2 IS BEGIN
    RETURN 'Id:  '   ||  TO_CHAR(id)   ||   ', Name:  '   || name; END; END;
    

    Заметим, что при необходимости можно запретить наследование командой

    ALTER TYPE person_typ NOT FINAL;
    

    и вновь разрешить его. Создаём подтип (наследуемый тип). Сначала определяем спецификацию типа

    CREATE OR REPLACE TYPE student_typ UNDER person_typ (
    dept_id NUMBER, major VARCHAR2(30),
    OVERRIDING MEMBER FUNCTION show RETURN VARCHAR2)
    NOT FINAL;
    

    затем тело типа

    CREATE OR REPLACE TYPE BODY student_typ AS OVERRIDING MEMBER FUNCTION show RETURN VARCHAR2 IS BEGIN
    RETURN (self AS person_typ).show || ' -- Major:  '   || major;
    END; END;
    

    Слово UNDER в спецификации типа обозначает наследование. Major это профилирующий предмет, изучаемый студентом.

    10.3.5 Коллекции

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

  • Вложенные таблицы (nested tables) - одномерные, неограниченные коллекции однородных элементов.
  • Массивы переменной длины (variable-size arrays) VARRAY представляют одномерную ограниченную коллекцию однородных элементов. В отличие от вложенных таблиц, порядок элементов при обработке и запоминании сохраняется. Количество элементов от 0 до указанного при определении максимального значения. Обращаются к элементам коллекции по индексу, как в массиве.
  • Важнейшее преимущество коллекции - возможность передать ее всю между базой и PL/SQL за одну последовательность чтений, что может существенно увеличить скорость обмена.

    Массивы переменной длины

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

    Простой пример: Пусть необходимо хранить в таблице идентификатор, фамилию, имя, отчество (одним полем) и до трех номеров телефонов. Определим тип Phones_typ как коллекцию типа VARRAY

    CREATE TYPE phones_typ AS VARRAY(3)  OF CHAR(9);
    

    Теперь определим таблицу этого типа

    CREATE TABLE clients ( id NUMBER, name VARCHAR2(50), phones Phones_typ
    );
    

    Вставим в неё строку

    INSERT INTO clients VALUES(42,'Сидоров Иван Петрович', phones_typ(  '11-22-66', '57-23-56'));
    

    Получить всю информацию о массивах VARRAY можно из представления словаря user_varrays, а обо всех доступных пользователю массивах из представления all_varrays. Достаточно выполнить запрос вроде

    SELECT * FROM user_varrays;
    

    Вложенные таблицы

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

    Рассмотрим простой пример.

    Для задания вложенной таблицы создадим объектный тип Doc_typ:

    CREATE TYPE Doc_typ AS OBJECT ( dno CHAR(5), dname CHAR(20), dtype CHAR(3)
    );
    

    Используя его, создадим тип вложенной таблицы:

    CREATE TYPE Documents_typ   AS   TABLE   OF Doc_typ;
    

    Теперь определим таблицу Document:

    CREATE TABLE Document (
    dno  CHAR(5)   PRIMARY KEY,
    dname  CHAR(20) UNIQUE,
    docum  Documents_typ
    ) NESTED   TABLE  docum   STORE   AS docum_table;
    

    С помощью команды describe обнаруживаем, что таблица doc-um_table действительно существует.

    Информация о всех вложенных таблицах получается запросом

    SELECT * FROM user_nested_tables;
    

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

    10.3.6 Объектные представления

    Существуют объектные представления (object view), которые могут создаваться над реляционными данными. Все расширения DML, предназначенные для работы с объектными таблицами, поддерживают и объектные представления. На их основе можно создавать объектно-ориентированные приложения, не модифицируя существующие реляционные схемы. Свойства объектных представлений подобны свойствам объектных таблиц.

    Создадим таблицу и тип данных:

    CREATE TABLE emp_table ( empnum NUMBER(5), ename VARCHAR2(20), salary NUMBER(9,2), job VARCHAR2(40)
    );
    

    Заполним её

    INSERT INTO emp_table
    VALUES(33,  'Иванов',  25000, 'начальник'); INSERT INTO emp_table
    VALUES(33,  'Петров',  20000, 'разработчик');
    

    Создадим тип

    CREATE OR REPLACE TYPE employee_typ AS OBJECT (
    empno NUMBER(5), ename VARCHAR2(20), salary NUMBER(9,2), job VARCHAR2(20)
    );
    

    и объектное представление

    CREATE OR REPLACE VIEW emp_view2 OF employee_t WITH OBJECT OID (empno) AS
    SELECT e.empnum, e.ename,    e.salary, e.job
    FROM emp_table e
    WHERE job = 'разработчик';
    

    Объектное представление выглядит для пользователя как объектная таблица со строками типа employee_typ. Каждая ее строка имеет уникальный объектный идентификатор.

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

    10.3.7 Сравнение полученных объектных моделей данных

    Итак, изучены две, технически достаточно различные объектные модели. Сравним их, имея в виду, что речь идёт о конкретных решениях, а не о соотношениях объектной и объектно-реляционной моделей данных вообще.

    Сначала перечислим общие особенности. Это, в первую очередь, появление персистентных классов, объекты которых хранятся в базе данных. Как следствие, использование двух объектных ссылок OID и OREF, обеспечивающих доступ к объектам в памяти и на диске.

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

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

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

    Поскольку Cache позволяет пользователю гораздо больше "самостоятельности", чем Oracle, в ней можно изменять структуры хранения и в пользовательских программах иметь доступ к промежуточным результатам работы СУБД. Но это отличия реализаций, а не моделей.

    Ещё один, наверное неожиданный, результат использования единой архитектуры Cache — "втаскивание" наследования в табличную модель данных.

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

    Вернуться к учебному плану