Объектные модели данных
Чтобы класс стал доступным для использования, его нужно откомпилировать (позиция меню "Собрать > Компилировать").
Выясним, как создаются экземпляры класса (объекты) и где они хранятся.
Запустим портал управления системой, выбрав пункт " Утилиты > Портал управления системой" в главном меню Studio (рисунок 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.
Нажав на Просмотр, получаем подробную информацию о глобале:
^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).
Столбец | Тип данных | Столбец # | Обязательный | Уникальный | Сортировка | Скрыто | 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).
Столбцы "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. Однако, не каждое дерево может быть отображено одной таблицей.