Кубанский государственный университет
Опубликован: 24.12.2013 | Доступ: свободный | Студентов: 681 / 8 | Длительность: 24:28:00
Лекция 10:

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

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

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

Запустим портал управления системой, выбрав пункт " Утилиты > Портал управления системой" в главном меню 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.

Область USER

Рис. 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. От родителя все такие классы получают следующие методы внешнего интерфейса:

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

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

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

Таблица 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).

Таблица 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
Пример 10.1. Столбцы "ID" и "#" до удаления строки

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

SELECT * FROM A
# ID Name
1 1 John
2 3 Michael
Пример 10.2. Столбцы "ID" и "#" после удаления строки

Понятно, что левая колонка с именем "#" — это номер строки результата, а колонка "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
Пример 10.3. Введённая строка

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

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

Пример 10.4. Структура глобала AUser.TD

В узле глобала ^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")

Пример 10.5. В таблицу T добавлена строка (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
Пример 10.6. Вторая строка изменена командой ObjectScript SELECT *

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

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

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

SELECT * FROM T
# c1 c2
1 22 QQ
2 11 A
3 7 Z
Пример 10.7. Строка добавлена командой ObjectScript

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

SELECT * FROM T
# c1 c2
1 22 QQ
2 11 A
3 33 HH
Пример 10.8. Новая строка замещает старую

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

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
Пример 10.9. Вставка строки выполнена успешно

И теперь при добавлении новой строки, например (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
Пример 10.10. Вставка строки в SQL выполнена успешно

Обратимся к словарю. В 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. Однако, не каждое дерево может быть отображено одной таблицей.