Как стать автором
Обновить

Виртуальная СУБД. Язык манипулирования данными (DML)

Уровень сложностиСредний
Время на прочтение32 мин
Количество просмотров936

Эта статья является продолжением серии статей посвященной новой системе разработки клиентских приложений KISS Virtual XML DBMS.

Язык манипулирования данными представлен в виртуальной СУБД базовым классом recset (набор записей). Набор записей представляет собой коллекцию записей, выбранных из физической таблицы по заданному критерию отбора. Квази-родителем для набора записей является схема виртуальной таблицы (базовый класс tblschema). При создании объекта recset необходимо указать имя класса связанной с ним схемы виртуальной таблицы. При этом набор записей наследует из схемы определение записи виртуальной таблицы (объект .Rec) и список представлений (объект .Views). 

Создание свободного объекта recset
Создание свободного объекта recset

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

Связь с источником данных

c .:AsClass /* возвращает имя схемы таблицы

c .TableDBCS /* возвращает код СУБД

tip

M - MySQL

S - SQL Server

O - Oracle

P - PostgreSQL

F – Firebird

V - Visual FoxPro

endtip

c .TableBase /* возвращает имя базы данных

c .TableName /* возвращает имя таблицы

o .Rec /* структура записи

o .Views /* список представлений

Объект recset чаше всего создается на основании определений, сделанных в назначенной ему схеме таблицы (tblschema). Привязка к нужной схеме таблицы возможно только при создании нового объекта recset. Для этого в окне добавления объектов нужно выбрать нужную схему (tblschema) из списка ранее определенных схем. Имя выбранной схемы будет присвоено фундаментальному свойству .:AsClass созданного объекта recset. Все дочерние объекты recset, созданные на основе привязанного к схеме родительского объекта, наследуют эту схему. Нельзя изменить имя выбранной схемы. Тип используемой СУБД, имя базы данных и имя физической таблицы, назначенной схеме, отображается в объекте recset соответственно в свойствах .TableDBCS, .TableBase и .TableName. В случае замены таблицы в определении схемы, созданный объект recset автоматически унаследует это новое имя.

Курсор

c .CursorType /* тип курсора

tip

L - Linked (Default)

T - Temporary

O - On Table

endtip

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

Тип используемого курсора устанавливается с помощью доступного для чтения и записи свойства .CursorType:

  • L - Linked (Default) - используется для указания Системе, что изменения, сделанные в наборе записей отображаются одновременно в курсоре и в исходной таблице. Это происходит либо после каждой команды корректировки, либо, если установлен переключатель .BatchUpdating, пакетом команд. Таким образом, курсор и исходная таблица синхронизированы при выполнении команд корректировки.

  • T - Temporary - автоматически присваивается Системой в случае, если схема таблицы не привязана к физической таблице. Этот курсор создается на основании определений сделанных в назначенной схеме. Курсор представляет собой пустую временную таблицу, не имеющую источника данных, которая автоматически удалятся после закрытия набора записей. Данный тип курсора полезен для временного хранения на внешнем носителе промежуточных данных при выполнении каких-либо операций.

  • O - On Table- Система предоставляет возможность использовать курсор, наложенный на таблицу (только для dbf-таблиц), который будем называть курсором на таблице, чтобы отличить его от стандартного курсора. В этом случае в качестве курсора используется сама таблица. Порядок сортировки и фильтрация записей определяется выбором в качестве текущего одного из доступных физических индексов таблицы. Преимущество курсора на заключается в том, что нет необходимости тратить ресурсы Системы на физическое перемещение записей таблицы в курсор, а сам набор записей всегда содержит актуальные данные.

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

  • Тип курсора <O> может быть указан, только если в качестве источника данных используется dbf-таблица;

  • Тип курсора <T> можно установить для того, чтобы разорвать связь между курсором и источником данных. В этом случае все изменения будут отражаться только в курсоре.

  • Тип курсора <L> может быть использован для любых источников данных.

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

Структура записи набора (.Rec)

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

Можно добавлять в виртуальную запись новые структуры (объекты базового класса element), новые свойства (атрибуты) непосредственно не привязанные к физическим полям (альтернативные имена, вычисляемые атрибуты и обычные атрибуты), ссылки на внешние структуры и внешние атрибуты, не входящие в состав объекта .Rec. Можно скрыть атрибуты или сделать их доступными только для чтения, а также переопределить их начальные значения (если только в схеме не запрещено переопределение этих атрибутов). Удалить атрибуты и структуры, определенные в схеме нельзя (можно только их скрыть)!

Можно исключить (свойство :excluded) из выборки часть атрибутов, привязанных к физическим полям и которые не используются в данном запросе, что может значительно уменьшить время выборки набора записей из физической таблицы.

Можно назначить атрибутам и структурам виртуальной записи XML-имена, а также исключить (свойство :prohibited) атрибуты и структуры из XML-документа.

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

Из сказанного выше становится понятно, что определение структуры виртуальной записи не заканчивается определениями, сделанными в схеме виртуальной таблицы. Структура виртуальной записи в схеме виртуальной таблицы определяет объектный образ хранимого в базе данных XML-документа. Получить соответствующий этому образу реальный XML-документ можно с помощью метода .:XMLText(). Функция привязки атрибутов, представлений и виртуальной таблицы к физическим полям, индексам и таблице доступна только в схеме виртуальной таблицы. Именно там нужно позаботиться об оптимальном размещении значений атрибутов в соответствующих им полях физической записи.

Выбор и сортировка набора записей.

l .Select([uArg[,...]]) /* выбрать набор записей

l .ArgPrepare(uArg[,...]) /* подготовить значения аргументов для .Select()

l .ArgChanged /* возвращает признак изменения значений и/или количества аргументов

i .ArgCount /* возвращает количество сохраненных аргументов

u .ArgValue(iArgNo | cParName [,uValue]) /* возвращает/устанавливает значения сохраненных аргументов

o .Views /* список доступных представлений

c .ViewName /* возвращает/устанавливает имя представления

c .ViewType /* возвращает тип представления

tip

R – Regular

U – Unique

P – Primary

endtip

c .Parameters /* возвращает список параметров представления

i .ParCount /* возвращает количество параметров представления

c .WhereExpr /* возвращает/устанавливает текст логического выражения для отбора

o .Indexes /* список определенных индексов

c .IndexName /* возвращает/устанавливает имя текущего индекса

c .IndexType /* возвращает тип текущего индекса

tip

R – Regular

U – Unique

endtip

c .OrderBy /* возвращает текущий список полей сортировки набора

c .FilterExpr /* возвращает текущее выражение отбора для индекса

l .Descending /* возвращает/устанавливает признак сортировки по убыванию

l .Requery() /* обновить набор записей без изменения условий отбора.

l .Clear() /* удалить текущий курсор (набор записей)

i .Count /* возвращает общее количество записей набора

i .RecCount /* возвращает количество записей набора с учетом фильтра

c .State /* возвращает код текущего состояния набора записей

tip

C – Closed

O – Open

S – Selected

endtip

Выбор набора записей .Select()

Для выборки набора записей используется метод Select([arg1[,...]]).

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

Для исключения ненужных полей нужно установить признак :Excluded в подложке атрибута, соответствующего этому полю. Это можно сделать программным путем (oRec.AttrName:Excluded = true) или, что существенно проще, с помощью Конструктора запросов.

Для максимально быстрого отбора нужных записей таблицы необходимо выбрать имя представления таблицы (свойство .ViewName), которое определяет параметры отбора - количество параметров (свойство .ParCount) и список ключей отбора (свойство .Parameters). Выбранное представление одновременно определяет список параметров процедуры .Select([arg1[,...]]), т.е. в методе .Select необходимо указать значения (аргументы), которые будут использованы в статье where команды SQL Select:

where

key1 == arg1 and /* -> view par1

key2 == arg2 and /* -> view par2

...

Причем, количество переданных методом .Select() аргументов может быть меньше, чем количество параметров, определенных в представлении. Значения аргументов, переданных методом .Select() запоминаются во внутреннем скрытом списке объекта recset для дальнейшего контроля и использования. Количество переданных аргументов запоминается в свойстве .ArgCount, а с помощью метода .ArgValue(iArgNo | cParName [,cValue]) можно прочитать и даже изменить значение указанного аргумента.

Подготовка для выполнения выборки .ArgPrepare()

Система позволяет сформировать указанный выше список значений аргументов без последующего выполнения собственно выборки. Для этих целей предназначен метод .ArgPrepare(Arg1[,...]), который имеет тот же список параметров, что и метод .Select().

При вызове метода .ArgPrepare() Система выполняет следующие действия:

  • Проверяет на соответствие количество и тип предаваемых аргументов количеству и типу параметров представления. Если обнаружены ошибки, то выдается сообщение об ошибке и выполнение .ArgPrepare() завершается без изменения текущего состояния (настроек) объекта recset.

  • Запоминает полученные значения аргументов во внутреннем списке объекта recset.

  • Если количество и значения аргументов в списке не изменилось, процедура не меняет текущее состояние набора записей и возвращает значение false, в противном случае автоматически выполняется метод .Clear() для очистки текущего набора записей и возвращается значение true. Возвращаемое значение процедуры сохраняется в доступном только для чтения свойстве . ArgChanged.

Таким образом, процедура .ArgPrepare() не выполняет выборку записей набора, а только подготавливает объект recset к выполнению запроса и контролирует состояние набора записей. Для выполнения подготовленного запроса нужно вызвать метод .Select() без параметров.

Если вызывается .Select() с непустым списком аргументов, то Система вызывает процедуру .ArgPrepare() для сохранения полученных аргументов представления и проверки необходимости отбора записей (установки значения свойства . ArgChanged). Система проверяет текущее значение свойства .ArgChanged. Если .ArgChanged == true, то процедура .Select() автоматически вызывает метод .Requery() для выборки нового набора записей и возвращает значение true, в противном случае, новый набор записей не выбирается и процедура .Select возвращает false.

Дополнительные условия отбора (.WhereExpr)

Свойство .WhereExpr используется для определения сложного логического выражения, определяющего дополнительное, а если не указано имя представления, то и единственное условие отбора. В этом выражении допустимы все операции сравнения и логические операции (==, <>, >, <, <=, >=, or, not, and). В качестве операндов кроме имен атрибутов можно указывать константы, глобальные переменные (g.tVarName), внешние параметры (e.tVarName), а также единые для всех физических СУБД функции:

left(), right(), lower(), upper(), trim(), ltrim(), rtrim(), abs(), space(), floor(), ceil(), mod(), round(), sign(),...

Статья Where команды SQL Select может выглядеть следующим образом:

where

[Attr1 == arg1 and /* -> view par1

 Attr2 == arg2 and /* -> view par2

 ... and]

(Attr3 >= e.dParDate and Attr4 <= abs(Attr2) ...) /* -> .WhereExpr

Сортировка и фильтрация выбранного набора записей.

После того, как набор записей был выбран, он может быть отсортирован и отфильтрован по любому выражению над значениями привязанных к полям таблицы атрибутов записи. Объект recset имеет контейнер .Indexes, в котором могут быть определены временные индексы для выбранного набора записей (объекты базового класса index), которые определяют список ключей сортировки и логические выражения для условия фильтрации. То есть после того, как набор записей был выбран в соответствии заданным условиям отбора, выбранный набор записей может быть упорядочен произвольным образом без повторного отбора. Более того, на выбранный набор записей может быть наложен произвольный фильтр. То есть отбор можно продолжить после получения набора записей, причем, по самым различным критериям. Для смены сортировки нужно в свойстве объекта recset .IndexName указать имя нужного индекса.

Объект базового класса index имеет следующие свойства

c .OrderBy /* список ключей сортировки

c .FilterExpr /* логическое выражение для условия отбора (фильтрации)

c .IndexType /* тип индекса

tip

R – Regular

U – Unique

endtip

Контейнер .Indexes содержит обязательный объект базового класса index с именем "default". Этот индекс определяет порядок сортировки, устанавливаемый автоматически после отбора записей, если в свойстве объекта recset .IndexName явно не указано имя другого индекса из списка .Indexes.

Список ключей сортировки, определенный в данном индексе определяет статью Order by команды SQL Select:

Order by key1[,key2,...]

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

Если в индексе "default" не указан список ключей сортировки, то в его качестве используется список ключей представления, не использованных в методе .Select(). Если в запросе использованы все ключи отбора, определенные в представлении, то набор записей останется не отсортированным (отсутствует статья Order by).

Методы .Requery() и .Clear()

Бывает необходимо обновить набор записей без изменения условий отбора. Для этого можно выполнить метод .Requery(). При вызове метода .Requery() Система выполняет следующие действия:

  • Принудительно выполняет выборку нового или обновление текущего набора записей независимо от текущего значения свойства .ArgChanged.

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

  • При успешном выполнении метода свойство .State принимает значение S (Selected), даже если набор записей окажется пустым.

  • Если выбран пустой набор записей, то очищаются все свойства структуры .Rec (кроме ссылок) пустыми значениями (обратите внимание: пустыми значениями, а не значениями по умолчанию!)

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

  • Удаляет курсор для набора записей (если набор был выбран)

  • Очищает все свойства структуры .Rec (кроме ссылок) пустыми значениями

  • Свойство .State принимает значение O (Open)

Древовидные таблицы.

l .MoveChild(cKey) /* выбрать дочерние записи

l .MoveParent() /* вернуться к родительской записи

i .Level /* уровень вложенности текущего курсора

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

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

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

Система имеет специальные средства, обеспечивающие простой и быстрый способ обхода дерева записей.

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

Для возврата к родительской записи нужно использовать процедуру .MoveParent(). При выполнении этой процедуры Система удаляет текущий курсор и выбирает из стека ссылку на сохраненный курсор и другие данные для восстановления состояния набора на момент вызова процедуры .MoveChild().

Для определения текущего уровня вложенности наборов данных можно использовать значение доступного только для чтения свойства .Level. Значение этого свойства указывает количество элементов стека и устанавливается Системой автоматически. При выполнении процедуры .MoveChild() уровень вложенности увеличивается на единицу, а текущие данные добавляются в вершину стека. При выполнении процедуры .MoveParent() данные удаляются из вершины стека, а уровень вложенности уменьшается на единицу.

Буфер обмена с курсором и текущая запись

.Rec /* структура для текущей записи набора

i .RecNo /* возвращает абсолютную позицию записи в наборе

i .RecIndex(iRecNo) /* возвращает относительную позицию записи в наборе

i .CacheSize /* возвращает/устанавливает максимальный размер буфера

l .FirstBlock /* признак первой порции набора

l .LastBlock /* признак последней порции набора

i .BufferSize /* возвращает фактический размер буфера

i .BufferIndex /* возвращает/устанавливает позицию текущей записи в буфере

u .Moved() /* событие возникает при смене текущей записи набора

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

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

Для контроля состояния буфера имеются два свойства-переключателя, доступные только для чтения – .LastBlock и .FirstBlock. Значению этих переключателей автоматически присваивается значение true, если прочитанная порция записей является соответственно последней или первой порцией набора записей. Если одновременно установлены оба переключателя, то это означает, что набор записей целиком находится в буфере.

Текущая запись – это самая последняя выбранная из буфера запись набора, раскрытая в структуру .Rec. Позиция текущей записи в наборе определяется значением свойства .RecNo, которое является физическим номером записи курсора.

Это значение является абсолютным значением, которое не изменяется при сортировке или фильтрации курсора. Получить относительную позицию записи в наборе с учетом фильтра и сортировки можно с помощью метода .RecIndex(iRecNo).

Позиция текущей записи в буфере обмена определяется значение свойства .BufferIndex. Это свойство доступно для чтения и корректировки. В случае изменения значения этого свойства меняется и текущая запись. Текущей записью становится запись буфера с указанной позицией в буфере и данные этой записи автоматически раскрываются в структуру .Rec. При смене текущей записи возникает событие .Moved().

В случае выборки пустого набора записей структура .Rec заполняется пустыми значениями в соответствие с их типом данных.

Связанные наборы данных

l .AutoSelect /* возвращает/устанавливает признак автоматического выбора связанного набора

c .LinkName /* возвращает/устанавливает имя связи

c .LinkType /* возвращает тип связи

tip

0 – (1:0) Parent to Child

1 – (1:1) Child to Parent

2 – (1:M) Parent to Children

3 – (M:1) Children to Parent

endtip

c .LinkKeys /* возвращает список внешних ключей

Объекты recset могут быть вложенными друг в друга, образую при этом дерево вложенных объектов. Корнем этого дерева является свободный набор записей, не включенный в другой объект базового класса recset. Все остальные наборы записей являются связанными. Характер связи с родительским набором данных определяется связью (объект базового класса link), определенной в родительской схеме виртуальной таблицы. Таким образом, дерево вложенных объектов определяет навигацию по виртуальной базе данных и является основой для определения сложных запросов, использующих несколько связанных между собой виртуальных таблиц.

Если родителем создаваемого объекта recset является объект этого же класса, то Система проверяет, имеются ли в схеме родительского объекта recset список связей. Если связи существуют, то Система заполняет список имен доступных связей в окне добавления нового объекта и делает этот список доступным для выбора. При выборе нужной связи Система автоматически назначит новому объекту имя схемы и имя представления, взятые их определения выбранной связи (link) в схеме виртуальной таблицы родительского объекта. При этом имя выбранной связи запоминается в свойстве нового объекта .LinkName. Непустое значение этого свойства является признаком связанного набора записей. Доступное только для чтения свойство .LinkKeys в добавленном наборе записей содержит список внешних ключей связи.

Создание связанного объекта recset
Создание связанного объекта recset

Связь наборов записей означает, что при смене текущей записи родительского набора (событие .Moved()) Система автоматически сформирует из значений определенных в родительском наборе внешних ключей список значений ключей отбора записей дочернего набора и с помощью метода .ArgPrepare() сохранит значения аргументов отбора в скрытом списке дочернего объекта recset. Если значение свойство .AutoSelect дочернего набора равно true, то Система автоматически выполнит процедуру Select() для отбора записей дочернего набора с использованием списка полученных значений в качестве аргументов указанной процедуры.

Если значение свойство .AutoSelect равно false, то действия системы ограничится сохранением значений полученных ключей отбора в списке аргументов дочернего набора. При вызове на выполнение процедуры Select() без параметров или процедуры .Requery() для отбора записей набора в качестве значений ключей отбора будут использованы значения сохраненных в списке аргументов.

При добавлении нового связанного набора записей, Система автоматически добавляет в объект .Rec родительского объекта ссылку (L-атрибут) с именем, совпадающем с именем использованной связи и привязывает (bind) эту ссылку к объекту .Rec добавленного набора записей. Таким образом, автоматически формируется единое отдельное дерево данных, повторяющее структуру дерева вложенных объектов. Глубина вложенности дерева объектов и соответствующего ему дерева данных ограничена лишь рамками здравого смысла.

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

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

 Физическая СУБД

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

В результате анализа команды SQL Select, СУБД создает исполняемый код запроса (компилирует запрос) и вызывает его на выполнение. Результат выполнения запроса в виде курсора возвращается Клиентской части приложения.

Указанный выше процесс выполняется средствами СУБД для каждой команды SQL Select, т.е. каждый запрос (простой или сложный) заново компилируется перед его выполнением.

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

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

Универсальность команды SQL Select стимулирует "грязное" программирование. При этом неоправданно сложные и не прозрачные запросы затруднят сопровождение написанного Вами Приложения.

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

Виртуальная СУБД

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

Для определения сложного запроса к базе данных виртуальная СУБД использует представления и связи, определенные в схеме таблицы. Это позволяет создавать максимально эффективные запросы, поскольку при выполнении запроса СУБД, как правило, нет необходимости создавать временные индексы и таблицы (хотя в виртуальной СУБД имеется и такая возможность).

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

Виртуальная СУБД не использует хранимые в базе данных СУБД представления, поскольку использование этих представлений сделает Ваше Приложение зависимым от конкретной физической СУБД. Тем не менее, Система при первом выполнении запроса автоматически компилирует текст запроса в динамическое представление. Динамическое представление не сохраняется в базе данных конкретной СУБД, а привязывается к shared-соединению, назначенному данному набору записей. Динамическое представление удаляется при удалении набора записей (объекта recset) или заново создается, если условия запроса изменились. Таким образом, Система вместо команды SQL Select вызывает подготовленное динамическое представление. Динамическое представление использует все преимущества представления, хранимого в СУБД (максимально быстрое выполнение) и не имеет его недостатков, поскольку динамическое представление создается для всех запросов и не сохраняется на Сервере.

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

Принципиальным отличием способа выполнения сложного запроса виртуальной СУБД от способа выполнения сложного SQL-запроса физической СУБД, является отсутствие в качестве результата запроса нового набора записей, содержащего общий результат запроса. Выборка Системой определенных в запросе данных происходит в результате обхода дерева наборов записей, которые создаются по мере необходимости. Результат запроса формируется в событиях .Moved() указанных наборов в процессе продвижение по дереву наборов. То есть виртуальная СУБД позволяет получить конечный результат за один проход дерева базы данных. Система не накладывает никаких ограничений на вид этого результата. Например, результатом может быть произвольный текстовый отчет, XML-документ, массив и т.п.

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

Таким образом, сложный запрос представляет собой серию простых запросов, т.е. вместо вызова одного сложного SQL-запроса, выполнение которого может занимать много времени, Система выполняет несколько простых и максимально быстрых запросов. Этот процесс похож на квантование времени процессора. Результат формируется в событиях .Moved() после выполнения очередного простого запроса в процессе обхода выбранного набора записей. Указанная выше технология выполнения сложных запросов позволяет максимально использовать ресурсы Сервера, а также уменьшить время ожидания выполнения запроса на стороне Клиента.

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

Полученное дерево вложенных объектов recset может стать основой для создания параметрического запроса к базе данных. Для этого нужно определить список параметров запроса в какой-нибудь созданной процедуре корневого набора записей, например, в процедуре .QueryExec(e.dParDate,e.fCallBack,...). и сохранить определение этого объекта в библиотеке классов. В качестве параметров лучше всего указывать внешние параметры (e.dParDate,...), значения которых доступны во всех событиях .Moved(), независимо от уровня вложенности соответствующих им объектов recset. Значащие внешние параметры можно указывать в выражении .WhereExpr, т.е. использовать их значение в качестве критериев отбора записей. Использование функций обратного вызова в качестве внешних параметров очень эффективно для определения алгоритма обработки данных запроса и вида результата запроса.

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

Конструктор запросов к Базе Данных.

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

Вызвать Конструктор запросов можно только из Конструктора объектов. Для этого нужно в списке объектов сделать текущим выбранный Вами объект базового класса recset. В этом случае в нижнем правом углу формы Конструктора объектов появится кнопка для вызова Конструктора запросов. При нажатии на эту кнопку откроется форма Конструктора запросов, показанная на следующем скриншоте.

Определение параметров текущего простого запроса
Определение параметров текущего простого запроса

В левой части формы отображается объектное представление дерева вложенных объектов recset <Object View>.

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

  • <Select> - Определение параметров текущего простого запроса

  • <Record Filter> - Определение фильтра для всех наборов записей, участвующих в запросе

  • <Data Base Navigator> - Создание и корректировка дерева обхода Базы данных для выполнения запроса

  • <Data View> - Представление данных, определяющее результат запроса

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

Определение фильтра для всех наборов записей, участвующих в запросе
Определение фильтра для всех наборов записей, участвующих в запросе

Страница <Record Filter> предназначена для пометки атрибутов записей, которые не используются в запросе и должны быть исключены из из списка выбираемых полей. Это позволяет уменьшить объем получаемых с Сервера данных. Для пометки или её отмены нужно дважды счелкнуть по выбранному атрибуту или изменить значение переключателя <excluded>. Помеченные для исключения атрибуты выделяются цветом, как недоступные.

Создание и корректировка дерева обхода Базы данных для выполнения запроса
Создание и корректировка дерева обхода Базы данных для выполнения запроса

Страница <Data Base Navigator> предназначена для построения дерева наборов записей, определяющего навигацию по базе данных в процессе выполнения запроса. Для того, чтобы добавить в дерево новый связанный набор записей, нужно сделать текущим узлом этого дерева родительский набор записей, с которым нужно связать добавляемый набор. Список <Link> содержит имена всех связей, определенных в схеме таблицы для текущего родительского набора. Нужно выбрать из этого списка нужную Вам связь и нажать кнопку слева от указанного списка. Удалить из дерева текущий набор можно с помощью кнопки, расположенной в левом верхнем углу страницы.

Представление данных, определяющее результат запроса
Представление данных, определяющее результат запроса

Страница <Data View> предназначена для определения производных структур (XML-документов) и производных атрибутов, необходимых в запросе данных. Как было сказано выше, наряду с деревом наборов записей автоматически формируется единое дерево хранимых данных, повторяющее структуру дерева наборов записей. То есть все элементы и атрибуты этого дерева данных доступны из любого его узла, что позволяет добавлять в дерево данных любые новые элементы или атрибуты и привязывать их к источнику данных декларативным способом с помощью свойства :BoundTo, определив в нем путь до источника данных. То есть можно "нарисовать" любые производные XML-документы или производные атрибуты, привязанные к хранимым или другим производным элементам или атрибутам. Производные документы могут быть использованы в качестве конечного или промежуточного результата запроса.

При перемещении по какому-нибудь набору данных (событие .Moved()) автоматически меняются значения атрибутов в структуре .Rec этого набора записей и во всех или в нескольких структурах .Rec вложенных в данный набор данных (в зависимости от значения свойства-переключателя .AutoSelect). При обращении к производным документам или атрибутам Вы получите "свежие" данные из привязанных к ним атрибутов.

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

  • Первые две позиции предназначены для присвоения XML-имени элементу или атрибуту, если зти имена не должны совпадать и именами, использованными в объектном представлении.

  • Кнопка удаления текущего узла дерева данных (элемента или атрибута)

  • Кнопка вызова построителя для привязки атрибута или ссылки на элемент к источнику данных.

  • Кнопка для добавления нового атрибута

  • Кнопка для добавления нового элемента

  • Переключатель для пометки/отмены пометки исключенных из XML представления элементов или атрибутов

Перемещение по набору записей

o .Rec /* структура для текущей записи набора

i .RecNo /* абсолютный номер текущей записи набора

i .RecIndex(iRecNo) /* возвращает относительную позицию указанной записи

l .MoveTo(iRecNo) /* выбрать указанную запись набора

l .MoveFirst() /* выбрать первую запись набора

l .MoveLast() /* выбрать последнюю запись набора

l .MoveNext() /* выбрать следующую запись набора

l .MovePrev() /* выбрать предыдущую запись набора

l .Find(cKey[,...]) /* поиск и выбор записи по ключам сортировки

i .Seek(cKey[,...]) /* поиск записи по ключам сортировки

l .FindNear  /* признак поиска ближайшего значения

i .EOS /* признак конца набора

l .BOS /* признак начала набора

u .Moved() /* событие возникает при смене текущей записи набора

u .EndOfSet() /* событие возникает при перемещении ниже последней записи набора

u .BegOfSet() /* событие возникает при перемещении выше первой записи набора

 Следующие методы предназначены для навигации по выбранному набору записи (смены текущей записи):

  • .MoveFirst() или .MoveLast() - текущей записью становится соответственно первая или последняя запись набора.

  • .MoveTo(iRecNo) - текущей записью становится запись с указанным номером.

  • .MoveNext() -  если имеется следующая по порядку запись набора, то она становится текущей записью, в противном случае текущая не меняется, а доступному только для чтения свойству .EOS присваивается значение true и вызывается событие .EndOfSet().

  • .MovePrev() -  если имеется предыдущая по порядку запись набора, то она становится текущей записью, в противном случае текущая не меняется, а доступному только для чтения свойству .BOS присваивается значение true и вызывается событие .BegOfSet().

  • .Find(cKey[,...]) - поиск записи по ключам сортировки. Если запись с указанным ключом найдена, то она запись становится текущей; в противном случае текущая не меняется. Метод .Seek(cKey[,...]) аналогичен методу .Find() за исключением того, что не приводит к смене текущей записи, а только возвращает номер найденной записи или 0, если запись не найдена. Обе указанные команды поиска учитывают признак поиска ближайшего значения, установленный в свойстве .FindNear.

При смене текущей записи при выполнении указанных выше методов возникает событие .Moved().

Корректировка таблицы

l .Add() /* добавить новую запись в набор

l .Set() /* сохранить все измененные атрибуты текущей записи

l .Remove([lAllRecords]) /* удалить текущую запись или все записи набора

l .Increase(cAttrList, nIncr[,...]) /* изменить значения указанных счетчиков

c .RecUsageMode /* возвращает/устанавливает код режима использования структуры .Rec

tip

R – Read /* режим только чтения

A – Add /* режим добавления

E – Edit /* режим корректировки

endtip

c .PrimaryKey /* возвращает имя первичного ключа

c .PrimaryView /* возвращает имя первичного представления

c .KeyStamp /* возвращает имя штампа ключа

c .UpdStamp /* возвращает имя штампа корректировки

Структура .Rec предназначена для отображения и корректировки атрибутов текущей записи.

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

  • R - Read

  • E - Edit

  • A - Add

По умолчанию значение свойства .RecUsageMode равно <R>, а это означает, что все атрибуты в структуре .Rec доступны только для чтения (read only). Попытка изменить их значения любым способом вызовет сообщение об ошибке.

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

  • A - Добавление новой записи. Включается режим добавления новой записи. Всем атрибутам структуры .Rec присваиваются пустые значения или значения по умолчанию, если таковые имеются. Для необязательных пустых атрибутов устанавливается признак :Null. Все атрибуты записи, для которых нет явных ограничений для записи, становятся доступными для корректировки. После того, как установлены все необходимые значения атрибутов записи, необходимо выполнить метод .Add() для добавления новой сформированной записи. Если структура записи имеет атрибут k-типа, то при добавлении Система автоматически присвоит этому атрибуту сгенерированный уникальный ключ записи, а атрибуту p-типа присваивается пустое значение. Если не указаны значения обязательных атрибутов, то Системы выдаст сообщение об ошибке, запись не добавляется, но при этом режим добавления не сбрасывается. Метод .Add() генерирует SQL-команду Insert для добавления этой записи в физическую таблицу. Добавлять новые записи можно без предварительной выборки набора записей. Если набор записей не выбран, то новая запись добавляется только в таблицу, а не одновременно в курсор и таблицу. Если не включен режим пакетного редактирования, то новая запись сразу добавляется в физическую таблицу, в противном случае, SQL-команда добавляется в пакет корректировки. После успешного выполнения метода .Add() Система автоматически включает режим только чтение (.RecUsageMode = R), а текущей записью набора становится добавленная запись.

  • E -Корректировка текущей записи. Включается режим корректировки текущей записи. Все атрибуты записи, для которых нет явных ограничений для записи, становятся доступными для корректировки. У всех указанных выше атрибутов сбрасывается признак :Changed (:Changed = false), который определяет, было ли изменено значение этого атрибута. Признак устанавливается (:Changed = true) в результате выполнения любой операции присваивания значения данному атрибуту. Признак :Changed доступен для корректировки, т.е. его значение можно установить или сбросить программно, не изменяя значение самого атрибута. Основное назначение указанного признака - это пометка изменяемых атрибутов для выполнения метода .Set(). Будут изменены значения только тех атрибутов, у которых установлен признак :Changed. Если структура записи имеет атрибут p-типа, то Система автоматически присвоит этому атрибуту сгенерированный штамп корректировки и установит для него признак :Changed. Метод .Set() автоматически сгенерирует SQL-команду Alter для корректировки соответствующей записи таблицы. Все изменения отображаются в курсоре. Если включен режим пакетной корректировки, то SQL-команда сразу не выполняется, а добавляется в пакет корректировки. После успешного выполнения метода .Set() Система автоматически включает режим "только чтение" (.RecUsageMode = R) В режиме корректировки вместо метода Set() можно выполнить метод Add(), т.е. Система предоставляет средство для добавления новых записей "по образцу". При добавлении новой записи значения признаков :Changed игнорируются, атрибуту k-типа присваивается сгенерированный уникальный ключ записи, атрибуту p-типа присваивается пустое значение, и выполняются все другие действия, предусмотренные в методе Add().

  • R - Включается режим чтения. Восстанавливаются из буфера значения атрибутов текущей записи, и устанавливается ограничение "только чтение" для всех атрибутов записи. Этот режим автоматически включается после успешного выполнения методов Add() или Set(). Система автоматически устанавливает режим чтения при выполнении любого метода, приводящего к смене текущей записи набора. При этом все данные структуры .Rec "затираются" данными из новой текущей записи набора.

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

Удаление записей

Для удаления текущей записи или всех записей набора используется метод .Remove([lAllRecords])

Для выполнения корректировки (.Set) и удаления (.Remove) текущей записи таблицы Системе необходимо предоставить уникальный ключ текущей записи, по которому данная запись может быть однозначно определена. В любой таблице, в которой предусмотрены операции на уровне отдельной записи, должен быть определен первичный клич и соответствующий ему индекс для поиска нужной записи.

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

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

  • .PrimaryKey - возвращает имя первичного ключа

  • .PrimaryView - возвращает имя первичного представления

Значение первичного ключа из текущей записи используется в статье Where SQL-команд, сгенерированных методами .Set() и .Remove()

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

Если в методе Remove([lAllRecords]) указан признак удаления всех записей набора, то в качестве статьи Where для отбора удаляемых записей используется выражение отбора набора записей.

Счетчики связи

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

Специально для корректировки счетчиков связи предназначен метод .Increase(cAttrList, nIncr[,...]). Процедура выполняет корректировку значений числовых атрибутов на заданное число (приращение).

Указанную выше операцию можно выполнить с помощью процедуры .Set(). Зачем тогда нужна отдельная процедура? Дело в том, что Система предназначена для одновременной работы нескольких пользователей. После того, как Вы выбрали набор записей, другие пользователи могли успешно изменить в таблице значения полей записи набора. Если не предпринимать определенных действий (блокировки обновления), выполнив процедуру .Set(), Вы можете уничтожить изменения, сделанные другими пользователями.

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

Пусть Вас не смущает название процедуры "increase" (увеличить). Можно указать отрицательное значение приращения. В этом случае для указанного поля-счетчика процедура будет работать как "decrease" (уменьшить).

Пакетная корректировка (batch updating)

l .BatchUpdating /* возвращает/устанавливает признак пакетной корректировки

l .BatchUpdate() /* выполнить пакет

l .BatchCancel() /* отменить выполнение

i .BatchSize /* возвращает размер текущего пакета

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

Для инициализации пакетной корректировки необходимо присвоить свойству-переключателю .BatchUpdating значение true. После этого все процедуры корректировки будут происходить только с записями курсора без обновления источника данных. При этом все определения команд корректировки будут сохраняться в пакете. Количество команд пакета хранится в доступном только для чтения свойстве .BatchSize.

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

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

После выполнения любой из вышеуказанных процедур Система автоматически выключит режим пакетной корректировки (.BatchUpdating = false). Выключение режима пакетной корректировки без выполнения этих процедур равносильно выполнению процедуры .BatchCancel. При выполнении одной из процедур .Select(), .Requery() или .Clear() Система автоматически сбрасывает значение переключателя .BatchUpdating, что приводит к потере всех изменении, сделанных в курсоре.

Что дальше ?

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

Следующие две статьи будут посвящены средствам контроля доступа к базе данных (System Access) и конструктору таблиц (DB Workbench).

PS

Как-то так получилось, что сложилось устойчивое представление о том, что XML базы данных не имеют никакого отношения к реляционным базам данных, а реляционные базы данных подходят только для хранения и выборки текста XML документов. То есть реляционная и XML модели данных "и близко не лежали". Результат - до сих пор нет ни одной эффективной системы управления XML базой данных (эта тема совсем завяла), а в любых реляционных СУБД, "поддерживающих" (слово-то какое придумали) работу с XML документами, так и не появилась возможность работы на уровне узлов XML документа. Одним словом, тупик...

Прочитав эту и предыдущую статью, Вы можете не только убедиться в том, что реляционная и XML модели могут быть полностью совместимыми, но и узнать, какими средствами эта совместимость была достигнута, и почему классическая реляционная СУБД является лишь частным случаем системы управления XML базой данных. И еще одно важное замечание. Вместо аббревиатуры XML, Вы можете, с полным основанием, использовать аббревиатуры JSON, BSON, YSON, HTML и тому подобные. Виртуальная СУБД поддерживает любые древовидные структуры данных! Выбор пал на XML модель только потому, что она строго формализована и является основой для всех указанных выше моделей данных.

Виртуальная СУБД позволяет создать Приложение, реально независимое от типа используемых физических СУБД. Более того, она обладает уникальной возможностью одновременно использовать в одной виртуальной базе таблицы различных физических СУБД, которые при этом могут быть связаны отношениями с таблицами других СУБД, поскольку связи между таблицами определяются только в виртуальной СУБД. Обращения ко всем физическими СУБД происходит одинаковым, максимально эффективным для них способом. Виртуальная СУБД, чаще всего, посылает им самые "вкусные" запросы (запросы, которые могут быть выполненные с помощью одних индексов, без обращения к записям таблицы). Таким образом, при выборе подходящей для Вашего Приложения конкретной реляционной СУБД, виртуальная СУБД может стать объективным арбитром. Вы можете протестировать эффективность любой СУБД не на каких-то специально подготовленных тестах, а непосредственно на Вашем работающем Приложении. Замена одной физической СУБД на другую выполняется очень просто средствами Конструктора таблиц (DB Workbench).

 

Теги:
Хабы:
0
Комментарии2

Публикации

Ближайшие события