Название книги в оригинале: Ковязин А Н. Мир InterBase. Архитектура, администрирование и разработка приложений баз данных в InterBase/FireBird/Yaffil

A- A A+ White background Book background Black background

На главную » Ковязин А Н » Мир InterBase. Архитектура, администрирование и разработка приложений баз данных в InterBase/FireBird/Yaffil .





Читать онлайн Мир InterBase. Архитектура, администрирование и разработка приложений баз данных в InterBase/FireBird/Yaffil . Ковязин А Н.

InterBase - быстрый старт.

 Сделать закладку на этом месте книги

Установка InterBase

 Сделать закладку на этом месте книги

Перед тем как начать разрабатывать приложения баз данных с помощью InterBase, необходимо позаботиться о его установке. Обычно InterBase устанавливают как на сервер, так и на рабочую станцию программиста, разрабатывающего приложение. Разработчику InterBase нужен для внутренних экспериментов и отладки рабочих версий базы данных и программ, а InterBase на сервере используется для тестирования программы пользователями или совместной разработки в случае, если над проектом работает команда. Благодаря своей легковесности и нетребовательности к ресурсам InterBase можно спокойно устанавливать прямо на рабочие станции разработчиков, не беспокоясь о снижении быстродействия. Когда InterBase не обслуживает подключений к базам данных, находясь в ожидании запросов, то занимает памяти меньше, чем такие популярные программы, как ICQ или WinAmp.

Что ставить?

 Сделать закладку на этом месте книги

Прежде чем устанавливать InterBase, надо решить, какой из его клонов (версий) мы выберем для работы. Прежде всего надо сказать, какую версию однозначно НЕ РЕКОМЕНДУЕТСЯ использовать. Это бета-версия InterBase 6.0.хх, одна из самых первых версий, выпущенных после объявления InterBase 6 бесплатным. Версия InterBase 6.x послужила основой для выпуска трех клонов - Firebird, InterBase 6.5 и Yaffil.

Клон Firebird появился вскоре после опубликования исходных кодов InterBase 6.x. Он является абсолютно бесплатным продуктом в открытых кодах (open source). В его создании принимало участие множество бывших сотрудников InterBase Software Corporation. Версия Firebird 1.0 отлично подойдет для освоения материала этой книги и начала работы с InterBase.

Компания Borland в конце 2001 года выпустила версию InterBase 6.5. Ее также можно установить и использовать для изучения материала этой книги. Для этих целей можно воспользоваться триал-версией с ограничением времени использования в 90 дней. Эту версию InterBase 6.5 можно скачать с сайта компании Borland.

Помимо Firebird и InterBase, существует еще и российская ветка InterBase 6.x - Yaffil, который отпочковался от Firebird в 2001 году. Yaffil существует пока только для платформы Windows, и часть его кода оптимизирована именно для этой ОС. Подробнее о новых свойствах Yaffil можно узнать в приложении «Yaffil - российский клон InterBase 6.x». Yaffil также можно использовать для изучения материала этой книги.

Следует отметить, что все клоны InterBase 6.x совместимы между собой и базу данных с одного сервера можно легко перевести под другой с помощью процесса backup/restore  (см. главу «Миграция» (ч. 4)). Поэтому ничего страшного не произойдет, если вы пожелаете сменить одну версию (и даже клон) InterBase 6.x на другую.

Многих разработчиков пугает значительное разнообразие версий и потомков InterBase 6 и вызывает чувство неуверенности - много разных СУБД, которые непонятно чем отличаются. На самом деле это совершенно нормальная ситуация, типичная для проекта Open Source и свидетельствующая о том, что InterBase 6 активно развивается. Аналогичная ситуация сложилась и в Linux- сообществе, где количество возможных сочетаний ядро + дистрибутив давно перевалило за сотню.

Немного «пообщавшись» с InterBase (с любым его клоном), вы легко сможете выбрать, что же больше подходит для конкретной задачи.

Вопрос - где находятся дистрибутивы различных клонов InterBase? Различные варианты дистрибутивов представлены на сайте поддержки данной книги www.InterBase-world.corn, а также на сайте https://www.ibase.ru. Там же обычно можно получить ссылку на самый «свежий» дистрибутив всех клонов InterBase, а также массу другой полезной информации по использованию InterBase и разработке приложений баз данных.

Непосредственно из первоисточника дистрибутивы и исходные коды InterBase можно взять на сайте https://sourceforge.net/projects/InterBase, а дистрибутивы Firebird - на сайте https://sourceforge.net/projects/Firebird. В общем, хорошая поисковая машина вам поможет в получении практически любого дистрибутива.

На компакт-диске с Delphi Client/Server и Enterprise Edition всех версий (3.x, 4.x, 5.x и 6.x) также содержится InterBase, обычно версии 5.х/6.х. Его также можно использовать для работы с материалом этой книги, за исключением некоторых моментов, которые описывают функциональность, присущую только некоторым версиям клонов InterBase.

Определившись с версией InterBase, следует выбрать, под какой ОС мы будем работать. Для нашего случая предположим, что работа будет происходить на ОС Windows (версии старше 95 или NT 4.0). Если вы поклонник другой ОС, не расстраивайтесь: существуют дистрибутивы для Linux/Unix, FreeBSD, Solaris, и даже Mac OS X Darwin. Здесь мы рассмотрим процесс установки для платформ Windows и Linux, предполагая, что именно одна из этих ОС установлена на вашем рабочем компьютере.

Если в будущем потребуется «переехать» на другую ОС, нет проблем: это несложная процедура, которую мы рассмотрим в главе «Миграция».

Установка InterBase на платформе Windows

 Сделать закладку на этом месте книги

Подготовка к установке

 Сделать закладку на этом месте книги

Инсталлятор (программа, которая занимается установкой продукта) любого клона InterBase занимает всего около 5,5 Мбайт. Для запуска установки просто запустите файл установки (он может называться xxWin32SetupXX.exe, где хх определяют различные версии клонов InterBase).

Давайте рассмотрим установку клона InterBase 6.x - Firebird 1.0. Установка других клонов ничем кардинально от Firebird не отличается.

Если на компьютере, куда устанавливается InterBase, уже установлена любая версия InterBase, то ее сначала необходимо обязательно остановить. Для этого следует воспользоваться либо «Панелью управления» Windows, апплетом «Службы», если установка идет на Windows NT72000/XP, либо иконкой InterBase- сервера на панели задач Windows, если устанавливаем на Windows 95/98/Ме.

Внимание!  Остановка уже существующей версии InterBase при установке новой обязательна. Рекомендуется вообще удалить предыдущую версию, чтобы не сталкиваться с конфликтами обновления файлов. Желательно удалить файлы gds32.dll из всех папок данного компьютера, указанных в переменной пути поиска Path (проще всего это сделать так: поискать на локальных дисках gds32.dll и удалить их отовсюду, кроме папок с дистрибутивами).

Установка

 Сделать закладку на этом месте книги

После запуска инсталлятора появится оповещение о том, что ставится именно та версия, которая нам нужна, - в данном случае Firebird 1.0. Нажмите Next для перехода к следующему шагу установки. На экране появится текст InterBase Public License. Выберите I agree и перейдите к следующему шагу. Появится окно, в котором предлагается выбрать путь для установки Firebird. Это важная опция. Обычно по умолчанию инсталлятор предлагает путь C:\Program FilesMnterBase. Однако часто имеет смысл переопределить этот путь во что-то вроде C:\IBServer.

Выбрав путь для установки, нажмите Next для перехода к следующему шагу установки. При этом на экране появится окно, изображенное на рис. 1.1. (вполне возможно, что дизайн его может отличаться для различных версий InterBase, но смысл остается тем же). На этом шаге мы выберем компоненты, необходимые для работы. Этот выбор достаточно прост - выберите все компоненты, находящиеся в списке. Как видите, они займут весьма скромное место на диске - всего около 16 Мбайт.

Если же на компьютере, куда устанавливается InterBase, существует недостаток дискового пространства, то можете ограничиться только опциями Server for Windows, Client for Windows и Command Line Tools. Абсолютно минимальная установка InterBase описана в главе «Установка InterBase - взгляд изнутри» (ч. 4).

Нажмите Next для перехода к следующему шагу.

Внимание!  В этот момент инсталлятор может выдать сообщение «InterBase is running on this machine...» и прекратить установку. Это означает, что на компьютере уже установлен и запущен InterBase (возможно, он был установлен вместе с Delphi). В этом случае надо удалить предыдущую версию InterBase, согласно рекомендациям раздела «Подготовка к установке». После чего можно продолжить установку.

Если же после выбора компонентов не возникло никаких проблем и началось копирование файлов, то все в порядке, просто дождитесь завершения копирования, затем нажмите Finish.

Вот и все - установка InterBase закончилась. Обратите внимание, что для ее завершения не понадобилась перезагрузка компьютера - сервер уже запустился. Если вы используете ОС Windows 95/98/Ме, то работающий InterBase отобразится на панели задач в виде значка среди системных иконок, если NT 4/2000 - то сервер запустится как служба (service) и на экране никак его функционирование не отразится - наблюдать за его статусом и управлять работой можно лишь через апплет «Панели управления» «InterBase Manager» или через аплет «Службы» («Services»).







Рис 1.1. Выбор компонентов при установке Firebird

Установка InterBase на платформе Linux/Unix

 Сделать закладку на этом месте книги

Установка InterBase под Linix немного сложнее, чем на Windows, если вы не являетесь знатоком этой ОС. Для Linux InterBase (а также Firebird) существуют два варианта архитектуры сервера - SuperServer и Classic. Про их различия, а также про достоинства и недостатки поговорим позже, в главе "Classic и SuperServer", а пока будем считать, что речь идет о сервере с архитектурой SuperServer.

Инсталляционный пакет под Linux бывает двух видов - в формате rpm (для некоторых дистрибутивов Linux типа RedHat) и tar.gz (более универсальный пакет). Что это такое, можно узнать в любом приличном руководстве по Linux/Unix. Скачайте инсталляционный пакет того или иного вида (например, с сайта www.lnterBase-world.com)  и можно приступать к установке.

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

* Необходимо войти в систему как пользователь root.

* Для варианта Firebird SuperServer необходимо добавить в файл /etc/hosts.equiv строку вида localhost 127.0.0.1. Если такого файла нет, то его необходимо создать.

* Если необходимо получать доступ к установленному серверу с удаленных машин, то надо прописать имена и ГР-адреса этих машин в файле hosts.equiv или позаботиться о разрешении имен с помощью DNS.

Для установки из rpm пакета необходимо выполнить следующую команду:


$rpm -Uvh InterBase.x.x.x.rpm


где InterBase.x.x.x.rpm является именем скачанного инсталляционного пакета.

Для установки с использованием пакета в формате tar.gz необходимо выполнить следующее:


$tar -xzf InterBase.x.x.x.tar.gz

$cd install

$./install.sh


Выполнение этих команд приводит к одному и тому же результату - на Linux-машине будет установлен InterBase (Firebird для нашего примера). Вариант с архитектурой SuperServer будет функционировать в виде демона, a Classic - в виде сервиса (см. главу "Classic и SuperServer" (ч. 4)).

Надо заметить, что приведенный порядок установки годится практически для любой версии InterBase 6.х/Firebird 1.0. Правда, для различных дистрибутивов Linux могут существовать свои особенности и хитрости при установке, так что лучше посмотреть рекомендации по установке для вашей версии ОС Linux/Unix.

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

Установка основного программного обеспечения производится в каталог /opt/InterBase, а библиотек и заголовков - в каталоги /usr/InterBase и /usr/include соответственно. В процессе установки изменяется пароль SYSDBA, причем каждый вид установочного пакета делает это по-разному: rpm создает случайный пароль и помещает его в файл /opt/InterBase/SYSDBA.password, a tar.gz запрашивает пароль в процессе установки.

Интересный вопрос - как запускается InterBase/Firebird под Linux? Вариант с архитектурой Classic запускается через inetd, как только поступает входящий запрос на соединение на порт, к которому привязан InterBase/Firebird (для InterBase 4.x, 5.x - только 3050, для Firebird - по умолчанию 3050, однако можно изменить это значение), inetd запускает новый экземпляр сервера и передает ему управление. Если нет соединений, то ничего и не запущено.

Вариант InterBase с архитектурой SuperServer прописывает стартовый скрипт в /etc/re.d/init.d/Firebird и запускается в виде демона.

Для проверки инсталляции под Linux необходимо сначала проверить локальное соединение:


$cd /opt/InterBase/bin

$isql -user sysdba -password <password>

>connect /opt/InterBase/examples/employee.gdb;

>select * from sales;

>quit;


Затем необходимо протестировать удаленное подключение:


$cd /opt/InterBase/bin

$isql -user sysdba -password <password>

>connect <hostname>:/opt/InterBase/examples/employee.gdb;

>select * from sales;

>quit;


Если выполняются запросы к Sales, то все в порядке - InterBase/Firebird работает.

Установка инструментария для администрирования InterBase

 Сделать закладку на этом месте книги

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

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

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

Среди самых известных и популярных можно перечислить InterBase Expert, IBManagen, IBWorkbench и IBAdmin. Часть из этих продуктов являются свободно распространяемым (freeware), т. е. они полностью бесплатные, другая часть - условно бесплатные (shareware), т. е. имеется возможность использования ознакомительной версии. Со списком различных полезных инструментов и их краткими характеристиками можно ознакомиться в приложении "Инструменты администратора и разработчика InterBase".

Выберите инстр\мент по своему вкусу. Ряд этих продуктов разрабатывается российскими программистами, которые, понимая экономическую ситуацию в РФ и ближнем зарубежье, позволяют коллегам из бывшего СССР пользоваться своими инструментами бесплатно! Поэтому обязательно воспользуйтесь предоставленной возможностью и обзаведитесь удобным инструментом для работы с InterBase.

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

Заключение

 Сделать закладку на этом месте книги

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

Создаем базу данных

 Сделать закладку на этом месте книги

Итак, нам необходимо создать новую базу данных. Это проще всего сделать при помощи какой-либо программы администрирования (например, BlazeTop: http-//www.blazetop.com). Запустите BlazeTop, затем выберите в меню "Файл\Новый\Сервер". На экране появится диалог регистрации сервера InterBase. Он изображен на рис. 1.2.







Рис 1.2. Диалог регистрации сервера InterBase

Укажите тип подключения к серверу. Он может быть двух типов: локальным или удаленным. Имя сервера - это название компьютера в сети, на котором установлен InterBase. Укажите также сетевой протокол, который используется для подключения к серверу. По умолчанию это TCP/IP, а значит, в качестве названия сервера вы можете указывать прямой IP-адрес компьютера с установленным InterBase. Имя для подключения к серверу - это имя администратора базы данных. Для InterBase это всегда SYSDBA, это имя нельзя изменять. Однако настоятельно рекомендуется изменить пароль для SYSDBA. По умолчанию это 'masterkey'. После регистрации сервера мы можем создавать базу данных. Выберите в меню "Файл\Новый\База данных". Появится диалог регистрации/создания базы данных (рис. 1.3).

Строка соединения

 Сделать закладку на этом месте книги

В свойстве ServerName укажите имя сервера из списка зарегистрированных (очевидно, что вы можете иметь несколько серверов на разных компьютерах). DatabaseName - это локальный путь к файлу базы данных на сервере. Если вы хотите зарегистрировать новую базу данных в BlazeTop, вы также можете присвоить базе какой-либо псевдоним при помощи свойства AliasName. Для собственно создания базы данных это не является необходимым. Укажите также дополнительные свойства: PageSize (размер страницы базы данных), CharSet (кодировка или кодовая страница по умолчанию для строковых полей), SQLDialect (только для InterBase 6.x и Firebird). Вы можете также создать базу данных от имени любого пользователя, зарегистрированного на сервере.







Рис 1.3. Диалог создания базы данных

По умолчанию это, конечно, SYSDBA. Теперь BlazeTop сам создаст необходимый DDL-код для создания базы данных с теми свойствами, которые вы указали. Сначала он сформирует полный путь к базе данных, который называется строкой соединения (connection string).

Вы уже знаете, что создать базу данных при помощи BlazeTop можно как на локальном компьютере, так и на удаленном. Если сервер InterBase находится над машиной под управлением какой-либо версии Windows (именно этот случай мы рассматривали в главе "Установка InterBase" в качестве типичного), а в качестве сетевого протокола установлен TCP/IP, то формат строки соединения, в котором указывается путь к вновь создаваемой базе и имя файла этой базы данных, будет следующий:


<имя_компьютера>:<Путь на компьютере\имя_базы_данных.gdt»


Несколько важных замечаний:

1 Формат строки соединения для других сетевых протоколов здесь не рассматривается - для получения информации о том, как сконфигурировать InterBase для работы по другим протоколам, обратитесь к разделу документации Operation Guide - Network configuration

2 В данном примере рассматривается случай, когда InterBase-сервер (и база данных соответственно) находится на компьютере под управлением ОС Windows В этом случае путь к базе данных начинается с буквы диска и каталоги разделяются обратной косой чертой. В случае, если InterBase-сервер и база данных расположены на машине под управлением *шх, то <путь на компьютере> будет начинаться с прямой косой черты и выглядеть примерно так:

/opt/database/firstbase.gdb.

Например, если мы создаем базу данных с именем firstbase.gdb у себя на компьютере в каталоге C:\Temp, то строка соединения будет выглядеть следующим образом:


localhost:С:\temp\firstbase.gdb


Здесь localhost - имя компьютера, на котором создается база, C:\Temp - путь к вновь создаваемой базе данных, firstbase.gdb - имя базы данных. Localhost - это имя, зарезервированное для текущего (т.е. локального, Вашего, того, на котором запускаете программу) компьютера. Если понадобится создать базу данных на удаленном компьютере, например, server_nt, где-нибудь в каталоге CAdatabase, то путь будет, соответственно:


server_nt:C:\database\firstbase.gdb


Если на компьютере server_nt не будет каталога C:\Database, то вы получите ошибку. Также ошибка создания возникнет, если на server_nt не запущен (или вообще не установлен) InterBase - т. е. просто некому будет обработать запрос на создание базы данных. Момент создания и подключения к базе данных обычно вызывает массу проблем у новичков — они не сразу понимают, как правильно составить строку соединения. Запомните, что надо указывать путь к базе данных на том компьютере, где она находится (или будет находиться, если мы создаем базу).

Помните, что имена netbios sharename, присваиваемые каталогам, отданным в совместное использование, никакого отношения к <пути_на_компьютере> не имеют Также нет необходимости (и очень нежелательно с точки зрения безопасности) предоставлять разрешения на доступ к файлам баз данных пользователям

Диалект базы данных

 Сделать закладку на этом месте книги

Вернемся к окну создания базы данных, изображенному на рисунке 1.3. Выбор диалекта базы данных очень важен. Свойство может принимать только два возможных значения: 1 или 3. Какое же выбрать?

Диалект 1 и Диалект 3 отличаются друг от друга следующими принципиальными вещами:

* Диалект 3 позволяет использовать расширенный набор типов данных, таких, как типы для работы с большими целыми числами, типы для работы с датой и временем - DATE и ТГМЕ.

* Диалект 3 различает регистр идентификаторов, если идентификатор заключен в двойные кавычки Table 1 и TABLE1 в обоих диалектах будут равнозначны, а вот "Tablel" и "TABLE!" (TABLE1) - сервер будет интерпретировать как разные идентификаторы.

* Диалект 3 не поддерживает неявное приведение типов данных (как это было в Диалекте 1). Это означает, что в Диалекте 1 выражение '25'+5 будет корректным и в результате мы получим 30. В Диалекте 3 это выражение вызовет ошибку несоответствия типов.

Помимо этого, есть еще ряд отличий. Например, Borland Database Engine (BDE) вплоть до версии 5.2 плохо работает с 3-м диалектом - возникают проблемы с поддержкой новых типов данных.

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

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

* выбираем Диалект 3, если мы будем проектировать базу данных для приложения, которое будет использовать только современные библиотеки прямого доступа к InterBase, которые полностью поддерживают Диалект 3;

* выбираем Диалект 1, если нам важна совместимость с более ранними библиотеками доступа к InterBase, такими, как BDE.

Может возникнуть вопрос, почему диалекты бывают 1 и 3, а где же 2? Диалект 2 действительно существует, но используется в качестве промежуточного этапа при миграции с Диалекта 1 на Диалект 3.

Размер страницы

 Сделать закладку на этом месте книги

Выбор размера страницы очень важен для обеспечения эффективной работы сервера InterBase с базой данных. Файл базы данных разбивается на страницы фиксированного размера, и все обращения к диску, которые выполняет InterBase, считывают и записывают информацию постранично. Выбирать следует размер страницы не менее 4096 байт, а лучше еще больше. Почему так? Дело в том, что если установить малый размер страницы, то записи большой длины (например, представьте себе 10 строковых полей в таблице, заполненных строками размером в 255 символов) будут занимать несколько страниц, и для чтения единственной записи InterBase будет вынужден осуществить несколько обращений к диску! Очевидно, что это не лучшим образом скажется на быстродействии.

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

Рекомендации по выбору размера страницы следующие:

* Для дисковых накопителей с файловой системой NTFS выбираем размер страницы, равный 4096 байтам. Перед этим следует убедиться, что размер кластера у NTFS-диска установлен в 4096 байт (если не знаете, что такое кластер, спросите у вашего системного администратора).

* Для дисков с FAT32 (думаю, что FAT16 редко используется в наш век дешевых гигабайтов) устанавливаем размер страницы 8192 или 16384 байта (хотя стоит заметить, что размер страницы 16384 байта есть далеко не во всех клонах InterBase).

Кодировка (CharSet)

 Сделать закладку на этом месте книги

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

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

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

Имя пользователя и пароль

 Сделать закладку на этом месте книги

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

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

Конечно, очень удобно иметь полный набо


убрать рекламу






р прав "в одном флаконе", когда разрабатываешь базу данных, но во время промышленной эксплуатации часто требуется разделить права между различными группами пользователей. Вот тогда применяются пользователи (USERS) и роли (ROLES) для того, чтобы на уровне базы данных разграничить доступ к информации, хранящейся в базе данных. Подробнее о безопасности в InterBase см. главу "Безопасность в IlnterBase: Пользователи, роли и права" (ч. 4).

Рекомендованный пользователь для создания базы данных - SYSDBA. Это позволяет на этапе начального проектирования базы не беспокоиться о распределении прав - этим можно будет заняться несколько позже. Пароль для SYSDBA по умолчанию - 'masterkey'. Конечно, этот пароль лучше сменить, особенно на серверах, находящихся в промышленной эксплуатации.

Что получилось

 Сделать закладку на этом месте книги

В данном примере мы создаем базу данных InterBase с именем 'my.gdb' в каталоге 'C:\Database' на локальном компьютере. Размер страницы создаваемой базы данных равен 16384 байтам, в качестве кодировки по умолчанию выбрана WIN1251- набор символов, включающих кириллицу. Выберем для базы SQLDialect 3.

BlazeTop предоставляет очень удобный интерфейс для создания базы данных, однако надо сказать, что создание базы данных "вручную" не намного сложнее. Для этого достаточно создать два файла. Первый из них - это файл с командами SQL (его называют файлом скрипта),  который создаст базу данных, второй - командный файл WINDOWS, который передаст этот SQL-файл на выполнение утилите isql.exe. Эта утилита выполнит выполнит скрипт создания базы данных. Вот содержимое этих двух файлов для нашего случая: файл скрипта crebas.sql:


SET SQL DIALECT 3; SET NAMES WIN1251;

CREATE DATABASE 'Localhost:С:\Database\my.gdb1

USER 'SYSDBA' PASSWORD 'masterkey'

PAGE_SIZE 16384

DEFAULT CHARACTER SET WIN1251;


командный файл runscr.bat:


"C:\IBServer\Bin\isql.exe" -i "C:\temp\crebas.sql"


Естественно, в командном файле должны быть прописаны реальные пути к isql.exe (это интерпретатор команд SQL, входящий в комплект InterBase) и к нашему файлу скрипта crebas.sql.

Результат выполнения runscr.bat будет точно таким же, как и использование BlazeTop для создания базы данных.

Нажмем "Create" для создания базы данных InterBase.

Типы данных

 Сделать закладку на этом месте книги

Несмотря на то, что типы данных подробно описаны в документации (см. [1, гл. 4]), необходимо рассмотреть ряд понятий, которые будут часто использоваться в последующих главах книги. Помимо изложения сведений общего характера будут рассмотрены также примеры использования типов данных в базах данных InterBase и изложены рекомендации по их использованию и преобразованию. Также подробно рассмотрим отличия в типах данных, существующие 1-м и 3-м диалектах базы данных InterBase.

О типах данных

 Сделать закладку на этом месте книги

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

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

В InterBase существует 12 типов данных, которые способны удовлетворить практически любые потребности разработчика в хранении данных. Эти типы условно подразделяются на 6 следующих групп:

* для хранения целых чисел - INTEGER и SMALLINT;

* для хранения вещественных чисел - FLOAT и DOUBLE PRECISION;

* для чисел с фиксированной точностью - NUMERIC и DECIMAL;

* для хранения даты, времени и даты/времени - DATE, TIME и TIMESTAMP;

* для хранения символов - CHARACTER (сокращенно CHAR) и VARYING CHARACTER (VARCHAR);

* Для хранения динамически расширяемых данных - BLOB.

Также возможно определять массивы значений элементарных типов, т.е. всех перечисленных типов, кроме BLOB.

Большинство типов данных InterBase соответствуют типам, определенным в стандарте SQL92, однако, помимо этого, есть и собственные "изюминки" - массивы элементарных типов данных и BLOB.

Массивы в InterBase могут содержать множество данных одного типа в одном поле, например можно определить массив значений типа INTEGER. Причем массивы могут иметь несколько размерностей!

Тип данных BLOB - это динамически расширяемый тип данных, название которого часто расшифровывается как Binary Large OBject - "большие двоичные объекты". Надо сказать, что BLOB - это изобретение разработчиков InterBase, которое позже распространилось и прижилось во всех современных SQL-серверах.

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

 Сделать закладку на этом месте книги

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


< datatype> =

(SMALLINT | INTEGER | FLOAT | DOUBLE PRECISION}[ <array_dim>] {DATE | TIME | TIMESTAMP} [ <array_dim>]

{DECIMAL | NUMERIC) [( precision [, scale])] [ <array_dim>] {CHAR | CHARACTER | CHARACTER VARYING | VARCHAR) [( int)]

[ <array_dim>] [CHARACTER SET charname]

| {NCHAR NATIONAL CHARACTER | NATIONAL CHAR)

[VARYING] [( int)] [ <array_dim>]

| BLOB [SUB_TYPE { int | subtype_name}] [SEGMENT SIZE int]

[CHARACTER SET charname]

| BLOB [(seglen [, subtype])]


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

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

 Сделать закладку на этом месте книги

К целочисленным типам относятся SMALLINT и INTEGER. Надо сказать, что SMALLINT представляет собой урезанную версию INTEGER и имеет длину 2 байта, в отличие от 4 байт, выделяемых для хранения INTEGER. Обычно экономить на дисковом пространстве не следует, и поэтому общей рекомендацией будет использовать для хранения целых значений тип INTEGER.

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

Вещественные типы данных

 Сделать закладку на этом месте книги

К вещественным типам (их еще называют типами чисел с плавающей точкой) относятся FLOAT и DOUBLE PRECISION. Сразу следует предостеречь читателя от использования типа FLOAT - его точность недостаточна для хранения большинства дробных значений. Особенно не рекомендуется хранить в нем денежные величины - в переменных типа FLOAT очень быстро нарастают ошибки округления, что может сильно удивить бухгалтера при подведении итогов.

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

Надо отметить, что в 3-м диалекте InterBase для хранения денежных величин существует механизм хранения типов с фиксированной точкой длиной 64 бита. Использование этих типов обеспечивает наилучшую точность.

Типы данных с фиксированной точкой

 Сделать закладку на этом месте книги

К этим типам данных относятся NUMERIC и DECIMAL. Часто звучит вопрос, чем NUMERIC отличается от DECIMAL. Оба этих типа имеют одинаковую разрядность - от 1 до 18 знаков, одинаковую точность - от нуля до разрядности.

Напомним, что разрядность - это общее число цифр в числе, а точность - число знаков после запятой

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

Создаем таблицу следующего вида:


SQL> CREATE TABLE test (

CON> Num_field NUMERIC(15,2),

CON> Dec_field DECIMAL(15,2));


Затем даем команду показать структуру таблицы:


SQL> show tables test;


И наблюдаем такую картину:


NUM_FIELD NUMERIC(15, 2) Nullable

DEC_FIELD NUMERIC(15, 2) Nullable


Как видите. InterBase сообщает о том. что оба данных столбцы имеют тип NUMERIC!

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

Вот таблица из [1], которая иллюстрирует, как хранятся различные целочисленные типы (табл. 1.1). Как видите, хранение данных в 3-м диалекте отличается для чисел с большой разрядностью:

Хранение чисел с фиксированной точкой 

Разрядность

Диалект 1

Диалект З

От 1 до 4

SMALLINT для NUMERIC INTEGER для DECIMAL

SMALLINT

От 5 до 9

INTEGER

INTEGER

От 10 до 18

DOUBLE PRECISION

INT64

Итак, теперь мы точно можем сказать, чем отличаются типы NUMERIC и DECIMAL: в случае определения поля (переменной) с малой разрядностью (до четырех) первый хранится в виде 2 байтового целого числа SMALLINT, а второй - в виде 4 байтового INTEGER.

Таким образом, в случае разрядности, большей четырех, типы DECIMAL и NUMERIC окажутся абсолютно эквивалентными!

Обратите внимание на отличие реализации типов с большой разрядностью в 1-м и 3-м диалектах. В 1-м диалекте число с фиксированной точкой превращалось из целого в вещественное, к которому применялись механизмы округления! В 3-м диалекте эта странность была ликвидирована - большие целые числа хранятся действительно как целые - с использованием механизма INT64, который может хранить 64-битовые числа в диапазоне +/- 2Л32. Поэтому рекомендуется хранить данные о денежных средствах в базах данных, созданных с использованием 3-го диалекта, - только при использовании механизма INT64 можно гарантировать сохранность малых денежных остатков.

Типы для хранения даты и времени

 Сделать закладку на этом месте книги

Типы для хранения даты и времени изменились в версии InterBase 6.x и его клонах по сравнению с 4.x и 5.x. Чтобы не путаться в исторических хитросплетениях с этими типами, рассмотрим ситуацию именно в 6-й версии InterBase, а затем на основе этого кратко упомянем о том, что было раньше, - это делается для тех пользователей, кто все еще работает на ранних версиях InterBase

Итак, в InterBase 6.x существует 3 типа для хранения даты и времени - это DATE, TIME и ТГМЕ8ТАМР.

* Тип DATE хранит даты с точностью до дня. Диапазон возможных значений - от 1 января 100 года н. э. до 29 февраля 32768 года.

* Тип TIME хранит данные о времени с точностью до десятитысячной доли секунды. Диапазон возможных значений - от 00:00 AM до 23:59.9999 РМ.

* Тип TIMESTAMP представляет собой комбинацию типов DATE и TIME.

Как работать с датами? Если речь идет о работе на уровне сервера в хранимых процедурах или триггерах, то все достаточно просто - мы всегда можем объявить переменную нужного нам типа и присваивать ей значения из таблиц и наоборот. Однако необходимо передавать данные из базы данных в приложение и обратно. В этом случае есть два подхода - либо использовать библиотеки, которые применяют оригинальный формат дат InterBase для доступа к объектам этих типов и преобразуют этот формат в привычные внутриязыковые типы даты/времени (примером такой библиотеки является FIBPlus), либо использовать механизм преобразования дат в строки, встроенный в InterBase.

Что делать, если нужно вырезать из полной даты только год или месяц? Для этого используется группа функций EXTRACT (доступная во всех клонах InterBase 6.x), которая позволяет выделить из даты только нужную часть. Используются эти функции следующим образом:


EXTRACT (MONTH FROM DATE_FIELD)

EXTRACT (YEAR FROM DATE_FIELD)


Полный список параметров в функции EXTRACT таков: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEKDAY, YEARDAY. Их назначение очевидно следует из их названия, поэтому не будем приводить здесь расшифровки.

Типы данных для хранения текста

 Сделать закладку на этом месте книги

В InterBase существует два типа, предназначенных для хранения текстовой информации - CHAR и VARCHAR. Полные их названия, - CHARACTER и CHARACTER VARYING, однако нет никакой причины пользоваться длинными именами - даже команда Show tables в утилите isql выдает краткие наименования типов.

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


CREATE TABLE testCHARLen(

Fieldl CHAR(255),

Field2 CHAR);


В результате создания этой таблицы поле Fieldl будет иметь длину 255 символов, a Field2 - 1 символ.

Типы CHAR и VARCHAR во многом схожи - оба могут содержать до 32768 символов, однако есть и отличия. Хотя хранятся эти два типа в базе данных одинаково, но работает с ними InterBase по-разному. Это можно продемонстрировать следующим примером:


SQL> create table testCHAR ( cl char(10), c2 varchar(10));

SQL> insert into testCHAR(cl,c2) values('Test','Test');

SQL> SELECT '{' |cl||')', '('||c2 |')' from testCHAR;


В результате получим следующий результат:


(Test ) (Test)


Как видите, после значения Test', выбранного из поля cl, оказались пробелы. Это означает, что при выборке данных из поля типа CHAR возвращаемое значение дополняется пробелами до полной длины поля. Сложно предположить, для чего необходимо подобное поведение, которое приводит к значительному увеличению сетевого трафика (загрузки сети).

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

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

Чтобы создать символьное поле с явным указанием набора символов, необходимо в описании столбца (в предложениях CREATE TABLE или ALTER TABLE) добавить описание набора символов. Для поддержки русского языка обычно используется набор символов WEN1251 (подробнее об использовании русского языка в InterBase см. главу "Русификация InterBase" (ч. 1)). Вот пример таблицы, содержащей символьное поле с явно описанным набором символов WIN1251:


CREATE TABLE TestCHARSET(

Fieldl VARCHAR(255),

Field2 VARCHAR(255) CHARACTER SET winl251);


Здесь Fieldl - поле без явного указания набора символов, поэтому для него будет использоваться тот набор символов, который был указан при создании базы данных. Для поля Field2 явно определено, что в нем будут храниться символы в кодировке WIN 1251.

Помимо указания набора символов, для символьных полей возможно также указывать порядок сортировки (COLLATION ORDER), который определяет, как будут сортироваться символы этого набора данных. Для русского языка существуют два варианта сортировки - WIN1251 и PXW_CYRL. Подробнее об использовании COLLATION ORDER рассказано в главе "Русификация InterBase".

Полный список наборов символов и применяемых для них COLLATION ORDER можно найти в документации [1, гл. 13].

Внимание!  В документации на InterBase 6 сказано, что символьных типов 4: помимо указанных выше типов данных существуют еще NCHAR и NCHAR VARYING, однако ниже в той же документации объясняется, что последние два типа являются теми же типами CHAR и VARCHAR, только используют по умолчанию набор символов ISO8859_1. To есть фактически использование псевдотипа NCHAR равносильно применению CHAR DEFAULT CHARACTER SET ISO8859_1. Аналогично и для NCHAR VARYING, только там вместо CHAR используется VARCHAR. Очевидно, что применение этих псевдотипов ориентировано на пользователей в Западной Европе и США, для поддержки языков в которых и создан набор символов ISO8859_1.

Тип данных BLOB

 Сделать закладку на этом месте книги

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

Чтобы определить самое простое поле типа BLOB в таблице, не нужно ничего сверх того, что обычно требуется для определения поля любого элементарного типа:


CREATE TABLE testBLOB(

myBlobField BLOB);


В результате будет создано поле myBlobField, в котором можно хранить данные большого размера. Но несмотря на то что поля BLOB по способу определения никак не отличаются от других, реализация их внутри базы данных значительно отличается. He-BLOB-поля расположены на странице данных (см. главу "Структура базы данных InterBase" (ч. 4)) рядом друг с другом, а в случае BLOB на странице данных хранится только идентификатор BLOB, а сам BLOB располагается на специальной странице. Именно такая организация данных позволяет хранить данные нефиксированного размера.

У типа BLOB имеется возможность определять набор нескольких подтипов и специальных процедур, называемых фильтрами  (BLOB filters), для работы с этими подтипами. Существует несколько предопределенных подтипов BLOB, которые встроены в InterBase. Все эти подтипы имеют неотрицательные номера, например subtype 0 - это данные неопределенного типа, subtype 1 - текст, subtype 2 - BLR (Binary Language Representation, см. глоссарий и главу "Структура базы данных InterBase") и т. д. Пользователь также может определять свои подтипы BLOB, которые могут иметь отрицательные значения. Каждому типу может быть поставлен в соответствие фильтр, который преобразует поле этого подтипа в другой подтип.

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

Массивы

 Сделать закладку на этом месте книги

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

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

Для того чтобы создать в таблице поле типа массив чисел INTEGER, необходимо написать что-то вроде следующего:


CREATE TABLE test(

myOneDimArray INTEGER[12],

myTwoDimArray INTEGER[5,4],

myThreeDimArray INTEGER[2,10,8]);


При этом создадутся 3  поля типа массив: поле myOneDimArray, содержащее одномерный массив длиной 12 чисел, myTwoDimArray, содержащее двумерный массив (матрицу) 5x4 чисел Integer, и поле myThreeDimArray - трехмерный массив 2x10x8. Надо отметить, что при таком определении элементы массива нумеруются начиная с единицы, т. е. первый элемент имеет номер 1, второй - номер 2 и т. д. Если кто-то хочет указать границы массива самостоятельно, например с 0 до 5, то он должен задать определение поля так:


myArray INTEGER[0:5]


Массивы реализованы на базе полей типа BLOB, поэтому не следует опасаться, что многомерный массив "загрязнит" вашу таблицу невероятным количеством данных: InterBase аккуратно разместит данные массива на отдельных страницах, чтобы оптимизировать операции ввода-вывода в этих полях.

Как использовать массивы? Они предоставляют удобный механизм для хранения однотипных объектов. Однако в 80 % случаев вместо массивов разработчики предпочитают держать множественные данные в подчиненных (detail) таблицах, поэтому массивы не так часто используются в клиентских приложениях СУБД InterBase. Этому немало способствует то, что поставляемые в комплекте с Delphi и C++Builder библиотеки доступа, такие, как BDE и ГВХ, не имеют возможности работать с массивами. В документации по InterBase упоминается о возможности работать с массивами с помощью препроцессора gpre, однако это не самый удобный способ для разработчика Delphi/C-H-Builder. К счастью, в библиотеке FIBPlus имеется поддержка полей-массивов в InterBase, о чем подробно рассказано в главе "Специальные возможности FIBPlus". Клиентская библиотека IBProvider, позволяющая создавать клиентские приложения для InterBase с помощью средств разработки компании Microsoft, также поддерживает работу с массивами (см. главу "Разработка клиентских приложений СУБД InterBase с использованием технологии Microsoft OLE DB" (ч. 3)).

Заключение

 Сделать закладку на этом месте книги

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

Таблицы. Первичные ключи и генераторы

 Сделать закладку на этом месте книги

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

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

Нормализация таблиц в базе данных хорошо и подробно рассмотрена в книге [14], и потому мы не будем пытаться объять необъятное и вернемся к нашей конкретной области - к таблицам InterBase. Рассмотрим синтаксис предложения DDL (DDL - это Data Definition Language, подробнее см. в глоссарии), которое позволяет создавать таблицы:


CREATE TABLE table [EXTERNAL [FILE] "<filespec>"]

( <col_def> [, <col_def> | <tconstraint> ...]);


Здесь table - имя создаваемой таблицы, <col_def> - описание столбцов (иногда мы будем говорить - полей) создаваемой таблицы. Опция table [EXTERNAL [FILE] "<filespec>"] означает, что будет создана так называемая внешняя таблица, которая хранится не в общем файле базы данных, а в отдельном файле с именем <filespec>.

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


<col_def> = col { datatype COMPUTED [BY] (< expr>) | domain}

[DEFAULT { literal NULL | USER}]

[NOT NULL] [ <col_constraint>]

[COLLATE collation]


Довольно большое определение, однако лишь небольшая часть приведенных в определении столбца предложений является обязательной. Каждый столбец в таблице должен иметь имя, уникальное в пределах таблицы, а также либо тип данных, определяемый предложением datatype, либо выражение <ехрг> для вычисления значения столбца (для вычисляемых столбцов), либо домен (см. ниже), определяемый domain. Типы данных были рассмотрены в главе "Типы данных", поэтому вы легко можете понять, как формируется SQL-выражение для создания таблицы.

Давайте подключимся к нашей базе данных FIRSTBASE.gdb, созданной ранее в главе "Создаем базу данных", и попробуем поработать с таблицами на практике. Для создания, удаления и модифицирования таблиц подойдет как любой из административных инструментов InterBase - из тех, что перечислены в приложении "Инструменты администратора и разработчика InterBase", так и стандартная утилита isql.exe из комплекта поставки любого клона InterBase.

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


CREATE TABLE Table_example (

ID INTEGER,

NAME VARCHAR(80),

PRICE_1 DOUBLE PRECISION);


Эта таблица иллюстрирует наиболее часто встречающийся в процессе разработки базы данных случай.

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


CREATE DOMAIN D_ID AS INTEGER;

CREATE TABLE Table_example (

ID D_ID,

NAME VARCHAR(80) ,

PRICE_1 DOUBLE PRECISION);


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


убрать рекламу






переменных.

Третий способ задать столбец в таблице - это определить его как вычисляемый (COMPUTED BY) и задать условие, согласно которому будет вычисляться его значение. Например, мы можем пожелать иметь в нашей таблице столбец, вычисляющий 10% от значения поля PRICE_1. Для этого мы можем ввести следующую команду:


CREATE TABLE Table_example ( '

ID INTEGER,

NAME VARCHAR(80),

PRICE_1 DOUBLE PRECISION,

PRICE_10 COMPUTED BY (PRICE_1*0.1)) ;


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

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

Итак, мы рассмотрели 3 основных способа задания полей в таблице. Теперь давайте подробнее рассмотрим опции, которые можно задать при создании столбца.

Опция [DEFAULT {literal | NULL | USER}] - позволяет задать значение столбца по умолчанию. Это очень удобная возможность для автоматического заполнения данных. Существует 3 возможности задавать значения по умолчанию. Первая обозначена как literal и позволяет задавать значения по умолчанию в виде текстовых констант, чисел или даты. Например, мы можем сформировать следующие выражения для создания столбца с текстовыми значениями по умолчанию:


NAME VARCHAR(SO) DEFAULT 'Василий Станиславович'


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

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


PRICE_1 DOUBLE PRECISION DEFAULT NULL


И третий способ задать значение по умолчанию - это указать DEFAULT USER в определении столбца. При этом во вновь создаваемых записях в это поле будет заноситься имя текущего пользователя, т. е. пользователя, который установил соединение с InterBase и выполнил эту вставку (подробнее о пользователях см. главу "Безопасность в InterBase: пользователи, роли и права" (ч. 4)).

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


NAME VARCHAR(SO) NOT NULL


При этом создастся поле, в котором нельзя хранить неопределенные (NULL) значения. Обычно ограничение NOT NULL сочетается с опцией DEFAULT, которая гарантированно присваивает какое-либо корректное значение этому полю.

Но часто ограничения NOT NULL бывает недостаточно. Например, в случае хранения в базе данных каких-либо цен совершенно очевидно, что они не могут принимать отрицательные значения (хотя было бы здорово, чтобы нам приплачивали при покупке какого-нибудь товара). Чтобы заставить сервер проверять заносимые в базу данных значения цен на условие положительности, следует так определить столбец:


PRICE_1 DOUBLE PRECISION CHECK (PRICE_1>0)


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


PRICE_1 DOUBLE PRECISION NOT NULL CHECK (PRICE_1>0)


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

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

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

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


DROP TABLE Table_example;

CREATE TABLE Table_example(ID NUMERIC(15,2);


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

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


ALTER TABLE Table_example ADD Patronimic VARCHAR(SO);


После выполнения этой команды в нашей таблице Table_example появится новый столбец с именем Patronimic и типом VARCHAR(SO). А если мы пожелаем удалить из таблицы столбец с именем NAME, то следует выполнить


ALTER TABLE Table_example DROP Name;


Полный синтаксис предложения ALTER TABLE можно узнать в [1]. Это очень полезная команда, и мы будем часто ею пользоваться.

А что делать, спросите вы, если надо изменить сам столбец? Например, мы решили, что для хранения имен лучше использовать поле HUMAN_NAME, а не просто NAME. В этом случае мы можем применить команду ALTER TABLE <table> ALTER COLUMN, которая позволяет изменять наименование столбца.

Это новая команда, поддерживаемая только в InterBase 6.x и его клонах. В InterBase более ранних версий для изменения имени и любых параметров столбца пришлось бы создавать временное поле, удалять существующий столбец, создавать новый - с нужным именем и параметрами - и затем копировать данные из временного поля в новый столбец. А в 6-м InterBase это можно сделать одной командой, например такой:


ALTER TABLE Table_example

ALTER COLUMN NAME TO HUMAN_NAME;


Если же мы решили изменить тип поля, например увеличить число символов, хранимых в поле, то придется изменять домен этого поля, используя предложение ALTER DOMAIN (см. выше главу "Типы данных").

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

Первичные ключи в таблицах

 Сделать закладку на этом месте книги

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

Будем двигаться постепенно и начнем с самого распространенного типа ключей - с первичного ключа.

Итак, что же такое первичный ключ!  Это одно или более полей в таблице, однозначно идентифицирующих записи в пределах этой таблицы. Звучит сложно, однако на самом деле все очень просто. Представьте себе обыкновенную таблицу, например бухгалтерскую ведомость. Что является самым первым столбцом? Правильно, порядковый номер - 1, 2, 3... Этот номер указывает на уникальную строку в пределах таблицы, и достаточно знать этот номер, чтобы найти в этой таблице строку. В данном примере это и будет первичный ключ.

Абсолютное большинство таблиц в реляционной базе данных обязательно имеют первичный ключ (часто пишут РК - сокращение от Primary key). Общей рекомендацией при создании таблиц является создавать первичный ключ. Создать первичный ключ можно как при создании таблицы, так и позже. Допустим, мы уже к моменту создания таблицы решили, что первичным ключом у нас будет поле ID Тогда добавить первичный ключ можно следующим образом:


CREATE TABLE Table_example (

ID INTEGER NOT NULL,

NAME VARCHAR(80),

PRICE_1 DOUBLE PRECISION,

CONSTRAINT pkTable PRIMARY KEY (ID));


Итак, что необходимо сделать, чтобы создать первичный ключ на таблицу table_example. Внимательно рассмотрим, что изменилось в определении таблицы? Во-первых, колонка ID получила дополнительное определение NOT NULL. Это важно - первичный ключ должен быть уникальным и не допускать неопределенных значений. A NULL, как вы знаете, это неопределенное значение. Таким образом, все поля, входящие в первичный ключ, должны иметь ограничение NOT NULL.

Чтобы завершить создание первичного ключа, в конце таблицы дописывается предложение вида CONSTRAINT <имя_ключа> <тип_ключа> (<поля_входя- щие_ в_ключ>). Полный синтаксис ограничений рассмотрен в главе "Ограничения базы данных" ч. 1, и для нашего примера первичного ключа будет иметь вид:


CONSTRAINT pkTable PRIMARY KEY (ID)


Здесь - pkTable - имя первичного ключа, a ID - столбцы, входящие в него.

Такой способ определять первичные ключи для таблиц удобен при массовом создании таблиц (например, при построении прототипа базы данных на основе скриптов, получаемых с помощью различных CASE-средств). Но что делать, если нам нужно добавить/удалить первичный ключ в таблицу, когда она уже существует и наполнена данными? Для этого следует воспользоваться еще одним расширением команды - ALTER TABLE. Пример добавления первичного ключа в нашу таблицу:


ALTER TABLE TABLE_EXAMPLE ADD CONSTRAINT FF PRIMARY KEY (ID);


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


ALTER TABLE Table_example DROP CONSTRAINT pkTable;


При этом ключ с именем pkTable будет удален из базы данных.

Генераторы - лучшие друзья первичных ключей

 Сделать закладку на этом месте книги

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

Как уже было сказано, первичный ключ может содержать несколько полей. При этом будет отслеживаться уникальность сочетания значений этих полей. Например, если мы определим ключ на поля ID и NAME, то сервер будет следить за тем, чтобы во всей таблице не было одинаковых сочетаний этих полей. То есть сочетания полей ID и NAME вроде 1 и "Иванов", 2 и "Иванов" будут корректными, поскольку они отличаются значениями поля ID.

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

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

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

Как уже было сказано, обычно первичным ключом является счетчик. Некоторые СУБД, такие, как Paradox и MS SQL, имеют специальный тип - счетчик (auto increment). При добавлении в таблицу новой записи значение поля с этим типом автоматически увеличивается на величину приращения - обычно на единицы. В InterBase нет поля типа счетчик, однако есть возможность реализовать подобное поведение. Для создания поля, которое бы заполнялось автоматически при добавлении записи в таблицу, используется совокупность средств: первым из них является генератор.

Что такое генератор!  Говоря по-простому, генератор - это именованный счетчик. Внутри базы данных мы можем создать счегчик, дать ему уникальное имя в пределах этой базы и управлять значениями этого счетчика. Это и будет генератор. Чтобы это пояснить - вот пример предложений DDL:


CREATE GENERATOR gl;

SET GENERATOR gl TO 2445;


В этом примере в первой строчке в базе данных создается генератор с именем gl, а во второй - этому генератору присваивается значение 2445. Теперь возникает вопрос, как нам использовать полученный генератор. Чтобы получать и изменять значения генераторов, существует встроенная в InterBase функция GEN_ID. Эта функция принимает в качестве параметров имя генератора и величину приращения, которую нужно применить к данному генератору, а возвращает целочисленное значение, соответствующее значению генератора, полученному в результате прибавления к нему приращения. Вот пример вызова функции GEN_ID в тригере или хранимой процедуре:


Current_value = GEN_ID (gl, 1)


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


SELECT GEN_ID(gl, 1)FROM

RDB$ DATABASE


Так как в таблице RDB$ Database всегда содержится только одна запись, то мы получим в результате данного запроса значение генератора gl.

Здесь current_value - переменная (как использовать переменные в InterBase - см. в следующих главах), gl - генератор, 1 -приращение. В этом примере в переменную current_value попадет значение генератора gl после прибавления к нему приращения 1, т. е. следующее значение генератора!

Обратите внимание, что приращение может быть не равно единице! Более того, оно может быть даже отрицательным:


Current_value = GEN_ID (gl, -23)


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

Люди, знакомые с базами данных, могут задать вопрос: "А что будет, если одновременно несколько клиентов попробуют внести данные в одну и ту же таблицу и одновременно "дернут" генераторы? Получат ли они одно или разные значения генератора?" Однозначно, что они получат РАЗНЫЕ значения генератора. Какой бы "одновременной" ни была попытка получить значение генератора, каждый обратившийся получит свое уникальное значение. Это гарантируется самой "конструкцией" генераторов: они работают на самом низком уровне сервера и никакие процессы записи и вставки не влияют на них - часто говорят, что генераторы работают "вне контекста транзакций". Что такое транзакции, вы можете узнать в главе "Транзакции. Параметры транзакций" (ч. 1), а как устроены генераторы - в главе "Структура базы данных InterBase" (ч. 4).

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

Для этого есть два способа - вставка первичного ключа на стороне клиента и на стороне сервера. Чтобы освоить первый способ, следует обратиться к главе "Использование основных компонентов FIBPlus", а чтобы понять второй - к главе "Триггеры" (ч. 1). Здесь мы лишь кратко скажем, в чем заключается суть обоих способов.

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

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

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

Заключение

 Сделать закладку на этом месте книги

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

Индексы

 Сделать закладку на этом месте книги

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

Что же представляет собой индекс? Индекс - это упорядоченный указатель на записи в таблице. Указатель  означает, что индекс содержит значения одного или нескольких полей в таблице и адреса страниц данных, на которых располагаются эти значения (про страницы данных см. главу "Структура базы данных InterBase") (ч 4). Другими словами, индекс состоит из пар значений "значение поля" - "физическое расположение этого поля". Таким образом, по значению поля (или полей), входящего в индекс, при помощи индекса можно быстро найти то место в таблице, где располагается запись, содержащая это значение.

Упорядоченный -  означает, что значения полей, хранящихся в индексе, упорядочены.

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

Для чего нужны индексы?

 Сделать закладку на этом месте книги

Единственное, чему способствуют индексы, - это ускорению поиска записи по ее индексированному полю (индексированное - значит входящее в индекс).

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

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

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

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

Как устроены индексы

 Сделать закладку на этом месте книги

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

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

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

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

Применение индексов

 Сделать закладку на этом месте книги

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

* Ускорение выполнения запросов. Индексы создаются для полей, которые используются в условиях поиска SQL-запросов.

* Обеспечение уникальности значений в полях; Ограничение первичного ключа (о которых рассказывалось в главе "Таблицы. Первичные ключи") требует, чтобы во всей таблице не нашлось двух одинаковых значений полей, входящих в первичный ключ. Чтобы выполнить это условие, необходимо при каждой вставке новой записи производить поиск такого же значения, которые будет вставлено. Для поиска записи используется особая разновидность индекса - уникальный индекс (см. ниже).

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

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

Ускорение выполнения запросов с помощью индексов

 Сделать закладку на этом месте книги

Выше описано, что применение индексов может значительно ускорить выполнение запросов. Это действительно так для большинства случаев, но есть и определенные оговорки. Сначала ответим на вопрос, часто возникающий у тех, кто познакомился с индексами. Раз индексы ускоряют выборку из базы данных, почему бы не проиндексировать все поля в таблице? Есть два момента, препятствующих всеобщей индексации, - это дисковое пространство и издержки при модификации данных в таблице. Каждый создаваемый индекс имеет объем, равный объему данных в индексированном поле, плюс объем данных о расположении записей. Если создать индексы на каждое поле в таблице, то их суммарный объем будет больше, чем объем данных в таблице! Поэтому создание большого количества индексов приводит к большому расходу дискового пространства.

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

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


убрать рекламу






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

Оптимизатор - это совокупность механизмов, которые разрабатывают таи  выполнения запроса. Когда пользователь передает InterBase какой-либо SQL-запрос, он указывает, ЧТО должен вернуть сервер в результате выполнения запроса, но не определяет, КАК сервер должен выполнять запрос. Оптимизатор на основе переданного запроса строит план его выполнения, т. е. откуда и в каком порядке будут браться данные для выполнения запроса, какие индексы будут при этом использоваться. Когда сервер анализирует условия на выборку (это в основном части выражения WHERE, ORDER BY и т. д.), то для каждого поля, входящего в условие, сервер пытается использовать индекс. К сожалению, алгоритм создания плана несовершенен и оптимизатор часто использует индексы, которые не слишком эффективны для конкретного запроса, из-за чего может существенно замедлиться время выполнения. Поэтому создание лишних индексов может привести к созданию неоптимальных планов.

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

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

Итак, основные ограничения на создание индексов мы рассмотрели. Теперь следует рассмотреть вопрос, когда следует использовать индексы, чтобы добиться улучшения производительности. Существует 3 основных случая, когда необходимо проиндексировать поле:

* Когда это поле используется в условиях поиска в запросах.

* Когда соединения таблиц (JOIN) используют это поле.

* Когда это поле используется в предложениях сортировки ORDER BY.

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

Давайте рассмотрим синтаксис создания индексов. Вот полный формат команды DDL, который позволяет создавать индексы:


CREATE [UNIQUE] [ASC[ENDING] | DESC[ENDING]]

INDEX index ON table (col [, col ...]);


Минимальным выражением, создающим индекс, является следующее:


CREATE INDEX my_index ON Table_example(ID)


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

Как видно из описания синтаксиса, индекс может содержать не одно, а несколько полей. Такой индекс используется при часто выполняющихся запросах, которые содержат в условиях поиска или сортировки сочетание индексированных полей. Например, если у нас есть таблица, содержащая поля Фамилия, Имя, Отчество, то при запросе, использующем сортировку по ФИО, будет применен 1акой индекс. Вообще говоря, необязательно вводить условия на все 3 поля, применяемые в индексе, чтобы использовать его преимущества. Если мы желаем сортировать результат запроса, то индекс будет использован в случае, если первое поле в условии сортировки совпадает с первым полем в индексе, например наш индекс будет задействован в случае сортировки по Фамилии и Имени.

В документации для оптимизации выполнения запроса, содержащего в предложении WHERE соединение полей с условием OR рекомендуется, использовать не составной индекс, а несколько одинарных индексов на все поля, входящие в условие OR.

К вопросу о порядке сортировки индекса: как видно, он может быть либо возрастающим (ASQENDING]), либо убывающим (DESCENDING]). Зачем нужны разные порядки сортировки? Очевидно, для разных сортировок! Если мы желаем сортировать людей по фамилии в возрастающем порядке, то создаем возрастающий индекс (ASC), а если в убывающем (от Я до А) - то убывающий! А если хотим и то и другое, то необходимо создавать оба индекса.

Обеспечение ссылочной целостности с помощью индексов

 Сделать закладку на этом месте книги

В определении индекса имеется еще одна опция - UNIQUE. Если ее указать, то индекс позволит заносить в таблицу только уникальные значения. Фактически это служит основой для реализации уникальных ключей (UNIQUffi KEY). Уникальные ключи широко используются в базах данных. То есть РК - это уникальный ключ-индекс, но не всякий UK - это РК. Выше речь шла только о РК. Первичный ключ (Primary key) - самый распространенный вид уникального ключа. При создании первичного ключа на таблицу автоматически создается уникальный индекс, который получает имя, составленное из RDBSPRIMARYNNN, где NNN - последовательный уникальный в пределах базы данных номер. Таким образом, с помощью уникального индекса реализуются два из важнейших ограничений ссылочной целостности - уникальный ключ и первичный ключ. Очевидно, что понятие уникальности несовместимо с понятием неопределенного значения, т. е. другими словами, в полях, содержащихся в уникальных индексах, не должно быть значений типа NULL. Перед созданием уникального индекса на поле следует придать ему статус NOT NULL. Если индекс создается для уже существующих данных, то при создании будет проверено, не содержит ли индексированное поле повторяющихся значений. И если содержит, то в создании индекса будет отказано.

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

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

Оптимизация производительности индексов

 Сделать закладку на этом месте книги

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

Что же случается с индексами? Почему они "теряют форму"? Нам придется еще раз сказать о том, что индексы реализованы в виде двоичного дерева И когда в таблицу добавляется (изменяется, удаляется - выберите по вкусу) новая запись, в дерево добавляется новая веточка. Причем веточки добавляются не в середину дерева, а на концах других веточек. Постепенно дерево становится все более "раскидистым" (также говорят - несбалансированным), а поиск по нему - все менее эффективным. Поправить положение может перестройка дерева или (в некоторых случаях) пересчет статистики. Периодически требуется пересоздавать индекс, чтобы восстанавливать его производительность. Пересоздание индекса происходит в следующих случаях:

* При перестройке индекса с помощью команды ALTER INDEX.

* При удалении и повторном создании индекса командами DROP INDEX и CREATE INDEX.

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

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

Рассмотрим подробнее все эти способы оптимизации индексов.

Использование команды ALTER INDEX имеет следующий формат:


ALTER INDEX name {ACTIVE | INACTIVE};


Здесь name - имя индекса, a ACTIVE и INACTIVE - два состояния индекса, в которые его можно перевести при помощи команды ALTER INDEX. Параметр ACTIVE означает, что индекс активен и может применяться во всех запросах и процедурах. Установка индекса в INACTIVE (неактивен) приводит к отключению его использования. Для перестройки дерева надо последовательно выполнить две команды:


ALTER INDEX name INACTIVE;

ALTER INDEX name ACTIVE;


При этом индекс будет перестроен. Использование ALTER INDEX имеет ряд ограничений: с его помощью нельзя перестроить индексы, используемые в первичных, уникальных и внешних ключах; нельзя перестроить индекс, если он используется в данный момент каким-либо запросом; а также для изменения индекса необходимо иметь права администратора (SYSDBA) или быть создателем данного индекса.

Пересоздание индекса с помощью команд DROP INDEX и CREATE INDEX приводит к полному удалению индекса из базы данных, а затем к его созданию с чистого листа. Синтаксис команды DROP INDEX очевиден:


DROP INDEX имя_индекса;


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

У способа перестройки индекса путем его полного пересоздания есть ограничения, аналогичные ограничениям на использование ALTER INDEX.

Третий способ перестройки индекса основан на свойстве резервных копий баз данных InterBase, которые создаются с помощью утилиты gbak. Дело в том, что при резервном копировании данные, входящие в индекс, не сохраняются в резервной копии, а хранится только определение индекса. При восстановлении из резервной копии индекс создается заново. Подробнее о резервном копировании см. главу "Резервное копирование и восстановление из резервной копии" (ч. 4).

Четвертый способ улучшить производительность индекса - это собрать статистику по индексам с помощью команды SET STATISTICS Статистика таблицы - это величина в пределах от 0 до 1, значение которой зависит от числа различных (неодинаковых) записей в таблице. Оптимизатор InterBase использует статистику для определения эффективности применения того или иного индекса в запросе Когда число записей в таблице может сильно изменяться (например, при большом количестве вставок или удалений), то пересчет статистики может значительно улучшить производительность.

Команда пересчета статистики следующая:


SET STATISTICS INDEX name;


Здесь name - имя индекса, для которого пересчитывается статистика.

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

Мы рассмотрели несколько способов улучшить производительность индексов. С помощью команд ALTER INDEX и DROP/CREATE INDEX можно перестраивать любые индексы, за исключением системных, создаваемых автоматически индексов, служащих для поддержания ссылочной целостности. Чтобы перестроить эти индексы, необходимо воспользоваться командами изменения и создания таблиц - ALTER TABLE и CREATE TABLE, так как эти индексы являются неотъемлемой частью табличных ключей.

Ограничения базы данных

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

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

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

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

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

Виды ограничений в базе данных

 Сделать закладку на этом месте книги

Существуют следующие виды ограничений в базе данных InterBase:

* первичный ключ - PRIMARY KEY;

* уникальный ключ - UNIQUE KEY;

* внешний ключ - FOREIGN KEY

- может включать автоматические триггеры ON UPDATE и ON DELETE;

* проверки - CHECK.

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

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


<col_constraint> = [CONSTRAINT constraint] <constraint_def>

[ <col_constramt> . . . ]

<constraint_def> = {UNIQUE | PRIMARY KEY

| CHECK ( <search_condition>)

| REFERENCES other_table [( other_col [, other_col ...])]

[ON DELETE (NO ACTION|CASCADE|SET DEFAULT|SET NULL}]

[ON UPDATE (NO ACTION CASCADEjSET DEFAULTjSET NULL}]

}


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


<tconstraint> = [CONSTRAINT constraint] <tconstraint_def>

[< tconstraint> ...]

<tconstraint_def> = {{PRIMARY KEY | UNIQUE} ( col [, col ...] )

FOREIGN KEY ( col [, col ...] ) REFERENCES other_table [ ( other_col [ , other_col ...] ) ]

[ON DELETE (NO ACTION|CASCADE SET DEFAULT|SET NULL}] [ON UPDATE {NO ACTION|CASCADE|SET DEFAULT|SET NULL}]

| CHECK ( <search_condition>)}


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

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

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

 Сделать закладку на этом месте книги

Фактически ограничения на основе одного поля являются частным сл\чаем ограничений на основе нескольких полей.

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

Первичный ключ с использованием синтаксиса ограничения на основе одного поля.


CREATE TABLE testl(

ID_PK INTEGER CONSTRAINT pktest NOT NULL PRIMARY KEY) ;


В этом примере создается первичный ключ с именем pktest на поле ID_PK. Получаем весьма компактное описание в одну строчку.

Для той же самой цели можно воспользоваться синтаксисом ограничений на основе нескольких полей:


CREATE TABLE test2(

ID_PK INTEGER NOT NULL,

CONSTRAINT pktst PRIMARY KEY (ID_PK));

Создание ограничений

 Сделать закладку на этом месте книги

Давайте рассмотрим создание ограничений подробнее. Первой в описании общего синтаксиса ограничений идет опция [CONSTRAINT constraint]. Как видите, эта опция взята в квадратные скобки и, значит, необязательна.

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

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

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

Первичный и уникальный ключи

 Сделать закладку на этом месте книги

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

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

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

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


<pkukconstraint> = [CONSTRAINT constraint] {PRIMARY KEY |

UNIQUE}


Примеры первичных и уникальных ключей:


CREATE TABLE pkuk(

pk NUMERIC(15,0) NOT NULL PRIMARY KEY, /*первичный ключ*/

ukl VARCHAR(SO) NOT NULL UNIQUE,/*уникальный ключ */

uk2 INTEGER NOT NULL UNIQUE /* еще уникальный ключ */);


Синтаксис создания первичного и уникального ключей на основе нескольких полей:


<pkuktconstraint> = [CONSTRAINT constraint] {PRIMARY KEY |

UNIQUE) ( col [, col ...] )


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


CREATE TABLE pkuk2(

Number1 INTEGER NOT NULL,

Namel VARCHAR(SO) NOT NULL,

Kol INTEGER NOT NULL,

Stoim NUMERIC(15,4) NOT NULL,

CONSTRAINT pkt PRIMARY KEY (Numberl, Namel), /*первичный ключ pkt на

основе двух полей*/

CONSTRAINT uktl UNIQUE (kol, Stoim) ); /*уникальный ключ uktl на основе

двух полей*/


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

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


ALTER TABLE tablename

ADD [CONSTRAINT constraint] {PRIMARY KEY | UNIQUE) ( col [, col ...] )


Давайте рассмотрим пример создания первичного и уникального ключа с помощью ALTER TABLE. Сначала создаем таблицу:


CREATE TABLE pkalter(

ID1 INTEGER NOT NULL,

ID2 INTEGER NOT NULL,

UID VARCHAR(24));


Затем добавляем ключи. Сначала первичный:


ALTER TABLE pkalter

ADD CONSTRAINT pkall PRIMARY KEY (idl, id2);


Затем уникальный ключ:


ALTER TABLE pkalter

ADD CONSTRAINT ukal UNIQUE (uid) ;


Важно отметить, что добавление (а также удаление) ограничений первичных и уникальных ключей к таблице может производить только владелец этой таблицы или системный администратор SYSDBA (подробнее о владельцах и пользователе SYSDBA см. главу "Безопасность в InterBase: пользователи, роли и права") (ч. 4).

Внешние ключи

 Сделать закладку на этом месте книги

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

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

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

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

Для реализации такого поведения таблица заголовка соединяется с таблицей подробностей с помощью ограничения внешнего ключа.

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

Для этого создадим две таблицы для хранения накладной - таблицу TITLE для хранения заголовка и таблицу INVENTORY для хранения информации о товарах, входящих в накладную.


CREATE TABLE TITLE(

IDJTITLE INTEGER NOT NULL Primary Key,

DateNakl DATE,

NumNakl INTEGER,

NoteNakl VARCHAR(255));


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

Теперь определим таблицу для хранения информации о товарах, входящих в накладную:


CREATE TABLE INVENTORY(

ID_INVENTORY INTEGER NOT NULL PRIMARY KEY,

FK_TITLE INTEGER NOT NULL,

ProductName VARCHAR (255),

Kolvo DOUBLE PRECISION,

Positio INTEGER);


Давайте рассмотрим, какие поля входят в таблицу INVENTORY. Во-первых, это ID_INVENTORY - первичный ключ этой таблицы. Затем идет целочисленное поле FK_TITLE, которое служит для ссылки на идентификатор заголовка ID_TITLE в таблице заголовков накладных. Далее идут поля ProductName, Kolvo и Positio. описывающие наименование товара, его количество и позицию в накладной.

Для нашего примера важнее всего поле FK_TITLE. Если мы захотим вывести информацию о товарах определенной накладной, то нам следует воспользоваться следу ющиУ1 запросом, в котором параметр mas_ID_TITLE определяет идентификатор заголовка:


SELECT *

FROM INVENTORY II

WHERE II.FK_TITLE=?mas_ID_TITLE


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

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

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

Такое ограничение можно организовать с помощью внешнего ключа. Для данного примера необходимо наложить ограничения внешнего ключа на поле FK_TITLE и связать его с первичным ключом ID_TITLE в TITEE. Добавить внешний ключ в уже существующую таблицу можно следующей командой:


ALTER TABLE INVENTORY

ADD CONSTRAINT fktitlel FOREIGN KEY(FK_TITLE) REFERENCES

TITLE(ID_TITLE)

Часто при добавлении внешнего ключа возникает ошибка object is in use (объект используется) Дело в ю, что для создания внешнею ключа, необходимо открьпь базу данных в монопольном режиме - чтобы оиювременно не бьпо других пользователей Также нетьзя производить никаких обращений к модифицируемой таблице-это может вызвать object is in use


Здесь INVENTORY - имя таблицы, на которую накладывается ограничение внешнего ключа; fktitlel - имя внешнего ключа; FK_TITLE - поля, составляющие внешний ключ; TITLE — имя таблицы, предоставляющей значения (ссылочную ОСНОВУ) для внешнего ключа; ID_TITLE — поля первичного или уникального ключа в таблице TITLE которые являются ссылочной основой для внешнего ключа.

Полный синтаксис ограничения внешнего ключа (с возможностью создавать ограничения на основании нескольких полей) приведен ниже:


<tconstraint> = [CONSTRAINT constraint] FOREIGN KEY ( col [,

col } ) REFERENCES other_table [ ( other__col [ , other_col ...] } ]

[ON DELETE {NO ACTION CASCADE|SET DEFAULT|SET NULL}]

[ON UPDATE (NO ACTION|CASCADE|SET DEFAULT SET NULL}]


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

Чаще всего употребляются декларативная форма ограничения внешнего ключа, когда указывается набор полей (col [, col ...]), которые будут составлять ограничение; таблица other_table, которая содержит в полях [( other_col [, other_col ...]) список возможных значений для внешнего ключа.

Пример такого определения при создании таблицы:


CREATE TABLE Inventor


убрать рекламу






y2(

...

FK_TABLE INTEGER NOT NULL CONSTRAINT fkinv REFERENCES

TITLE(ID_TITLE)

...) ;


Обратите внимание, что в этом определении опущены ключевые слова FOREIGN KEY, а также подразумевается, что в качестве внешнего ключа будет использоваться единственное поле - FK_TITLE.

А в следующем примере приведена более полная форма создания внешнего ключа одновременно с таблицей:


CREATE TABLE Inventory2(

...

FK_TABLE INTEGER NOT NULL,

CONSTRAINT fkinv FOREIGN KEY (FKJTABLE) REFERENCES

TITLE(IDJTITLE)

...) ;

Использование NULL в полях внешнего ключа

 Сделать закладку на этом месте книги

В полях, на основе которых создается внешний ключ, допускается применение NULL-полей. Эта возможность добавлена для разрешения взаимных ссылок. Например, еспи есть две таблицы, ссылающиеся друг на друга с помощью внешних ключей Ьсли не разрешить пустую ссылку (т. е. на NULL) в этих внешних ключах, то в связанные таблицы невозможно будет добавить ни одной записи: чтобы добавить запись в первую таблицу, надо будет иметь запись во второй таблице, и наоборот.

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

Расширенные возможности поддержки ссылочной целостности с помощью внешнего ключа

 Сделать закладку на этом месте книги

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


[ON DELETE {NO ACTION|CASCADE|SET DEFAULT|SET NULL}]

[ON UPDATE {NO ACTIONjCASCADEjSET DEFAULT]SET NULL}]


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

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


ALTER TABLE INVENTORY

ADD CONSTRAINT fkautodel

FOREIGN KEY (FK_TITLE) REFERENCES TITLE(ID_TITLE)

ON DELETE CASCADE


Фактически для реализации этих действий создается системный триггер, который и выполняет определенные действия. В табл. 1.2 приведено описание происходящих действий при различных опциях (обратите внимание, что опции NO ACTION|CASCADE|SET DEFAULT|SET NULL не могут использоваться совместно в одном предложении ON XXX).


Событие

Действие

NO ACTION

CASCADE

SET DEFAULT

SET NULL

ON DELETE

При удалении внешнего ключа ничего не делать - используется по умолчанию

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

При изменении установить поле внешнего ключа в значение по умолчанию

При изменении установить поле внешнего ключа в NULL

ON UPDATE

При изменении ничего не делать - используется по умолчанию

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

При удалении установить поле внешнего ключа в значение по умолчанию

При удалении установить поле внешнего ключа в NULL

Если мы ничего не указываем или указываем NO ACTION, то необходимо позаботиться об изменении внешнего ключа (в случае изменения первичного) самостоятельно, а при удалении первичного ключа предварительно удалить записи из подчиненной таблицы.

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

Ограничение CHECK

 Сделать закладку на этом месте книги

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

Синтаксис его достаточно прост:


<tconstraint> = [CONSTRAINT constraint] CHECK (

<search_condition>)}


Здесь constraint - имя ограничения; <search_condition> - условие поиска, в котором в качестве параметра может участвовать вставляемое/изменяемое значение. Если условие поиска выполняется, то вставка/изменение этого значения разрешаются, если нет - возникает ошибка.

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


create table checktst(

ID integer CHECK(ID>0));


Эта проверка устанавливает, больше ли нуля вставляемое/изменяемое значение поля ID, и в зависимости от результата позволяет вставить/изменить новое значение или возбудить исключение (см. главу "Расширенные возможности языка хранимых процедур InterBase" (ч. 1)).

Возможны и более сложные варианты проверок. Полный синтаксис условия поиска <search_condition> следующий:


<search_condition> = {<val> <operator>

{ <val> | (<select_one>)}

|<val> [NOT] BETWEEN <val> AND <val>

| <val> [NOT] LIKE <val> [ESCAPE <val>]

| <val> [NOT] IN ( <val> [ , <val> ...] | <select_list>)

| <val> IS [NOT] NULL

| <val> {[NOT] {= | < | >} | >= | <=}

{ALL | SOME | ANY} (<select_list>)

|EXISTS ( <select_expr>)

| SINGULAR ( <select_expr>)

| <val> [NOT] CONTAINING <val>

|<val> [NOT] STARTING [WITH] <val>

| (<search_condition>)

| NOT <search_condition>

| <search_condition> OR <search_condition>

| <search_condition> AND <search_condition>}


Таким образом, CHECK предоставляет большой набор опций для проверки вставляемых/изменяемых значений. Необходимо помнить о следующих ограничениях в использовании СНЕК:

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

* Поле может иметь только одно ограничение CHECK.

* Если для описания поля использовался домен, который имеет доменное ограничение CHECK, то его нельзя переопределить на уровне конкретного поля в таблице.

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

Удаление ограничений

 Сделать закладку на этом месте книги

Часто приходится удапять различные ограничения по самым разным причинам. Чтобы удалить ограничение, необходимо воспользоваться предложением ALTER TABLE следующего вида:


ALTER TABLE cablename

DROP CONSTRAINT constraintname


где constraintname - имя ограничения, которое следует удалять. Если при создании ограничения было задано какое-то имя, то следует им воспользоваться, а если нет, то надо открыть какое-либо средство администрирования InterBase, поискать все связанные с ним ограничения и выяснить, какое системное имя сгенерировал InterBase для искомого ограничения.

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

Представления

Те, кто знаком с языком SQL, не нуждаются в подробных объяснениях эиио предмета, но для сохранения порядка изложения приведем все же краткое определение представлений.

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

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

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

Также представления позволяют более просто организовать безопасность в базе данных InterBase. Определенные пользователи могут иметь права только на чтение/изменение данных в представлении, но не иметь никаких прав (и даже никакого понятия) о таблицах, лежащих в основе представления! Подробнее о вопросах безопасности в InterBase см. главу "Безопасность в InterBase: пользователи, роли и права" (ч. 4).

Синтаксис DDL для работы с представлениями

 Сделать закладку на этом месте книги

Для создания и удаления представлений существуют команды, определенные DDL (Data Definition Language - подмножество SQL, см. глоссарий), которые мы сейчас рассмотрим.

Чтобы создать представление в InterBase, необходимо использовать предложение следующего синтаксиса:


CREATE VIEVJ viewname [ (view_column [, view_column...] ) ]

AS <SELECT> [WITH CHECK OPTION];


Здесь viewname - имя представления, которое должно быть уникальным в пределах базы данных, далее идет группа не всегда обязательных наименований полей, входящих в представление: [(view_column [, view_column...])]. Обязательно необходимо определить предложение <SELECT>, которое выбирает данные, включаемые в представление. Необязательный параметр WITH CHECK OPTION мы обсудим ниже - в разделе "Модифицируемые представления".

Чтобы изменить какое-либо представление, придется его пересоздать, т. е. удалить и создать заново. При удалении представления необходимо также удалить все зависимые от него объекты - триггеры, хранимые процедуры и другие представления. В этом заключается одно из главных неудобств работы с представлениями - необходимость пересоздавать дерево использующих представление объектов (существуют утилиты, которые позволяют сделать это более "безболезненно", например IBAlterView, см. приложение "Инструменты администратора и разработчика InterBase"). Чтобы удалить представление, необходимо воспользоваться следующей командой DDL:


DROP VIEW viewname;

Примеры представлений

 Сделать закладку на этом месте книги

Вот пример простого представления:


CREATE VIEW MyView AS

SELECT NAME, PRICE_1

FROM Table_example;


В этом примере мы создаем представление на основе запроса к таблице Table_example, которую мы рассматривали в главе "Таблицы. Первичные ключи и генераторы". В данном случае представление будет состоять из двух полей - NAME и PRICE_1, которые будут выбираться из таблицы Table_example без всяких условий, т. е. число записей в представлении MyView будет равно числу записей в Table_example.

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

Чтобы подробнее рассмотреть использование этого применения представлений, давайте создадим две таблицы, связанные отношением один-ко-многим (часто такое отношение называют мастер-деталью или master-detail). Вот DDL-скрипт для создания этих таблиц:


/* Table: WISEMEN */

CREATE TABLE WISEMEN (

ID_WISEMAN INTEGER NOT NULL,

WISEMAN_NAME VARCHAR(80));

/* Primary keys definition */

ALTER TABLE WISEMEN ADD CONSTRAINT PK_WISEMEN PRIMARY KEY

(ID_WISEMAN);

/* Table: WISEBOOK */

CREATE TABLE WISEBOOK (

ID_BOOK INTEGER NOT NULL,

ID_WISEMAN INTEGER,

BOOK VARCHAR (80) ) ;

/* Primary keys definition */

ALTER TABLE WISEBOOK ADD CONSTRAINT PK_WISEBOOK PRIMARY KEY

(ID_BOOK);

/* Foreign keys definition */

ALTER TABLE WISEBOOK ADD CONSTRAINT FK_WISEBOOK FOREIGN KEY

(ID_WISEMAN) REFERENCES WISEMEN (ID_WISEMAN);


Итак, мы создали две таблицы - WISEMEN и WISEBOOK, которые связали между собой отношением master-detail с помощью ограничения внешнего ключа - FOREIGN KEY. Предположим, что эти таблицы будут хранить информацию о великих китайских мудрецах и их произведениях. Теперь мы можем создать несколько представлений на основе этих таблиц. Например, создадим представление, которое показывает, сколько произведений есть у каждого мудреца:


CREATE VIEW WiseBookCount (WISEMAN,

HOW_WISEBOOKS) AS SELECT M.WISEMAN_NAME, COUNT(B.BOOK)

FROM WISEMEN M, WISEBOOK В

WHERE (M.ID_WISEMAN = В.ID_WISEMAN)

GROUP BY M.WISEMAN_NAME


Обратите внимание, что при использовании любых вычисляемых выражений вроде агрегатных функций COUNTQ, SUMQ, МАХ() и т. д., необходимо использовать явное именование полей представления, т. е. давать имена всем полям, возвращаемым запросом. Как видно из этого примера, эти имена не обязательно должны совпадать с именами полей запроса, но их количество должно совпадать с количеством полей, возвращаемых запросом. Установление того, какое поле, возвращаемое запросом, соответствует какому полю представления, осуществляется по порядковому номеру — первое поле запроса отобразится в первое поле представления, второе - во второе и т. д.

А если мы захотим узнать, какой же из мудрецов написал больше всего книг? И попытаемся добавить в запрос, лежащий в основе представления, выражение для сортировки - ORDER BY. Однако эта попытка будет неудачной: использование сортировки ORDER BY в представлениях не допускается и при попытке создать представление с запросом, содержащим ORDER BY, возникнет ошибка. Если мы желаем отсортировать результаты, возвращаемые представлением, то придется это сделать на стороне клиента:


SELECT * FROM WiseBookCount ORDER BY HOW_WISEBOOKS


Выполнение этого SQL-запроса приведет к желаемому результату.

Помимо ограничения на использование выражения ORDER BY в представлениях, шк/ке нсмьзя использовать в качес!ве источника данных набор данных, получаемых в результате выполнения хранимых процедур (см. чуть ниже главу "Хранимые процедуры").

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


CREATE VIEW WiseMen2

(WISEMAN) AS

SELECT M.WISEMAN_NAME

FROM WISEMEN M

WHERE M.WISEMAN_NAME LIKE 'K%'


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

Модифицируемые представления

 Сделать закладку на этом месте книги

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

Есть два способа сделать представление модифицируемым. Первый способ применим, когда представление создается на основе единственной 1аблицы (или другого модифицируемого представления), причем все столбцы данной таблицы должны позволять наличие NULL. При этом запрос, на котором основано представление, не может содержать подзапросов, агрегатных функций. UDF, хранимых процедур, предложений DISTINCT и HAVING. Если выполняются все эти условия, то представление автоматически становится модифицируемым, т. е. для него можно выполнять запросы DELETE, INSERT и UPDATE, которые будут изменять данные в таблице-источнике.

Список условий довольно внушительный и сильно ограничивает применение таких модифицируемых представлений, поэтому они ИСПОЛЬЗУЮТСЯ относительно редко.

Чтобы сделать модифицируемым представление, которое нарушает любое из вышеперечисленных условий, применяется механизм триггеров. Подробнее о том. что такое триггер, рассказывается в главе "Триггеры" (ч 1). а сейчас мы лишь рассмотрим общие принципы организации изменения данных во VIEW.

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

Создать 3 триггера для данного представления на события: BEFORE DELETE, BEFORE UPDATE и BEFORE INSERT. В этих триггерах описать, что должно происходить с данными при удалении, изменении и вставке.

Затем следует использовать данное представление в запросах на модификацию - DELETE, INSERT или UPDATE. Когда InterBase примет этот запрос, то проверит, существуют ли для данного представления соответствующие триггеры, т. е. BEFORE DELETE/INSERT/UPDATE. Если триггер для выполняемого действия существует, то InterBase вызовет его для модификации реальных данных в таблицах, лежащих в основе представления (хотя это могут быть и другие данные - каких-либо ограничений на текст этих триггеров нет), а затем перечитает строку (или строки), над которой производилась модификация

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

В описании синтаксиса создания представления упоминалась опция WITH CHECK OPTION. Если при создании модифицируемого представления будет указана эта опция, то каждая строка данных, вставляемая или изменяемая в этом представлении, будет проверена на условие "попадания" в представление. Это можно объяснить так: если новая запись, вставляемая пользователем или получившаяся в результате обновления существующей записи, не удовлетворяет условиям запроса, который является поставщиком данных для VIEW, то вставка этой записи будет отменена и возникнет ошибка.

Заключение

 Сделать закладку на этом месте книги

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

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

Хранимые процедуры

 Сделать закладку на этом месте книги

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

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

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

Так что же такое хранимые процедуры в InterBase?

Хранимая процедура (ХП) - это часть метаданных базы данных, представляющая собой откомпилированную во внутреннее представление InterBase подпрограмму, написанную на специальном языке, компилятор которого встроен в ядро сервера InteiBase

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

Основой мощных возможностей, заложенных в ХП, является процедурный язык программирования, имеющий в своем составе как модифицированные предложения обычного SQL, такие, как INSERT, UPDATE и SELECT, так и средства организации ветвлений и циклов (IF, WHILE), а также средства обработки ошибок и исключительных ситуаций Язык хранимых процедур позволяет реализовать сложные алгоритмы работы с данными, а благодаря ориентированности на работу с реляционными данными ХП получаются значительно компактнее аналогичных процедур на традиционных языках.

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

Пример простой хранимой процедуры

 Сделать закладку на этом месте книги

Настало время создать первую хранимую процедуру и на ее примере изучить процесс создания хранимых процедур. Но для начала следует сказать несколько слов о том, как работать с хранимыми процедурами Дело в том, что своей славой малопонятного и неудобного инструмента ХП обязаны чрезвычайно бедным стандартным средствам разработки и отладки хранимых процедур. В документации по InterBase рекомендуется создавать процедуры с помощью файлов SQL-скриптов, содержащих текст ХП, которые подаются на вход интерпретатору isql, и таким образом производить создание и модификацию ХП Если в этом SQL-скрипте на этапе компиляции текста процедуры в BLR (о BLR см главу "Структура базы данных InterBase" (ч. 4)) возникнет ошибка, то isql выведет сообщение о том, на какой строке файла SQL-скрипта возникла эта ошибка. Исправляйте ошибку и повторяйте все сначала. Про отладку в современном понимании этого слова, т. е. о трассировке выполнения, с возможностью посмотреть промежуточные значения переменных, речь вообще не идет. Очевидно, что такой подход не способствует росту привлекательности хранимых процедур в глазах разработчика

Однако помимо стандартного минималистского подхода к разработке ХП существуют также инструменты сторонних разработчиков, которые делают работу с хранимыми процедурами весьма удобной Большинство универсальных продуктов для работы с InterBase, перечисленных в приложении "Инструменты администратора и разработчика InterBase", предоставляют удобный инструментарий для работы с ХП. Мы рекомендуем обязательно воспользоваться одним из этих инструментов для работы с хранимыми процедурами и изложение материала будем вести в предположении, что у вас имеется удобный GUI-инструмент, избавляющий от написания традиционных SQL-скриптов

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


CREATE PROCEDURE name

[ ( param datatype [, param datatype ...] ) ]

[RETURNS ( param datatype [, param datatype ...])]

AS

<procedure_body>;

< procedure_body> = [<vanable_declaration_list>]

< block>

< vanable_declaration_list> =

DECLARE VARIABLE var datatype;

[DECLARE VARIABLE var datatype; ...]

<block> =

BEGIN

< compound_statement>

[< compound_statement> ...]

END

< compound_statement> = (<block> statement;}


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

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


CREATE PROCEDURE SP_Add(first_arg DOUBLE PRECISION,

second_arg DOUBLE PRECISION)

RETURNS (Result DOUBLE PRECISION)

AS

BEGIN

Result=first_arg+second_arg;

SUSPEND;

END


Как видите, все просто: после команды CREATE PROCEDURE указывается имя вновь создаваемой процедуры (которое должно быть уникальным в пределах базы данных) - в данном случае SP_Add, затем в скобках через запятую перечисляются входные параметры ХП - first_arg и second_arg - с указанием их типов.

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

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

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

Если процедура не должна возвращать параметры, то слово RETURNS и список возвращаемых параметров отсутствуют.

После RETURNS указано ключевое слово AS. До ключевого слова AS идет заголовок,  а после него - тело  процедуры.

Тело хранимой процедуры представляет собой перечень описаний ее внутренних (локальных) переменных (если они есть, подробнее рассмотрим ниже), разделяемый точкой с запятой (;), и блок операторов, заключенный в операторные скобки BEGIN END. В данном случае тело ХП очень простое - мы просто складываем два входных аргумента и присваиваем их результат выходному, а затем вызываем команду SUSPEND. Чуть позже мы разъясним суть действия этой команды, а пока лишь отметим, что она нужна для передачи возвращаемых параметров туда, откуда была вызвана хранимая процедура.

Разделители в хранимых процедурах

 Сделать закладку на этом месте книги

Обратите внимание, что оператор внутри процедуры заканчивается точкой с запятой (;). Как известно, точка с запятой является стандартным разделителем команд в SQL - она является сигналом интерпретатору SQL, что текст команды введен полностью и надо начинать его обрабатывать. Не получится ли так, что, обнаружив точку с запятой в середине ХП, интерпретатор SQL сочтет, что команда введена полностью и попытается выполнить часть хранимой процедуры? Это п


убрать рекламу






редположение не лишено смысла. Действительно, если создать файл, в который записать вышеприведенный пример, добавить команду соединения с базы данных и попытаться выполнить этот SQL-скрипт с помощью интерпретатора isql, то будет возвращена ошибка, связанная с неожиданным, по мнению интерпретатора, окончанием команды создания хранимой процедуры. Если создавать хранимые процедуры с помощью файлов SQL-скриптов, без использования специализированных инструментов разработчика InterBase, то необходимо перед каждой командой создания ХП (то же относится и к триггерам) менять разделитель команд скрипта на другой символ, отличный от точки с запятой, а после текста ХП восстанавливать его обратно. Команда isql, изменяющая разделитель предложений SQL, выглядит так:


SET TERM <new_term><old_term>


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


SET TERM ^;

CREATE PROCEDURE some_procedure

... . .

END

^

SET TERM ;^

Вызов хранимой процедуры

 Сделать закладку на этом месте книги

Но вернемся к нашей хранимой процедуре. Теперь, когда она создана, ее надо как-то вызвать, передать ей параметры и получить возвращаемые результаты. Это сделать очень просто - достаточно написать SQL-запрос следующего вида:


SELECT *

FROM Sp_add(181.35, 23.09)


Этот запрос вернет нам одну строку, содержащую всего одно поле Result, в котором будет находиться сумма чисел 181.35 и 23.09 т. е. 204.44.

Таким образом, нашу процедуру можно использовать в обычных SQL- запросах, выполняющихся как в клиентских программах, так и в других ХП или триггерах. Такое использование нашей процедуры стало возможным из-за применения команды SUSPEND в конце хранимой процедуры.

Дело в том, что в InterBase (и во всех его клонах) существуют два типа хранимых процедур: процедуры-выборки (selectable procedures) и исполняемые процедуры (executable procedures). Отличие в работе этих двух видов ХП заключается в том, что процедуры-выборки обычно возвращают множество наборов выходных параметров, сгруппированных построчно, которые имеют вид набора данных, а исполняемые процедуры могут либо вообще не возвращать параметры, либо возвращать только один набор выходных параметров, перечисленных в Returns, где одну строку параметров. Процедуры-выборки вызываются в запросах SELECT, а исполняемые процедуры - с помощью команды EXECUTE PROCEDURE.

Оба вида хранимых процедур имеют одинаковый синтаксис создания и формально ничем не отличаются, поэтому любая исполнимая процедура может быть вызвана в SELECT-запросе и любая процедура-выборка - с помощью EXECUTE PROCEDURE. Вопрос в том, как поведут себя ХП при разных типах вызова. Другими словами, разница заключается в проектировании процедуры для определенного типа вызова. То есть процедура-выборка специально создается для вызова из запроса SELECT, а исполняемая процедура - для вызова с использованием EXECUTE PROCEDURE. Давайте рассмотрим, в чем же заключаются отличия при проектировании этих двух видов ХП.

Для того чтобы понять, как работает процедура-выборка, придется немного углубиться в теорию. Давайте представим себе обычный SQL-запрос вида SELECT ID, NAME FROM Table_example. В результате его выполнения мы получаем на выходе таблицу, состоящую из двух столбцов (ID и NAME) и некоторого количества строк (равного количеству строк в таблице Table_example). Возвращаемая в результате этого запроса таблица называется также набором данных SQL Задумаемся же, как формируется набор данных во время выполнения этого запроса Сервер, получив запрос, определяет, к каким таблицам он относится, затем выясняет, какое подмножество записей из этих таблиц необходимо включить в результат запроса. Далее сервер считывает каждую запись, удовлетворяющую результатам запроса, выбирает из нее нужные поля (в нашем случае это ID и NAME) и отсылает их клиенту. Затем процесс повторяется снова - и так для каждой отобранной записи.

Все это отступление нужно для того, чтобы уважаемый читатель понял, что все наборы данных SQL формируются построчно, в том числе и в хранимых процедурах! И основное отличие процедур-выборок от исполняемых процедур в том, что первые спроектированы для возвращения множества строк, а вторые - только для одной. Поэтому они и применяются по-разному: процедура-выборка вызывается при помощи команды SELECT, которая "требует" от процедуры отдать все записи, которая она может вернуть. Исполняемая процедура вызывается с помощью EXECUTE PROCEDURE, которая "вынимает" из ХП только одну строку, а остальные (даже если они есть!) игнорирует.

Давайте рассмотрим пример процедуры-выборки, чтобы было понятнее. Для упрощения создадим хранимую процедуру, которая работает точно так же, как запрос SELECT ID, NAME FROM Table_Example, т е она просто делает выборку полей ID и NAME из всей таблицы. Вот этот пример:


CREATE PROCEDURE Simple_Select_SP

RETURNS (

procID INTEGER,

procNAME VARCHAR(80))

AS

BEGIN

FOR

SELECT ID, NAME FROM table_example

INTO :procID, :procNAME

DO

BEGIN

SUSPEND;

END

END


Давайте разберем действия этой процедуры, названной Simple_Select_SP. Как видите, она не имеет входных параметров и имеет два выходных параметра - ID и NAME. Самое интересное, конечно, заключено в теле процедуры. Здесь использована конструкция FOR SELECT:


FOR

SELECT ID, NAME FROM table_example

INTO :procID, :procNAME

DO

BEGIN


/*что-то делаем с переменными procID и procName*/


END


Этот кусочек кода означает следующее: для каждой строки, выбранной из таблицы Table_example, поместить выбранные значения в переменные procID и procName, а затем произвести какие-то действия с этими переменными.

Вы можете сделать удивленное лицо и спросить: "Переменные? Какие еще переменные?" Это нечто вроде сюрприза этой главы - то, что в хранимых процедурах мы можем использовать переменные. В языке ХП можно объявлять как собственные локальные переменные внутри процедуры, так и использовать входные и выходные параметры в качестве переменных.

Для того чтобы объявить локальную переменную в хранимой процедуре, необходимо поместить ее описание после ключевого слова AS и до первого слова BEGIN Описание локальной переменной выглядит так:


DECLARE VARIABLE <vanable_name> <variable_type>;


Например, чтобы объявить целочисленную локальную переменную Mylnt, нужно вставить между AS и BEGIN следующее описание


DECLARE VARIABLE Mylnt INTEGER;


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

Но двоеточие перед именем переменной необходимо использовать только внутри SQL-запросов. Вне текстов обращение к переменной делается без двоеточия, например:


procName='Some name';


Но вернемся к телу нашей процедуры. Предложение FOR SELECT возвращает данные не в виде таблицы - набора данных, а по одной строчке. Каждое возвращаемое поле должно быть помещено в свою переменную: ID => procID, NAME => procName. В части DO эти переменные посылаются клиенту, вызвавшем) процедуру, с помощью команды SUSPEND

Таким образом, команда FOR SELECT... DO организует цикл по записям, выбираемым в части SELECT этой команды. В теле цикла, образуемого частью DO, выполняется передача очередной сформированной записи клиенту с помощью команды SUSPEND.

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

Циклы и операторы ветвления

 Сделать закладку на этом месте книги

Помимо команды FOR SELECT... DO, организующей цикл по записям какой-либо выборки, существует другой вид цикла - WHILE...DO, который позволяет организовать цикл на основе проверки любых условий. Вот пример ХП, использующей цикл WHILE.. DO. Эта процедура возвращает квадраты целых чисел от 0 до 99:


CREATE PROCEDJRE QUAD

RETURNS (QUADRAT INTEGER)

AS

DECLARE VARIABLE I INTEGER;

BEGIN

I = 1;

WHILE (i<100) DO

BEGIN

QUADRAT= I*I;

I=I+1;

SUSPEND;

END

END


В результате выполнения запроса SELECT * FROM QUAD мы получим таблицу, содержащую один столбец QUADRAT, в котором будут квадраты целых чисел от 1 до 99

Помимо перебора результатов SQL-выборки и классического цикла, в языке хранимых процедур используется оператор IF...THEN..ELSE, позволяющий организовать ветвление в зависимости от выполнения каких-либо условий. Его синтаксис, похож на большинство операторов ветвления в языках программирования высокого уровня, вроде Паскаля и Си.

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

* Вычисляет среднюю цену в таблице Table_example (см. глава "Таблицы Первичные ключи и генераторы")

* Далее для каждой записи в таблице делает следующую проверку, если существующая цена (PRICE) больше средней цены, то устанавливает цену, равную величине средней цены, плюс задаваемый фиксированный процент

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

* Возвращает все измененные строки в таблице.

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


CREATE PROCEDURE IncreasePrices (

Percent2lncrease DOUBLE PRECISION)

RETURNS (ID INTEGER, NAME VARCHAR(SO), new_price DOUBLE

PRECISION) AS


Процедура будет называться IncreasePrices, у нее один входной параметр Peiceni21nciease, имеющий тип DOUBLE PRECISION, и 3 выходных параметра - ID, NAME и new_pnce. Обратите внимание, что первые два выходных параметра имеют такие же имена, как и поля в таблице Table_example, с которой мы собираемся работать Это допускается правилами языка хранимых процедур.

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


DECLARE VARIABLE avg_price DOUBLE PRECISION;


Теперь перейдем к телу хранимой процедуры Откроем тело ХП ключевым словом BEGIN.

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


SELECT AVG(Price_l)

FROM Table_Example

INTO :avg_price,-


Этот запрос использует агрегатную функцию AVG, которая возвращает среднее значение поля PRICE_1 среди отобранных строк запроса - в нашем случае среднее значение PRICE_1 по всей таблице Table_example. Возвращаемое запросом значение помещается в переменную avg_price. Обратите внимание, что переменная avg_pnce предваряется двоеточием -для того, чтобы отличить ее от полей, используемых в запросе.

Особенностью данного запроса является то, что он всегда возвращает строго одну-единственную запись. Такие запросы называются singleton-запросами И только такие выборки можно использовать в хранимых процедурах. Если запрос возвращает более одной строки, то его необходимо оформить в виде конструкции FOR SELECT...DO, которая организует цикл для обработки каждой возвращаемой строки

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

С начала opганизуем перебор каждой записи из таблицы Table_example


FOR

SELECT ID, NAME, PRICE_1

FROM Table_Example

INTO :ID, :NAME, :new_price

DO

BEGIN

/*_здесь оОрсшатыьаем каждую запись*/

END


При выполнении этой конструкции из таблицы Table_example построчно будут выниматься данные и значения полей в каждой строке будут присвоены переменным ID, NAME и new_pnce. Вы, конечно, помните, что эти переменные объявлены как выходные параметры, но беспокоиться, что выбранные данные будут возвращены как результаты, не стоит: тот факт, что выходным параметрам что-либо присвоено, не означает, что вызывающий ХП клиент немедленно получит эти значения! Передача параметров осуществляется только при исполнении команды SUSPEND, а до этого мы можем использовать выходные параметры в качестве обычных переменных - в нашем примере мы именно так и делаем с параметром new_price.

Итак, внутри тела цикла BEGIN.. .END мы можем обработать значения каждой строки. Как вы помните, нам необходимо выяснить, как существующая цена соотносится со средней, и предпринять соответствующие действия. Эту процедуру сравнения мы реализуем с помощью оператора IF:


IF (new_price > avg_price) THEN /*если существующая цена больше средней цены*/

BEGIN

/*то установим новую цену, равную величине средней цены, плюс фиксированный процент */

new_price = (avg_price + avg_price*(Percent2Increase/100));

UPDATE Table_example

SET PRICE_1 = :new_price

WHERE ID = :ID;

END

ELSE

BEGIN

/* Если существующая цена меньше или равна средней цене, то установим цену, равную прежней цене, плюс половина разницы между прежней и средней ценой */

new_price = (new_pnce + ( (avg_pnce new_price)/2) ) ;

UPDATE Table_example

SET PRICE_1 = :new_price

WHERE ID = .ID;

END


Как видите, получилось достаточно большая конструкция IF, в которой трудно было бы разобраться, если бы не комментарии, заключенные в символы /**/.

Для того чтобы изменить цену в соответствии с вычисленной разницей, мы воспользуемся оператором UPDATE, который позволяет модифицировать существующие записи - одну или несколько. Для того чтобы однозначно указать, в какой записи нужно изменять цену, мы используем в условии WHERE поле первичного ключа, сравнивая его со значением переменной, в которой хранится значение ID для текущей записи: ID=:ID. Обратите внимание, что переменная ID предваряется двоеточием.

После выполнения конструкции IF...THEN...ELSE в переменных ID, NAME и new_price находятся данные, которые мы должны возвратить клиенту, вызвавшему процедуру. Для этого после IF необходимо вставить команду SUSPEND, которая перешлет данные туда, откуда вызвали ХП На время пересылки действие процедуры будет приостановлено, а когда от ХП потребуется новая запись, то она будет вновь продолжена, - и так будет продолжаться до тех пор, пока FOR SELECT...DO не переберет все записи своего запроса.

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

При этом в случае, когда процедура вызывалась оператором SELECT и завершена по EXIT, последняя извлеченная строка не будет возвращена. То есть, если вам нужно прервать процедуру и все-таки получить эту строку, надо воспользоваться последовательностью


SUSPEND;

EXIT;


Основное назначение EXIT - получение singleton-наборов данных, возвращаемых параметров путем вызова через EXECUTE PROCEDURE. В этом случае устанавливаются значения выходных параметров, но из них не формируется набор данных SQL, и выполнение процедуры завершается.

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


CREATE PROCEDURE IncreasePrices (

Percent2Increase DOUBLE PRECISION)

RETURNS (ID INTEGER, NAME VARCHAR(80),

new_price DOUBLE PRECISION) AS

DECLARE VARIABLE avg_price DOUBLE PRECISION;

BEGIN

SELECT AVG(Price_l)

FROM Table_Example

INTO :avg_price;

FOR

SELECT ID, NAME, PRICE_1

FROM Table_Example

INTO :ID, :NAME, :new_price

DO

BEGIN

/*здесь обрабатываем каждую запись*/

IF (new_pnce > avg_price) THEN /*если существующая цена больше средней цены*/

BEGIN

/*установим новую цену, равную величине средней цены, плюс фиксированный процент */

new_price = (avg_price + avg_price*(Percent2lncrease/100));

UPDATE Table_example

SET PRICE_1 = :new_price

WHERE ID = :ID;

END

ELSE

BEGIN

/* Если существующая цена меньше или равна средней цене, то устанавливает цену, равную прежней цене, плюс половина разницы между прежней и средней ценой */

new_price = (new_price + ((avg_price - new_price)/2));

UPDATE Table_example

SET PRICE_1 = :new_price

WHERE ID = :ID;

END

SUSPEND;

END

END


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

Рекурсивные хранимые процедуры

 Сделать закладку на этом месте книги

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

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

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

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


-Товары

- Бытовая техника

- Холодильники

- Трехкамерные

- Двухкамерные

- Однокамерные

- Стиральные машины

- Вертикальные

- Фронтальные

- Классические

- Узкие

- Компьютерная техника

....


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


Стиральные машины - Вертикальные

Стиральные машины - Фронтальные Классические

Стиральные машины - Фронтальные Узкие


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


CREATE TABLE GoodsTree

(ID_GOOD INTEGER NOT NULL,

ID_PARENT_GOOD INTEGER,

GOOD_NAME VARCHAR(80),

constraint pkGooci primary key (ID_GOOD) );


Создаем одну таблицу GoodsTree, в которой всего 3 поля: ID_GOOD - уникальный идентификатор категории, ID_PARENT_GOOD - идентификатор категории - родителя для данной категории и GOOD_NAME - наименование категории. Чтобы обеспечить целостность данных в этой таблице, наложим на эту таблицу ограничение внешнего ключа:


ALTER TABLE GoodsTree

ADD CONSTRAINT FK_goodstree

FOREIGN KEY (ID_PARENT_GOOD)

REFERENCES GOODSTPEE (ID__GOOD)


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

Давайте занесем в нашу таблицу следующие данные:

ID_GOOD

1

2

3

4

5

6

7

8

9

10

11

12

ID_PARENT_GOOD

0

1

1

2

2

4

4

4

5

5

10

10

GOOD_NAME

GOODS

Бытовая техника

Компьютеры и комплектующие

Холодильники

Стиральные машины

Трехкамерные

Двухкамерные

Однокамерные

Вертикальные

Фронтальные

Узкие

Классические

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

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

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


CREATE PROCEDURE GETFULLNAME (ID_GOOD2SHOW INTEGER)

RETURNS (FULL_GOODS_NAME VARCHAR(1000),

ID_CHILD_GOOD INTEGER)

AS

DECLARE VARIABLE CURR_CHILD_NAME VARCHAR(80);

BEGIN

/*0рганизуем внешний цикл FOR SELECT по непосредственным потомкам товара с ID_GOOD=ID_GOOD2SHOW */

FOR SELECT gtl.id_good, gtl.good_name

FROM GoodsTree gtl

WHERE gtl.id_parent_good=:ID_good2show

INTO:ID_CHILD_GOOD, :full_goods_name

DO

BEGIN

/"Проверка с помощью функции EXISTS, которая возвращает TRUE, если запрос в скобках вернет хотя бы одну строку. Если у найденного узла с ID_PARENT_GOOD = ID_CHILD_GOOD нет потомков, то он является "листом" дерева и попадает в результаты */

IF (NOT EXISTS(

SELECT * FROM GoodsTree

WHERE GoodsTree.id_parent_good=:id_child_good))

THEN

BEGIN

/* Передаем "лист" дерева в результаты */

SUSPEND;

END

ELSE

/* Для узлов, у которых есть потомки*/

BEGIN

/*сохраняем имя узла-родителя во временной переменной */

CURR_CHILD_NAME=full_goods_name;

/* рекурсивно запускаем эту процедуру */

FOR

SELECT ID_CHILD_GOOD, full_goods_name

FROM GETFULLNAME (:ID_CHILD_GOOD)

INTO:ID_CHILD_GOOD, :full_goods_name

DO BEGIN

/*добавляем имя узла-родителя к найденном., имени потомка с помощью операции конкатенации строк || */

full_goods_name=CURR_CHILD_NAME| ' ' | full_goods_name,-

SUSPEND; /* возвращаем полное имя товара*/

END

END

END

END


Если мы выполним данную процедуру с входным параметром ID_GOOD2SHOW= 1, то получим следующее:

FULL_GOODS__NAME

Бытовая техника Холодильники Трехкамерные

Бытовая техника Холодильники Двухкамерные

Бытовая техника Холодильники Однокамерные

Бытовая техника Стиральные машины Вертикальные

Бытовая техника Стиральные машины Фронтальные Узкие

Бытовая техника Стиральные машины Фронтальные Классические

Компьютеры и комплектующие

ID_CHILD_GOOD

6

7

8

9

11

12

3

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

Заключение

 Сделать закладку на этом месте книги

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

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

Расширенные возможности языка хранимых процедур InterBase

 Сделать закладку на этом месте книги

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

Обработка исключений и ошибок

 Сделать закладку на этом месте книги

Исключения

 Сделать закладку на этом месте книги

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

Исключения InterBase во многом похожи на исключения других языков высокого уровня, однако имеют свои особенности Фактически исключение InterBase - это сообщение об ошибке, которое имеет собственное, задаваемое программистом имя и текст сообщения об ошибке. Создается исключение следующим образом:


СРЕАТЕ EXCEPTION <имя_исключения> <текст_исключения>;


Например, мы можем создать исключение такого вида:


CREATE EXCEPTION test_except 'Test exception';


Исключение легко удалить или изменить - удаление совершается командой DROP EXCEPTION <имя_удаляемого_исключения>, а изменение - ALTER EXCEPTION <имя_исключения> <текст_исключения>

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


EXCEPTION <имя_исключения>;


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

Для нашего примера создадим следующее исключение:


CREATE EXCEPTION zero_divide 'Cannot divide by zero!';


Создадим хранимую процедуру, использующую это исключение:


CREATE PROCEDURE SP_DIVIDE (

DELIMOE DOUBLE PRECISION,

DELITEL DOUBLE PRECISION)

RETURNS (

RESULT DOUBLE PRECISION)

AS

BEGIN

if (Delitel<0.0000001) then

BEGIN

EXCEPTION zero_divide;

Result=0;

END

ELSE

BEGIN

Result=Delimoe/Delitel;

END

SUSPEND;

END


Как видите, текст ХП тривиален - на входе получаем Delitel и Delimoe, затем сравниваем Delitel с 0.0000001, т. е. фактически с нулем, в пределах выбранной погрешности в одну десятимиллионную (так как вещественные числа невозможно непосредственно сравнивать из-за погрешностей в дробной части). Если Delitel близок к нулю в пределах выбранной погрешности, то мы возбуждаем исключение zero_divide. Что же происходит в случае возникновения исключения? Если мы попробуем вызвать исключение, выполняя процедуру SP_divide с нулевым делителем в isql. то получим следующее


SQL> select * from sp_divide(300,0);

RESULT

==========

Statement failed, SQLCODE = -836

exception 1

-Cannot divide by zero!


Если мы вызовем эту ХП с н


убрать рекламу






улевым делителем в каком-либо другом приложении, то скорее всего получим сообщение об ошибке следующего вида:







Рис 1.4. Сообщение о возникновении исключения

Другими словами, сообщение об ошибке - это результат обработки нашего исключения сервером InterBase. Когда InterBase обнаруживает возникшее в ХП или триггере исключение он прерывает работу этой хранимой процедуры и откатывает все изменения, сделанные в текущем блоке BEGIN END, причем если ХП является процедурой-выборкой, то отменяются действия лишь до последнего оператора SUSPEND.

Это значит, что если в процедуре-выборке есть цикл, в котором производятся какие-то действия, и в теле цикла есть SUSPEND, то при возбуждении исключения О1меня1ся все дейс1вия, выполненные в этом цикле до последнего оператора SUSPEND.

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


WHEN EXCEPTION <имя_исключения> DO

BEGIN

/*обработка исключения*/

END



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

Когда возбуждается исключение, происходит следующее: выполнение хранимой процедуры (или триггера) прерывается. InterBase начинает искать конструкцию WHEN EXCEPTION...DO для обработки возникшего исключения в текущем блоке BEGIN...END. Если не находит, то поднимается на уровень выше (выше в том смысле, если имеются вложенные блоки BEGIN...END или когда одна ХП вызвана из другой) и ищет обработчик исключения там и т. д., пока либо не найдет подходящий обработчик исключения, либо не закончится вложенность уровней хранимой процедуры. Если обработчик исключения так и не был найден, то возвращается стандартное сообщение об ошибке, включающее текст исключения. Если обработчик найден, то выполняются действия в его блоке BEGIN...END и управление передается на первый оператор, следующий за END обработчика.

Давайте рассмотрим пример обработки исключения, возбуждаемого в нашей процедуре SP_DIVIDE. Предположим, что мы имеем некоторую внешнюю процедуру SP_divide_all, вызывающую SPJDFVIDE для того, чтобы поделить два числа. Конечно, пример сильно утрирован, но он позволяет пояснить способ и смысл использования исключений.

Итак, вот текст нашей хранимой процедуры:


CREATE PROCEDURE sp_test_except(Delltel DOUBLE PRECISION)

RETURNS (rslt DOUBLE PRECISION, status VARCHAR(SO))

AS

BEGIN

Status='Everything is Ok';

SELECT result FROM sp_divide(12,:Delitel) INTO :rslt;

SUSPEND;

WHEN EXCEPTION Zero_divide DO

BEGIN

Status='zero value found!';

rslt=-l;

SUSPEND;

END

END


Эта процедура вызывает процедуру SP_DIVIDE. Если параметр Delitel не равен нулю, то процедура SP_DIVIDE выполняется без проблем и в возвращаемое значение rslt помещается частное от деления, а статусная переменная status принимает значение 'Everything is Ok'. В случае, если возникла исключительная ситуация деления на нуль, то результирующая переменная rslt будет равна -1, а в переменной status будет содержаться сообщение об ошибке - 'zero value found!'. Разумеется, в обработчике исключения можно произвести и более сложную обработку, например записать некорректные данные в особую таблицу или попытаться изменить данные для выполнения операции и вновь попробовать ее выполнить и т. д.

Обработка ошибок SQL и InterBase

 Сделать закладку на этом месте книги

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

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

Конструкция, с помощью которой производится обработка ошибок, такая же, как и для обработки исключений, только вместо EXCEPTION стоит либо GDSCODE, либо SQLCODE:


WHEN GDSCODE SQLCODE <код_ошибки> DO

BEGIN

/*обрабатываем ошибку*/

END


В зависимости от того, стоит ли в конструкции обработки ошибок GDSCODE или SQLCODE, обрабатываются различные ошибки. Если стоит SQLCODE, то обрабатываются ошибки SQL, а если GDSCODE - то ошибки InterBase. Примером ошибки SQL является ошибка с SQLCODE=-802 "Arithmetic exception, numeric overflow, or string truncation" или SQLCODE=-817 "Attempted update during read-only transaction" Список ошибок SQL и соответствующих им значений SQLCODE приведен в таблице "SQLCODE codes and messages'. Примером ошибки InterBase является ошибка isc_bad_dbkey 335544322L "invalid database key".

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

А что делать, если нужно прореагировать на любую ошибку? Не прописывать же обработчики всех сотен возможных ошибок? Конечно же, нет. Для того чтобы написать безусловный обработчик, реагирующий на любую ошибку - SQL, InterBase или исключение, следует воспользоваться конструкцией WHEN DO с ключевым словом ANY:


WHEN ANY DO

BEGIN

/*действия при любой нестандартной операции ошибке

или исключении */

END


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

Работа с массивами в хранимых процедурах

 Сделать закладку на этом месте книги

Массивы, как было сказано в главе "Типы данных", позволяют хранить в одном поле набор данных какого-нибудь одного элементарного типа. Однако "простым" SQL-запросом данные не извлечь и не изменить. Необходим особый подход для работы с массивами InteiBase в клиентских приложениях - см paздел "Поддержка array-полей в FIBPIus" в главе (ч 2) и "Разработка клиентских приложений СУБД InterBase с использованием технологии Microsoft OLE DB" (ч. 3). К тому же не все библиотеки доступа поддерживают работу с массивами.

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


CREATE TABLE table_array(

ID_table INTEGER,

Arrayl INTEGER[3,2]);


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


SELECT Arrayl[:i, :j] FROM table_array INTO :ElemValue;


Давайте оформим механизм доступа к массиву в виде следующей хранимой процедуры:


CREATE PROCEDURE SelectFromArr(ID_row INTEGER,

x INTEGER, у INTEGER, vl integer)

Returns (ElemValue INTEGER)

AS

BEGIN

SELECT arrayl[:x,:y]

FROM table_array

WHERE id_cable=:ID_row

INTO :ElemValue;

SUSPEND;

END


Как видите, текст ХП очевиден - просто извлекаем нужные значения из элемента массива с заданными индексами х и у в строке таблицы table_array с идентификатором ID_table=ID_row. К сожалению, такой синтаксис доступа к элементам массива доступен только внутри хранимых процедур и только для чтения. Для заполнения массива придется воспользоваться программой, применяющей либо InterBase API, либо библиотеки доступа, поддерживающие работу с массивами InterBase - FIBPIus для Delphi/C-H-Builder/Kylix или IBProvider для продуктов Microsoft.

Заключение

 Сделать закладку на этом месте книги

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

Триггеры

 Сделать закладку на этом месте книги

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

Что же такое триггер?

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

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

Как вы знаете. SQL дает возможность нам вставлять, удалять и модифицировать данные в таблицах базы данных при помощи соответствующих команд - INSERT, DELETE и UPDATE. Согласитесь, что было бы неплохо иметь возможность перехватить передаваемую команду и что-нибудь сделать с данными, которые добавляются, удаляются или изменяются. Например, записать эти данные в специальную табличку, а заодно записать, кто и когда произвел операцию над данной таблицей. Или сразу же проверить вставляемые данные на какое- нибудь хитрое условие, которое невозможно реализовать с помощью опции CHECK (см. выше главу "Ограничения базы данных"), и в зависимости от результатов проверки принять проводимые изменения или отвергнуть их; изменить эти данные на основании какого-либо запроса или изменить данные в других связанных таблицах.

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

Фактически триггер представляет собой набор команд процедурного языка InterBase, который исполняется при выполнении операций INSERT/DELETE/UPDATE В отличие от хранимых процедур, триггер никогда ничего не возвращает (да и некому возвращать. ведь триггер явно не вызывается) По той же причине он не имеет также входных параметров, но вместо них имеет контекстные переменные NEW и OLD. Эти переменные позволяют получить доступ к полям таблицы, к которой присоединен триггер (мы расскажем об этом чуть позже).

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

Триггер всегда привязан к какой-то определенной таблице или представлению и может "перехватывать" данные только этой таблицы. Давайте рассмотрим классификацию триггеров и назначение каждого вида. Как уже было сказано, существует 3 основных SQL-операции, применимые к данным, - INSERT/DELETE/UPDATE. Соответственно первое разделение триггеров - по обслуживаемым операциям. Каждый конкретный триггер привязан к какой-либо операции, т. е. триггер срабатывает, когда в "его" таблице происходит данная операция.

В клоне Yaffil 1 0 реализована поддержка универсальных триггеров, срабатывающих при любой операции.

Также срабатывание триггера может происходить "до" и "после" операции. Таким образом, мы получаем 6 возможных видов триггеров на таблицу - до и после каждой из трех возможных SQL-операций.

Пример триггера

 Сделать закладку на этом месте книги

Давайте рассмотрим простой пример триггера, который срабатывает ДО ВСТАВКИ в таблицу и заполняет поле первичного ключа. Мы воспользуемся в качестве основы для триггера таблицей из примера в главе "Таблицы. Первичные ключи и генераторы" этой части:


CREATE TABLE Table_example (

ID INTEGER NOT NULL,

NAME VARCHAR(80),

PRICE_1 DOUBLE PRECISION,

CONSTRAINT pkTable PRIMARY KEY (ID));


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


CREATE GENERATOR GEN_TABLE_EXAMPLE_ID;


И устанавливаем его начальное значение в единицу:


SET GENERATOR GEN_TABLE_EXAMPLE_ID TO 1;


Теперь необходимо создать триггер. Надо сказать, что триггер, как и хранимая процедура, может содержать в своем теле несколько операторов, разделенных точкой с запятой. Поэтому если вы не используете один из инструментов, рекомендованных в приложении "Инструменты администратора и разработчика InterBase", а работаете с isql, то вам необходимо воспользоваться командой смены разделителя команд SET TERM, как это было описано в главе "Хранимые процедуры". Мы же будем приводить тексты триггеров без обрамления командами смены разделителя.

Итак, рассмотрим текст нашего триггера:


CREATE TRIGGER Table_example_bi FOR Table_example

ACTIVE BEFORE INSERT POSITION 0

AS

BEGIN

IF (NEW.ID IS NULL) THEN

NEW.ID = GEN_ID(GEN_TABLE_EXAMPLE_ID, 1);

END


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

Описание команды создания триггера начинается с ключевых слов CREATE TRIGGER, после которых следует имя триггера - Table_example_bi. Потом следует ключевое слово FOR, после которого указано имя таблицы, для которой создается триггер, - Table_example.

На второй строке команды приводится описание сущности триггера - ключевое слово ACTIVE указывает, что триггер является "активным". Триггер также может быть переведен в состояние INACTIVE. Это означает, что он будет храниться в базе данных, но он не будет срабатывать. Сочетание ключевых слов BEFORE INSERT определяет, что триггер срабатывает ДО ВСТАВКИ; а ключевое слово POSITION и число 0 указывают очередность (позицию) создаваемого триггера среди триггеров того же типа для данной таблицы. Позиция триггера нужна потому, что в InterBase возможно создать более 32000 триггеров каждого вида (например, BEFORE INSERT или AFTER UPDATE), и серверу нужно указать, в каком порядке эти триггеры будут выполняться. Триггеры с меньшей позицией выполняются первыми. Если имеется несколько триггеров с одинаковой позицией, то они будут выполняться в алфавитном порядке.

Все рассмотренное выше до ключевого слова AS образует заголовок триггера. После AS следует тело триггера. Собственно в теле и осуществляется вставка значения в поле первичного ключа. Но сначала с помощью уже знакомого вам оператора IF.. .THEN проверяется, не было ли заполнено это поле на клиенте. Выражение проверки возвращающей булеву TRUE (истина) или FALSE (ложь), выгядит так:


NEW.ID IS NULL


"Интересно, что такое NEW?" - спросите вы. Это одна из особенностей, присущая только триггерам, - контекстная переменная.  Давайте взглянем, как она действует

Контекстные переменные

 Сделать закладку на этом месте книги

Как уже говорилось выше, триггер похож на цензора, бесцеремонно досматривающего все. что относится к интересующему его предмету Интерес нашего триггера-"цензора" описан сочетанием ключевых слов BEFORE INSERT - это значит, что все операции вставки (INSERT) вызовут срабатывание триггера. Причем он сработает ДО (BEFORE) того, как вставка физически осуществлена. То есть в момент срабатывания триггера данные, присланные кем-либо на вставку, еще не занесены в таблицу. Они находятся в некотором промежуточном буфере. И у триггера есть возможность обращаться к этому буферу, чтобы проверить и/или изменить значения данных-кандидатов на вставку. Эта возможность реализована с помощью контекстной переменной NEW. Можно рассматривать эту переменную как структуру (что-то подобное struct в Си или record в Pascal), элементы которой представляют собой значения, присланные для осуществления операции (INSERT в нашем примере). То есть внутри триггера мы можем обратиться ко всем полям еще не вставленной записи, используя для этого обращение: New.ID, New.NAME и New PRICE_1.

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


IF (NEW.ID IS NULL) THEN

NEW.ID = GEN_ID(GEN_TABLE_EXAMPLE_ID, 1) ;


Сначала в операторе IF...THEN проверяем идентификатор ID на наличие какого-либо значения, ведь он может быть сгенерирован на клиенте. Если значением NEW.ID является NULL, то вызываем функцию GEN_ID, которая увеличивает значение генератора GEN_TABLE_EXAMPLE_ID на единицу и затем возвращает полученное число, которое присваивается полю NEW.ID Таким образом, мы "налету" изменили значения во вставляемой записи!

Кроме контекстной переменной NEW, существует ее зеркальный аналог - переменная OLD. В отличие от NEW, OLD содержит старые значения записи, которые удаляются или изменяются. Например, мы можем использовать переменную OLD для получения значений записей, которые удаляются из таблицы:


CREATE TRIGGER Table_example_adO FOR Table_example

ACTIVE AFTER DELETE POSITION 0

AS

BEGIN

IF (OLD.id>1000) THEN

BEGIN

/*..do something..*/

OLD.ID=10;

END

END


Здесь мы создаем триггер, который срабатывает ПОСЛЕ УДАЛЕНИЯ (AFTER DELETE) Как видите, мы можем получить доступ к уже удаленным данным Конечно, присвоение OLD.1D=10; не имеет никакого смысла - присвоенное значение пропадет на выходе из триггера. Однако этот пример показывает, что мы можем перехватить удаляемые значения и записать, например, в некую таблицу, где хранится история всех изменений.

Использование контекстных переменных часто вызывает множество вопросов Дело в том. что в различных видах триггеров NEW и OLD используются по- разному, а в некоторых их вообще невозможно использовать. Если мы рассмотрим триггер в нашем примере, то он вызывается ДО ВСТАВКИ. О каких значениях OLD может идти речь? Ведь вставляется совершенно новая запись! И действительно, контекстная переменная OLD не может быть использована в триггерах BEFORE/AFTER INSERT. А переменная NEW не может быть использована в BEFORE/AFTER DELETE. Обе этих переменные одновременно могут быть использованы в триггерах BEFORE/AFTER UPDATE, причем изменять что-либо можно, только используя переменную NEW (действительно, что можно изменять в удаляющихся значениях, доступных через OLD?), и только в триггерах BEFORE INSERT/UPDATE.

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

В этой таблице для каждой контекстной переменной заведено по два столбца - "Читать" и Изменять", отражающих возможные действия с этими переменными В столбце 6 строчек - по числу типов триггеров. Например, если на пересечении типа триггера и возможного действия с контекстной переменной NEW стоит Y, это значит, что в данном типе триггер можно читать или одновременно читать и менять данные Если стоит N/A. то в этом триггере нельзя осуществить это действие с данной контекстной переменной.

Использование контекстных переменных NEW и OLD  в триггерах

Тип триггера

Контекстныепеременные

NEW

OLD

Читать

Изменять

Читать

Изменять

BEFORE INSERT

Y

Y

N/A

N/A

AFTER INSERT

Y

N/A

N/A

N/A

BEFORE UPDATE

Y

Y

Y

N/A

AFTER UPDATE

Y

N/A

Y

N/A

BEFORE DELETE

N/A

N/A

Y

N/A

AFTER DELETE

N/A

N/A

Y

N/A

Наиболее широкие возможности предоставляет использование NEW и OLD в операции обновления. Ведь таким образом мы можем сравнить текущее (OLD) и новое (NEW) значения и предпринять какие-то действия. Например, такой триггер будет очень полезен для вычисления текущих остатков товара на складе при приходе/расходе товара

Управление состоянием триггера

 Сделать закладку на этом месте книги

По умолчанию триггер создается активным, т. е. он будет срабатывать при осуществлении соответствующей операции. Состоянием триггера управляет ключевое слово ACTIVE в заголовке Если же триггер сделать неактивным, то он не будет исполняться при возникновении операции. Это бывает полезным при осуществлении каких-либо внеплановых операций надданными, например массовой заливке данных или ручном исправлении данных. Чтобы отключить триггер, необходимо выполнить команду DDL:


ALTER TRIGGER <tngger_name> INACTIVE;


Обратите внимание, что это команда относится к Data Definition'Language, и ее нельзя вызвать из хранимых процедур или других триггеров. Вообще говоря, существует способ управлять состоянием триггеров с помощью модификации системных таблиц. Конечно, модификация системных таблиц является недокументированным способом работы с триггерами и рекомендовать ее мы не будем, но для иллюстрации возможностей работы с системными таблицами InterBase приведем пример. Для того чтобы установить состояние триггера в INACTIVE, достаточно выполнить следующую команду:


UPDATE rdb$triggers trg

SET erg id >$t.rigger_inactive = l

WHERE trg.rdb$trigger_name='TABLE_EXAMPLE_AD0'


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

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

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

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

В общем, это был пример того, что не следует делать в триггерах. Другим примером того, чего не следует делать в триггерах, является изменение данных в той же таблице, к которой привязан триггер, не через контекстные переменные, а с помощью обычных SQL-команд INSERT/UPDATE/DELETE. Например некий триггер на вставку вызывает хранимую процедуру, внутри которой происходит вставка записи в ту же таблицу. Вставка опять вызовет срабатывание нашего триггера, и возникнет зацикливание. Следует очень внимательно относиться к использованию триггеров, так как зацикливание в ряде случаев может привести к аварийному завершению сервера InterBase.

Ошибки и исключения в триггерах

 Сделать закладку на этом месте книги

Если база достаточно сложная (лучше сказать, достаточно реальная), то вам никак не избежать появления ошибок. Более того, ошибки типа "конфликт с другими пользователями" являются повседневным и нормальным явлением в многопользовательской среде. Как InterBase обрабатывает ошибки в триггерах? Ведь ситуация может быть достаточно нетривиальная - например, вставка записи в главную таблицу запускает хранимую процедуру, которая вставляет записи в подчиненные таблицы, причем при вставке в подчиненные таблицы срабатывают триггеры на вставку, которые получают новые значения генераторов и подставляют их в нужные поля. Можно представить не один подобный уровень вложенности. Что произойдет, когда где-то в "дальних" ветках этого дерева событий возникнет ошибка?

При возникновении ошибок на любом этапе - в триггере, в вызываемых им ХП или в неявно активизируемых других триггерах - InterBase сообщит об ошибке и откатит изменения в таблицах, проведенные в рамках инициировавшего эту цепочку оператора. Оператор - это предложение INSERT/UPDATE/DELETE или SELECT, а также EXECUTE PROCEDURE.

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

Теперь, когда мы знаем, что делает InterBase при возникновении ошибки в триггере, неплохо бы понять, что можем сделать мы, чтобы обработать ошибочную ситуацию. Если мы будем верить в то, что все наши триггеры и ХП не имеют ошибок и конфликтов между действиями пользователей быть не может, то можем вообще не обрабатывать ошибки на уровне базы данных. Если же ошибка возникнет, InterBase пошлет нашему клиентскому приложению сообщение об ошибке, которое мы вольны обработать или нет, - в любом случае InterBase уже выполнил свою миссию - откатил ошибочное действие в триггере. Однако есть и другой путь.

Мы можем воспользоваться обработкой ошибочных ситуаций непосредственно в теле триггера (или хранимой процедуры) с помощью конструкции WHEN...DO. Использование этой конструкции аналогично применению ее в хранимых процедурах, и подробнее об использовании WHEN...DO см. главу "Расширенные возможности языка хранимых процедур InterBase" (ч. 1).

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

События InterBase

 Сделать закладку на этом месте книги

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

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


POST_EVENT 'текст_сообщения';


Надо сказать, что 'текст_сообщения' может браться из переменной и, таким образом, можно порождать события динамически, например так:

...

If (<какое-то булево выражение>) then

BEGIN

Event_text ='IT IS TRUE!';

END

ELSE

BEGIN

Event_text ='IT IS FALSE!';

END

FALSE_EVENT :Event_text;


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

Для регистрации (подписки) на получение нужных событий используют специальные функции InterBase API, которые реализованы, например, в библиотеке FIBPlus - в компоненте SuperlBAlerter.

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

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

Заключение

 Сделать закладку на этом месте книги

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

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

User Defined Functions

 Сделать закладку на этом месте книги

Зачастую от программистов, использующих другие серверы баз данных, можно услышать мнение, что SQL InterBase не отличается большим разнообразием встроенных функций. Формально такая точка зрения имеет основания, однако разработчики InterBase сознательно пошли на это ограничение. Как уже неоднократно было сказано, InterBase отличается скромными системными требованиями и занимает мало места на жестком диске. Небольшой совокупный размер файлов продиктован, в частности, тем, что InterBase не перегружен разнообразными дополнительными и, в общем-то, редко используемыми функциями Зато InterBase включает возможность расширить стандартный набор функций любыми дополнениями, которые нужны в конкретной базе данных. Таким образом, разработчик может реализовать для своих приложений даже такие функции, которые никогда не входят в поставку серверов баз данных.

Механизм подключения функций

 Сделать закладку на этом месте книги

Специально для расширения функциональности SQL InterBase предлагает механизм функций, определяемых пользователем (user defined functions). Вы можете создать динамическую библиотеку (Dynamic Link Library) при помощи любой системы разработки, которая позволяет создавать выполнимые файлы данного формата. В частности, можно использовать Borland Delphi, Borland C++ Builder, Microsoft Visual C++ и т. д. Далее, необходимо поместить полученную DLL в каталог, из которого InterBase сможет вызывать библиотеку, и декларировать нужные функции из DLL в своей базе данных при помощи команды DECLARE EXTERNAL FUNCTION. После этого вы сможете вызывать указанные функции, как если бы они были встроенными функциями InterBase.

InterBase до версии 6.0 требовал, чтобы DLL находилась в любом из каталогов, указанных в системной переменной PATH InterBase 6 и выше (включая клоны Firebird и Yaffil) требует, чтобы DLL была помещена в специальный каталог UDF, находящийся в общем каталоге установки InterBase

Создание собственных функций

 Сделать закладку на этом месте книги

Мы не будем подробно останавливаться на всех особенностях создания пользовательских функций, поскольку данный механизм достаточно прост, однако продемонстрируем написание и подключение одной функции на примере. В качестве средства разработки мы будем использовать Borland Delphi. Для примера также будет применяться стандартная база данных EMPLOYEE.GDB.

В нашем примере мы создадим функцию, которая будет преобразовывать строку к верхнему регистру. Подобная функция может оказаться полезной, если вы не задали опцию COLLATE для ваших строковых полей. Кроме того, работа со строковыми параметрами, как правило, вызывает наибольшее количество вопросов при создании пользовательских функций. Разумеется, мы исходим из предположения, что вы знакомы с принципом создания DLL при помощи Delphi.


library TestUDF;

uses SysUtils;

function malloc(Size: Integer): Pointer; cdecl; external

'msvcrt.dll';

function StrUpperCase(sz: PChar): PChar; cdecl; export;

var Tmp: string;

begin

Tmp := AnsiUpperCase(sz);

Result := malloc(length(Tmp) + 1);

StrPCopy(Result, Tmp);

end;

exports

StrUpperCase;

begin

end.


Динамическая библиотека экспортирует только одну функцию: StrUpperCase. Для передачи строковых параметров, равно как и результата функции, используется тип Pchar, т е. динамическая строка, ограниченная символами #0. Смысл кода нашей функции очевиден: мы приводим строку sz у верхнему регистру, используя стандартную функцию AnsiUpperCase. Данная функция корректно работает с русскими буквами, если в системе установлена русская кодовая страница. После этого выделяем память для результирующей переменной, используя malloc - стандартную функцию Windows. Остается только скопировать значение временной переменной Tmp в переменную Result. Скомпилируйте библиотеку и поместите полученный файл TestUDF.dll в нужный каталог. Если вы используете InterBase 6.x или его клоны, то это каталог \Udf, который находится в каталоге установки сервера. Необходимо зарегистрировать функцию в базе данных. Для регистрации необходимо выполнить команду DECLARE EXTERNAL FUNCTION, которая имеет следующий синтаксис:


DECLARE EXTERNAL FUNCTION name [datatype | CSTRING (int)

[, datatype | CSTRING (int) ...] ]

RETURNS (datatype [BY VALUE] | CSTRING (int)} [FREE_IT]

ENTRY_POINT 'entryname'

MODULE_NAME ' modulename';


Параметр name - это имя пользовательской функции внутри базы данных. Он не обязательно должен совпадать с реальным названием функции в DLL.

Параметр datatype определяет тип параметров. На параметры накладываются следующие ограничения:

* все параметры передаются по ссылке;

* выходной параметр (значение функции) может возвращаться по значению;

* параметры не могут быть массивами.

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

Параметр FREE_IT указывает InterBase, что после выполнения функции необходимо автоматически освободить память, выделенную для параметров. Очевидно, данная опция нужна только в том случае, если наша библиотека сама мм делила память под какие-либо параметры функции.

В параметре entryname необходимо указывать название функции в DLL, ко торую мы собственно и хотим декларировать как пользовательскую функцию.

В параметре modulename необходимо указать название файла DLL, в котором находится декларируемая функция пользователя.

Стоит заметить, что параметры entryname и modulename регистрочувствительные.

Таким образом, чтобы добавить нашу функцию в базу данных, нам необходимо выполнить следующую команду:


DECLARE EXTERNAL FUNCTION USTRUPPERCASE

cstring(254)

RETURNS cstnng(254) FREE_IT

ENTRY_POINT 'StrUpperCase' MODULE_NAME 'TestUDF.dll'


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


SELECT USTRUPPERCASE(DEPARTMENT) FROM DEPARTMENT


Запрос вернет названия отделов из таблицы DEPARTMENT:







Рис 1.5. Результат использования UDF USTRUPPERCASE

Заключение

 Сделать закладку на этом месте книги

Мы рассмотрели на примере, как можно расширять набор доступных SQL-функций при помощи механизма User Defined Functions. Имея этот простой, но очень мощный механизм, вы сможете сделать обработку бизнес-правил в ваших базах данных гораздо более эффективно и удобно. В сущности, механизм пользовательских функций InterBase имеет только одно серьезное ограничение - он не позволяет обрабатывать NULL-параметры. В остальном функциональность пользовательских "расширений" SQL зависит только от ваших потребностей.

Широкий выбор UDF-библиотек, а также более подробную информацию об их использовании и разработке всегда можно найти на сайтах www.InterBaseworld.com и www.ibase.ru.

Русификация InterBase

 Сделать закладку на этом месте книги

Borland InterBase 6.x и его клоны Firebird и Yaffil - продукты, ориентированные на международного потребителя и позволяющие работать со множеством разных языков, в том числе и русским. Однако по умолчанию InterBase будет ориентироваться на работу с английским языком, поэтому для того, чтобы хранить в базе данных кириллицу и иметь возможность извлекать ее в читабельном виде, необходимо произвести ряд действий по "русификации" базы данных и включению поддержки кириллицы в клиентских приложениях.

Русификация базы данных InterBase

 Сделать закладку на этом месте книги

Наборы символов

 Сделать закладку на этом месте книги

Чтобы указать InterBase, как интерпретировать и хранить помещаемую в базу данных символьную информацию, необходимо указать набор символов  (.character set), который будет использоваться для представления этих символов в нужном виде (см. выше главу "Типы данных" для информации о хранении символьных данных). Чтобы пользоваться набором символов, необходимо указать его как атрибут объектов в базе данных, а также указать его для использования в клиентском приложении.

Если вы уверены в том, каким набором символов будете пользоваться в вашей базе данных, то можете установить единый набор символов по умолчанию для всей базы данных. Для этого в команде создания базы данных следует указать набор символов по умолчанию с помощью опции DEFAULT CHARACTER SET. Для того чтобы работать с русскими буквами, следует указывать набор символов WIN1251. Можно также использовать набор UNICODE_FSS, который поддерживает любые символы UNICODE в представлении UTF-8. Однако большинство производителей библиотек доступа не полностью поддерживают этот набор символов, поэтому лучше использовать проверенный WIN1251.

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


CREATE DATABASE 'C:\Database\rusbase.gdb' USER 'SYSDBA' PASWORD

'masterkey' DEFAULT CHARACTER SET WIN1251;


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


CREATE TABLE langTable(

NAME_RUS VARCHAR(255) CHARACTER SET WIN1251,

NAME_ENG VARCHAR(255) CHARACTER SET WIN1250,

NAME_UNI VARCHAR(255) CHARACTER SET UNICODE_FSS);


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

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

Хранение символьных данных без использования наборов символов

 Сделать закладку на этом месте книги

Считать данные таблицы, в которой символьные поля имеют разные наборы символа с помощью одного SQL-запроса также не получится, если мы укажем один из трех наборов данных, то InterBase попытается привести все данные к этому набору, а это может у него не получиться - и тогда возникнет ошибка "Cannot transliterate characters between character sets".

Однако существует еще один, недокументированный способ хранить в базе данных символы из различных наборов. Этот способ заключается в том, чтобы вообще не указывать набор символов, тогда по умолчанию будет применять character set NONE, использование которого дает понять InterBase, что символьные данные должны храниться так, как они есть, без всяких интерпретаций. При этом всю ответственность за интерпретацию данных берет на себя разработчик клиентского приложения. Таким образом, отказавшись от использования наборов символов, мы получаем возможность читать и хранить любые символьные данные.

Казалось бы, надо всегда пользоваться CHARACTER SET NONE и избегать различных проблем, однако, отказываясь от наборов символов, мы отказываемся и от полезных свойств, которые они с собой несут. Прежде всего, использование CHARACTER SET, а, точнее, их COLLATION ORDERS (способов упорядочения) позволяет корректно сортировать русские символы и приводить их к верхнему регистру.

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

Вносим ясность

 Сделать закладку на этом месте книги

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

Основа всего - символьные типы InterBase, которые позволяют хранить в каждом поле до 32767 байт. Однако байты в общем случае не равнозначны символам, потому что в различных системах кодировки для представления одного символа могут использоваться 1, 2 или даже 3 байта. Таким образом, когда мы определяем поле типа CHAR или VARCHAR, то мы задаем количество символов, которые там могут поместиться, а количество байтов определяется умножением заданной длины на максимальный размер символа для данного набора символов Для набора символов WIN1251 любой символ занимает 1 байт, поэтому размер поля в байтах будет равен объявленной длине. А вот при использовании кодировки UNICODE_FSS максимальный размер символа составляет 3 байта; таким образом, реальная длина поля будет равна утроенному объявленному размеру. Вы не сможете создать поле с кодировкой UNICODE_FSS с длиной более чем 32767 div 3 = 10921 символ. В то же время реальный размер символа может быть меньше максимального, использовавшегося InterBase для расчета размера поля, т. е. вы сможете записать в такое поле больше символов, чем его объявленная длина!

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

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

Когда мы создаем символьное поле и указываем набор символов явно или с помощью установок по умолчанию, то в этом случае мы неявно задаем, сколько символов поместится в это поле. Например, набор символов UNICODE_FSS использует 2 байта для кодирования русских букв. Следовательно, мы можем поместить в поле, объявленное как VARCHAR(255) CHARACTER SET UNICODE_FSS, количество русских символов, равное 255 * 3 div 2, т. е. 382.

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

Как же изменить сортировку символов по умолчанию внутри набора символов? Для этого применяются порядки (или способы) сортировки наборов символов - так называемые COLLATION ORDERS. Дополнительно с каждым COLLATION ORDER связаны таблицы преобразования в нижний и верхний регистр. Для каждого набора символов существуют свои определенные порядки сортировки Например, для самого распространенного русского набора символов WIN1251 существуют два способа сортировки: WIN1251, который задается по умолчанию, и опциональная сортировка PWX_CYRL. В порядке сортировки PXW_CYRL одни и те же прописные и строчные буквы имеют одинаковый вес, т. е. этот порядок сортировки не зависит от регистра символов (case insensitive). Русские буквы будут располагаться в следующем порядке: аАбБвВ...яЯ.

Как устроены способы сортировки? В COLLATION ORDERS строится дополнительная таблица пересортировки, определяющая порядок (условный вес) символов при сортировке. И в этой таблице для представления порядка символа может использоваться несколько байтов (2 или 3, например). В частности, в COLLATION ORDER WIN1251 используется 1 байт для представления символа и его порядка, а в опциональной PWX_CYRL - целых 3 байта!

Размер представления веса символа в таблице порядка сортировки имеет значение при использовании индексов по полям символьных типов данных. Дело в то, что в индексе хранятся не исходные символьные строки, а ключи, полученные из строки на основе таблицы сортировки. Размер ключа может быть больше размера исходной строки: так, для порядка сортировки PXW_CYRL размер ключа может быть больше максимум в 3 раза, чем исходная строка. InterBase использует максимальный коэффициент для ограничения размера индекса по символьным полям. Таким образом, при максимальном размере индекса 254 байта вы не сможете создать индекс по полю с длиной более 254 div 3 = 84 символа Поэтому использование COLLATION ORDER может оказаться вещью, требующей достаточно многих ресурсов. Тем не менее весь вопрос в том, как ее применять Параметр COLLATE можно использовать и по требованию, прямо в тексте запроса, не указывая его в самом определении поля Когда мы указываем в запросе использовать для сортировки или приведения к верхнему регистру какой-либо способ сортировки, то InterBase сам достраивает данные из этого поля с учетом указанного способа

Использовать конкретный COLLATE в запросе очень легко, правда, в этом случае не может быть использован индекс:


SELECT *

FROM table1

ORDER BY SYMBOLIC_FIELD1 COLLATE PXW_CYRL


Другой пример - для встроенной функции UPPER.


SELECT UPPER(fieldl COLLATE PXW_CYRL) From tablel


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

Русификация клиентских приложений InterBase

 Сделать закладку на этом месте книги

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

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

Способ указания того, какой набор символов использовать, различен для каждой библиотеки доступа. Для библиотек FEBPlus и ЕВХ для подключения к базе данных с набором символов, например, WIN125I, нужно указать в параметрах подключения lc_type=WIN1251.

Для работы с базами данных, использующими WIN1251, через Borland Database Engine необходимо указать в параметрах псевдонима LANGDRJVER=PDOX ANSI Cyrr.

Для работы с базой данных через JDBC необходимо указать в настройках драйвера строку charset=cp!251 (подробнее см. в главе "Разработка клиента InterBase на Java" (ч. 3)).

Собственные наборы символов и способы сортировки

 Сделать закладку на этом месте книги

Сами кодировки хранятся в файле gdsintl.dll, который находится в каталоге %INTERBASE%\Intl. Вы можете самостоятельно разрабатывать и подключать свои собственные наборы символов и COLATION ORDERS в InterBase и во все его клоны. Для их разработки существует специальный инструментарий, ссылки на который вы можете найти на сайте www InterBase-world.com.

Транзакции. Параметры транзакций

 Сделать закладку на этом месте книги

Концепция транзакций

 Сделать закладку на этом месте книги

Что такое транзакции?

 Сделать закладку на этом месте книги

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

Понятие транзакции само по себе чрезвычайно простое и очевидное.

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

Возможность отмены - только одно из свойств. Определение обычно дается очень обтекаемое, транзакция - последовательность операций с базой данных, логически выполняемая как единое целое. Транзакция обладает свойствами атомарности, согласованности, изоляции и долговременности (по-английски ACID - Atomicity, Consistency, Isolation, Durability).

Давайте рассмотрим более подробно это определение. Операции, о которых идет речь в определении - это INSERT/UPDATE/DELETE и, конечно, SELECT. Если транзакция объединяет какие-то операции в единый блок, то говорят, что эти действия выполняются в контексте  данной транзакции.

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

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

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

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

Прежде всего давайте внесем важное уточнение в определение транзакции:

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

Обратите внимание на смещение акцента в определении: "транзакция - это механизм..."! Именно от представления о транзакции как о механизме, выполняющем определенные функции, мы и будем отталкиваться в дальнейших рассуждениях.

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

* Механизм транзакций обязательно используется для ВСЕХ операций в базе данных (о некоторых особых случаях будет рассказано ниже). Возможно, некоторые разработчики, пользующиеся высокоуровневыми инструментами разработки приложений баз данных, могут заявить, что они никогда не применяли транзакции и не видят в них нужды. Но это всего лишь означает, что всю работу по управлению транзакциями брал на себя инструмент разработки (и вряд ли он управлял ими достаточно эффективно!).

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


убрать рекламу






оге - пользователь).

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

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

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

Изолированность транзакций

 Сделать закладку на этом месте книги

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

Целостное состояние базы данных - состояние, в котором база данных содержит корректную информацию, соответствующую правилам бизнес-логики, применяющимся в данном принижении

Отсюда следует еще одно определение транзакции:

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

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

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

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

Давайте поясним эти мысли с помощью такого примера.

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

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

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

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

Таким образом, можно сформулировать наиболее общее определение механизма транзакций:

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

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

Механизм транзакций в InterBase

 Сделать закладку на этом месте книги

Надо сказать, что реализация транзакций в InterBase отличается от реализации транзакции в большинстве других СУБД. Это связано с особой архитектурой баз данных InterBase, именуемой Multi Generation Architecture (MGA) - многоверсионной архитектурой.

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

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

Многоверсионная архитектура InterBase

 Сделать закладку на этом месте книги

InterBase - это первая в мире СУБД, в которой реализована многоверсионная архитектура. Именно многоверсионная архитектура позволяет организовать взаимодействие пользователей таким образом, что читающие пользователи не блокируют пишущих, а также дает возможность очень быстро восстанавливаться после сбоев в базе данных и отказаться от ведения протокола транзакций (transaction log), а также предоставляет массу других преимуществ.

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

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

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

Далее, транзакция может либо подтвердиться, либо отмениться. При подтверждении транзакции произойдет следующее: InterBase попытается пометить предыдущую (исходную) версию записи как удаленную и сделать текущую (измененную в рамках этой завершающейся транзакции) версию основной. Если только один пользователь менял запись, то именно так все и произойдет - измененная версия записи станет основной и именно ее увидят все остальные операции в транзакциях, которые запустятся позже подтверждения.

Предположим теперь, что после запуска описанной в примере транзакции (назовем ее № 1), но до подтверждения ее результатов запустится транзакция № 2, в которой пользователь желает прочитать изменяющуюся запись. Так как неподтвержденные в № 1 изменения нельзя увидеть (в том числе и в транзакции № 2) по крайней мере до подтверждения этой транзакции, то транзакция № 2 увидит предыдущую версию записи!

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

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

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

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

Реализация многоверсионности. Страницы учета транзакций

 Сделать закладку на этом месте книги

Каждая транзакция в InterBase имеет свой уникальный идентификатор - transaction ID или TID (фактически это номер транзакции с момента создания базы данных или последнего restore). Каждая транзакция, запускаясь, получает свой номер, который последовательно увеличивается - т. е. более старые транзакции имеют меньшие номера, чем новые.

Для учета транзакций используются страницы учета транзакций (Transaction Inventory Page, TIP). Когда в InterBase начинается транзакция, то на странице учета транзакций появляется отметка о том, что транзакция с определенным идентификатором ТГО# находится в состоянии выполнения (т. е. она является активной - active).

Всего имеется 4 возможных состояния транзакции: активная (active), подтвержденная (Commited), отмененная (Rolled back) и лимбо (limbo). Статус активной имеют выполняющиеся в данный момент транзакции.

Подтвержденные транзакции - обычно те транзакции, что завершились командой Commit. Отмененные транзакции - это обычно те транзакции, которые завершились командой Rollback.

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

Когда операция в контексте транзакции с идентификатором ТID# изменяет какие-либо записи, то для этого изменения создаются версии записей. И каждая версия помечается идентификатором ТID# - на физическом уровне это выглядит как номер транзакции в заголовке версии записи.

Если транзакция с номером ТID# подтверждается, т. е. переходит в состояние Commited (а процесс подтверждения часто называют commit), то на странице учета транзакций производится отметка об этом. При этом никаких действий над измененными записями не происходит!

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

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

Если транзакция, создавшая версию записи, откачена (т. е. находится в состоянии Rollbacked), то такая версия является мусором (garbage)  и ее необходимо удалить.

Если же транзакция, создавшая версию записи, подтверждена (находится в состоянии Committed), то такую запись можно считать полноценной претенденткой на самую актуальную запись.

"Почему претенденткой?" - может спросить уважаемый читатель. А потому что может быть две и больше версий записей, которые созданы подтвержденными на текущий момент транзакциями. И поэтому читающая запись выберет среди этих версий записей в качестве актуальной ту версию, в которой ТID# больше.

Сборка мусора

 Сделать закладку на этом месте книги

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

Главное, что следует отметить в сборке "мусора" -  это то, что она является кооперативной. Как вы поняли из предыдущего описания, транзакции, изменяющие данные, не "убирают за собой": когда происходит завершение, то на странице учета транзакций просто ставится отметка о том, что транзакция с определенным TID подтверждена (committed). При этом не происходит удаления старых версий записей.

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

Взаимодействие транзакций

 Сделать закладку на этом месте книги

Интересен процесс определения, является ли текущая версия мусором или, возможно, она еще нужна какой-то транзакции.

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

Итак, определения:

Заинтересованная транзакция -  это транзакция, конкурирующая с текущей.

Старейшая заинтересованная транзакция (oldest interesting transaction) -  это старейшая транзакция, конкурирующая с текущей транзакцией.

Каждая транзакция (и текущая тоже, разумеется) имеет "маску транзакций",  которая представляет собой снимок страницы учета транзакций, начиная от старейшей заинтересованной транзакции до текущей.

Старейшая активная транзакция (oldest active transaction, OAT) -  это транзакция, которая была активной в тот момент, когда запускалась самая старая из активных транзакций в момент запуска текущей.

Именно старейшая активная транзакция и занимается сборкой мусора, так как все остальные транзакции и их изменения "моложе" ее.

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

Разумеется, здесь приведено лишь краткое изложение вопросов, связанных с многоверсионной архитектурой InterBase и ее особенностями. На сайтах www.InterBase-world.com и www.ibase.ru читатель сможет ознакомиться с множеством статей по данной проблеме.

Уровни изоляции транзакций

 Сделать закладку на этом месте книги

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

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

Должен или нет - это определяется уровнем изоляции.

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

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

Транзакции в InterBase могут иметь 3 основных возможных уровня изоляции: READ COMMITTED, SNAPSHOT и SNAPSHOT TABLE STABILITY. Каждый из этих трех уровней изоляции определяет правила видимости тех действий, которые выполняются другими транзакциями. Давайте рассмотрим уровни изоляции более подробно.

* READ COMMITTED. Буквально переводится как "читать подтвержденные данные", однако это не совсем (точнее, не всегда) так. Уровень изоляции READ COMMITTED используется, когда мы желаем видеть все подтвержденные результаты параллельно выполняющихся (т. е. в рамках других транзакций) действий. Этот уровень изоляции гарантирует, что мы НЕ сможем прочитать неподтвержденные данные, измененные в других транзакциях, и делает ВОЗМОЖНЫМ прочитать подтвержденные данные.

* SNAPSHOT. Этот уровень изоляции используется для создания "моментального" снимка базы данных. Все операции чтения данных, выполняемые в рамках транзакции с уровнем изоляции SNAPSHOT, будут видеть только состояние базы данных на момент начала запуска транзакции. Все изменения, сделанные в параллельных подтвержденных (и разумеется, неподтвержденных) транзакциях, не видны в этой транзакции. В то же время SNAPSHOT не блокирует данные, которые он не изменяет.

* SNAPSHOT TABLE STABILITY. Это уровень изоляции также создает "моментальный" снимок базы данных, но одновременно блокирует на запись данные, задействованные в операциях, выполняемые данной транзакцией. Это означает, что если транзакция SNAPSHOT TABLE STABILITY изменила данные в какой-нибудь таблице, то после этого данные в этой таблице уже не могут быть изменены в других параллельных транзакциях. Кроме того, транзакции с уровнем изоляции SNAPSHOT TABLE STABILITY не могут получить доступ к таблице, если данные в них уже изменяются в контексте других транзакций.

Параметры транзакций

 Сделать закладку на этом месте книги

В первом разделе этой главы была сделана попытка рассмотреть механизм работы транзакций в СУБД InterBase в целом. Теперь необходимо рассмотреть практические аспекты применяющие транзакций в InterBase.

Программисты, использующие такие современные библиотеки для доступа к базам данных InterBase, как FIBPlus, IBProvider, IBX и IBObjects (см. главу "Обзор библиотек доступа к InterBase"), имеют возможность гибко управлять параметрами транзакций для получения наилучших результатов. Поэтому имеет смысл рассматривать параметры транзакций именно в интерпретации для этих библиотек.

Настройка параметров транзакции осуществляется с помощью перечисления набора констант, определяющих поведение транзакции, например, уровень изоляции. Эти константы пришли из InterBase API и имеют следующий вид: isc_tpb_read, isc_tpb_write, isc_tpb_ read_committed и т. д.

Обычно префикс isc_tpb_ опускается и константы для определения параметров транзакции пишутся без него.

Давайте рассмотрим значение и синтаксис применения каждой константы.

Виды параметров транзакции

 Сделать закладку на этом месте книги

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

Параметры транзакций 

Группы параметров

Константа

Краткое описание константы

Режим доступа

Read

Разрешает только операции чтения

write

Разрешает операции записи

Режим блокировки

Wait

Устанавливает режим отсроченного разрешения конфликтов. См. ниже раздел "Режим блокировки"

nowait

При возникновении конфликта немедленно возникает ошибка

Уровень

read_committed rec_version

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

read_committed no_rec_version

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

concurrency

При запуске транзакции создается мгновенный "снимок" состояния базы данных (точнее, копируется "маска транзакций" на этот момент), поэтому изменения, сделанные в других транзакциях, не видны в этой транзакции

consistency

Аналогичен уровню concurrency, но помимо этого блокирует таблицу на запись. См. ниже

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

Режим доступа

 Сделать закладку на этом месте книги

Режим доступа определяет, какие операции могут осуществляться в контексте транзакции. По умолчанию (т. е. если ничего не указывать) ставится режим чтения-записи, т. е. могут осуществляться любые операции. Часто задают вопрос, имеет ли смысл запускать транзакции в режиме только для чтения, если не предполагается операций по изменению данных. Ответ: да, имеет. Особенно в последних клонах InterBase 6.x - InterBase 6.5, Yaffil и Firebird. Транзакции с режимом доступа только для чтения меньше нагружают сервер, так как не создают лишних версий записей.

Для установки режима чтения-записи используется сочетание констант read write. Обычно константы записывают в столбик одну под другой, вот так: read write

Режим блокировки

 Сделать закладку на этом месте книги

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

Соответственно есть два варианта режима блокировки - wait и nowait. По умолчанию используется режим wait.

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

* Рассмотрим случай, когда транзакция А вставляет  запись, но еще не подтвердила ее. Затем в рамках другой транзакции, Б, делается попытка вставить запись с тем же самым первичным ключом, уже вставлена в транзакции А. Вот здесь и начинаются отличия между режимами блокировки:

* если транзакция Б запущена в режиме wait, то она будет ожидать завершения транзакции А, и если А завершится подтверждением (commit), то вставленная в Б запись будет признана неактуальной и возникнет ошибка Deadlock, а если она откатится (rollback), то изменения в транзакции Б будут приняты и она сможет подтвердить их (т. е. сделать commit);

* если транзакция Б запущена в nowait, то немедленно возникнет ошибка 'lock conflict on no wait transaction'.

* Рассмотрим другой случай: транзакция А изменила  запись, но еще не подтвердила ее изменения. Транзакция Б пытается удалить или изменять эту же самую запись. Опять влияет режим блокировки:

* если Б в режиме wait, то она будет ждать пока А не подтвердится или не отменится; если А подтвердится, то в Б возникнет ошибка 'Deadlock - update conflict with concurrent update', - потому как А подтвердила свои изменения, изменения в Б признаются неактуальными; если же транзакция А откатится, Б получит возможность подтвердиться;

* если Б в режиме nowait, то немедленно возникнет ошибка 'lock conflict on no wait transaction'.

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

Вы, вероятно, заметили, что в сообщении об ошибке конфликта блокировки фигурирует слово "deadlock", однако это слово выбрано не совсем удачно. В переводе с английского оно означает "мертвая блокировка", или "взаимоблокировка". В нашем случае, несмотря на грозное сообщение, никаких взаимоблокировок не возникает.

Что же такое взаимоблокировка на самом деле и когда она может возникнуть?

Взаимоблокировка

 Сделать закладку на этом месте книги

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

и два ресурса - А и В; в контексте разговора о базах данных ресурсами могут быть, например, записи в некоторой таблице. Допустим, выполняется такая последовательность действий:

* Транзакция Т1 блокирует ресурс А, после чего благополучно работает с ним.

* Транзакция Т2 блокирует ресурс В, после чего также с ним работает.

* Транзакция Т1 желает поработать с ресурсом В, для чего она пытается установить на него блокировку. Т


убрать рекламу






ак как в это время ресурс В уже занят транзакцией В, транзакция Т1 входит в состояние ожидания.

* Транзакция Т2 желает поработать с ресурсом А, пытается выполнить его блокирование и также переходит в состояние ожидания.

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

Алгоритм распознавания ситуации взаимоблокировки в InterBase не запускается сразу при возникновении конфликта, что сделано из соображений производительности. Вместо этого выдерживается определенный интервал времени, задаваемый параметром DEADLOCK_TIMEOUT в конфигурационном файле InterBase ibconfig, только после этого и производится сканирование таблицы блокировок на предмет взаимного блокирования.

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

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

Установка уровней изоляции

 Сделать закладку на этом месте книги

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

Как было сказано в разделе "Уровни изоляции", в InterBase есть 3 основных уровня изоляции. Теоретически существует также четвертый уровень изоляции, так называемое DIRTY READ - "грязное чтение". Транзакции с уровнем изоляции DIRTY READ могут читать неподтвержденные данные в других транзакциях. В InterBase пользователю нельзя запускать транзакции с таким уровнем изоляции, хотя теоретически многоверсионная архитектура могла бы обеспечить такой уровень изоляции.

Давайте перейдем к реально существующим в InterBase уровням изоляции. Сначала рассмотрим уровень Read Committed,  задающийся константой read_committed. Транзакция, запущенная с таким уровнем изоляции, может читать изменения, произведенные из параллельно выполняющихся транзакций.

Этот уровень изоляции часто используется для получения самого "свежего" состояния базы данных.

Как видно из таблицы 1.4, существуют две разновидности этого уровня изоляции: read_committed rec_version и read_committed no_rec_version. По умолчанию используется вариант с параметром rec_version. Это означает, что при чтении какой-либо записи просто считывается последняя подтвержденная версия , записи.

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

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

* Если wait, то наша транзакция ждет, пока не завершится транзакция, в которой создана неподтвержденная запись. И если она подтвердилась или отменилась, то считывается последняя подтвержденная версия.

* Если блокировка nowait, то немедленно возникает ошибка "Deadlock".

Очевидно, что уровень изоляции read_committed no_rec_version может привести к множеству конфликтов, и использовать его следует с большой осторожностью.

Уровень изоляции SNAPSHOT  задается параметром concurrency. Можно сказать, что SNAPSHOT - самый "родной" режим InterBase, при котором преимущества версионности проявляются наиболее полно. При его использовании транзакция делает "снимок" маски транзакций в базе данных на момент запуска, и поэтому, пока она длится, видит те же самые данные, которые существовали на момент ее запуска. Никакие изменения, которые делаются параллельно выполняющимися транзакциями, ей не видны. Ей видны только свои изменения. При попытке в этой транзакции изменить данные, измененные другими транзакциями уже после ее запуска (имеются в виду как уже подтвержденные, так и еще неподтвержденные данные), возникает конфликт.

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

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

Уровень изоляции SNAPSHOT TABLE STABILITY  задается параметром consistency. Этот уровень изоляции аналогичен уровню SNAPSHOT, но дополнительно блокирует таблицу на запись. Суть идеи проста - если транзакция с уровнем изоляции consistency проводит изменения на какой-либо таблице, то транзакции с уровнями изоляции read_committed и concurrency могут только читать эту таблицу, а транзакции с таким же уровнем изоляции (т. е. consistency) не смогут даже читать.

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

Рекомендации по использованию параметров транзакций

 Сделать закладку на этом месте книги

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

Рассмотрим типичное приложение базы данных, с помощью которого пользователь желает читать и изменять данные. В приложении имеется сетка (dbGrid в Delphi/C++Builder), в которой пользователь просматривает текущее содержание какой-то таблицы. Сетка содержит lookup-поля, которые заполняются значениями из справочников. Когда пользователь находит запись, которую нужно изменить (или просто желает добавить запись в таблицу), то он нажимает кнопку добавления/редактирования и в появившемся диалоге заполняет/изменяет поля записи и затем сохраняет/отменяет редактирование.

Как же настроить транзакции для такого приложения?

Для запроса SELECT. ., который читает данные в сетку, следует использовать транзакцию с доступом "только для чтения" с уровнем изоляции READ COMMITED, чтобы получить самые "свежие" данные из таблицы, как только они будут обновлены/добавлены (не надо забывать о том, что наше приложение многопользовательское и одновременно могут работать несколько приложений). Примерный набор параметров такой:


read

read_committed

rec_version

nowait


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

Такую транзакцию можно длительное время держать открытой - сервер не нагружается версиями записей.

Для запроса на изменение/добавление данных можно использовать транзакцию с уровнем изоляции concurrency. Запрос на обновление в этом случае должен быть очень коротким: пользователь заполняет необходимые поля, запускается транзакция, делается попытка выполнить запрос, и затем, если не возник н> конфликта на запись с другой транзакцией, подтверждение нашей транзакции или откат, если был конфликт (на уровне клиентского приложения конфлнмы проявляются в виде исключений, которые удобно отлавливать с помощью коп струкций try.. .except или try.. .catch)

Параметры такой транзакции будут следующими:


write

concurrency

nowait


Такой набор параметров позволит нам сразу (nowait) выявить то, что запись редактируется/изменяется другим пользователем (возникнет ошибка), а также предотвратить попытки других пользователей начать изменение записей, трансформированных нашей транзакцией (у претендента возникнет ошибка "update conflict"). Надо отметить, что перед редактированием нужно перечитать запись, потому что она могла быть изменена, а в кеше сетки может все еще находиться старая версия

Для запросов, которые применяются для построения отчетов, однозначно нужно использовать транзакцию с режимом доступа "только для чтения" и с уровнем изоляции concurrency:



read

concurrency

nowait


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

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

За пределами транзакций

 Сделать закладку на этом месте книги

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

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

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

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

рации вставки и выборки (INSERT/SELECT). Отсутствие обновлений во внешних таблицах позволяет отказаться от хранения версий записей в этих таблицах, поэтому там всегда находятся актуальные данные, что позволяет отказаться от применения механизма транзакций для работы с данными в этих таблицах.

Двухфазное подтверждение транзакций

 Сделать закладку на этом месте книги

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

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

Такая транзакция запустится сразу на двух серверах. Чтобы синхронизировать процесс завершения этой транзакции, вводится особое состояние, называемое Prepared. Это состояние означает, что транзакция завершилась на одном сервере и готова перейти в состояние Committed, как только транзакции на остальных серверах также перейдут в состояние Prepared. Если же транзакция хотя бы на одном из участвующих в процессе серверов завершится Rollback, то все транзакции из состояния Prepared тоже откатятся.

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

Не следует использовать двухфазное подтверждение транзакций на серверах, соединенных медленными каналами связи (модемами, например).

Заключение

 Сделать закладку на этом месте книги

Транзакции - один из наиболее сложных для понимания и объяснения вопросов в разработке приложений баз данных, независимо от того, о каком сервере баз данных идет речь. Поэтому изучение их применения является необходимой задачей для каждого разработчика приложений баз данных, если он хочет достигнуть вершин мастерства в своей профессии. Материал данной главы дает необходимые минимальные сведения о транзакциях и пищу для размышлений, однако для полного понимания транзакций следует обратиться к специальным статьям, посвященным различным аспектам этого вопроса. Эти статьи всегда можно найти на сайтах www.InterBase-world.com и www.ibase.ru.

Обзор библиотек доступа к InterBase

 Сделать закладку на этом месте книги

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

Основа библиотек доступа к InterBase

 Сделать закладку на этом месте книги

Какова бы ни была библиотека доступа, для какой бы среды разработки она ни предназначалась, в любом случае основой является InterBase API.  InterBase API предоставляет базовый набор функций низкого уровня для работы с базами данных. Таким образом, любая библиотека доступа представляет собой "обертку" (wrapper) над функциями API. Библиотеки доступа организовывают функции API в соответствии с идеологией сречы разработки дня которой предназначена библиотека.

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

Библиотеки доступа

 Сделать закладку на этом месте книги

В мире приложений баз данных сложилось несколько устоявшихся подходов к работе с базами данных. Для InterBase можно выделить около пяти таких подходов: это работа с базой данных через BDE (Borland Database Engine, см. глоссарий), использование ODBC, применение OLE DB (ADO), работа через dbExpress и библиотеки прямого доступа.

Долгое время использование BDE для доступа к базам данных InterBase было фактически монопольной технологией. BDE представляет обобщенный механизм работы сразу со многими SQL-серверами, в том числе и с InterBase. Технология BDE поддерживалась в основном в продуктах компании Borland: Delphi, C++ Builder и др. Преимущество BDE, состоящее в универсальности подхода к работе с различными SQL-серверами, что значительно облегчает перенос приложений с одного сервера на другой, является также и его недостатком. Прежде всего это невозможность воспользоваться уникальными особенностями каждого SQL-сервера. Реализованная в BDE модель управления транзакциями, основной целью которой было облегчить перенос приложений с Paradox, не отвечала требованиям современных клиент-серверных приложений. Таким образом, в целом BDE не может рассматриваться как эффективная библиотека доступа именно к InterBase, хотя, как уже было сказано, в определенных условиях данный механизм может оказаться удобным. Тем не менее в этой книге мы не будем рассматривать работу с InterBase через BDE, тем более что этот вопрос освещается практически в любой книге о разработке приложений баз данных при помощи Borland Delphi.

ODBC (Open Database Connectivity) является одним из наиболее распространенных стандартов, которые обеспечивают доступ к базам данных. В огличие от BDE, ODBC позволяет разрабатывать приложения баз данных InterBase практически в любых средах программирования, а не только в продуктах компании Borland. В настоящее время существует несколько ODBC-драйверов, поддерживающих все функции семейства InterBase 6.x и эффективно реализующих работу с базами данных. Работа с одним из наиболее распространенных ODBC- драйверов - Gemini - коротко описана в данной книге.

Несмотря на существование ODBC, корпорация Microsoft в настоящее время продвигает новый ключевой механизм доступа - технологию OLE DB. Разумеется, существуют продукты, поддерживающие эту технологию для InterBase. В данной книге мы рассмотрим, в частности, IBProvider - мощную OLE DB-библиотеку для работы с InterBase. Разработчики приложений на Visual C++, Visual Basic, ASP и других популярных систем могут создавать с помощью IBProvider эффективные приложения, использующие все преимущества как технологии OLE DB, так и СУБД InterBase. В числе уникальных возможностей IBProvider хочется отметить возможность прозрачной интеграции данных баз данных InterBase с базами данных MS SQL Server.

Технология dbExpress, появившаяся в последних версиях продуктов компании Borland (Delphi, C++Builder и Kylix), позволяет проектировать приложения, переносимые между различными SQL-серверами, и в то же время в полной мере использовав уникальные особенности каждого из серверов. Тем не менее на сегодняшний день существует не так много приложений, использующих эту новую технологию, поскольку разработчики все еще предпочитают устоявшиеся методы работы с базами данных InterBase. Мы не станем рассматривать использование dbExpress, хотя возможно, что этот вопрос будет включен в следующие издания этой книги.

И наконец, мы можем сказать несколько слов о библиотеках прямого доступа - наиболее эффективном способе работы с базами данных InterBase. Используя такие библиотеки, можно добиться минимального количества "прослоек" между непосредственно кодом клиентского приложения и вызовами InterBase API. Это позволяет получить в приложениях максимальную производительность; полную поддержку всех возможностей InterBase, а также минимальный объем конечного продукта - для работы приложений на базе библиотек прямого доступа требуется лишь минимальная установка клиента InterBase (см. главу "Установка InterBase - взгляд изнутри" (ч. 4)).

В нашей книге подробно рассмотрено применение наиболее прогрессивной, по мнению авторов, библиотеки доступа - Devrace™ FIBPlus™, которая поддерживает все версии InterBase, начиная с 4.x, а также все клоны InterBase: Firebird и Yaffil.

Несомненно, нельзя забывать про использование баз данных в приложениях, ориентированных на Интернет. Прежде всего это работа с базами данных в Java, а также в CGl-приложениях. Этим вопросам в книге посвящены две главы, рассматривающие простые примеры работы с базами данных InterBase.

Для популярных языков Perl, Python и РНР также существуют свои собственные библиотеки доступа к базам данных InterBase, с помощью которых можно легко построить динамический сайт с поддержкой базы данных. Эги библиотеки перечислены ниже в таблице 1.6, содержащей общий список всех библиотек доступа к InterBase.

Завершая этот обзор, необходимо добавить, что большинство библиотек доступа поддерживают возможность разработки кросс-платформенных клиентских приложений, которые можно легко переносить с Windows на Linux или другие ОС.

Список библиотек доступа к InterBase

 Сделать закладку на этом месте книги

Широкое распространение InterBase и его клонов по всему миру и использование в самых различных ипостасях привело к тому, что было создано множество библиотек доступа к InterBase/Firebird, ориентированных на самые различные среды программирования. Ниже, в таблице 1.6, приведен список наиболее популярных продуктов:


Библиотека

Краткое описание

Где взять

Free IB Components (FIBC)

Четыре компонента, написанные в 1998 г. Грегори Детцем. Идеи, заложенные в FIBC, послужили основой для создания библиотек FIBPlus и IBX

ftp.ibphoenix.com /download

FIBPlus

Библиотека прямого доступа, оформленная в виде компонентов, применяется в Delphi 3-6, C++ Builder 3-6, Kylix. Поддерживает интеграцию со стандартными data-aware-компонентами. Основана на коде Free IB Components

www.fibplus.net

IBObjects

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

www.ibobjects.com

InterBase Express (IBX)

Набор компонентов для работы с базами данных InterBase, позволяющий использовать data-aware- компоненты для представления данных. Продукт основан на коде FreelBComponents и входит в стандартную поставку Borland Delphi/C++ Builder Enterprise Edition

codecentral.borland.com/ codecentral/ccweb.exe/ author?authorid=102

Zeos Database Objects

Набор компонентов для работы с различными серверами баз данных, в том числе и InterBase. Позволяет использовать стандартные data- aware-компоненты для представления данных, а также содержит свои собственные визуальные компоненты

www.zeoslib.org

SQLAPI++

Библиотека классов для C++, позволяющая работать со многими SQL- серверами, в том числе и InterBase

www.sqlapi.com

Open Source Firebird and InterBase ODBC Driver

ODBC-драйвер для InterBase/Firebird. Существует в виде открытых кодов. Также поддерживает возможность организации "моста" ODBC-JDBC

www.ibphoenix.com /ibp_60_odbc.html

Gemini InterBase ODBC Driver

ODBC-драйвер, поддерживает все версии InterBase, начиная с 4.x, а также предоставляет поддержку всех возможностей InterBase 6.5 и Firebird 1.0

www.ibdatabase.com

IBProvider

OLE DB-провайдер для доступа к базам данных InterBase. Полностью поддерживает все свойства InterBase б.х/Firebird 1.0

WWW. lbprovider.com

SIBProvider

OLE DB-провайдер для доступа к базам данных InterBase

www.sibprovider.com

IBPerl

Объектно-ориентированная библиотека для PerlS для работы с базами данных InterBase

www.karwin.com/ibperl

DBD .InterBase

DBI-драйвер для InterBase

dbi-lnterBase.sourceforge.net

PHPLib for InterBase

Библиотека доступа к InterBase для языка РНР

www.intelicom.si

ADODB - InterBase PHP4 Database Wrapper

Библиотека доступа к InterBase для языка РНР

php.weblogs.com/adodb

Zope Driver for InterBase

Библиотека доступа к InterBase для языка Python

www.zope.org/Members /mcdonc/RS/lnterBaseStorage

InterClient

JDBC-драйвер для доступа к базам данных InterBase из Java

Включен в стандартную поставку Borland InterBase

Самый полный и "свежий" список библиотек доступа всегда можно найти на сайте поддержки данной книги www.InterBase-world.com, а также на сайте www.ibase.ru

Разработка приложений баз данных InterBase на Borland Delphi/C++ Builder/Kylix

 Сделать закладку на этом месте книги

Что такое InterBase Express?

 Сделать закладку на этом месте книги

Пользователи Borland Delphi 5-7 и Borland C++ Builder 5-6 редакций Professional и Enteiprise наверняка уже обратили внимание на закладку InterBase в палитре компонентов. Именно эта закладка и представляет собой набор компонент под общим названием InterBase Express или IBX. Это компоненты для работы с базами данных InterBase, которые используют прямое InterBase API, т. е. обращаются к серверу непосредственно, без каких-то промежуточных (middle-ware) средств.

Фактически если разрабатывать приложения баз данных с использованием IBX, то для работы таких приложений нужно лишь наличие GDS32.DLL на диске в доступном месте.

Официально разработка IBX ведется в рамках IPL - InterBase Public License, т. е. компоненты доступны в полных исходных текстах и вы можете использовать их совершенно бесплатно. Однако фактически данные компоненты разрабатываются только сотрудниками корпорации Borland, совместимы только с определенными версиями и редакциями Delphi и C++ Builder и недоступны для публичной разработки, как это принято в обычных Open Source-проектах.

Компоненты IBX позволяют разрабатывать приложения, ориентированные на идеологию и архитектуру InterBase. К особенностям IBX можно отнести:

* явное управление транзакциями;

* поддержке расширений InterBase 6.0-7.0;

* поддержку event-alerters;

* использование генераторов для значений ключевых полей;

* управление сервером через Services API;

* поддержку стандартных и сторонних визуальных компонентов отображения данных;

* поддержку встроенных и сторонних генераторов отчета;

* совместимость с Delphi 5-7, C++ Builder 5-6 и Kylix

Основой кода IBХ является библиотека FreelBComponents, написанная Грегори Дилтцом в 1998 году. Основные изменения, сделанные в Borland, касались поддержки нового стандарта идентификаторов в SQLDialect 3, а также приведение компонентов к виду, аналогичному существовавшим компонентам (мы поясним суть этой аналогии позднее). Теперь с выходом каждой очередной версии Delphi или C++ Builder IBX включается в поставку, однако все равно желательно проверять наличие исправлений или дополнительных сборок на сайте https://codecentral.borland.com.

Общее описание основных компонентов, включенных в состав IBX

 Сделать закладку на этом месте книги




TIBDatabase - предназначен для подключения к базе данных. Основные методы: Open, Close.





TIBTransaction - предназначен для явного управления транзакцией. Основные методы: StartTransaction, Commit, Rollback, CommitRetaining, RollbackRetaining.





TIBTable - аналог стандартного TTable. Компонент предназначен для получения данных из одной таблиц


убрать рекламу






ы или представления базы данных. Основное свойство - TableName. Основные методы: Open, Close. Набор данных, полученных при помощи TIBTable, является редактируемым, если речь идет о таблице базы данных или обновляемом представлении. Компонент совместим с визуальными компонентами.





TIBQuery - аналог стандартного TQuery. Компонент предназначен для получения данных на основе SQL-запроса. Этот набор данных не всегда будет редактируемым, зачастую необходимо использовать дополнительный компонент TIBUpdateSQL, чтобы иметь возможность редактировать полученные сведения. Основное свойство - SQL. Основные методы: Open, Close, ExecSQL. Компонент совместим с визуальными компонентами.





TIBDataSet - предназначен для получения и редактирования данных, является потомком стандартного класса TDataSet и полностью совместим со всеми визуальными компонентами. Основные методы: Prepare, Open, Close, Insert, Append, Edit, Delete, Refresh.





TIBStoredProc - предназначен для выполнения хранимых процедур и получения набора данных на основе результатов выполнения процедуры. Получаемый набор данных является нередактируемым. Компонент совместим с визуальными компонентами. Основное свойство - StoredProcName. Основной метод - ЕхесРгос.





TIBUpdateSQL - аналог TUpdateSQL. Используется в паре с TIBQuery и предназначен для создания модифицируемых наборов данных. Основные свойства: DeleteSQL, InsertSQL, ModifySQL и RefreshSQL.





TIBSQL - предназначен для выполнения SQL-запросов. В отличие от TIBQuery или TIBDataSet, TIBSQL не имеет локального буфера для набора данных и несовместим с визуальными компонентами.





TIBDatabaselnfo - позволяет получить системную информацию о некоторых свойствах базы данных, соединения и сервера. Например, UserNames - список пользователей, подключенных к базе данных, PageSize - размер страницы базы данных.





TIBSQLMonitor - предназначен для перехвата и отслеживания всех запросов, которые выполняют приложения, использующие ШХ.





TIBEvents - предназначен для получения пользовательских событий InterBase. Основное свойство - Events. Основные методы: RegisterEvents, UnresisterEvents.

Компоненты-оболочки для Services API

 Сделать закладку на этом месте книги




TIBConfigService - предназначен для настройки параметров базы данных.





TIBBackupService предназначен для создания резервных копий (backup) баз данных.





TIBRestoreService - предназначен для восстановления базы данных из резервной копии.





TIBValidationService - предназначен для проверки целостности базы данных и согласования внутренних данных о транзакциях.





TIBStatisticalService - предназначен для получения статистики о базе данных.





TIBLogService - предназначен для создания и просмотра лог-файла работы сервера.





TIBSecurityService - предназначен для редактирования списка пользователей на сервере.





TIBLicensingService - предназначен для добавления и удаления сертификатов, регулирующих количество и свойства клиентских подключений к серверу InterBase.





TIBServerProperties - предназначен для получения информации о сервере, параметров конфигурации и т. д.





ТIBInstall - предназначен для установки InterBase installation-компонента.





TIBUnlnstall - предназначен для установки InterBase un-installation компонента.

Использование основных компонентов InterBase eXpress (IBX)

 Сделать закладку на этом месте книги

Исторически сложилось так, что первое издание книги не содержало материалов по IBX. То есть данная глава написана специально для второго издания. После выхода книги мы получили ряд отзывов, которые наглядно показали нам, что многие разработчики (особенно те, кто впервые работает с ЮХ или FffiPlus) не представляют, как в целом взаимодействуют компоненты ЮХ между собой. В итоге, несмотря на аккуратное воспроизведение всех примеров из главы по FffiPlus, некоторые программисты не могут ни на шаг отойти от описанных ситуаций, поскольку спотыкаются буквально на совершенно очевидных вопросах. Чтобы осветить технологию несколько с других позиций, мы решили спланировать материал этой главы немного иначе, чем это было сделано с материалами по FffiPlus

Иерархия компонентов в IBX

 Сделать закладку на этом месте книги

Поскольку вы работаете с Delphi (или с C++ Builder), то предполагается, что вы знакомы с объектно-ориентированным программированием Таким образом, разобравшись, как именно и от кого унаследованы различные компоненты IBX, можно будет более полно представить себе, как именно их нужно использовать. Рассмотрим рис. 2.1.







Рис 2.1. Иерархия компонентов IBX

Внимательно рассмотрев эту схему, можно сделать сразу несколько выводов.

Во-первых, очевидна несовместимость IBX с версиями Delphi меньше 5, поскольку класс TCustomConnection появился лишь в Delphi 5.

Во-вторых, становится ясно, почему компонент TIBSQL невозможно использовать вместе с визуальными db-aware-компонентами вроде TDBGrid или TDBEdit. Все стандартные визуальные db-aware-компоненты работают только с потомками класса TDataSet. Поэтому для db-aware компонентов невозможна связка с TIBSQL, который не унаследован от TDataSet.

Из той же схемы видно, что в IBX есть компоненты, совместимые с db-aware-компонентами (TDBGrid и т. д.). Это потомки внутреннего класса TIBCustomDataSet - TIBDataSet, TIBTable, TIBQuery и TIBStoredProc Вообще говоря, почти вся данная "ветка" классов по своему назначению близка к аналогичной ветке компонентов для работы с BDE - TTable, TQuery и TstoredProc - и предназначена для "быстрой" миграции старых приложений с BDE на IBX.

Также следует обратить внимание на компонент TIBUpdateSQL, который является аналогом компонента TUpdateSQL, предназначенного для работы с BDE

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

Прежде всего это связано с управлением транзакциями и обработкой большого котичества записей Ниже мы остановимся на этом вопросе подробнее, а теперь перейдем к рассмотрению особенностей компонентов TIBTable, TIBQuery и TIBStoredProc

Особенности TIBTable, TIBQuery и TIBStoredProc

 Сделать закладку на этом месте книги

Фактически, компонент TIBCustomDataSet имеет всю необходимую функциональность для получения базы данных InterBase и поддерживает возможность редактирования этой информациии с помощью визуальных db-aware- компонентов

Для выборки данных, их изменения, удаления и вставки в TIBCustomDataSet используется набор свойств, представляющих собой SQL-запросы для манипулирования данными, - это SelectSQL, DeleteSQL, InsertSQL и ModifySQL.

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

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

В свойствах DeleteSQL, InsertSQL и ModifySQL указываются соответствующие запросы, которые будут вызываться автоматически самим компонентом при вызове операций Delete, Insert и Edit для удаления, вставки и редактирования записей.

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

TIBTable

 Сделать закладку на этом месте книги

Компонент ТШТаЫе прячет все указанные выше свойства, а вместо этого пользователю предоставляется свойство TableName. Пользователь указывает имя таблицы в свойстве TableName, а компонент автоматически формирует набор "спрятанных" запросов.

Например, для таблицы с именем Tablel запрос в SelectSQL будет иметь вид:

SELECT * FROM Tablel

Легко представить, что в нашей таблице несколько миллионов записей и этот запрос попытается получить их их в полном объеме на клиента. Например, при вызове Locate, который так любят пользователи BDE, если запись, соответствующая условиям поиска, не найдена в загруженном наборе записей, то TIBTable будет запрашивать оставшиеся записи, пока не найдется подходящая запись или пока не закончатся записи в таблице.

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

TIBQuery

 Сделать закладку на этом месте книги

Аналогично ТГВТаЫе-компонент TIBQuery скрывает запросы для получения и редактирования данных. Вместо скрытого в этом компоненте свойства SelectSQL разработчику предлагается использовать свойство SQL. На самом деле после присвоения свойства SQL компонент присваивает его значение свойству SelectSQL.

Но самое примечательное с точки зрения проектирования классов начинается тогда, когда мы хотим сделать наш запрос редактируемым (live-query).

Поскольку свойства DeleteSQL, InsertSQL и ModifySQL спрятаны, то TIBQuery сам по себе не может предоставить разработчику редактируемые запросы.

Однако, как уже было сказано, TIBQuery был сделан как аналог TQuery и для полной аналогии в ГВХ введен компонент TIBUpdateSQL. Он содержит собственные свойства DeleteSQL, InsertSQL и ModifySQL и может подключаться к TIBQuery. После чего TIBQuery начинает использовать свойства компонента TIBUpdateSQL для редактирования собственных данных! Получается, что готовую функциональность TIBCustomDataSet, уже заложенную в него с самого на- чача, приходится дублировать в отдельном компоненте.

Отметим, тем не менее, что в данной связке имеется несомненный смысл, если речь идет о миграции готовых BDE-приложений на IBX. В общем-то, вероятно, только ради этого данные классы и были введены

Мы можем принять это как выгоду с точки зрения замены старых BDE-компонентов на нечто похожее из IBX, однако, если вы пишете новое приложение, которое с самого начала базируется на IBХ, мы бы рекомендовали вам использовать TDBDataSet как вместо ТIBTable, так и вместо TffiQuery.

Некоторые разработчики используют TIBQuery для выполнения только модифицирующих запросов, т. е. тех, которые не возвращают результата. Например, это может быть запрос с предложением INSERT или DELETE.

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

Поэтому для выполнения запросов, которые не возвращают результатов (в том смысле, что не возвращают набор результирующих строк), не имеет смысла использовать компоненты, предназначенные для взаимодействия с визуальными db-aware- компонентами. Вместо этого лучше использовать компонент TIBSQL, который не буферизует получаемые данные и предназначен именно для простого и быстрого выполнения SQL-запросов, в частности не возвращающих набор строк.

TIBStoredProc

 Сделать закладку на этом месте книги

Данный компонент предназначен для выполнения исполняемых (executed) хранимых процедур. Он также является потомком TffiCustomDataSet и полностью совместим с визуальными компонентами.

Являясь прямым наследником TffiCustomDataSet, компонент TffiStoredProc прячет все основные свойства предка и добавляет такое свойство, как ProcedureName.

Следует понимать, что в данном случае слово "прячет" относится больше к идеологии проектирования классов, нежели к технической стороне дела. Очевидно, что спрятать свойства предка в Object Pascal нельзя. TIBCustomDataSet уже имеет все необходимые свойства, которые уже готовы для публикации, их просто не вынесли в секцию published. И хотя мы согласимся, что в данном случае нельзя однозначно сказав, насколько правильным является данное проектирование классов, однако считать его красивым довольно трудно.

В результате указания названия процедуры (например, procl) компонент сформирует SQL-запрос следующего вида:

EXECUTE PROCEDURE Proс1

В целом возможно, что проще вызвать хранимую процедуру с помощью соответствующего SQL-запроса, используя TIBDataSet, если мы хотим показать результат в db-aware-компонентах, или TIBSQL, если мы хотим вызывать хранимую процедуру вида executable (т. е. не возвращающую набор данных).

Единственным преимуществом использования TIBStoredProc по сравнению с TIBDataSet является тот факт, что TIBStoredProc самостоятельно формирует список параметров процедуры по ее имени, обращаясь к системным таблицам.

Подключение к базе данных

 Сделать закладку на этом месте книги

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

Для наших примеров в этой главе мы будем использовать базу данных Employee.gdb, которая поставляtnся вместе с InterBase.  

Поместите TIBDatabase на форму и вызовите контекстное меню (рис. 2.2).







Рис. 2.2. Вызов редактора IBDatabase

Из рисунка видно, что у TIBDatabase существует специальный редактор, который позволяет вам настраивать определенные параметры компонента во время разработки приложения (рис. 2.3).







Рис. 2.3. Редактор TIB Database

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

Немного подробнее относительно использования удаленного сервера. Предположим, что InterBase установлен у вас в сети на компьютере с сетевым именем SERV_IB и доступен по протоколу TCP/IP. Пусть база данных находится на сервере на диске Е: в файле MY_DB.GDB. В данной ситуации в редакторе компонента вам надо будет указать тип подключения Remote, имя сервера SERV_1B, а путь к базе данных оставить локальным относительно сервера, т. е. , ЕЛ MY_DB.GDB. Это важный момент! Вы указываете серверу путь к локальному файлу базы на сервере, а не указываете вашей программе сетевой путь к базе данных. В сущности, именно об этом говорится в главе "Создаем базу данных" в разделе "Строка соединения", а здесь мы лишь хотим еще раз подчеркнуть, что данный редактор создает точно такую же строку соединения, которую вы вводите вручную, например, при использовании инструментов администратора и разработчика InterBase.

Вы можете протестировать параметры подключения не выходя из диалога при помощи кнопки Test. Если все указано правильно, то вы увидите сообщение Successful connection.

Управление транзакциями

 Сделать закладку на этом месте книги

Необходимо помнить, что любое действие с базой данных происходит в рамках той или иной транзакции. Работа с InterBase основана на явном управлении транзакциями, а поскольку библиотека IBX - это обертка вокруг соответствующих функций InterBase API, то использование этих компонентов также предполагает, что программист явным образом будет управлять транзакциями из своего приложения. Для контроля транзакций в IBX существует специальный компонент TIBTransaction (рис. 2.4):







Рис 2.4. Свойства TIBTransaction

Как видно из рисунка, компонент не слишком перегружен свойствами. Фактически основным является свойство Params, в котором можно указать уровень Изоляции транзакции. Для этого необходимо знать соответствующие системные •константы из InterBase API. Однако для большинства ситуаций вам вполне хватит использования одного из четырех заранее заданных уровней изоляции. Для того чтобы выбрать один из них, можно вызвать редактор компонента (рис. 2.5).







Рис. 2.5. Вызов редактора TIBTransaction

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







Рис 2.6. Редактор TIBTransaction

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

TIBTransaction ссылается на компонент базы данных при помощи свойства DetaultDatabase. Если также указать свойство DefaultTransaction у TIBDatabase, то в дальнейшем любые компоненты (TIBDataSet, TffiSQL и т. д.), которые ссылаются на TIBDatabase, будут автоматически "подхватывать" и указанную транзакцию.

Теперь рассмотрим свойства AllowAutoStart и AutoStopAction. Как вам уже известно, любой запрос к базе данных должен выполняться в контексте транзакции. То есть, прежде чем выполнить даже простейший запрос вида SELECT * FROM TABLE 1, необходимо предварительно запустить транзакцию при помощи вызова IBTransaction.StartTransaction.

Такой "ручной" вызов не всегда удобен. Более того, каждый раз совершенно определенно известно: если мы хотим выполнить запрос, то мы должны запустить транзакцию. Чтобы избежать лишнего кода, связанного с запуском транзакций, можно установить значение свойства AllowAutoStart равным True. В этом случае если мы попробуем, например, открыть TIBDataSet, то он сам автоматически запустит соответствующую транзакцию.

Аналогичный смысл имеет свойство AutoStop Action. Когда мы закрываем все запросы, выполнявшиеся в контексте автоматически запущенной транзакции, то TIBTransaction выполняет действие, указанное в AutoStopAction. Например, автоматически подтверждает всю транзакцию при помощи метода Commit, если свойство AutoStopAction равно saCommit. Таким образом, разработчику предоставляется возможность указать, как компоненты должны автоматически взаимодействовать друг с другом!

Выполнение запросов при помощи TIBDataSet

 Сделать закладку на этом месте книги

В данной главе сознательно не рассматривается работа с TIBTable или TIBQuery, так как авторы книги не считают их использование целесообразным. Если вы все же желаете использовать эти компоненты (например, для облегчения миграции приложения с BDE), то для из>че- ния их функций можно обратиться к документации по аналогичным BDE-компонентам, а также к документации по IBX.

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

IBDataSetl: TIBDataSet;

DataSourcel: TdataSource

DBGridl: TDBGrid

Необходимо указать свойства Database и Transaction у IBDataSetl, задать DataSourcel.DataSet равным IBDataSetl, а также указать DBGridl.DataSource равным DataSourcel.

Теперь необходимо указать тот запрос, который мы хотим выполнить, в свойстве SelectSQL у IBDataSetl (рис. 2.7).







Рис 2.7. Редактор свойства SelectSQL

Теперь мы можем открыть запрос прямо в design-time, задав свойство Active в True. Результат наших действий приведен на рис. 2.8.







Рис 2.8. Открытие запроса в IBDataSet1

Чтобы получить то же самое во время выполнения программы, напишите следующий обработчик события OnFormCreate у основной формы приложения'

procedure TForml.FormCreate(Sender: TObject);

begin

IBDatabasel.Open;

IBDataSetl.Open;

end;

He забудьте закрыть подключение к базе данных перед сохранением проекта, иначе попытка IBDatabase I .Open вызовет ошибку. Вы также можете написать этот код иначе: IBDatabasel Connected := True; IBDataSetl.Active := True. Он не вызовет сообщения об ошибке, если IBDatabasel и IBDataSetl были активны в design-time.

Редактируемые запросы

 Сделать закладку на этом месте книги

Редактирование данных при помощи визуальных компонентов

 Сделать закладку на этом месте книги

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

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

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

Итак, мы открыли запрос в SelectSQL. Как видно из рисунка, первая полученная запись содержит имя сотрудника Robert. Предположим, мы хотим исправить это имя на John.

В момент изменения произойдет следующее: в локальном буфере IBDataSetl у текущей записи значение поля FIRST_NAME будет изменено с Robert на John. В базе данных данное изменение пока никак не отражается. Чтобы произвести фактическое изменение данных на сервере, необходимо выполнить соответствующий запрос с UPDATE. Этот запрос необходимо заранее указать у компонента IBDataSetl в свойстве ModifySQL:

UPDATE EMPLOYEE

SET

EMP_NO = :EMP_NO,

FIRST_NAME = :FIRST_NAME,

LAST_NAME = :LAST_NAME,

PHONE_EXT = :PHONE_EXT,

HIRE_DATE = :HIRE_DATE,

DEPT__NO = :DEPT_NO,

JOB_CODE = :JOB_CODE,

JOB_GRADE = :JOB_GRADE,

JOB_COUNTRY = :JOB_COUNTRY,

SALARY = :SALARY WHERE

EMP_NO = :OLD_EMP_NO

Как видно из примера, вместо реальных значений в этом запросе указаны параметры, названия которых совпадают с названием реальных полей. Таким образом, когда пользователь изменит значения полей конкретной записи, ю jtBDataSetl сам задаст значения всех параметров, взяв их из соответствующих |полей. В частности, значение параметра :FIRST_NAME будет равно John. Запрос шз ModifySQL выполнится, и только после этого изменения, сделанные пользователем, окажутся в базе данных.

Аналогичная последовательность действий связана с запросами в свойствах HnsertSQL и DeleteSQL - они выполняются при вставке новой записи и удалении записи пользователем.

Обратите внимание на префикс OLD_ в названии параметра :OLD_EMP_NO. Данный префикс означает, что IBDataSet должен подставить в параметр значение поля до изменения пользователем.

Итак, если мы сформируем все модифицирующие запросы, то наш IBDataSet позволит пользователям редактировать данные, т. е. мы фактически получим запрос, который разработчики на Delphi/C-H-Buildei обычно называют "живым"(live query).

Существует еще одна важная особенность при создании "живых" запросов После выполнения любого модифицирующего действия IBDataSet 1 выполнит запрос, указанный в свойстве RefreshSQL. Этот запрос должен возвращать только одну запись - текущую и нужен для обновления значений полей текущей записи после сделанных исправлений.

SELECT

EMP_NO,

FIRST_NAME,

LAST_NAME,

PHONE_EXT,

HIRE_DATE,

DEPTMTO,

JOB_CODE,

JOB_GRADE,

JOB_COUNTRY,

SALARY,

FULL_NAME

FROM EMPLOYEE

WHERE

EMP_NO = :EMP_NO

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

Именно такой механизм и реализуется в BDE, когда мы вынуждены целиком переоткрывать все запросы. В IBX без этого легко можно обойтись, используя RefieshSQL, значительно сэкономив при этом сетевой трафик и снизив нагрузку на сервер, поскольку получение всего лишь одной измененной записи гораздо более эффективно, чем переоткрытие запроса целиком.

IBX предоставляет нам возможность быстро сгенерировать необходимые модифицирующие запросы при п


убрать рекламу






омощи редактора IBDataSet (рис 2.9.)

Выбрав из списка Table Name нашу таблицу и нажав кнопку Get Table Fields мы сформируем списки Key Fields и Update Fields. В списке Key Fields нужно выделить те поля, которые будут формировать условие WHERE в наших запросах. Очевидно, что это должны быть поля, которые определяют первичный ключ у таблицы. Если такой ключ существует для выбранной таблицы, то вы можете просто нажать на кнопку Select Primary Keys, чтобы автоматически выделить нужные поля.







Рис 2.9. Генератор модифицирующих запросов

В списке Update Fields необходимо выделить те поля, которые потом пользователь сможет редактировать. На рисунке видно, что поле FULL_NAME не включено в список, поскольку это CALCULATED-поле и его значение нельзя менять

К сожалению, надо самому точно знать, какие поля необходимо исключать из модифицирующих запросов поскольку компоненты не дадут ни одной подсказки. Даже при подготовке данного раздела пришлось потратив впустую некоторое время, чтобы понять, почему IBDataSetl никак не "хотел" становиться модифицируемым. Только выяснив, что поле FULL_NAME является вычислимым, стало понятно, почему возникает ошибка IBDataSetl пытался выполнить команду Prepare для каждого из запросов и сервер каждый раз сообщал, что не может изменить read-only-поле! Хочется отметить, что этой проблемы нет в генераторе запросов, реализованном в FIBPlus.

Остается нажать кнопку Generate SQL, чтобы получить все запросы flnsertSQL, ModifySQL, DeleteSQL и RefreshSQL.

Программное редактирование данных

 Сделать закладку на этом месте книги

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

Например, периодически возникает вопрос: "А как мне автоматически вставить новую запись в IBDataSet? Может быть, стоит разместить рядом отдельный компонент IBSQL, в котором написать запрос INSERT, выполнить его, а потом переоткрыть IBDataSet? Или, может быть, можно синхронизировать новую запись в IBSQL и существующие записи в IBDataSet? Существуют ли методы для прямого редактирования локального буфера записей в IBDataSet?"

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

В самом начале главы сказано, что TIBDataSet порожден классом TDataSet, а значит, наследует основные принципы его работы. У TIBDataSet как наследника TDataSet существует три основных метода для изменения данных: Delete, Insert (Append) и Edit.

Предположим, что мы хотим вставить новую запись в наш IBDataSetl по нажатии кнопки:

procedure TForml.ButtonlClick(Sender: TObject);

begin

with IBDataSetl do begin

Insert;

FieldByName('EMP_NO').Aslnteger := 147;

FieldByName('DEPT_NO').Aslnteger := 600;

FieldByName('JOB_CODE').AsString := 'VP';

FieldByName('JOB_GRADE') .Aslnteger : = 2 ;

FieldByName('SALARY').Aslnteger := 105900;

FieldByName('HIRE_DATE').AsDateTime := Now;

FieldByName('JOB_COUNTRY').AsString := 'USA';

FieldByName('FIRST_NAME').AsString := 'Иван';

FieldByName('LAST_NAME').AsString := 'Иванов';

Post;

end;

end;

Теперь поясним, как это работает. После того как мы вызываем метод Insert, IBDataSetl формирует пустой буфер для нашей (пока еще не введенной) записи. Далее мы задаем значения нужных полей при помощи вызовов метода FieldByName и заканчиваем (точнее, подтверждаем) редактирование вызовом метода Post. В этот момент IBDataSetl выполняет запрос, прописанный в свойстве InsertSQL, подставив вместо параметров те значения полей, которые мы задали.

Если запрос выполнился успешно, то IBDataSetl автоматически выполняет RefreshSQL для обновления только что вставленной записи - для проверки изменений, внесенных на стороне базы данных.

Аналогичным образом мы можем редактировать записи. Например, мы можем модифицировать все записи в нашем запросе:

procedure TForml.ButtonlClick(Sender: TObject);

begin

with IBDataSetl do begin

First;

while not Eof do begin

Edit;

FieldByName('LAST_NAME').AsString :=

trim(FieldByName('LAST_NAME').AsString) + ', esquire;

Post;

Next ;

end;

end;

end;

Принцип тот же самый, что и при вставке записей. Мы вызываем метод Edit, подготавливая буфер текущей записи для редактирования, потом меняем значения поля при помощи FieldByName, а потом заканчиваем редактирование, вызвав метод Post. В результате IBDataSetl подставляет значения полей записи в ModifySQL и выполняет запрос.

Оговоримся, что в реальной практике подобная обработка большого множества записей не является хорошим решением. Гораздо легче было бы выполнить одну команду UPDATE, которая добавила бы строку ', esquire' ко всем записям таблицы. Пример выше дан только для демонстрации метода Edit.

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

И снова про транзакции

 Сделать закладку на этом месте книги

Новичков иногда пугает "особенность" IBX закрывать все запросы при подтверждении или "откате" транзакции. Разместим на нашей форме две кнопки, как показано на рис. 2.10: Button 1 (свойство Caption равно Commit) и Button2 (Rollback).







Рис 2.10. Кнопки управления транзакцией

Далее напишем следующие обработчики событий нажатия на эти кнопки:

procedure TForml.ButtonlClick(Sender: TObject);

begin

IBTransactionl.Commit;

end;

procedure TForml.Button2Click(Sender: TObject);

begin

IBTransactionl.Rollback;

end;

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

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

Однако для тех разработчиков, которые до сих пор пользовались BDE, такое поведение компонентов непривычно. Существует два метода. Первый: запоминать активные записи во всех открытых запросах перед закрытием транзакции, потом заново открывать все запросы и перемещать указатели текущих записей на "старое место". Именно так и работает механизм неявного переоткрытия, реализованный в BDE. Второй - это использовать методы CommitRetaining и RollbackRetaining, впервые появившиеся в InterBase 5.x. Эти методы в целом аналогичны методам Commit и Rollback, однако они реализованы таким образом, что сервер автоматически перезапускает транзакцию и не закрывает открытых запросов. Использование CommitRetaining и RollbackRetaining позволит вам избегать массовых операций переоткрытия ваших запросов.

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

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

Использование генераторов для автоинкрементных полей

 Сделать закладку на этом месте книги

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

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

В IBDataSet существует специальное свойство, которое позволяет нам удобно использовать генераторы для создания уникальных значений первичного ключа, а именно GeneratorField. У этого свойства существует специальный редактор, доступный в design-time (рис. 2.11).







Рис 2.11. Настройка автоинкрементного поля

Укажите название генератора, имя поля в таблице, значения для которого будут генерироваться, шаг увеличения счетчика (в большинстве случаев это 1), а также опцию, указывающую, когда будет генерироваться значение поля: сразу при вставке новой записи (On New Record), при завершении вставки (On Post) или при помощи триггера (On Server). В последнем случае вы не увидите значения сгенерированного поля, пока не переоткроете весь запрос, однако данная опция все равно может оказаться полезной. Поскольку наше поле EMP_NO входит в первичный ключ, то его значение не может формально быть незаданным (NULL). Если не указывать свойство GeneratorField, то DBDataSetl будет требовать от нас указывать значение поля EMP_NO в обязательном порядке и мы не сможем "переложить ответственность" на триггер.

Указав же явным образом, что значение поля будет получено именно в триггере, мы обойдем это ограничение. Тем не менее мы рекомендуем вам использовать опции On New Record или On Post, поскольку они лучше укладываются в общую идеологию применения IBX.

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

Механизм master-detail

 Сделать закладку на этом месте книги

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

Добавим на нашу форму новые компоненты (рис. 2.12).

IBDataSet2: TIBDataSet;

DataSource2: TdataSource;

DBGrid2: TDBGrid;







Рис 2.12. Разработка связи мастер-деталь

Свяжем DataSource2 с IBDataSet2, a DBGrid2 с DataSource2. Укажем следующие запросы для IBDataSet2:

SelectSQL:

SELECT BUDGET, DEPARTMENT, DEPT_NO, HEAD_DEPT, LOCATION,

MNGR_NO, PHONE_NO

FROM DEPARTMENT

InsertSQL:

INSERT INTO DEPARTMENT

(BUDGET, DEPARTMENT, DEPT_NO, HEAD_DEPT, LOCATION, MNGR_NO,

PHONE_NO)

VALUES

(:BUDGET, :DEPARTMENT, :DEPT_NO, :HEAD_DEPT, :LOCATION,

:MNGR_NO, :PHONE_NO)

DeleteSQL:

DELETE FROM DEPARTMENT

WHERE DEPT_NO = :OLD_DEPT_NO

ModifySQL:

UPDATE DEPARTMENT SET

BUDGET = :BUDGET,

DEPARTMENT = :DEPARTMENT,

DEPT_NO = :DEPT_NO,

HEAD_DEPT = :HEAD_DEPT,

LOCATION = :LOCATION,

MNGR_NO = :MNGR_NO,

PHONE_NO = :PHONE_NO WHERE

DEPT_NO = :OLD_DEPT_NO

RefreshSQL:

SELECT

DEPT_NO,

DEPARTMENT,

HEAD_DEPT,

MNGR_NO,

BUDGET,

LOCATION,

PHONE_NO,

DEPT_NO1 FROM DEPARTMENT WHERE

DEPT_NO = :DEPT_NO

Очевидно, что их можно сгенерировать при помощи DataSet Editor, как это было описано выше

Теперь необходимо внести изменения в IBDataSetl. Во-первых, нужно задать свойство IBDataSetl.DataSource равным DataSource2. Во-вторых, надо изменить IBDataSetl. SelectSQL:

SELECT DEPT_NO, EMP_NO, FIRST_NAME, FULL_NAME, HIRE_DATE,

JOB_CODE, JOB_COUNTRY, JOB_GRADE, LAST_NAME, PHONE_EXT, SALARY

FROM EMPLOYEE

WHERE DEPT_NO = :DEPT_NO

Значение параметра :DEPT_NO будет автоматически браться из одноименного поля IBDataSet2: DEPT_NO.

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

Еще одна особенность заключается в том, чтобы настроить IBDataSetl таким образом, что при добавлении новой записи о работнике автоматически подставлялся номер текущего отдела из IBDataSet2. Для этого напишем обработчик OnNewRecord у IBDataSetl:

procedure TForml.IBDataSetlNewRecord(DataSet: TDataSet);

begin

DataSetl.FieldByName('DEPT_NO').Aslnteger :=

IBDataSet2.FieldByName('DEPT_NO').Aslnteger;

end;







Puc 2.13. Запущенное приложение со связью мастер—деталь

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

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

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

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

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

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

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

Управление транзакциями тоже зависит только от вас и требований вашей задачи. Не существует единственно правильного способа распределения компонентов TIBTransaction. Некоторые предпочитают все делать в контексте одной общей транзакции, вовремя вызывая Commit или Rollback; другие предпочитают для каждого TIBDataSet иметь отдельную транзакцию, следя за правильным уровнем изоляции. Только вы можете решить, какие транзакции нужны для правильной работы ваших запросов.

Заключение

 Сделать закладку на этом месте книги

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

Также заметим, что в главе по FTBPlus вы сможете найти многие вещи, которые окажутся полезными и при работе с ШХ. Например, вы вполне сможете использовать материалы, описывающие работу с генератором отчетов FastReport, работу с BLOB-полями, локальную фильтрацию и обработку событий (event alerts).

Что такое FIBPIus?

 Сделать закладку на этом месте книги

Предыдущие главы книги содержали описание самого сервера InterBase, но для практического его использования необходимо разрабатывать клиентские программы. Само название технологии Client/Server говорит о наличие двух сторон: сервера, который в нашем случае обслуживает базу данных, и клиента, который является специализированной программой, взаимодействующей с сервером InterBase.

Для написания клиента, или клиентской части, вы в принципе можете использовать практически любые среды разработки, которые позволяют вызывать функции из внешней DLL-библиотеки. Суть взаимодействия с InterBase состоит в том, что ваша программа вызывает некоторые функции InterBase API для выполнения тех или иных запросов к базе данных. InterBase API заключено в специальной библиотеке gds32.dll. Таким образом, вы имеете возможность писать свои программы на чем угодно, обращаясь к функциям gds32.dll. Тем не менее, такой низкоуровневый подход неудобен для создания прикладных программ, в которых основное внимание уделяется обработке данных на достаточно абстрактном уровне. Если разработчик вынужден каждый раз заботиться, прежде всего, о том, чтобы правильно сформировать внутренние структуры для вызова функций gds32.dll, то процесс разработки значительно усложняется и затягивается.

Этого можно избежать, если вы используете для разработки клиентской части Borland Delphi, Borland C++ Builder или Borland Kylix. Идеология этих продуктов поощряет создание специализированных наборов компонент для решения определенных системных задач, облегчая, таким образом, жизнь прикладного разработчика.

Devrace FIBPIus - это библиотека компонент для Delphi, C++ Builder и Kylix, предназначенных для работы с InterBase и его клонами (Firebird и Yaffil). Компоненты используют прямой InterBase API для взаимодействия с сервером, поэтому для нормальной работы программ, написанных с использованием FIBPIus, не нужно ничего, кроме наличия gds32.dll. Кроме того, использование прямого API дает клиентским приложениям максимальную производительность, гибкость и возможность использования "специальных" уникальных вещей, реализованных в InterBase. В частности, можно упомянуть array-поля, которые невозможно использовать при работе со стандартными Delphi компонентами ТТаЫе, TQuery, взаимодействующими с InterBase через Borland Database Engine (BDE).

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

* Полная совместимость со Borland Delphi 3-7, C++ Builder 3-6 и Kylix 3.

* Полная совместимость с существующими версиями InterBase 4.X-7.0, Firebird, Yaffil в настоящий момент, а также поддержка всех версий InterBase и его основных клонов в будущем.

* Поддержка всех особенностей и специальных возможностей InterBase: event alerters, array-полей, blob-полей.

* Встроенные макросы в SQL-выражениях, позволяющие использовать параметры-подстановки даже в там, где это не позволяет сам InterBase.

* Удивительно гибкий механизм работы с master-detail запросами, позволяющий свести "ручное" кодирование практически к нулю и при этом иметь возможность гибко настраивать любые связки master-detail без написания обработчиков событий, улучшая общую производительность приложения за счет снижения лишнего сетевого трафика.

* Улучшенная производительность при выполнении запросов со средним и небольшим количеством результирующих записей (т. е , для 90-95% всех запросов в обычных приложениях!).

* Гибкое управление транзакциями. FIBPlus, которое позволяет, с одной стороны, явно управлять каждой транзакцией, а с другой стороны автоматизирует большинство операций, автоматически запуская транзакции, и подтверждая их в режиме AutoCommit. Кроме этого, FIBPlus реализует уникальный механизм работы одного компонента в контексте двух транзакций: читающей и пишущей, что позволяет совершенно избегать DEADLOCK при работе приложения.

* Реализация и поддержка дополнительных возможностей, например, эмуляция Boolean-полей, отсутствующих в явном виде в InterBase. Качественная поддержка режима CachedUpdates позволяет использовать FIBPlus в приложениях с нестабильными удаленными подключениями к базе данных (например, посредством dial-up). Используя CachedUpdates, вы можете подключиться к базе данных, выбрать данные, отключиться, изменить данные и после повторного подключения (или восстановления подключения), вернуть изменения на сервер. Данная возможность реализована только в FIBPlus, поскольку, например, в IBX даже при режиме CachedUpdates необходимо, тем не менее, иметь активные транзакции и подключение к серверу.

* Поддержка локальной сортировки и локальной фильтрации данных (без дополнительных запросов на сервер).

* Автоматическая генерация модифицирующих запросов в run-time и design- time. Уникальная возможность автоматически генерировать модифицирующие запросы, в которых участвуют не все поля записи, а только те, значения которых реально были изменены, что позволяет снизить сетевой траффик приложения.

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

* Возможность централизованной обработки ошибок и исключений при помощи компонента TpFIBErrorHandler.

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

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

Компоненты FffiPlus построены таким образом, чтобы их можно было использовать со всеми стандартными визуальными db-компонентами и сторонними продуктами, поддерживающими стандарт TDataSet-TDataSource. Таким образом, разработчик, выбравший FffiPlus для доступа к InterBase, не ограничен в выборе сторонних компонент для отображения данных, или печати отчетов (мы покажем, в частности, как использовать FffiPlus в связке с FastReport).

Для любознательного читателя, возможно, будет также интересно узнать некоторые факты из истории появления и развития FffiPlus. В 1998 году независимый разработчик Грегори Дилтц создал минимальный набор компонентов для работы с InterBase API, назвав его FreeffiComponents (FffiC). Уже с 1999 года одесский программист Сергей Бузаджи стал дописывать эту бесплатную библиотеку своими более функциональными компонентами, назвав эти дополнения FffiPlus. Тогда же корпорация Borland выпустила Delphi 5, включив в нее компоненты InterBase Express (ffiX), полностью основанные на коде FIBC. Сам Грегори Дилтц отказался от продолжения развития своей библиотеки. К этому времени изменения в FffiC, сделанные Сергеем Бузаджи, были столь значительны и затрагивали практически весь первоначальный код Грегори Дилтца, что было решено развивать FIBPIus как отдельный проект, в рамках условий лицензионного соглашения Грегори Дилтца. С этого времени поддержкой и развитием библиотеки занимается компания Devrace, Сергей Бузаджи является ведущим разработчиком и идеологическим руководителем данного продукта. На текущий момент библиотека полностью совместима с Borland Delphi 3-7, Borland C++ Builder 3-5, Borland Kylix 3 и поддерживает Borland InterBase 4-7.0, а также все версии Firebird и Yaffil.

FIBPIus является коммерческим продуктом, однако компания Devrace ввела специальную цену для стран бывшего Союза, которая в десятки раз меньше цены для "западных" пользователей.Вы можете найти всю информацию о FIBPIus на сайтах: https://www.devrace.com, https://www.fibplus.net.

Общее описание компонент, включенных в состав FIBPIus

 Сделать закладку на этом месте книги




TpFIBDatabase - предназначен для подключения к базе данных, позволяет получать статистическую информацию о базе, свойствах базы данных и так далее. Основные методы: Open, Close.





TpFIBTransaction - предназначен для явного управления транзакцией. Основные методы: StartTransaction, Commit, Rollback, CommitRetaining, RollbackRbtainine.





TpFIBDataSet - предназначен для получения и редактирования данных, является потомком стандартного класса TDataSet и полностью совместим со всеми визуальными компонентами. Основные методы: Prepare, Open, Close, Insert, Append, Edit, Delete, Refresh.





TpFIBQuery - предназначен для простого выполнения запросов, позволяет также получать данные из базы данных. В отличие от TpFIBDataSet , компонент не содержит локального буфера для хранения всех полученных записей, и не совместим с визуальными компонентами. Основные методы: Prepare, ExecOuery, Close





TpFIBStoredProc - потомок TpFIBQuery, предназначен для выполнения хранимых процедур. Не рекомендуется для вызова процедур, возвращающих данные. Фактически, компонент является удобной "оберткой" вокруг команды EXECUTE PROCEDURE.





TpFIBUpdateObject - предназначен для автоматического выполнения дополнительных модифицирующих действий совместно с TpFIBDataSet.





TDataSetContainer - предназначен для централизации обработки типичных обработчиков событий TpFIBDataSet и внутренней нотификации, что иногда позволяет сильно упростить код.





TpFIBSQLMonitor - предназачен для отслеживания всех запросов, которые выполняются при помощи компонентов FIBPIus. Позволяет также отслеживать запросы из параллельных приложений, также написанных на основе FIBPIus.





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





SuperlBAlerter - предназначен для регистрации и получения событий (event alerters) InterBase.





TIBConfigService - предназначен для настройки параметров базы данных.





TIBBackupService предназначен для создания резервных копий (backup) баз данных.





TpFIBRestoreService - предназначен для восстановления базы данных из резервной копии.





TpFIBValidationService - предназначен для проверки цел


убрать рекламу






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





TpFIB Statistical Service - предназначен для получения статистики о базе данных.





TpFIBLogService - предназначен для создания и просмотра лог-файла работы сервера.





TpFIBSecurityService - предназначен для редактирования списка пользователей на сервере.





TpFIBLicensingService - предназначен для добавления и удаления сертификатов, регулирующих количество и свойства клиентских подключений к серверу InterBase.





TpFIBServerProperties - предназначен для получения информации о сервере, параметров конфигурации и так далее.





TpFIBInstall предназначен для установки компонента InterBase installation.





TpFIBUnlnstall - предназначен для установки компонента InterBase un- installation.

Использование основных компонентов FIBPIus

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

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

CREATE TABLE "Categories" (

"Id" INTEGER NOT NULL,

"Name" VARCHAR (50) character set WIN1251 collate PXW_CYRL,

"GoodsCount" INTEGER);

/* Unique keys definition */

ALTER TABLE "Categories" ADD CONSTRAINT "PK_Categories" PRIMARY

KEY ("Id");

SET TERM ^ ;

/* Trigger: "AI_Categories_Id" */

CREATE TRIGGER "AI_Categories_Id" FOR "Categories" ACTIVE

BEFORE INSERT POSITION 0

AS

BEGIN

IF (NEW."Id" IS NULL) THEN

NEW."Id" = GEN_ID("Categories_Id_GEN", 1);

END

^

SET TERM ; ^

CREATE TABLE "Goods" (

"Id" INTEGER NOT NULL,

"Name" VARCHAR (150) character set WIN1251 collate

PXW_CYRL,

"Price" DOUBLE PRECISION,

"IdCategory" INTEGER NOT NULL);

/* Unique keys definition */

ALTER TABLE "Goods" ADD CONSTRAINT "PK_Goods" PRIMARY KEY

("Id");

/* Foreign keys definition */

ALTER TABLE "Goods" ADD CONSTRAINT FK_GOODS FOREIGN KEY

("IdCategory") REFERENCES "Categories" ("Id") ON DELETE CASCADE

ON UPDATE CASCADE;

SET TERM ^ ;

/* Trigger: "AI_Goods_Id" */

CREATE TRIGGER "AI_Goods_Id" FOR "Goods" ACTIVE

BEFORE INSERT POSITION 0

AS

BEGIN

IF (NEW."Id" IS NULL) THEN

NEW."Id" = GEN_ID("Goods_Id_GEN", 1);

END

^

Очевидна связь master-detail между таблицами "Categories" и "Goods".

Обратите внимание, что мы с самого начала и в дальнейшем будем использовать особенности SQLDialect 3, который доступен в InterBase начиная с версии 6.0

Для печати прайс-листа мы будем использовать генератор отчетов FastReport f (https://www.fastreport.ru).

Подключение к базе данных, выполнение простых запросов

 Сделать закладку на этом месте книги

Рассмотрим с самого начала создание приложения, при помощи которого мы сможем редактировать прайс-лист. Необходимо поместить на форме основной компонент, который позволяет подключаться к базе данных InterBase (TpFIBDatabase) и вызвать редактор этого компонента (рис. 2.14 и 2.15).







Рис 2.14. Вызов редактора TpFIBDataBase







Рис 2.15. Bug редактора компонента TpFIBDataBase

Для подключения к базе как минимум необходимо указать путь (в данном примере это путь к локальному файлу), имя пользователя и пароль. Вы можете проверить правильность заданных параметров, нажав на кнопку Test. Мы также можем задать параметры подключения к базе в run-time, получив путь к базе данных из ini-файла:

procedure TMainForm.FormCreate(Sender: TObject);

begin

with TiniFile.Create('ib_price.ini') do begin

pFIBDatabasel.DBName := ReadString('Options', 'DBPath',

'C:\IBPRICE.GDB');

Free ;

end;

pFIBDatabasel.Open;

end;

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

function Execute(const SQL: string): boolean;

function QueryValue(const aSQL: string;

FieldNo:integer):Variant;

function QueryValueAsStr (const aSQL: string,-

FieldNo:integer): String;

Например, мы можем выяснить количество категорий товаров, выполнив простой запрос:

SnowMessage(pFIBDatabasel.QueryValueAsStr('select count("Id")

from "Categories"', 0));

Управление транзакциями

 Сделать закладку на этом месте книги

Фактически любые действия с данными должны происходить в контексте той или иной транзакции. Управление транзакциями в FIBPlus осуществляется при помощи компонентов класса TpFIBTransacdon. Все транзакции в FIBPlus являются "явными" (explicit) и запускаются при помощи метода StartTransaction. Тем не менее, чтобы вы могли избежать лишнего кодирования, TpFIBDataSet и TpFIBQuery самостоятельно запускают транзакции, если установлен ключ poStartTrasaction в свойстве Options. Завершать транзакцию в любом случае необходимо явным образом при помощи вызова соответствующих методов: Commit, Rollback, CommitRetaining и RollbackRetaining

Метод CommilRetammg появится в InteiBase только начиная с версии 5.1, а метод RollbaekRetammg - только в версии 6.0.

Планируя внутреннюю организацию транзакций в приложении, необходимо помнить про уровень изоляции транзакций Можно указывать уровень изоляции транзакции явным образом при помощи соответствующих констант InterBase в свойстве TRPaiams или используя заранее заданные уровни изоляции при помощи свойства TPBMode. По умолчанию любой компонент TpTransaction, помещенный на форму, имеет уровень изоляции "Read Committed".

FIBPlus также позволяет создавать и запоминать в системном реестре пользовательские уровни изоляции (свойство UserKmdTransaction) Необходимо вызвать редактор компонента, нажав на нем правой кнопкой мыши, и выбрать пункт "Edit transaction params" (рис. 2.16).







Рис 2.16. Редактор компанента TpFIBTransaction

После нажатия на кнопку New Kind нужно указать название для набора констант и перечислить необходимые константы в поле Settings. Теперь нужно сохранить константы нажатием кнопки Save kind Описания всех констант вы можете узнать в документации к InterBase В дальнейшем, вы можете использовать собственные созданные наборы констант, выбирая названия из списка в свойстве UserKmdTiansaction

Закрытие транзакции также имеет ряд особенностей, которые необходимо иметь в виду разработчикам Если вы закрываете транзакцию вызовом методов Commit или Rollback, то все активные запросы, которые работают в контексте этой транзакции, будут также закрыты. Такое поведение непривычно для разработчиков, ранее использовавших в своих приложениях BDE, где подтверждение транзакции оставляло курсоры открытыми. Нужно подчеркнуть, что механизм, реализованный в BDE, является лишь эмуляцией. То есть фактически завершение транзакции просто вызывало невидимое автоматическое "переоткрытие" всех активных запросов. Кроме того, важно иметь в виду, что все запросы при использовании BDE работают в контексте одной и той же транзакции, в отличие от FDBPlus, где каждый запрос может работать в рамках своей отдельно взятой транзакции.

Тем не менее если вы не хотите, чтобы подтверждение транзакции вызывало закрытие всех активных TpFIBDataSet, то вы можете использовать метод CommitRetaining. Этот метод подтверждает транзакцию и автоматически запускает новую с теми же самыми параметрами, не закрывая при этом пользовательских курсоров (запросов, выбирающих данные)

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

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

Данный метод появился в InteiBase версии 6.0.

Использование стандартных визуальных db-компонентов совместно с FIBPIus

 Сделать закладку на этом месте книги

Следующий этап - получение данных из базы данных и отображение этих данных при помощи визуальных компонентов (рис. 2.17).







Рис 2.17. Подключение визуальных компонентов к TpFIBDataSet

Для этого нам необходимы два компонента. TpFIBDataSet HTpFTBTiansdction. TpFIBDataSet является потомком класса TDataSet и полностью совместим со всеми стандартными визуальными компонентами. Для связки TpFIBDataSet с визуальным компонентом CategoriesGrid (TDBGrid) компонент CategoriesDataSource подключен к CategoriesDataSet. TpFIBTransaction, как уже было сказано, предназначен для управления транзакцией.

Необходимо указать уровень изоляции транзакции при помощи свойства TPBMode, а также "подключить" CategoriesTransaction к pFIBDatabasel (2.18).







Рис 2.18. Подключение транзакции к компоненту pFIBDataBasel

Аналогично необходимо указать свойства Database и Transaction для компонента CategoriesDataSet (рис. 2.19).







Рис 2.19. Подключение компонента CategoriesDataSet к pFIBDataBasel и транзакции CategoriesTransaction

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

SELECT * FROM "Categories"

Теперь изменим немного код в процедуре:

procedure TMainForm.FormCreate(Sender: TObject);

begin

with TiniFile.Create('ib_price.ini') do begin

pFIBDatabasel.DBName := ReadString('Options', 'DBPath',

'C:\IBPRICE.GDB');

Free ;

end;

pFIBDatabasel.Open;

CategoriesDataSet.Open;

end;

В свойстве DataSet у компонента CategoriesSource (TDataSource) мы укажем CategoriesDataSet, а в свойстве DataSource у компонента CategoriesGrid (TDBGrid) укажем CategoriesSource. Теперь все данные, полученные в результате запроса, будут отображены в CategoriesGrid. Нам не нужно явным образом запускать транзакцию перед открытием запроса, поскольку по умолчанию свойство CategoriesDataSet.Options содержит ключ poStartTransaction и компонент сам запускает транзакцию После запуска приложения и открытия базы данных мы увидим следующий результат (рис. 2.20):







Рис 2.20. Вид запущенного приложения

Вместо CategoriesGrid мы могли использовать любой визуальный db-компонент, который используется в связке с TDataSource. Это мог быть TDBEdit, TDBLabel, TDBMemo и т. д., а также любые сторонние компоненты, которые придерживаются данного стандарта (TDBGridEh, например).

Как сделать запрос редактируемым? Автоматическая генерация модифицирующих запросов в design-time и run-time

 Сделать закладку на этом месте книги

Если вы попробуете исправить какие-то значения в CategoriesGrid, то увидите, что в текущем состоянии мы имеем iead-only-запрос Для того чтобы наш запрос стал редактируемым, или "живым", нам надо написать несколько дополнительных запросов, которые будут автоматически выполнятся компонентом CategonesDataSet при редактировании данных в CategoriesGrid. Необходимо заполнить свойства (рис. 2.21).







Рис 2.21. Свойства CategonesDataSet с запросами для чтения и модификации данных

FIBPlus включает специальный design-time-редактор для редактирования и генерации модифицирующих запросов. Вы можете вызвать "Generator SQLs" из контекстного меню, если нажмете правой кнопкой мыши на компоненте CategonesDataSet (рис. 2.22)

Для генерации модифицирующих запросов необходимо сначала указать основную таблицу в списке Если в запросе участвует только одна таблица, то она будет выбрана в списке автоматически После этого необходимо нажать кнопку Get Table Fields для заполнения списков "Key Fields" и "Update Fields '. В первом списке нужно выделить те поля, которые будут вставлены в условие WHERE во всех модифицирующих запросах. В списке "Update Fields" необходимо выделить те поля, которые мы хотим редактировать. По умолчанию выделены все поля таблицы "Categories" Теперь достаточно нажать кнопку Generate SQLs и получить автоматически все модифицирующие запросы, которые вы можете увидеть на закладке SQLs Например, мы получим следующий запрос для свойства InsertSQL

INSERT INTO "Categories"(

"Id" ,

" Name" ,

"GoodsCount"

)

VALUES(

:"Id",

:"Name",

:"GoodsCount"

)








Puc 2.22. Редактор компонента TpFIBDataSet, предназначенный для генерации модифицирующих запросов

Обратите внимание также на запрос для свойства RefreshSQL:

select * from "Categories"

WHERE

(

"Categories"."Id" = :"OLD_Id"

)

Это такой же выбирающий запрос, как и запрос в свойстве SelectSQL, но возвращать он должен только одну запись - текущую Теперь, после создания всех запросов, когда пользователь будет редактировать запись в CategoriesGrid, CategonesDataSet автоматически заполнит значения параметров : "Id", . "Name" и т д., теми значениями полей, которые наберет пользователь. После выполнения метода Post (который, в частности, вызывается при нажатии клавиши Enter в CategonesGnd) CategoriesDataSet выполнит соответствующий модифицирующий запрос После редактирования записи будет выполнен запрос из свойства UpdateSQL, при вставке новой записи - из InsertSQL, а при удалении записи - из DeleteSQL После выполнения любого модифицирующего запроса (кроме удаления) CategoriesDataSet автоматически выполняет запрос из RefreshSQL, подставляя в качестве условия текущие значения параметров. Это позволяет "узнать" значения всех полей текущей записи, если они были изменены при помощи триггеров базы данных.

Несколько слов о специальных префиксах для параметров, которые используются в FIBPlus.

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

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

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

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

Тем не менее генерация модифицирующих запросов в design-time не всегда удобна FIBPIus позволяет разработчику автоматически генерировать нужные запросы во время работы приложения, основываясь на запросе из свойства SelectSQL Для этого нужно использовать свойство AutoUpdateOptions (рис 223).







Рис 2.23. Комплексное свойство AutoUpdateOptions

Необходимо "включить" ключ "AutoReWriteSqls", указать ключевое поле, которое будет использовано в условиях WHERE (в нашем случае, это поле "Id") и указать название модифицируемой таблицы ("Categories"), поскольку в реальности запросы могут быть многотабличными. Теперь свойства InsertSQL, DeleteSQL, UpdateSQL и RefreshSQL будут генерироваться автоматически при открытии CategonesDataSet. To есть фактически мы имеем генератор запросов в run-time, что бывает очень удобно.

Есть еще один момент, связанный с генерацией модифицирующих запросов Например, если мы редактируем только название категории, то после нажатия на Entei на сервер будет отправлен запрос, в котором изменены два поля и Name и GoodsCount Фактически же мы изменяли только одно поле - "Name".В итоге если запись таблицы содержит значительный объем данных (например, длинные строковые поля), то при малейшем исправлении даже одного поля на сервер будут "уходить" все данные. При многопользовательской работе это создаст дополнительный лишний трафик Данной ситуации можно избежать но для этого необходимо, чтобы приложение выполняло 6oлee эффективные изменяющие запросы, например запрос:

UPDATE "Categories" SET

"Name" = ."Name"

WHERE

"Id" = :"OLD_Id"

в котором, как вы видите, участвует только то поле, значение которого действительно было изменено. Такая возможность существует, если мы включим ключ UpdateOnlyModifiedFields (разумеется, этот ключ действует, только если мы используем режим генерации запросов в run-time). В случае редактирования только поля "Name" компонент CategoriesDataSet автоматически сгенерирует запрос, приведенный выше, и будет генерировать каждый раз новые запросы в зависимости от того, какие поля изменялись.

Поскольку теперь наш CategoriesDataSet стал модифицируемым, то мы можем написать обработчики событий OnClick у трех кнопок справа от CategoriesGrid:

procedure TMainForm AddCatButtonClick(Sender TObject);

begin

CategoriesDataSet Append;

end;

procedure TMainForm.EditCatButtonClick(Sender: TObject);

begin

CategoriesDataSet Edit;

end;

procedure TMainForm.DeleteCatButtonClick(Sender: TObject);

begin

if MessageDlg('Вы уверены, что хотите удалить категорию "' +

CategoriesDataSet.FieldByName('Name').AsString + '"?',

mtConfirmation,

[mbYes, mbNo], 0) = mrYes then

CategoriesDataSet.Delete;

end;

Правильный способ использования auto-increment полей в FIBPIus

 Сделать закладку на этом месте книги

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

Особенность использования генераторов при написании корректно работающих приложений на FIBPlus состоит в том, что мы должны получать новое значение генератора в приложении до того, как выполним запрос на вставку записи. Зачем это нужно? Дело в том, что, как уже было сказано, после выполнения любого модифицирующего запроса (кроме удаления) CategoriesDataSet автоматически выполняет запрос из RefreshSQL, подставляя в качестве условия текущие значения параметров. В нашем случае для подстановки надо использовать значение первичного ключа (поле "Id"). Если мы не получим его заранее, а будем генерировать его, используя триггер, то мы не сможем подставить значение параметра : "OLD_Id" в запрос RefreshSQL, а значит, не сможем перечитать измененную запись. Таким образом, если какие-то поля записи были изменены триггерами базы данных, то мы не увидим этих изменений, пока не переоткроем весь запрос целиком Если же мы сначала получим новое значение генератора, а потом вставим это значение наравне с остальными параметрами, то затем мы сможем использовать это же значение, чтобы "перечитать" текущую запись, и будем "в курсе" актуальных значений полей без излишних переоткрываний!

TpFIBDataSet позволяет автоматически получать и вставлять значения первичного ключа, используя генератор. Для этого нам необходимо заполнить некоторые дополнительные ключи в свойстве AutoUpdateOptions (рис. 2.24).

Нужно указать название генератора Categories_Id_GEN. Свойство WhenGetGenID может принимать три возможных значения:

wgOnNewRecord - получать значение генератора сразу после подготовки буфера для новой записи;

wgBeforePost - получать значение генератора непосредственно перед отправкой новой записи на сервер;

wgNever- не использовать механизм генерации ключевых значений.

В нашем примере мы укажем значение wgOnNewRecord, чтобы сразу видеть в CategoriesGrid те значения поля "Id", которые будут получены с сервера. Теперь мы можем запустить наше приложение и отредактировать какие-либо существующие записи и даже вставить новые записи (рис. 2.25).







Рис 2.24. Использование AutoUpdateOptions для генерации уникальных значений первичного ключа







Рис 2.25. Внешний вид "живого" запроса

Из рисунка видно, что было автоматически получено значение для поля "Id" равное 66

Разделенные транзакции: уникальная возможность избежать Deadlock. Режим AutoCommit

 Сделать закладку на этом месте книги

CategoriesDataSet позволяет автоматически подтверждать сделанные изменения, если задать свойство AutoCommit в True. Теперь после вызова метода Post компонент CategoriesDataSet будет автоматически вызывать CategonesTransaction CommitRetainmg, сохраняя сделанные пользователем изменения и не закрывая при этом сам запрос. Такой подход уже снижает вероятность Deadlock, который возможен при наличии длинных незакрытых транзакций, в контекст которых производились изменения данных.

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

Переименуем CategoriesTransaction в CategoriesReadTransaction и добавим еще один компонент CategoriesWriteTransaction. После этого зададим свойство UpdateTransaction у CategoriesDataSet в CategoriesWriteTransaction. Таким образом, теперь CategoriesDataSet подключен сразу к двум компонентам TpFTBTransaction (рис. 2.26).







Рис 2.26. Разделение читающей и пишущей транзакций

После вызова метода Post компонент CategoriesDataSet будет вызывать метод Commit у компонента CategoriesWriteTransaction. Однако, учитывая, что данные читаются в контексте совсем другой транзакции (CategoriesReadTransaction), это не вызовет закрытия всего CategoriesDataSet. To есть, используя FIBPlus, мы имеем настоящий режим AutoCommit, который уменьшает вероятность Deadlock и не мешает "видеть" нам актуальные значения всех записей. При использовании BDE в режиме AutoCommit вы не могли бы узнать реальные значения записей, пока не переоткроете запрос.

Кроме того, как уже упоминалось, для InterBase до версии 6.5 слишком частый вызов CommitRetaining мог привести к значительному "захвату" ресурсов сервером. При использовании механизма разделенных транзакций вы можете использовать режим AutoCommit без потерь производительности сервера для любых версий InterBase.

Механизм master-detail. Специальные опции TpFIBDatabase и TpFIBDataSet

 Сделать закладку на этом месте книги

Мы имеем возможность редактировать данные о категориях товаров и можем переходить к вопросам, связанным с построением связки master-detail. Для этого мы положим на форму дополнительные компоненты:

GoodsSource: TDataSource;

GoodsGrid: TDBGrid;

GoodsDataSet: TpFIBDataSet;

GoodsReadTransaction: TpFIBTransaction;

GoodsWriteTransaction: TpFIBTransaction;

AddGoodsButton: TButton;

EditGoodsButton: TButton;

DeleteGoodsButton: TButton;

"Свяжем" их так же, как и предыдущую группу компонентов, и напишем SelectSQL для GoodsDataSet:

SELECT * FROM "Goods"

WHERE "IdCategory" = :"Id"

Очевидно, что мы хотим выбрать при помощи detail-запроса только те товары, которые относятся к текущей категории. Значение параметра : "Id" должно браться из поля "Id" таблицы "Categories". Чтобы это происходило автоматически, необходимо задать свойство GoodsDataSet.DataSource равным CategoriesSource. Теперь сгенерируем модифицирующие запросы для GoodsDataSet так же, как мы делали это раньше для CategoriesDataSet.

После автоматической генерации мы должны внести некоторые изменения в полученные запросы. Рассмотрим, в частности, запрос для InsertSQL:

INSERT INTO "Goods"(

"Id",

"Name",

"Price",

"IdCategory" )

VALUES(

:"Id",

:"Name",

:"Price",

:"IdCategory"

)

Очевидно, что при добавлении нового товара, мы должны задать параметр : "IdCategory" текущим значением поля "Id" из таблицы "Categories". FIBPlus позволяет делать это автоматически при помощи префикса "MAS_", о котором мы уже упоминали выше:

INSERT INTO "Goods"(

"Id" ,

"Name",

"Price",

"IdCategory" )

VALUES(

:"Id",

:"Name",

:"Price",

:"MAS_Id"

)

Теперь мы указали явным образом, что значение для поля "Goods"."IdCategory" нужно брать из поля "Id" таблицы "Categories", которая является master- таблицей для таблицы "Goods". To же изменение необходимо внести в запрос UpdateSQL

UPDATE "Goods" SET

"Id" = :"Id",

"Name" = :"Name",

"Price" = :"Price",

"IdCategory" = :"MAS_Id"

WHERE

"Id" = 'OLD_Id"

Теперь изменим немного код в процедуре FormCreate:

procedure TMainForm.FormCreate(Sender: TObject);

begin

with TimFile. Create (' ib_price. mi ') do begin

pFIBDatabasel.DBName := ReadStringt'Options', 'DBPath',

'C:\IBPRICE.GDB');

Free;

end;

pFIBDatabasel.Open;

CategoriesDataSet.Open;

GoodsDataSet.Open;

end;

He забудем заполнить свойства AutoUpdateOptions, чтобы GoodsDataSet автоматически генерировал значения первичного ключа (рис 2 27)

Можно запустить приложение Двигаясь вверх и вниз по CategoriesGrid, вы можете наблюдать, как автоматически изменяется содержимое в GoodsGrid Связка master-detail уже работает.

FIBPlus позволяет также настраивать некоторые особенности работы механизма master-detail. В частности, как вы уже обратили внимание, в нашем примере мы открывали detail-dataset (GoodsDataSet) "вручную", при


убрать рекламу






помощи явного вызова метода Open В случае с простой связкой это нетрудно, однако если мы используем несколько цепочек master-detail или более длинные цепочки - master-detail-subdetail, то ручное открытие всех запросов может даже привести к ошибке. Вы всегда должны открывать подчиненный запрос после основного.

Чтобы избежать ненужного и совершенно очевидного кодирования, TpFEBDataSet содержит специальное свойство DetailConditions (рис. 2.28).







Рис 2.27. Использование AutoUpdateOptions длл генерации значений первичного ключа GoodsDdtabet







Рис 2.28. Свойаво DetailConditions

Задав ключ dcForceOpen. мы можем быть совершенно уверены, что detail- запрос будет автоматически открыт после открытия master-запроса. Теперь мы можем удалить строку GoodsDataSet.Open из метода CreateForm. Какова бы ни была глубина связок master-detail, все запросы будут открыты в нужном порядке.

Вторая важная опция - это ключ dcWaitEndMastei Scroll Представьте, что пользователь передвигается по CategonesGrid, пытаясь найти нужную категорию товаров. При каждом движении, когда меняется текущая запись в CategoriesGrid, GoodsDataSet автоматически открывает запрос заново. Очевидно, движение по mastei-таблице может вызвать серию довольно "тяжелых" запросов, которые значительно увеличат совершенно бесполезный сетевой трафик. На практике было нужно переоткрыть detail-запрос только один раз, когда пользователь все-таки нашел нужную ему категорию в CategoriesGrid. FIBPlus позволяет избежать ненужных запросов. Если ключ dcWaitEndMasterScroll добавлен в DetailConditions, то detail-запрос переоткрывается только после некоторой паузы (величину паузы можно регулировать, задавая значение свойства WaitEndMasterlnterval).

Таким образом, когда пользователь просто передвигается по CategoriesGrid, изменение текущей записи "включает" таймер detail-запроса. Если за время паузы пользователь успевает перейти на другую запись, то таймер сбрасывается. И так до тех пор, пока пользователь не остановится на нужной записи. Только после этого detail-запрос переоткрывается, а значит, GoodsDataSet выполняет только один запрос вместо целой серии.

Третий важный ключ в DetailConditions управляет поведением master- таблицы при изменении detail-таблицы. В нашем примере в таблице "Categories" существует поле "GoodsCount", в котором содержится количество наименований товаров для текущей категории. Когда мы добавляем или удаляем наименование товара из категории, то это значение должно меняться. Очевидно, что данный механизм легко реализуется парой триггеров для таблицы "Goods":

CREATE TRIGGER "Goods_AI" FOR "Goods" ACTIVE

AFTER INSERT POSITION 0

AS

BEGIN

update "Categories" set

"GoodsCount" = (select count("Id") from "Goods" where

"IdCategory" = New."IdCategory")

where "Id" = New."IdCategory";

END^

CREATE TRIGGER "Goods_BD" FOR "Goods" ACTIVE

BEFORE DELETE POSITION 0

AS

BEGIN

update "Categories" set

"GoodsCount" = (select count("Id") from "Goods" where

"IdCategory" = Old."IdCategory")

where "Id" = Old."IdCategory";

END^

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

В GoodsDataSet формируется значение ключевого поля "Id" при помощи генератора.

Значения полей заполняются пользователем.

После нажатия пользователем клавиши Enter GoodsData подготавливает запрос из свойства InsertSQL к выполнению и заполняет значения параметров соответствующими значениями полей.

Параметр :"IdCategory" заполняется текущим значением поля "Id" из CategoriesDataSet.

Запрос выполняется и выполняется триггер "Goods_AI".

Транзакция GoodsWriteTransaction автоматически подтверждает сделанные изменения при помощи метода Commit.

Выполняется запрос из свойства RefreshSQL, который перечитывает значения полей вставленной в таблицу "Goods" записи, основываясь на известном значении ключевого поля "Id".

В этот момент значение поля "GoodsCount" уже изменилось, но это изменение никак не отражается в CategoriesGrid. Мы должны обновить текущую запись в CategoriesDataSet, чтобы увидеть актуальное значение всех полей. Это можно сделать при помощи явного вызова метода CategoriesDataSet.Refresh. Однако этот же метод нам надо будет вызывать и в случае удаления записи из GoodsDataSet. А если бы нам нужно было создать более сложный механизм взаимодействия между master и detail-таблицами, то, скорее всего, метод Refresh нужно было бы вызывать также и после многих других операций.

Вместо всего этого мы можем просто добавить ключ dcForceMasterRetresh. Этот ключ автоматически вызывает метод Refresh у master, если detail была изменена. Если вы запустите приложение, то сами увидите, что значение поля "GoodsCount" автоматически изменяется при добавлении и удалении записей в GoodsDataSet.

Специальные опции в компонентах FIBPIus

 Сделать закладку на этом месте книги

Опции и настройки TpFIBDatabase

 Сделать закладку на этом месте книги

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

Хочется начать с описания механизма поддержки Alias в FIBPIus. Те из вас, кто уже работал с Interbase через Borland Database Engine, наверняка знают про существование такого понятия, как Database Alias, т. е. псевдоним базы данных. С точки зрения BDE Database Alias - это не что иное, как ряд параметров подключения к базе данных, которые хранятся в системном реестре Windows. Программист, который разрабатывает пользовательскую программу, подключающуюся к базе данных, может не указывать реальные параметры подключения, а просто "сослаться" на существующий алиас. Таким образом, если потом, например, хотелось поменять путь к базе данных, то достаточно было изменить информацию в алиасе и перезапустить программу.

Аналогичный механизм реализован и в FIBPIus. Вы можете задать свойство TpFIBDatabase.AliasName и забыть про такие параметры, как путь к базе данных, имя пользователя, character set, SQL Role и SQL Dialect. При запуске программы, если значение свойства AliasName не пустое, TpFIBDatabase будет пытаться получить значения соответствующих свойств из системного реестра. Встает вопрос: а как записать туда первоначальные значения? Для этого существует два способа.

Первый заключается в том, что вы устанавливаете свойство SaveDBParams в True, после чего все необходимые свойства автоматически сохраняются в системном реестре, как только вы подключитесь к базе данных. Фактически это одноразовая операция: установили свойства и имя алиаса, установили SaveDBParams в True, подключились к базе данных - параметры алиаса сохранены. Отключились от базы данных, выставили SaveDBParams в False. Теперь при последующих подключениях значения для свойств буду) брался из системного реестра.

Второй способ - использование утилиты AliasManager, которая написана специально для того, чтобы можно было настраивать значения алиасов на любом клиентском месте без сред Delphi и C++ Builder. Иными словами, вы можете использовать эту утилиту на тех компьютерах, на которые установите свою программу. Мы не будем приводить здесь описание утилиты, поскольку ее использование достаточно очевидно само по себе (рис. 2.29).







Рис 2.29. Внешний вид Alias Manager

Достаточно удобным и полезным также является свойство DesignDBOptions. Если выставить ключ ddoIsDefaultDatabase в True, то у всех компонент (TpFIBDataSet, TpFIBQuery, TpFIBTransaction), которые вы потом положите на форму, автоматически будет заполняться свойство Defaul(Database (или Database). Ключ ddoStoreConnected указывает - сохранять ли значение свойства TpFIBDatabase.Active при сохранении формы или проекта. Дело в том, что активное состояние базы данных зачастую бывает полезно в процессе разработки, когда мы используем визуальные средства FIBPlus: Database Editor, Transaction Editor, SQL Generator и различные свойства. Тем не менее, сохраняя активное подключение (а значит, и автоматически восстанавливая его при загрузке программы!), мы можем получить ошибку в работе приложения, которое может попытаться открыть уже активное подключение. Чтобы избежать подобной накладки, вы можете выставить свойство ddoStoreConnected в False.

Свойство SynchronizeTime предназначено для правильного вычисления default-значений полей. Вы знаете, что в Interbase существуют специальные функции для работы с датами и временем: NOW и CURRENT_TIME (в Firebird). Эти функции часто ИСПОЛЬЗУЮТ при описании полей:

CREATE TABLE NODES_LIST (

ID INTEGER NOT NULL,

INSERTED TIME NOT NULL DEFAULT 'NOW'

);

При вставке записи с пустым значением поля INSERTED сервер автоматически подставит текущее время. Однако существует некоторая особенность при работе клиентской части программы. Как вы понимаете, чтобы работать с таблицей NODES_LIST, нам придется заполнять значения свойств SelectSQL, InsertSQL, DeleteSQL, UpdateSQL и RefreshSQL у некоторого компонента TpFIBDataSet. При вставке TpFIBDataSet проверяет свойства полей таблицы и пытается автоматически заполнить поля новой записи значениями по умолчанию. В нашем случае он должен присвоить полю INSERTED текущее время. Однако поскольку мы можем иметь дело с сетевой системой, то время на сервере может отличаться от времени на клиентской машине. В нашем гипотетическом примере это, возможно, и не играет никакой роли, однако могут быть случаи, когда необходимо подставлять точное "серверное" время. Для этого и используется свойство SynchronizeTime. Если оно выставлено в True, то при подключении к базе данных TpFIBDatabase выясняет "разницу во времени" между сервером и клиентским компьютером, чтобы впоследствии подставлять правильные значения.

Свойство UpperOldNames было введено в FIBPlus на этапе перехода с Interbase 5.x на Interbase 6.x. Как вы знаете, в 3-м SQL Dialect Interbase 6.x позволяет использовать "регистро-чувствительные" имена объектов. В старых версиях Interbase имена INSERTED и inserted являлись одинаковыми, поэтому программисты в своих программах не придерживались точного написания. В частности, можно было писать и следующее:

MainDS.FieldByName('COST').AsFloat :=

ainDS.FieldByNamet'cost').AsFloat * 0,3;

И это работало без ошибок. Однако если просто перенести базу на Interbase 6.x и запустить то же самое приложение, то оно начинало работать с ошибками, поскольку поле COST действительно существовало, а вот поле cost - уже нет. Фактически подобная миграция означала, что нужно было проверять все приложение, чтобы найти подобные подводные камни. FIBPlus позволяет обойтись без подобных мероприятий. Если выставить UpperOldNames в True, то любые идентификаторы, в которых явным образом не используются двойные кавычки, приводятся "внутри" к верхнему регистру, а значит, запись

MainDS.FieldByName('COST').AsFloat :=

ainDS.FieldByName('cost').AsFloat * 0,3;

снова начинает работать без проблем.

Опции и настройки TpFIBDataSet

 Сделать закладку на этом месте книги

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

AllowedUpdateKinds. Свойство представляет собой множество из трех элементов: [okModity. uklnsert, ukDelete]. Достаточно очевидно, что, используя это свойство, мы можем разрешать и запрещать выполнение определенных модифицирующих операций TpFTBDataSet. не изменяя при этом свойства InsertSQL. DeleteSQL и UpdateSQL. To есть мы можем получить TpFIBDataSet, который будет считаться "живым", но не будет позволять, например, вставлять новые записи! Это, кстати, одна из стандартных проблем при работе с компонентом TDBGrid, который не позволяет ограничивать изменение данных, — он либо "совсем" редактируемый (а значит, пользователь может изменять поля, удалять записи и вставлять новые), либо "совсем" нередактируемый. При работе с FIBPlus мы можем регулировать это поведение без необходимости писать какие-то специальные обработчики событий. Есть и еще одна особенность в свойстве AllowedUpdateKinds. Как вы уже в курсе, Interbase позволяет регулировать права доступа к объектам базы данных. Таким образом, может возникнуть ситуация, когда конкретный пользователь, работающий с вашей программой, не имеет прав на удаление записей из конкретной таблицы. Писать для каждого пользователя отдельную программу, очевидно, слишком накладно. Однако TpFIBDataSet легко справляется с проблемой, поскольку если при попытке "скомпилировать" конкретный модифицирующий запрос компонент получает от сервера сообщение об ошибке из-за отсутствия прав, то TpFIBDataSet просто запрещает данное действие, убрав соответствующий элемент из свойства AllowedUpdateKinds. Дальнейшие попытки просто, таким образом, пресекаются, а пользователь работает с программой, даже не подозревая об ошибках или отсутствии прав.

Свойство DefaultFormats используется при заполнении свойства DisplayFormat у объектов TField при открытии запроса. Поэтому, если вы хотите автоматически задавать какой-то нужный вам формат представления, например, числовых полей, то вам достаточно указать его в DefaultFormats.NumericDisplayFormat. Правила заполнения масок можно прочитать в описании свойства DisplayFormat у стандартного класса TField.

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

poTrimCharFields - среди разработчиков идут постоянные споры, должны ли компоненты для работы с Interbase "обрезать" конечные пробелы у строковых полей или нет. С одной стороны, дополнение пробелами - это описанная техническая особенность Interbase, а с другой стороны - это не всегда удобно для пользователя. Нет нужды спорить, если можно отрегулировать этот процесс в отдельно взятом приложении (и даже в отдельно взятом запросе) Включив ключ PoTrimCharFields, вы можете быть уверены, что пробелов в конце строковых полей не будет.

poRep-eshAperPost позволяет вам отключить выполнение "обновляющего" запроса после редактирования записи. Как уже было сказано, после операции Post TpFIBDataSet автоматически выполняет запрос из свойства RefreshSQL, чтобы получить актуальные значения всех полей измененной записи, поскольку они могли быть изменены при помощи триггеров. Это дополнительный запрос, и если вы считаете, что в нем нет никакой нужды (например, вы точно уверены, что никакие триггеры не изменяют записи в таблице), то вы можете отключить этот режим.

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

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

poAutoFormatFields разрешает или запрещает автоматическое форматирование полей на основе форматов, заданных в свойстве DefaultFormats.

poProtectEdit руководит режимом "защищенного" редактирования. Этот режим иногда полезен, если вы хотите избежать коллизий при редактировании одной и той же записи разными пользователями. Если активизировать режим, то, как только вы начинаете редактировать запись (но до вызова метода Post), на сервер отправляется так называемый "холостой" update вида:

UPDATE TABLE

SET FIELD1 = FIELD1,

FIELD2= FIELD2, ...

To есть запрос, который формально является редактированием записи, а фактически не изменяет значения полей. Тем не менее сервер считает, что изменения произошли, а значит, пользователь, который первым начал редактирование, имеет преимущество перед всеми последующими. Фактически это эмуляция режима блокировок на уровне записей. Проблема, однако, состоит в том, что в Interbase не существует возможности отменить UPDATE, не отменив транзакции целиком. Поэтому даже если пользователь начал редактирование записи, а потом отказался и метод Post так и не был вызван, то запись все равно будет считаться измененной до конца транзакции. Кроме того, данный режим практически совершенно бесполезен, если вы используете режим работы в контексте двух транзакций, как это было описано выше, поскольку "блокировка" действует только до конца изменяющей транзакции.

Опция poKeepSorting нужна, если вы используете методы локальной сортировки DoSort и DoSortEx. Эти методы выполняют сортировку записей внутри локального буфера (без запроса на сервер). При изменении значений полей возможна ситуация, что измененная запись должна быть помещена в другое место (если изменилось, например, значение поля, по которому записи отсортированы). При активной опции poKeepSorting, после изменения записи, весь буфер автоматически сортируется и запись "встает" на нужное место.

poPersistentSorting - при активной опции, если вы вызывали методы DoSort или DoSortEx, то после переоткрытия запроса результат будет автоматически отсортирован вызовом DoSort с теми же параметрами.

Опция poAllowChangeSQLs блокирует или разрешает изменение свойств InsertSQL, DeleteSQL, UpdateSQL и RefreshSQL при открытом запросе.

Свойство PrepareOptions, в отличие от Options, позволяет задать некоторые особенности поведения TpFEBDataSet до открытия запроса:

pfSetRequiredFields - если опция активна, то для полей, которые в базе данных были описаны как NOT NULL, свойство TField.Required будет выставлено в True Если пользователь попробует не задать значение для таких полей, например, в TDBGnd, то на экран будет выведено стандартное предупреждение

При включенной опции pfSetReadOnlyFielcls для полей, которые описаны в базе как вычислимые (а значит, не изменяемые пользователем), свойство TField ReadOnly будет выставлено в True Это позволит избежать попыток изменения значений таких полей пользователем.

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

Опция psUseBooleanFields включает или выключает режим эмуляции Boolean-полей Вы знаете, что Interbase не поддерживает специальный тип для логических значений и разработчики вынуждены так или иначе заменять его своим пользовательским типом. Однако даже пользовательские типы не дают равноценной замены Boolean-типу, поскольку визуальные компоненты все равно видят такие поля либо как числовые, либо как строковые в зависимости от описания Таким образом, пропадает возможность использования каких-то специальных возможностей отображения логических полей визуальными компонентами (например, многие сторонние компоненты наподобие TDBGrid отображают логические поля галочками). FIBPlus позволяет эмулировать Boolean-поля гораздо более эффективно. Необходимо описать в базе данных домен вида:

CREATE DOMAIN T_BOOLEAN

AS INTEGER DEFAULT 0

NOT NULL CHECK (VALUE IN (0,1))

Поле должно быть целочисленным и содержать в названии слово "boolean". После этого все поля, которые вы создадите с использованием данного домена, будут "распознаваться" в FIBPlus как Boolean-поля, т. е. для них будут создаваться экземпляры класса TFIBBooleanField, который является прямым потомком стандартного TBooIeanField, значит, любой визуальный компонент будет работать с такими полями как с настоящими Boolean!

psSQL!NT64ToBCD - опция заставляет TpFIBDataSet создавать экземпляры TBCDField для полей INT64 с любым масштабом (в SQL Dialect 3), что гарантирует точность вычислений для любых полей INT64.

psApplyRepositary - опция включает или выключает режим использования FIBPlus Repository. Эта особенность FIBPlus требует отдельного разговора, и мы вернемся к ее рассмотрению позже в отдельном параграфе.

psGetOrderlnfo - при включенном режиме, FIBPlus анализирует запрос перед выполнением и автоматически заполняет структуру SortFields данными о полях, по которым происходит сортировка в запросе. Впоследствии это может быть использовано в методах локальной сортировки DoSort и DoSortEx

Опция psAskRecordCount предназначена для получения информации о количестве записей, которые должен вернуть запрос, до выполнения, собственно, запроса Дело в том, что фактически, значение свойства TpFIBDataSet RecoidCount показывает лишь количество записей, которые мы уже получили с сервера Это число может отличаться от полного количества записей, поскольку если мы не вызывали метод FetchAll, то сервер "отдает" нам записи по мере необходимости (при вызове метода Next). Иногда бывает полезно узнать точное количество заранее В этом случае нужно включить опцию psAskRecordCount. Однако необходимо учитывать, что данная опция имеет ряд ограничений. Поскольку единственным способом узнать количество записей в запросе заранее является выполнение другого запроса вида SELECT COUNT (*) с теми же условиями, что и в основном запросе, то для некоторых запросов мы выполнить такую операцию не можем Например, если мы имеем запрос

SELECT ID FROM TABLE1

UNION

SELECT ID FROM TABLE2

то запрос SELECT COUNT(*) .. попросту теряет смысл, поскольку результат будет ложным по определению В общем случае опция psAskRecordCount не работает с запросами, которые содержат команды UNION, ORDER и DISTINCT.

Использование FIBPIus совместно с генератором отчетов FastReport

 Сделать закладку на этом месте книги

Фактически компоненты FIBPIus совместимы с любыми генераторами отчетов, которые работают с потомками стандартного класса TDataSet. Таким образом, вы сможете использовать FIBPIus вместе с QuickReport, ReportBuilder, FastReport и даже со специализированными генераторами отчетов, такими, как, например, Afalina XLReport.

Все эти генераторы будут получать данные из TpFIBDataSet напрямую или через компонент TDataSource. Мы не будем останавливаться на рассмотрении всех особенностей всех генераторов, а просто продемонстрируем как быстро и просто можно получить отчет при помощи FastReport от FastReport Software. Отметим, что, как и FIBPIus, этот генератор отчетов можно использовать не только при разработке приложений в Borland® Delphi® для Microsoft® Windows®, но и в Borland® Kylix® для Linux®. Для этого предназначена специальная CLX-версия пакета.

Простой отчет

 Сделать закладку на этом месте книги

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

Идеология FastReport позволяет включать "внутрь" отчетов совершенно независимые источники данных (и даже отдельные независимые подключения к базе данных), чтобы получить, таким образом, отчеты, которые можно было бы создавать отдельно от приложения (например, если первоначальная постановка задачи не предусматривала каких-то специальных отчетов). Это потребует установки дополнительных компонентов для "привязки" к конкретному источнику данных. Вместе со стандартным пакетом FastReport поставляются, в частности, компоненты для использования FasrtReport с IBX, ADO, BDE и FIBPIus. Автором компонентов FastReport для FIBPIus является Виталий Бармин ([email protected]).

Итак, если вы хотите использовать и дополнительные возможности FastReport вместе с FIBPIus (этот вопрос будет рассмотрен в разделе "Генератор отчетов FastReport. Создание отчетов в run-time"), то вам необходимо установить соответствующий пакет (FrFib3.dpk, FrFib4.dpk, FrFib5.dpk или FrFib6.dpk, в зависимости от версии Delphi) входящий в поставку FastReport.

Вы также можете всегда найти эти компоненты Виталия Бармина на caйте FIBPlus https://www.fibplus.net/.

Напомним, что сам FastReport вы всегда сможете найти на сайте https://www.fastrepoit.ru

После установки вы увидите в палитре на странице FastReport дополнительный компонент: TfrFIBComponents. Теперь можно перейти к созданию отчета.

Поместим на нашу форму следующие компоненты: frReport: TfrReport; frFIBComponents: TfrFIB Components и frDBDataSetl: TfrDBDataSet (рис. 2.30).







Рис 2.30. Компоненты FastReport

Откроем дизайнер отчетов, дважды нажав на frReport (рис. 2.31).







Рис 2.31. Внешний вид дизайнера отчетов

Как видно из рисунка, оба компонента TpFIBDataSet уже доступны нам в дизайнере. Для начала рассмотрим самый простой вариант отчета, который распечатает нам список категорий товаров в прайс-листе (рис. 2.32).







Рис 2.32. Шаблон отчета о категориях товаров

Для этого мы поместим на страницу четыре полосы (band): Report Title, Page Header, Master Data и Page Footer. Объекты, помещенные на Report Title, будут распечатаны только один раз в самом начале отчета Объекты, размещенные на Page Header, будут выводиться на печать в начале каждой новой страницы. В нашем случае мы просто добавили туда переменную PAGE#, которая будет выводить номер страницы. Master Data нужен нам для вывода данных из компонента CategonesDataSet. Эта полоса (band) будет выведена на печать столько раз, сколько записей в нашем наборе данных. И наконец, Page Footer будет выводиться в конце каждой страницы отчета.

Укажем FastReport, откуда необходимо брать данные для отчета Для этого зададим свойство DataSource у компонента frDBDataSetl (рис. 2 33).







Рис 2.33. Свойства TfrDBDataSet

Теперь необходимо задать источник данных для Master Data Band в самом отчете Нажмите дважды на Master Data в дизайнере отчета и укажите источник данных в появившемся диалоге (рис. 2.34).







Рис 2.34. Источник данных для Master Data

Остается обратить внимание на объект TfrMemoView, в котором и будут выводится названия категорий в отчете (рис 2.35.)







Рис 2.35. Объект шаблона отчета TfrMemoView, необходимый для печати значений поля таблицы

Это выражение вы можете ввести вручную, а можете использовать специальный диалог редактирования объекта и нажать кнопку Insert data field, изображенную на рис 2 36.

На этом этапе фактически отчет готов. Вы можете нажать кнопку Preview и увидеть уже готовый список категорий, который можно распечатать Тем не менее добавим в наше приложение кнопку, которая будет вызывать отчет по названиям категорий товаров

Необходимо написать очень простую обработку нажатия на эту кнопку (предположим, что мы сохранили шаблон для отчета в файле 'price_categories.frf):

procedure TMainForm.PrintBClick(Sender: TObject);

begin

frReport LoadFromFile('price_categories.frf');

frReport PrepareReport;

frReport.ShowPreparedReport,

end;







Рис 2.36. Внешний вид диалога редактирования объекта TfrMemoView

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







Рис 2.37. Вид отчета о категориях товаров в прайс-листе

Отчеты вида master-detail

 Сделать закладку на этом месте книги

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


убрать рекламу






чета. Сначала добавим на нашу форму еще один компонент frDBDataSet2: TfrDBDataSet - и зададим его свойство DataSource равным GoodsSource. Теперь необходимо добавить две полосы (bands): Detail Header (заголовки детализованных данных) и Detail Data (сами данные). На Detail Data мы разместим два объекта типа TfrMemoView. В первом мы будем выводить названия товаров, а во втором - цену каждого товара (рис. 2.38):







Рис 2.38. Шаблон отчета для связи мастер-деталь

Очевидно, что мы должны указать источником данных для Detail Data компонент frDBDataSet2. Отчет готов. Сохраним его в файл 'price_list.frf и изменим немного обработчик клавиши "Печать":

procedure

TMainForm.PrintBClick(Sender: TObiect);

begin

GoodsDataSet.DetailConditions :=

GoodsDataSet.DetailConditions [dcWaitEndMasterScroll];

frReport.LoadFromFile('price.f rf');

frReport.PrepareReport ;

frReport.ShowPreparedReport;

GoodsDataSet.DetailConditions :=

GoodsDataSet DetailConditions + [dcWaitEndMasterScroll];

end,

Важный момент касается изменения DetailConditions. Как уже было сказано, ключ dcWaitEndMasteiScioll позволяет избегать лишних запросов при навигации по master-запросу. Однако в ел) чае с распечаткой полного отчета это может привести к тому, что для каждой категории товара мы будем иметь одни и те же наименования, поскольку GoodsDataSet не будет переоткрывать запрос, пока FastReport будет получать данные из CategonesDataSet Чтобы избежать такой ошибки, мы отключаем оптимизацию master-detail на период подготовки отчета и включаем ее вновь после того, как отчет уже готов

Теперь при запуске приложения мы можем легко распечатать весь наш прайс-лист (рис 2.39.)







Рис 2.39. Внешний вид отчета вида мастер-деталь

При желании мы можем выделить цветом или курсивом "горячие" (к примеру, самые дешевые) позиции прайса. Для этого в дизайнере выберем TfrMemoView, затем воспользуемся Objectlnspector и установим свойство Highlight. Для этого в поле "Condition" появившегося окна введем условие Value<0 и выберем способ выделения. В нашем примере все позиции прайса с ценой менее 1000 будут выделены ярко-зеленым цветом (рис. 2.40).







Рис 2.40. Диалог параметров условного форматирования

Создание отчетов в run-time

 Сделать закладку на этом месте книги

Одной из самых замечательных особенностей FastReport является возможность создавать отчеты прямо в run-time, т. е. во время работы приложения. Это позволяет добавлять в ваше приложение отчеты, которые не были предусмотрены заранее, и использовать в этих отчетах как существующие запросы, так и совершенно новые. Более того, фактически вы можете создавать в таких отчетах даже самостоятельные подключения к базам данных не меняя код вашей программы!

Все это возможно благодаря архитектуре FastReport и компонентам, написанным Виталием Барминым, о которых мы уже упоминали выше.

Для этого добавим в наше приложение компоненты TfrDesigner, TfrDialogControls (как видно из названия, этот компонент предназначен для построения и использования самостоятельных диалоговых окон в составе отчета), а также кнопку для вызова диалога редактирования шаблона отчета (рис. 2.41).

Обработчик для этой кнопки будет исключительно простым:

procedure TMainForm.DesignPClick(Sender: TObject);

begin

frReport.DesignReport;

end;

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







Рис 2.41. Компоненты для встраивания дизайнера отчетов в программу

Если вы обратите внимание, то шаблон для отчета по умолчанию имеет одну страницу, на которой мы и располагаем полосы для печати. Добавим к отчету специальную страницу - диалоговую, на которой расположим компоненты доступа к данным (рис. 2.42).







Рис 2.42. Добавление диалоговой страницы к шаблону отчета

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







Рис 2.43. Компоненты для доступа к базе данных из шаблона отчета

Положим на диалог компонент Query: TfrFIBQuery, укажем ему существующее подключение к базе данных в свойстве Database и напишем следующий запрос в свойстве SQL:

SELECT "Categories"."Name", "Categories"."GoodsCount"

FROM "Categories"

WHERE "Categories"."GoodsCount" > 0

Очевидно, что мы хотим вывести на печать только те категории товаров, количество наименований по которым больше нуля. Остается только создать сам шаблон печати, привязав соответствующие полосы (bands) к Query как к источнику данных (рис. 2.44).







Рис 2.44. Шаблон встроенного отчета

Укажем источник данных для Master Data (рис. 2.45).







Рис 2.45. Подключение полосы (band) к встроенному в шаблон запросу

И сохраним наш новый отчет, например, под именем "pricel.frf". Теперь можно несколько изменить реакцию нашего приложения на нажатие кнопки печати. Положим на форму компонент OpenDialog: TopenDialog - и напишем следующий обработчик нажатия на кнопку "Печать":

procedure TMainForm.PrintBClick(Sender: TObject);

begin

if not OpenDialog.Execute then exit;

GoodsDataSet.DetailConditions :=

GoodsDataSet.DetailConditions [dcWaitEndMasterScroll];

frReport.LoadFromFile(OpenDialog.FileName);

frReport.PrepareReport;

frReport.ShowPreparedReport;

GoodsDataSet.DetailConditions :=

GoodsDataSet.DetailConditions + [dcWaitEndMasterScroll] ;

end;

Таким образом, когда пользователь нажмет на кнопку "Печать", он сможет выбрать любой шаблон отчета и вывести его на печать. Учитывая, что при помощи дизайнера отчетов тот же самый пользователь (или вы, как разработчик) может подготовить неограниченное количество самых разных отчетов без необходимости изменения основного кода приложения, становится очевидным преимущество использования FIBPlus совместно с FastReport. Конечно, возможность создавать отчеты в run-time доступна в FastReport и для других пакетов работы с данными, в частности с IBX, однако FIBPlus делает эту возможность значительно более гибкой за счет использования макросов. Более подробно механизм макросов мы рассмотрим чуть позже, а сейчас лишь продемонстрируем, как, используя макросы, мы можем создавать более функциональные диалоговые отчеты.

Скопируем "pricel.frf" в "price2.frf", откроем "price2.frf" для редактирования в дизайнере отчетов и перейдем на диалоговую страницу. Там мы поместим дополнительные компоненты: ComboBoxl, Editl, Labell и Buttonl (рис. 2.46).







Рис 2.46. Внешний вид диалога, встроенного в шаблон отчета

Изменим запрос в Query.SQL:

SELECT "Categories"."Name", "Categories"."GoodsCount"

FROM "Categories"

WHERE "Categories"."GoodsCount" @COND

COND в данном случае является макросом FIBPlus, и вместо него мы можем поставить любой текст. Удобство же заключается прежде всего в том, что макросы в FIBPlus рассматриваются как полноценные параметры, а значит, FastReport тоже будет считать, что в данном случае имеет дело с параметром. В данном примере мы можем позволить пользователю самому указать условия для выборки категорий прайс-листа. Заполним ComboBoxl.Items символами сравнения:

>

<

>=

< =

<>

=

Теперь нам достаточно только указать FastReport, какое значение необходимо "подставить" вместо нашего макроса-параметра. Для этого надо изменить свойство Params у компонента Query (рис. 2.47).







Рис 2.47. Выражение для получения значение макроса COND

Параметр у нас один: COND. Его значение мы должны сформировать из двух частей: знака сравнения и значения, с которым будет сравниваться поле "GoodsCount". Что и было сделано, как видно из рисунка выше. Теперь при запуске отчета мы увидим диалог, в котором необходимо указать условие для выборки (рис. 2.48).







Рис 2.48. Полученный диалог во время получения отчета

А на рис. 2.49 изображен получившийся отчет о категориях товаров, в которых более семи наименований товаров.

Теперь мы можем определить реакцию на щелчок по любой категории товаров в подобном отчете таким образом, чтобы по нему выводился полный отчет о товарах из этой категории. Для этого у всех объектов FastReport есть свойство Tag: String. В окне preview пользователь может нажать на объект и при этом выполнится какое-либо действие (например, построится другой отчет). Цель свойства - облегчить распознавание нажатого объекта. Например, объект может содержать текст "12.25р", по которому идентификация невозможна. Но в свойство Tag можно поместить более развернутую информацию, например значение ключа таблицы, из которой был получен текст "12.25р", номер строки или колонки в отчете и пр. При этом необходимо использовать событие TfrReport.OnObjectClick.







Рис 2.49. Внешний вид получившегося динамического отчета

Логично использовать для вывода детализованного отчета тот же самый объект frReport, который уже лежит у нас на диалоговой форме приложения. Однако саму форму этого отчета надо будет разработать отдельно и сохранить на диске. Форма отчета будет выглядеть так же, как и в нашем отчете master-detail (рис. 2.50).

Кроме того, нам будет необходимо наложить фильтр на категории товаров:

SELECT "Categories"."Name", "Categories"."GoodsCount"

FROM "Categories"

WHERE "Categories"." Name " @NAME

Теперь укажем FastReport, какое значение следует подставить вместо макроса "@NAME" в компоненте Query. Здесь нам придется немного "схитрить" и присвоить этому макросу значение переменной FastReport. Назовем эту переменную Va .

К самой переменной мы присвоим значение в коде программы.

Сохраним эту форму отчета из дизайнера на диске в текущем каталоге под именем "detailed.frf".







Рис 2.50. Шаблон дополнительного отчета

Определим в коде программы для TfrReport событие OnObjectClick следующим образом:

procedure Tforml.frReportObjectClick(View: TfrView);

var str: string;

begin

str .= View.Memo.Text;

frReport.LoadFromFile('detailed.frf')

frReportl.Dictionary.Variables['Varl'] := str;

if frReport.PrepareReport then begin

frReport.OnObjectClick := nil; { чтобы при щелчке на детализованном отчете не показывался детализованный отчет }

frReport.ShowPreparedReport;

frReport.OnObjectClick := frReportObjectClick; { возвращаем событие для вывода детализованного отчета }

frReport.ShowReport;

end;

end;

Наиболее полную документацию, описание всех возможностей, а также последние версии FastReport можно найти на официальном Web-сайте https://www.fastreport.ru.

Использование специальных инструментов в design-time: FIBPIus Tools

 Сделать закладку на этом месте книги

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

Установка FIBPIus Tools

 Сделать закладку на этом месте книги

FIBPIus Tools - это эксперты для Delphi и C++ Builder, поставляющиеся в готовом, скомпилированном виде, поэтому для их установки в среде необходимо установить соответствующий пакет.

На момент создания книги опубликованы FIBPIus Tools для Borland Delphi 3-7 и Borland C++ Builder 5-6. Если вы используете другие версии продуктов Borland, мы рекомендуем вам проверить более новые версии FIBPIus Tools на сайте https://www.tlbplus.net/.

Рассмотрим установку в среде Delphi (рис. 2.51). Необходимо выбрать пункт основного меню Components -> Install Packages.







Рис 2.51. Установка FIBPIusTools в среду Delphi

Нажмите на кнопку Add и найдите соответствующий вашей версии Delphi пакет (табл. 2.1).

Табл 2.1.  FIBPlusTools: Поддерживаемые версии Delphi/C++ Builder

Версия Delphi/C++ Builder

Название пакета FIBPIus Tools

Delphi 3

PFIBPIusTools3.dpl

Delphi 4

pFIBPIusTools4.bpl

Delphi 5

pFIBPIusToolsS.bpl

Delphi 6

PFIBPIusTools6.bpl

Delphi 7

PFIBPIusTools7.bpl

C++ Builder 5

PFIBPIusTools_CB5.bpl

C++ Builder 6

pFIBPIusTools_CB6.bpl

После установки вы обнаружите пункт FIBPIus в основном меню Delphi (рис. 2.52).







Рис 2.52. Меню FIBPIus Tools

Последний пункт в меню FIBPIus фактически ничего не делает, но показывает номер установленной версии.

Preferences

 Сделать закладку на этом месте книги

Пункт Preferences позволяет настроить параметры основных компонентов по умолчанию (рис. 2.53).

На первой странице диалога вы можете задать значения по умолчанию для свойств Options, PrepareOptions и DetailsConditions для всех компонентов класса TpFIBDataSet. Вы можете задать определенные ключи для этих свойств. Например, если вы включите флажок SetRequiredFields то, когда вы положите новый компонент TpFIBDataSet на вашу форму, ее свойство PrepareOptions будет содержать ключ pfSetRequiredFields. Наиболее важным является тот факт, что умолчания, заданные в FIBPIus Tools Preferences, действуют во всех приложениях, которые вы будете создавать. Однако необходимо иметь в виду, что это только первоначальные умолчания. То есть если после помещения компонента на форму вы измените свойства, то это никак не коснется Preferences. Также изменение Preferences не коснется тех компонент значения свойств которых уже были заданы.







Рис. 2.53. Диалог настройки опций TpFIBDataSet в design-time.

Обратите внимание на поля "Prefix Generator name" и "Suffix Generator name". Задав их значения, вы сможете регулировать формирование имен для названий генераторов в свойстве AutoUpdateOptions у TpFIBDataSet. Имя генератора в AutoUpdateOptions генерируется из названия таблицы (UpdateTable), префикса и суффикса.

Следующие страницы диалога позволяют настраивать ключевые свойства компонентов TpFIBDataBase, TpFIBTransaction и TpFIBQuery.

В частности, если вы всегда работаете с новыми версиями InterBase, т. е. С версиями 6 и более (а также Firebird), то мы рекомендуем вам задать значение SQL Dialect на закладке TpFIBDatabase равным 3, чтобы каждый раз не переключать это свойство вручную.

SQL Navigator

 Сделать закладку на этом месте книги

Это наиболее интересная часть FIBPlus Tools, не имеющая аналогов в других продуктах. Фактически это инструмент централизованной обработки SQL в рамках целого приложения (рис. 2.54):







Рис 2.54. Внешний вид SQL Navigator

SQLNavigator позволяет разработчику сосредоточиться на написании и анализе SQL-кода в приложении. Нажмите кнопку Scan all forms of active project. SQLNavigator переберет все формы приложения и выделит из них те, которые содержат компоненты FIBPlus для работы с SQL: TpFIBDataSet, TpFIBQuery, TpFIBUpdateObject и TpFffiStoredProc. Нажмите в списке на любую из обнаруженных форм. Список справа будет заполнен компонентами, обнаруженными на этой форме. Нажатие на любой из компонентов позволит нам посмотреть соответствующие свойства, в которых содержится SQL-код. Для компонентов класса TpFIBDataSet будут выведены свойства: SelectSQL, InsertSQL, UpdateSQL, DeleteSQL и RefreshSQL Для компонентов TpFIBQuery, TpFIBUpdateObject и TpFIBStoiedPioc будет выведено значение свойства SQL

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







Рис 2.55. Выделение группы компонентов при помощи SQL Navigator

Теперь мы можем сохранить значения выделенных свойств во внешнем файле при помощи кнопки Save selected SQLs или проверить их корректность прямо в SQLNavigator при помощи кнопки Check selected SQLs Записанный файл с выделенными запросами можно использовать для дальнейшего анализа при помощи специализированных инструментов

Вы можете использовать SQLNavigator для поиска текста в SQL в рамках всего проекта Например, на рис. 2 56 вы можете видеть все свойства в проекте, которые содержат строку "ID"

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

Таким образом, SQLNavigator представляется очень эффективным инструментом для работы с SQL-кодом в клиентской части приложения базы данных, пожалуй, единственным в своем роде







Рис 2.56. Результат поиска текста в запросах FIBPIus

Специальные возможности FIBPIus

 Сделать закладку на этом месте книги

Обработка потери подключения к базе данных

 Сделать закладку на этом месте книги

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

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

Ключевым компонентом для обработки ситуации является TpFTBErrorHandler. Формально, потерю подключения мы будем обрабатывать при помощи компонента TpFIBDatabase в обработчике события OnLostConnect, однако без "глубокого" перехвата в TpFIBErrorHandler мы не сможем избавиться от лишних сообщений о потере подсоединения.

Итак, попробуем создать простое приложение, позволяющее редактировать данные в TDBGrid и обрабатывать потерю подключения к базе данных тремя способами, про которые уже было упомянуто (рис. 2.57).







Рис 2.57. Внешний вид главной формы примера Connection Lost

Заполним SelectSQL для CompaniesDataSet: select * from "Companies" и предоставим CompaniesDataSet самостоятельно генерировать модифицирующие запросы (рис. 2.58).







Рис 2.58. Свойства AutoUpdateOptions компонента CompaniesDataSet

Включим также режим CachedUpdates (CompaniesDataSet.CachedUpdates := True), чтобы корректно обрабатывать восстановление подключения без потери данных.

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

Close pFIBDataBase

Terminate application

Restore connect

Теперь обратим внимание на компонент pFIBErrorHandlerl (рис. 2.59).

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

TpFIBErrorHandler имеет только одно событие - OnFIBErrorEvent, обработка которого позволит нам обработать, в частности, потерю подключения к базе данных:

procedure TForml.pFibErrorHandlerlFIBErrorEvent(Sender:

TObject;

ErrorValue: EFIBError; KindlBError: TKindlBError; var

DoRaise: Boolean);

begin

if KindlBError = keLostConnect then begin

DoRaise := false;

Abort;

end;

end;







Puc 2.59. Свойства компонента pFIBErrorHandlerl

Вот в общем-то и весь обработчик - мы просто запрещаем вывод стандартного сообщения о потере подключения. На практике вы сможете использовать OnFIBEnorEvent для более сложной обработки разного рода исключительных ситуаций, используя значение параметра ErrorValue. Для нашего случая важно также знать что, кроме срабатывания QnFIBErrorEvent, TpFIBErrorHandler инициирует возникновение OnLostConnection у компонентов TpFIBDatabase Именно здесь мы и сосредоточим основную обработку потери подсоединения.

procedure TForml.DatabaseLostConnect(Database: TFIBDatabase; E:

EFIBError;

var Actions: TOnLostConnectActions);

begin

AttemptRest := 0;

case cmbKindOnLost.Itemlndex of 0: begin

Actions := laCloseConnect;

MessageDlg('Connection lost. TpFIBDatabase will be closed'',

mtlnformamon, [mbOk] , 0

);

end;

1: begin

Actions := laTerminateApp;

MessageDlg('Connection lost. Application will be closed',

mtlnformation, [mbOk], 0

);

end;

2: Actions := laWaitRestore;

end;

end;

Смысл обработчика очевиден - в зависимости от выбранного пользователем значения компонента cmbKindOnLost наше приложение либо "закрывает" активное подключение и все соответствующие компоненты, либо закрывает все приложение, либо включает режим восстановления подсоединения. Первые два случая очевидны и в общем-то не требуют никаких дополнительных шагов, кроме установления значения параметра Actions. Более подробно мы остановимся на восстановлении подключения.

Поскольку CompaniesDataSet находится в режиме CachedUpdates, то потеря подключения не влияет на возможность редактирования данных пользователем, поскольку все изменения будут сохраняться в локальном буфере компонента CompaniesDataSet Таким образом, в задачу TpFIBDataBase входит только одно: периодически пытаться восстано'вить подключение и сообщать об удачных и неудачных попытках. Когда подсоединение будет восстановлено, мы просто применим "отложенные" изменения к данным в базе данных Конечно, никто не гарантирует, что отложенные команды смогут быть выполнены сервером, поскольку на момент восстановления подключения наши локальные данные могут совершенно потерять актуальность.

Итак, напишем два простых обработчика Первый для события Database OnErrorRestoreConnect

procedure TForml.DatabaseErrorRestoreConnect(Database:

TFIBDatabase;

E: EFIBError; var Actions: TOnLostConnectActions);

begin

Inc(AttemptRest);

Label4.Caption := IntToStr(AttemptRest);

Label4.Refresh;

end;

Компонент Label4 будет показывать счетчик попыток восстановления подсоединения. Второе событие, которое нас интересует, - AfterRestoreConnect:

procedure TForml DatabaseAfterRestoreConnect;

begin

MessageDlg('Connection restored. You can apply cached updates',

mtlnformation, [mbOk], 0

) ;

end;

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

procedure TForml.ButtonlClick(Sender: TObject);

begin

with CompaniesDataSet do

try

if not DataBase.Connected then begin

try

DataBase.Connected . = True;

except

MessageDlg('Can''t restore connect',

mtlnformation, [mbOk], 0

);

Exit;

end

end;

if not Transaction.Active then Transaction StartTransaction;

ApplyUpdToBase;

Transaction.CoimutRetaining;

CommitUpdToCach;

except

if Transaction.Active then Transaction.RollBack;

end;

end;

На практике вы можете автоматически вызывать процедуру применения данных сразу из обработчика AfterRestoreConnect. В самой процедуре следует обратить внимание на методы ApplyUpdToBase и CommitUpdToCach. В отличие от стандартного метода ApplyUpdates, наследованного от TDataSet, который "не замечает" скрытые в результате локальной фильтрации записи (более подробно этот вопрос будет рассмотрен в разделе "Локальная фильтрация"), методы ApplyUpdToBase и CommitUpdToCach позволяют обойти эту ошибку VCL

Эмуляция Boolean-полей

 Сделать закладку на этом месте книги

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

CREATE DOMAIN TBOOLEAN_CHAR AS CHAR(1)

DEFAULT 'F' NOT NULL

CHECK (VALUE IN ('F', 'T'))

CREATE DOMAIN TBOOLEAN_INT AS INTEGER

DEFAULT 0 NOT NULL

CHECK (VALUE IN (0, 1))

При использовании любой библиотеки компонент для доступа к InterBase оба способа совершенно равноценны, поскольку для полей, созданных с такими доменами, в приложении все равно не создаются Boolean-поля. То есть если мы добавим некоторое логическое поле к нашей таблице:

ALTER TABLE "Categories" ADD IS_ACTIVE TBOOLEAN_INT

NOT NULL

А потом, используя, например, компоненты IBX, сделаем выборку при помощи компонента TIBDataSet:

SELECT "Categories"."Name", "Categories".IS_ACTIVE

FROM "Categories"

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

* TIBStringField для поля "Name" и

* TmtergerField для поля "IS_ACTIVE"

Последнее совершенно верно, поскольку формально поле "IS_ACTIVE" не являет


убрать рекламу






ся логическим. Таким образом, все визуальные компоненты типа TDBGnd и его расширенные аналоги от сторонних производителей не будут обрабатывать данное поле так, как нам бы хотелось. Например, если даже компонент умеет рисовать "галочки" для Boolean-полей, значения которых равно True, то поскольку он будет "видеть" всего лишь целочисленное поле, то и выводить он будет "0" и "1" Разумеется, если мы напишем соответствующие обработчики событий, то сможем добиться более или менее сносного отображения логических величин для поля TIntergerField, однако FIBPlus предоставляет гораздо более простое и качественное решение

Фактически оно состоит только в том, что нам необходимо добавить ключ poUseBooleanField в свойстве PrepareOptions (рис. 2.60).







Рис 2.60. Использование PrepareOptions для эмуляции Boolean—полей

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

Поддержка array-полей. Пример использования TpFIBUpdateObject и TDataSetContainer

 Сделать закладку на этом месте книги

InterBase с самых ранних версий позволял описывать в таблицах многомерные поля-массивы, делая хранение специализированных данных максимально удобным. Вы наверняка согласитесь, что матриц) проще всего хранить и обрабатывать в виде матрицы, а не раскладывать ее по отдельным полям и даже таблицам из-за ограничений реляционной модели Тем не менее поскольку array- поля не поддерживаются стандартом SQL, то и работа с такими полями на уровне SQL-запросов крайне затруднена. Фактически вы можете использовать массивы только поэлементно и только в операциях чтения Чтобы изменить значения array-поля, необходимо использовать специальные команды InterBase API. FIBPlus позволяет обойтись без подобных сложностей, взяв на себя всю рутину, связанную с array-полями

Мы продемонстрируем, как работать с array-полями при помощи FIBPlus на примере DemoArray5, входящем в стандартную поставку FIBPlus. Пример демонстрирует два варианта использования array-полей Первый способ позволяет редактировать array-поле при помощи специальных методов ArrayFieldValue и SetAiiayValue и работать с таким полем как с единой структурой (рис 2 6П







Рис 2.61. Внешний вид формы примера DemoArrayS Первый вариант использования array—полей

Рассмотрим запросы, заданные в соответствующих свойствах ArrayDataSet:

SeleetSQL

SELECT

JOB JOB_CODE,

JOB.JOB_GRADE,

JOB.JOB_COUNTRY,

JOB.JOB_TITLE,

JOB MIN_SALARY,

JOB.MAX_SALARY,

JOB.JOB_REQUIREMENT,

JOB.LANGUAGE_REQ

FROM

JOB JOB

ORDER BY 1,2,3

UpdateSQL:

UPDATE JOB SET

JOB_CODE = ?JOB_CODE,

JOB_GRADE = ?JOB_GRADE,

JOB_COUNTRY = ?JOB_COUNTRY,

JOB_TITLE = ?JOB_TITLE,

MIN_SALARY = ?MIN_SALARY,

MAX_SALARY = ?MAX_SALARY,

JOB_REQUIREMENT = ?JOB_REQUIREMENT,

JOB.LANGUAGE_REQ= ?LANGUAGE_REQ WHERE

JOB_CODE = ?OLD_JOB_CODE

and JOB_GRADE = ?OLD_JOB_GRADE

and JOB_COUNTRY = ?OLD_JOB_COUNTRY

InsertSQL

INSERT INTO JOB(

JOB_CODE,

JOB_GRADE,

JOB_COUNTRY,

JOB_TITLE,

MIN_SALARY,

MAX_SALARY,

JOB_REQUIREMENT,

JOB.LANGUAGE_REQ

)

VALUES(

?JOB_CODE,

?JOB_GRADE,

?JOB_COUNTRY,

?JOB_TITLE,

?MIN_SALARY,

?MAX_SALARY,

?JOB_REQUIREMENT,

?LANGUAGE_REQ

)

DeleteSQL:

DELETE FROM JOB

WHERE

J03_CODE = ? OLD_JOB_CODE

and JOB_GRADE = ?OLD_JOB_GRADE

and JOB_COUNTRY = ?OLD_JOB_COUNTRY

RefreshSQL:

SELECT

JOB.JOB_CODE,

JOB.JOB_GRADE,

JOB.JOB_COUNTRY,

JOB.JOB_TITLE,

JOB.MIN_SALARY,

JOB.MAX_SALARY,

JOB.JOB_REQUIREMENT,

JOB.LANGUAGE_REQ

FROM

JOB JOB

WHERE

(

JOB.JOB_CODE = ?OLD_JOB_CODE

and JOB.JOB_GRADE = ?OLD_JOB_GRADE

and JOB.JOB_COUNTRY = ?OLD_JOB_COUNTRY

)

Поле LANGUAGE_REQ является массивом (LANGUAGE_REQ VARCHAR(15) [1:5]) и, как видно из запросов, обрабатывается целиком, а не поэлементно. С одной стороны это удобно, но не позволяет использовать для редактирования таких полей специализированные визуальные компоненты типа TDBGrid. Если мы используем array-поля для хранения значительных массивов данных, мы в любом случае будем использовать "ручную" обработку данных, не прибегая к помощи визуальных компонент. Однако для наглядности примера мы позволим редактировать элементы массива в компонентах TEdit.

Фактически нам понадобится написать только два основных обработчика для событий: BeforePost и OnPostError

procedure TForml.ArrayDataSetBeforePost(DataSet: TDataSet);

begin

with ArrayDataSet do begin

SetArrayValue(FieldByName('LANGUAGE_REQ'),

VarArrayOf([

Editl.Text,

Edit2.Text,

Edit3.Text,

Edit4.Text,

Edits.Text

]) ) ;

end;

end;

procedure TForml.ArrayDataSetPostError(DataSet: TDataSet;

E: EDatabaseError; var Action: TDataAction);

begin

Action := daAbort;

MessageDlg('Error!', mtError, [mbOk], 0);

ArrayDataSet.Refresh;

end;

Метод SetArrayValue позволяет задать все элементы поля в виде массива. Важным моментом является обработчик ошибки ArrayDataSetPostError В случае неудачной операции Update или Insert необходимо восстанавливать внутренний идентификатор массива у редактируемой записи. Это правило диктуется функциями InterBase API, и мы должны их придерживаться. Для восстановления идентификатора необходимо получить значения полей текущей записи заново, что и делается при помощи явного вызова метода Refresh.

Для автоматического заполнения визуальных компонентов значениями элементов массива мы можем написать обработчик события AfterScroll:

procedure TForml.ArrayDataSetAfterScroll(DataSet: TDataSet);

var v: Variant;

begin

with ArrayDataSet do try

FInShowArrays := true;

v := ArrayFieldValue(FieldByName('LANGUAGE_REQ'));

Editl.Text := VarToStr(v[l]);

Edit2.Text := VarToStr(v[2]);

Edit3.Text := VarToStr(v[3]);

Edit4.Text := VarToStr(v[4]);

EditS.Text := VarToStr(v[5]);

finally

FInShowArrays:=false;

end;

end;

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

Рекомендуется иметь под руками полный текст примера, который доступен на сайте https://www.fibplus.net. В книге мы указываем только те части примера, которые имеют непосредственное и наибольшее значение для работ с array-полями. Однако без полного текста некоторые части исходного текста могут показаться не до конца наглядными.

Данный способ работы с array-полями нельзя применять в режиме CachedUpdates.

Второй способ работы с array-полями позволяет использовать их в "живых" запросах и редактировать при помощи стандартных визуальных компонентов (рис. 2.62).

Рассмотрим запросы, указанные в свойствах ArrayDataSet:

SelectSQL:

SELECT

JOB.JOB_CODE,

JOB.JOB_GRADE,

JOB.JOB_COUNTRY,

JOB.JOB_TITLE,

JOB.LANGUAGE_REQ[1] LQ1,

JOB.LANGUAGE_REQ[2] LQ2

FROM

JOB JOB

ORDER BY 1,2,3







Рис 2.62. Внешний вид формы примера DemoArrayS. Второй вариант использования array-полей

UpdateSQL:

UPDATE JOB SET

JOB_TITLE = -?JOB_TITLE,

JOB JOB_GRADE=?JOB_GRADE,

JOB.JOB_COUNTRY=?JOB_COUNTRY,

LANGUAGE_REQ = ''LQ

WHERE

JOB_CODE = ?OLD_JOB_CODE

and JOB_GRADE = ?OLD_JOB_GRADE

and JOB_COUNTRY = ?OLD_JOB_COUNTRY

InsertSQL:

INSERT INTO JOB(

JOB_CODE,

JOB_GRADE,

JOB_COUNTRY,

JOB_TITLE,

LANGUAGE_REQ

)

VALUES(

?JOB_CODE,

?JOB_GRADE,

?JOB_COUNTRY,

?JOB_TITLE,

?LQ

)

DeleteSQL:

DELETE FROM JOB

WHERE

JOB_CODE = ?OLD_JOB_CODE

and JOB_GRADE = ?OLD_JOB_GRADE

and JOB_COUNTRY = ?OLD_JOB_COUNTRY

RefieshSQL:

SELECT

JOB.JOB_CODE,

JOB.JOB_GRADE,

JOB.JOB_COUNTRY,

JOB.JOB_TITLE,

JOB.LANGUAGE_REQ[1] LQ1,

JOB.LANGUAGE_REQ[2] LQ2

FROM

JOB JOB

WHERE

(

JOB.JOB_CODE = ?OLD_JOB_CODE

and JOB. JOB_GRADE = ?OLD_JOB_GRADE

and JOB.JOB_COUNTRY = ?OLD_JOB_COUNTRY

)

На этот раз мы выбираем только два элемента из нашего поля-массива. Обратите внимание' несмотря на то что в выбирающем запросе мы явным образом выделяем два элемента массива, а в модифицирующих запросах мы обновляем поле целиком На самом деле, это, конечно, не совсем так, однако данный синтаксис наиболее удобен и близок к естественному SQL-запросу несмотря на то что во внутренней реализации FIBPlus использует специальные функции работы с массивами Обратим внимание на компонент QryAirField TpFIBUpdateObject. Именно он позволит нам правильно сформировать значение параметра "LQ" в модифицирующих запросах Разумеется, для этой цели вполне бы подошел и простой TpFIBQuery, однако удобство TpFIBUpdateObject заключается в первую очередь в том, что он выполняет запрос автоматически и сам подставляет туда нужные значения параметров в зависимости от значений TpFIBDataSet. Вот запрос, который выполняет QryArrField SELECT

JOB LANGUAGE_REQ,

JOB JOB_CODE

FROM

JOB JOB

WHERE

JOB JOB_CODE=?OLD_JOB_CODE and

JOB.JOB_GRADE=?OLD_JOB_GRADE and

JOB.JOB_COUNTRY=?OLD_JOB_COUNTRY

Обратим внимание на свойства QryArrField (рис 2 63)







Рис 2.63. Свойство компонента QryArrField

Обработчик QiyAnField AtteiExecute

procedure TForm2 QryArrFieldAfterExecute(Sender: TObject);

var v Variant,

begin

v = QryArrField Fields[0] GetArrayValues,

with ArrayDataSet do begin

v[l] = FieldByName('LQ1') AsString,

v[2] = FieldByName( LQ2 ) AsString,

QryArrField Fields[0] SetArrayValue(v);

QUpdate Params ByName[ LQ ] AsQuad :=

QryArrField.Fields[0] AsQuad;

end;

QryArrField Close;

end;

Теперь все становится совершенно очевидным Поскотьку свойство QiyAnField KindUpdate равно ukModify, то QiyArrField выполняет запрос при изменении записи в AnayDataSet а поскольку свойство QiyArrField ExecuteOider равно eoBeforeDefault, то запрос (QiyArrField SQL) выполняется до того, как AirayDataSet выполнит свой собственный UpdateSQL В обработчике QryArrField AfterExecute мы всего лишь получаем заново все текущие элементы массива из базы данных, подменяем два из них новыми значениями, которые указал пользователь, и задаем значение параметра для ArrayDataSet.UpdateSQL Это означает что при выполнении ArrayDataSet.UpdateSQL формально будут обновлены все пять элементов массива но фактически изменены значения тотько двух элементов, которые изменил пользователь в TDBGrid.

Как видите работа с массивами достаточно проста поскотьку все основные сложности решают компоненты FIBPlus Хотелось бы также рассмотреть еще один специализированный компонент, входящий в FIBPlus Пример DemoArray демонстрирует работу с TDataSetContamei, использованным для синхронизации значений двух TpFIBDataSet, редактирующих наше апау-поле (рис 2 64)







Рис 2.64. Использование TDatabetContamer

Компонент DataSetContamerl помещен вместе с Database и Transaction на DataModule в нашем приложении Оба компонента AnayDataSet из разных форм нашего приложения ссылаются на DataSetContamerl при помощи свойства Contamei (рис 2 65)







Рис 2.65. Подключение компонентов TpFIBDataSet к DataSetContamerl

Компонент TDataSetContamei позволяет централизованно обрабатывать события от разных компонентов TpFIBDataSet, а также (расширяя, таким образом, список стандартных событий) посылать им сообщения, при получении которых они могут производить какие-то дополнительные действия. В нашем примере DataSetContainerl имеет обработчики двух событий OnDataSetEvent и OnUserEvent


procedure TDataModule2.DataSetsContainerlDataSetEvent(DataSet:

TDataSet;

Event: TKindDataSetEvent);

var Info: string;

begin

if Event = deAfterPost then

if DataSet.Owner.Name = 'Forml' then

DataSetsContainerl.NotifyDataSets(DataSet,

'Form2.ArrayDataSet', 'JOB_TABLE_CHANGED', Info)

else

if DataSet.Owner.Name = 'Form2' then

DataSetsContainerl.NotifyDataSets(DataSet,

'Form1.ArrayDataSet', 'JOB_TABLE_CHANGED', Info);

end;

procedure TDataModule2.DataSetsContainerlUserEvent(Sender:

TObject;

Receiver: TDataSet; const EventName: String; var Info:

String) ;

begin

if EventName = 'JOB_TABLE_CHANGED' then begin

with TpFIBDataSet(Sender) do

if (not CachedUpdates) and

(not TpFIBDataSet(Receiver).CachedUpdates) then

if

TpFIBDataSet(Receiver).Locate('JOB_CODE;JOB_GRADE;JOB_COUNTRY', varArrayOf([

FieldByNamet'JOB_CODE').AsString,

FieldByName('JOB_GRADE').AsString,

FieldByName('JOB_COUNTRY').AsString

]), []) then TpFIBDataSet(Receiver) Refresh

end;

end;


Смысл действий сводится к следующему: после изменения записи в одном из наших компонентов ArrayDataSet происходит событие AfterPost. Поскольку DataSetContainerl перехватывает все события у подчиненных компонентов, то срабатывает обработчик OnDataSetEvent. Параметр Event равен deAfterPost, а параметр DataSet ссылается на тот компонент, в котором произошло изменение записи. При помощи вызова метода NotifyDataSets DataSetContainerl посылает сообщение оставшемуся компоненту ArrayDataSet о том, что произошло изменение записи. Поскольку оба компонента на самом деле редактируют одну и ту же таблицу, то желательно синхронизировать изменения. Синхронизация происходит в обработчике события OnUserEvent, то есть при получении "извещения" об изменении какого-либо из ArrayDataSet.

Если получено сообщение "JOB_TABLE_CHANGED" и ни в одном из наших двух ArrayDataSet не включен режим CachedUpdates (в этом случае Refresh просто ничего не даст), то мы позиционируемся на соответствующую запись и вызываем для нее метод Refresh, обновив, таким образом, запись в одном ArrayDataSet после того, как она была изменена в другом ArrayDataSet.

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

Работа с BLOB-полями

 Сделать закладку на этом месте книги

Достаточно часто желательно хранить в базе данных разнообразные неструктурированные данные: изображения, OLE-объекты, звук и т. д. Специально для этих целей существует специальный тип данных - BLOB Продемонстрируем использование BLOB-полей на примере простого приложения (см. рис. 2.66), использующего следующую таблицу:

CREATE TABLE BIOLIFE (

ID INTEGER NOT NULL,

CATEGORY VARCKAR (15) character set WIN1251 collate

WIN1251,

COMMON_NAME VARCHAR (30) character set WIN1251 collate

WIN1251,

SPECIES_NAME VARCHAR (40) character set WIN1251 collate

WIN1251,

LENGTH_CM_ DOUBLE PRECISION,

LENGTH_IN DOUBLE PRECISION,

NOTES BLOB sub_type 1 segment size 80,

GRAPHIC BLOB sub_type 0 segment size 80);

Для вывода изображений, сохраненных в поле GRAPHIC, мы будем использовать стандартный компонент DBIMagel: TDBImage. Очевидно также, что запросы при работе с BLOB-полями ничем не отличаются от запросов со стандартными типами полей:

SelectSQL:

SELECT * FROM BIOLIFE

UpdateSQL:

UPDATE BIOLIFE Set

ID=?NEW_ID,

CATEGORY=?NEW_CATEGORY,

COMMON_NAME=?NEW_COMMON_NAME,

SPECIES_NAME=?NEW_SPECIES_NAME,

LENGTH__CM_=?NEW_LENGTH_CM_,

LENGTH_IN=?NEW_LENGTH_IN,

NOTES=?NEW_NOTES,

GRAPHIC=?NEW_GRAPHIC

WHERE ID=?OLD_ID







Рис 2.66. Внешний вид формы приложения для работы с BLOB-полями

InsertSQL:

INSERT INTO BIOLIFE(

ID,

CATEGORY,

COMMON_NAME,

SPECIES_NAME,

LENGTH__CM_,

LENGTH_IN,

NOTES,

GRAPHIC

)

VALUES (

?NEW_ID,

?NEW_CATEGORY,

?NEW_COMMON_NAME,

?NEW_SPECIES_NAME,

?NEW_LENGTH_CM_,

?NEW_LENGTH_IN,

?NEW_NOTES ,

?NEW_GRAPHIC

)

DeleteSQL:

DELETE FROM BIOLIFE

WHERE ID=?OLD_ID

RefreshSQL:

SELECT * FROM BIOLIFE

WHERE

ID=?OLD_ID

Единственным отличием от обычных типов данных является то, что для присвоения значения BLOB-параметру необходимо использовать потоки (специализированные потомки стандартного класса TStream). Например, если мы хотим сохранить в нашем поле изображение из внешнего файла, то мы можем написать следующий обработчик нажатия на кнопку:

procedure TMainForm.OpenBClick(Sender: ТОbjесt);

var S. TStream;

FileS: TFileStream;

begin

if not OpenD.Execute then exit;

pFIBDataSetl.Edit;

S : =

pFIBDataSetl.CreateBlobStream(pFIBDataSetl.FieldByName('GRAPHIC') , bmReadWrite);

try

FileS := TFiIeStream.Create(OpenD.FileName, fmOpenRead);

S.CopyFrom(FileS, FileS.Size);

finally

FileS.Free;

S.Free;

pFIBDataSetl.Post;

end;

end;

Обратите внимание на важный момент - перед тем как присваивать значение BLOB-параметру, необходимо перевести pFIBDataSet в состояние редактирования данных. В данном случае это делается безусловным вызовом метода Edit. Вызов метода CreateBlobStream создает экземпляр специального внутреннего класса TFIBDSBlobStream. Скорее всего, вам не придется использовать этот класс напрямую. В нашем примере он нужен только для обмена данными между BLOB-параметром и потоком, который читает данные из файла с изображением. Параметр bmReadWrite означает, что мы собираемся изменять содержимое параметра. Поток S напрямую связан с BLOB-параметром, поэтому, копируя данные из файла (FileS) в поток S, мы фактически присваиваем значение параметру. Остается только сохранить изменения вызовом метода Post Аналогичным образом мы можем сохранить значение BLOB-поля в некоторый внешний файл:

procedure TMainForm.SaveBClick(Sender: TObject);

var S: TStream;

FileS: TFiIeStream;

begin

if not SaveD.Execute then exit;

if not pFIBDatasetl.FieldByName('GRAPHIC').IsNull then begin

S : =

pFIBDatasetl.CreaceBlobStream(pFIBDatasetl.FieldByName('GRAPHIC '), bmRead);

try

if FileExists(SaveD.FileName) then

FileS := TFileStream.Create(SaveD.FileName, fmOpenWrite)

else

FileS := TFileStream.Create(SaveD.FileName, fmCreate);

FileS.CopyFrom(S, S.Size);

finally

S.Free;

FileS.Free;

end;

end;

end;

Обратите внимание, что в этой процедуре мы используем параметр bmRead при создании потока S. Очевидно, что для сохранения содержимого BLOB-поля в файл нам не нужно изменять само поле, поэтому мы создаем поток только для чтения. Еще более простым способом мы можем очистить содержимое BLOB-поля:

procedure TMainForm.ButtonlClick(Sender: TObject);

begin

pFIBDataSetl .Edit;

pFIBDataSetl.FieldByName('GRAPHIC').Clear;

pFIBDataSetl.Post;

end;

В этом случае даже не требуется создавать какие-либо потоки. Иногда также нужно знать, является ли BLOB-поле пустым или нет. При использовании визуальных компонентов типа TDBImage мы не можем быть в этом уверены. Согласитесь, что никто не мешает нам "нарисовать" пустую картинку и сохранить ее в BLOB-поле В этом случае мы не сможем отличить "на глаз": есть ли какое-то изображение в BLOB-поле или нет. Однако мы можем написать обработчик события OnDataChange компонента DataSourcel: TDataSource:

procedure TMainForm.DataSourcelDataChange(Sender: TObject;

Field: TField) ;

begin

CheckBoxl.Checked :=

pFIBDataSetl.FieldByName('GRAPHIC').IsNull;

end;

Это событие вызывается, в частности, при навигации по DBGridl; таким образом, мы всегда можем узнать, является ли текущее поле пустым или нет.

Если вы используете TpFIBQuery для работы с BLOB-полями, то общий принцип остается тем же - необходимо использовать потоки, однако, в отличие от TpFIBDataSet, вам не потребуется создавать какие-то специальные потоки. Например, мы можем написать следующую процедуру, которая сохранит в файлы все изображения из нашей таблицы:

pFIBQuery.SQL: SELECT * FROM BIOLIFE

procedure TMainForm.Button2Click(Sender: TObject);

var SaveFile: TFileStream;

Index: Integer;

begin

with pFIBQueryl do begin ExecQuery;

Index := 1;

while not Eof do begin

SaveFile := TFileStream.Create(IntToStr(Index) + '.bmp',

fmCreate);

FN('GRAPHIC').SaveToStream(SaveFile);

Next;

inc(Index);

SaveFile.Free;

end;

Close;

end;

end;

Метод FN является аналогом FieldByName.

Смысл кода, приведенного выше, совершенно очевиден: мы получаем все записи из таблицы BIOLIFE, в цикле получаем от сервера очередную запись из запроса, создаем файл при помощи потока SaveFile, сохраняем в него значение поля GRAPHIC и запрашиваем следующую запись при помощи метода Next. Аналогичным образом мы могли бы присваивать значение BLOB-параметру: pFIBQuery.SQL: INSERT INTO BIOLIFE (GRAPHIC) VALUES (7GRAPHIC)

procedure TMainForm.Button2Click(Sender: TObject);

var SaveFile: TFileStream;

Index: Integer;

begin

with pFIBQueryl do begin

Prepare;

for Index := 1 to 3 do begin

SaveFile := TFileStream.Create(IntToStr(Index) + '.bmp', fmOpenRead);

Params[0].LoadFromStream(SaveFile);

SaveFile.Free;

ExecQuery;

end;

Transaction.Commit;

end;

end;

Данный пример вставляет три новые записи в таблицу BIOLIFE и сохраняет в них изображения из некоторых файлов "l.bmp", "2.bmp" и "3.bmp".

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

Локальная сортировка и локальная фильтрация

 Сделать закладку на этом месте книги

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

Локальная сортировка

 Сделать закладку на этом месте книги

Рассмотрим локальную сортировку на примере Sorting, который входит в поставку FIBPlus.

Как и все остальные примеры, приложение Sorting вы можете наши на сайте https://www.fibplus.net

Пример использует таблицу EMPLOYEE из базы данных Employee.gdb (рис. 2.67)







Рис 2.67. Внешний вид формы приложения, демонстрирующего локальную сортировку в TpFIBDataSet

MainDS.SelectSQL: SELECT * FROM EMPLOYEE

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

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

Предположим, что пользователь хочет отсортировать записи по полю FIRST_NAME. Очевидно, что мы можем сформировать новый запрос:

SELECT * FROM EMPLOYEE

ORDER BY FIRST_NAME

Чтобы получить новый порядок записей, необходимо переоткрыть наш запрос. Это работает, но означает, что мы будем повторно получать от сервера все •записи, несмотря на то что нам нужно было всего лишь поменять их визуальный порядок. Если предполагать, что нашим приложением могут пользоваться одновременно несколько пользователей, то затраты на подобные "сортировки" могут оказаться довольно значительными. Тем не менее выход существует, поскольку TpFIBDataSet позволяет сортировать данные внутри своего локального буфера, т. е. без необходимости получения всех записей заново.

Для этого мы должны воспользоваться одним из возможных вариантов метода DoSort или DoSortEx:

procedure DoSort(Fields: array of const; Ordering: array of Boolean); virtual;

procedure DoSortEx(Fields: array of integer; Ordering: array of Boolean); overload;

procedure DoSortEx(Fields: TStrings; Ordering: array of Boolean); overload;

Метод DoSortEx доступен в FIBPlus начиная с версии Delphi 4.

Первый параметр всех трех процедур - это список полей, по которым мы хотим отсортировать данные. В случае DoSort это могут быть названия полей или номера полей. Первый вариант DoSortEx позволяет нам использовать только список с номерами полей, а второй вариант DoSortEx предполагает, что мы заполнили список Fields названиями полей. Параметр Ordering во всех трех случаях указывает направление сортировки по каждому из полей. Таким образом, мы можем отсортировать наш запрос по полю FIRST_NAME следующим образом:

DoSort(['FIRSN_NAME'], [True]);

Или: DoSortEx([1], [True]);

[True] означает, что поле сортируется по возрастанию (ASCENDING).

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

type

TOrderStringList = class(TStringList)

protected

function GetAscending(Index: Integer): boolean;

procedure SetAscending (Index: Integer; Value: boolean);

public

property Ascending[Index: Integer]: boolean read

GetAscending write SetAscending;

end;

...

{ TOrderStringList }

function TOrderStringList.GetAscending(Index: Integer):

boolean;

begin

Result := boolean(integer(Objects[Index]));

end;

procedure TOrderStringList.SetAscending(Index: Integer; Value:

boolean);

begin

Objects [ Index] := pointer (integer (Value));

end;

Очевидно, что данный класс является списком строк - названия полей, а также хранит для каждого поля, включенного в список, порядок сортировки в виде свойства Ascending. Ниже вы видите описание класса формы, а также два основных обработчика событий (OnCreate, OnDestroy). При создании формы мы создаем экземпляр класса TOrderStringList, а при ее уничтожении - удаляем.

TMainForm = class(TForm)

MainDB: TpFIBDatabase;

MainDS: TpFIBDataSet;

MainTr: TpFIBTransaction;

DataSourcel: TDataSource;

DBGridl: TDBGrid;

Buttonl: TButton;

Label2: TLabel;

procedure DBGridlTitleClick(Column: TColumn);

procedure FormCreate(Sender: TObject);

procedure FormDestroy(Sender: TObject);

procedure ButtonlClick(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

SortFields: TOrderStringList;

procedure ReSort;

end;

procedure TMainForm.FormCreate(Sender: TObject);

begin

SortFields := TOrderStringList.Create;

end;

procedure TMainForm.FormDestroy(Sender: TObject);

begin

SortFields.Free;

end;

Теперь напи


убрать рекламу






шем обработчик события OnTitleClick у компонента DBGridl:

procedure TMainForm. DBGndlTitleCiiCK (Column: TColumn);

const OrderScr: array [boolean] of string = ('(DESC)',

'(ASC) ') ;

var aField: string;

aFieldlndex: integer;

begin

aField := Column.FieldName;

aFieldlndex := SortFields.IndexOf(aField) ;

if aFieldlndex = -1 then begin

SortFields.Add(aField);

SortFields.Ascending[SortFields.Count - 1] := true;

Column.Field.Display-Label := Column. Field.FieldName + OrderStr[true];

end

else begin

SortFields.Ascending[aFieldlndex] := not

SortFields.Ascending[aFieldlndex];

Column.Field.Display-Label := Column.Field.FieldName +

OrderStr[SortFields.Ascending[aFieldlndex]];

end;

ReSort;

end;

Смысл обработчика состоит в следующем: при нажатии пользователем на заголовок мы проверяем, есть ли данное поле в нашем списке сортировки. Если нет, то мы добавляем его и формируем новый заголовок для колонки, который теперь будет состоять из названия поля и порядка сортировки (ASC) или (DESC). Если же поле уже было включено в сортировку, то мы лишь меняем порядок сортировки. В обоих случаях мы должны вызвать процедуру ReSort, описанную ниже:

procedure TMainForm.ReSort;

var Orders: array of boolean;

Index: Integer;

begin

if SortFields.Count = 0 then begin MainDS.CloseOpen(false);

exit;

end;

SetLength(Orders, SortFields.Count);

for Index := 0 to pred(SortFields.Count) do

Orders[Index] := SortFields.Ascending[Index];

MainDS.DoSortEx(SortFields, Orders);

end;

Данная процедура формирует списки для метода DoSortEx на основе списка SortFields и пересортировывает записи. В случае, если наш список сортировки пустой, мы должны вернуться к "стандартному" порядку записей. Для этого вызывается метод CloseOpen.

Параметр False означает, что мы не хотим автоматически получать сразу все записи от сервера.

Если наш список полей не пустой, то мы должны сформировать массив Orders. Это делается, как видно, достаточно легко. Используя динамические массивы, мы сначала задаем длину Orders равной количеству полей в списке SortFields, а потом последовательно заполняем Orders значениями свойства Ascending списка SortFields. Остается только вызвать метод DoSortEx - и сортировка б\дет выполнена.

Чтобы закрыть вопрос целиком, нам остается только предоставить пользователю возможность исключать поля из сортировки. Для этого мы положим на форму кнопку Button 1 (см. рис. 46, заголовок "Delete column from Sorting"):

procedure TMainForm.ButtonlClick(Sender: TObject);

var aField: string;

aFieldlndex: integer;

begin

aField := DBGridl.SelectedField.FieldName;

aFieldlndex := SortFields.IndexOf(aField);

if aFieldlndex <> -1 then begin

SortFields.Delete(aFieldlndex);

DBGridl.SelectedField.DisplayLabel := aField;

ReSort;

end;

end;

Пользователь выделяет поле, которое хочет удалить из сортировки, и нажимает на кнопку "Delete column from Sorting", после чего это поле удаляется из списка SortFields и производится пересортировка записей.

Локальная фильтрация

 Сделать закладку на этом месте книги

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

Рассмотрим локальную фильтрацию на примере приложения Filtering. Этот пример включен в стандартную поставку FIBPlus. В примере используется база данных FIBPlus_Example.gdb (рис. 2.68).

Эта база данных в виде backup-файла доступна на сайте https://www.fibplus.net/







Рис 2.68. Использование локальной фильтрации TpFIBDataSet

Совершенно очевиден код для подключения к базе данных. Список элементов компонента FieldsC: TComboBox заполняется названиями полей из таблицы BIOLIFE.

FilteringDS.SelectSQL: SELECT * FROM BIOLIFE

procedure TMainForm.btnConnectClickfSender: TObject);

var Index: Integer;

begin

with MainDB do begin

ConnectParams.UserName := edtUserName.Text;

ConnectParams.Password := edtPassword.Text;

DBName := edtDataBase.Text;

try

Open;

FilteredDS.Open;

FieldsC.Items.Clear;

for Index := 0 to pred(FilteredDS.FieldCount) do

FieldsC.Items.Add(FilteredDS.Fields[Index].FieldName);

except

MessageDlgt'Error of connection to a database!', mtError, [mbOk], 0) ;

Close;

end;

end;

end;

Пользователь может выбрать поле из списка FieldsC, указать в поле FilterE: TEdit строку для поиска и после нажатия на кнопку Button I ("Activate Filter"):

procedure TMainForm.ButtonlClick(Sender: T0b3ect);

begin

FilteredDS.Filtered := false;

FilteredDS.Filtered := true;

end;

в DBGrid 1 останутся видны только те записи, которые содержат в заданном поле искомую строку Для этого нам необходимо написать обработчик события OnFilterRecord у FilteringDS:

procedure TMainForm.FilteredDSFilterRecord(DataSet: TDataSet;

var Accept: Boolean); begin

Accept := DOS(FilterE.Text,

FilteredDS.FieldByName(FieldsC.Text).AsString) <> 0

end;

После включения FilteredDS.Filtered := true OnFilterRecord вызывается для каждой записи в локальном буфере. Если мы хотим, чтобы конкретная запись оставалась видимой, мы должны задать для нее параметр Accept равным True. Из примера кода видно, что мы оставляем видными только те записи, которые содержат в искомом поле (FieldsC.Text) заданную строку (FilterE.Text). Если мы, например, выберем поле COMMON_NAME и укажем строку для поиска "А", то в результате получим только две видимые записи (рис. 2.69).







Рис 2.69. Пример двух "отфильтрованных" записей

Поскольку наше условие для фильтра сформулировано таким образом, что мы выбираем записи по точному включению искомой подстроки - в нашем случае это только те записи, которые содержат большую букву "А". Мы можем немного усложнить условие поиска. Положим на форму CaseC: TcheckBox (см. рис. 2.69, "Case sensitive") и перепишем обработчик OnFilterRecord следующим образом:

procedure TMainForm.FilteredDSFilterRecord(DataSet: TDataSet;

var Accept: Boolean);

begin

if CaseC.Checked then

Accept := pos(FilterE.Text,

FilteredDS.FieldByName(FieldsC.Text).AsString) <> 0

else

Accept := pos(AnsiUpperCase(FilterE.Text),

AnsiUpperCase(FilteredDS.FieldByName(FieldsC.Text).AsString))

<> 0;

end;

Очевидно, что теперь если пользователь отключит "флажок" "Case sensitive", то фильтр по строке "А" будет содержать гораздо большее количество записей - все записи, которые содержат хотя бы одну букву "А", неважно маленькую или большую (рис. 2.70).







Рис 2.70. Записи, содержащие букву "А"

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

Обработка событий InterBase при помощи FIBPIus

 Сделать закладку на этом месте книги

InterBase дает разработчику некоторый механизм для синхронизации приложений в многопользовательской среде Event Alerts. Суть данного механизма состоит в том. что вы можете вызывать пользовательские события из триггеров или хранимых процедур при помощи функции POST_EVENT:

CREATE PROCEDURE SHOW_EVENT(

EVENT_ID INTEGER)

AS

BEGIN

IF (:EVENT_ID = 1) THEN POST_EVENT 'TEST_EVENT1';

IF (.EVENT_ID = 2) THEN POST_EVENT 'TEST_EVENT2';

IF (.EVENT_ID = 3) THEN POST_EVENT 'TEST_EVENT3';

EXIT;

END

Создадим приложение (рис 2.71), которое будет вызывать данную процедуру и получать соответствующие события Если вы впоследствии попробуете запустить это приложение на двух компьютерах, то оба приложения будут получать сообщения вне зависимости оттого, кто их инициировал







Рис 2.71. Использование InterBase events, при помощи компонента TSIBfibEventAlerter

Для вызова хранимой процедуры мы будем использовать специальный компонент pFIBStoiedPioc TpFIBStoredPioc, задав свойство StoiedProcName (рис 2.72)







Рис 2.72. Вызов хранимой процедуры при помощи компонента TpFIBStoredPioc

Компонент TpFIBStoredProc является прямым потомком TpFIBQuery и сам формирует свойство SQL в виде ' EXECUTE PROCEDURE ...' Название процедуры задается свойством StoiedProcName

Компонент SffifibEventAlerterl: TSffifibEventAlerterlпредназначен для получения приложением событий InterBase Компонент ссылается на конкретное подключение к базе данных, т.е. на компонент типа TpFIBDatabase (рис 2.73)







Рис 2.73. Свойства SIBfibEventAlerterl

Установим свойство AutoRegister в False, чтобы собственноручно зарегистрировать нужные нам события, а в свойстве Events перечислим те события, которые нас интересуют (рис. 2.74).







Рис 2.74. Список зарегистрированных событий

Чтобы компонент SIBfibEventAlerterl "получал" события, мы должны зарегистрировать их после подключения к базе данных. Для этого напишем обработчик события OnConnect у компонента pFIBDatabasel:

procedure TForml.pFIBDatabaselConnect(Sender: TObject);

begin

if not SIBfibEventAlerterl.Registered then

SIBfibEventAlerterl.RegisterEvents;

end;

Соответственно перед закрытием подключения желательно "отключить" SIBfibEventAlerterl. Для этого создадим обработчик события BeforeDisconnect компонента pFIBDatabasel:

procedure TForml.pFIBDatabaselBeforeDisconnect(Sender:

TObject);

begin

if SIBfibEventAlerterl.Registered then

SIBfibEventAlerterl.UnRegisterEvents;

end;

Зададим свойство Tag у кнопок равным соответственно 1, 2 и 3, и напишем общий обработчик события OnClick для них:

procedure TForml.ButtonsClick(Sender: TObject);

begin

if not pFIBTransactionl.InTransaction then

pFIBTransactionl.StartTransaction;

pFIBStoredProcl.Params[0].Aslnteger := TButton(Sender).Tag;

try

pFIBStoredProcl.ExecProc;

pFIBTransactionl.Commit;

except

pFIBTransactionl.Rollback;

end;

end;

Смысл кода очевиден - мы задаем значение параметра процедуры, используя свойство Tag у кнопки, на которую нажал пользователь, выполняем процедуру при помощи метода ExecProc и либо подтверждаем транзакцию, либо отменяем ее в случае ошибки. Нам остается только написать обработчик события OnEventAlert компонента SIBfibEventAlerterl:

procedure TForml . SIBf ibEvent Alerter 1 Event Alert (Sender : TObject ;

EventName: String, EventCount: Integer);

begin

ShowMessage(EventName + ' : ' + IntToStr(EventCount));

end;

Параметр EventName возвращает название произошедшего события, a EventCount возвращает количество событий EventName. Значение параметра EventCount требует некоторых разъяснений. Итак, при работе с событиями важно помнить, что реально они вызываются только в случае подтверждения транзакции. Иными словами, если вы вызываете некоторое событие 'NEW_CUSTOMER' при вставке записей в таблицу, и вызываете Commit только после вставки 1000 записей, то обработчик OnEventAlert будет вызван только один раз, сразу после Commit, и значение параметра EventCount будет равно 1000. Очевидно, что событие произошло 1000 раз, но приложение получает нотификацию обо всех событиях только после подтверждения транзакции, в контексте которой они происходили.

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

"Низкоуровневая" работа с внутренним буфером TpFIBDataSet

 Сделать закладку на этом месте книги

TpFIBDataSet включает несколько специальных методов для работы с внутренним буфером, в котором хранятся записи. В общем-то, данные методы превращают TpFIBDataSet в аналог TClientDataSet, ориентированный на InterBase. Наиболее интересным примером использования данных методов будет, пожалуй, построение диалога выбора, состоящего из двух списков (рис. 2.75).







Рис 2.75. Использование локального кеша TpFIBDataSet для организации двойного списка выбора

В данном примере мы будем использовать базу данных EMPLOYEE.GDB, входящую в стандартную поставку InterBase и FIBPlus. Покажем запросы для SourceDS:

SelectSQL:

SELECT

CUS.CUST_NO,

CUS.CUSTOMER

FROM

CUSTOMER CUS

ORDER BY CUS.CUSTOMER

UpdateSQL:

UPDATE CUSTOMER SET

CUSTOMER = ?CUSTOMER

WHERE

CUST_NO = ?OLD_CUST_NO

InsertSQL:

INSERT INTO CUSTOMER(

CUST_NO,

CUSTOMER

)

VALUES(

?CUST_NO,

?CUSTOMER

)

DeleteSQL:

DELETE FROM CUSTOMER

WHERE

CUST_NO = ?OLD_CUST_NO

RefreshSQL:

SELECT

CUS.CUST_NO,

CUS.CUSTOMER

FROM

CUSTOMER CUS

WHERE

(

CUS.CUST_NO = ?OLD_CUST_NO

)

Те же самые запросы мы будем использовать в TargetDS, поскольку оба компонента должны обладать совместимой структурой полей. Откроем оба запроса сразу после создания формы:

procedure TDualListForm.FormCreate(Sender: TObject);

begin

SourceDS.Open;

TargetDS.CacheOpen;

end;

Если мы не указываем в коде явного открытия базы данных, то это означает, что свойство Connected > компонета TpFlBDatabase было задано в True в design-time.

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

procedure TDualListForm.MoveRec(Select: Boolean);

begin

if Select then begin

if (SourceDS.Active) and (not SourceDS.IsEmpty) then begin

TargetDS.CacheRefreshByArrMap(SourceDS, frklnsert,

['CUST_NO'], ['CUST_NO']);

SourceDS.CacheDelete;

end;

end

else begin

if (TargetDS.Active) and (not TargetDS.IsEmpty) then begin

SourceDS.CacheRefresh(TargetDS, frklnsert, nil);

TargetDS.CacheDelete;

end;

end;

end;

Если параметр Select равен True, то мы будем переносить текущую запись из левого списка в правый, т. е. из SourceDS в TargetDS. Рассмотрим подробнее метод CacheRefreshByArrMap.

procedure CacheRefreshByArrMap(FromDataSet: TDataSet; Kind:

TCachRefreshKind; const SourceFields, DestFields: array of

String);

Метод позволяет вставлять или модифицировать текущую запись в локальный буфер компонента, т. е. без выполнения соответствующих модифицирующих запросов. Параметр FromDataSet указывает, из какого TpFIBDataSet мы хотим получить запись. Параметр Kind может принимать два значения: frkEdit или frklnsert. Frklnsert указывает, что мы хотим вставить новую запись, a frkEdit - что мы хотим изменить существующую. Параметр SourceFields описывает список полей из компонента источника, а список DestFields - список полей в TpFIBDataSet-приемнике. Формально названия полей в списках могут не совпадать, но необходимо, чтобы совпадали типы данных, иначе обмен данными будет невозможен. В нашем примере, как вы можете видеть, мы вставляем в TargetDS текущую запись из SourceDS. Данный метод использован здесь только для демонстрации, поскольку мы вполне могли бы обойтись методом CacheRefresh, который копирует соответствующие поля автоматически. После вставки записи необходимо удалить ее из компонента источника. Это делается при помощи метода CacheDelete, который также не выполняет соответствующий запрос (DeleteSQL), а удаляет запись только из локального буфера в памяти. Теперь, когда мы знаем, что делают процедуры CacheRefreshByArrMap, CacheRefresh и CacheDelete, код процедуры достаточно очевиден: мы копируем запись из компонента-источника в компонент-приемник и удаляем ее из компонента-источника.

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

procedure TDualListForm.MoveAll(Select: Boolean);

var TmpDataSet: TpFIBDataSet;

begin

if Select then TmpDataSet := SourceDS else

TmpDataSet := TargetDS;

with TmpDataSet do begin

try

DisableControls;

First;

while not Eof do MoveRec(Select);

finally

EnableControls;

end;

end;

end;

Остается написать обработчики события OnClick для кнопок:

procedure TDualListForm.ButtonlClick(Sender: TObject);

begin

MoveRec(True); // SourceDS -> TargetDS

end;

procedure TDualListForm.Button2Click(Sender: TObject);

begin

MoveRec(False); // TargetDS -> SourceDS

end;

procedure TDualListForm.Button3Click(Sender: TObnect);

begin

MoveAll(True), // SourceDS -> TargetDS

end;

procedure TDualListForm. Button4Click (Sender : TObject) ;

begin

MoveAll(False); // TargetDS -> SourceDS

end;

Мы можем запустить наше приложение (рис 2.76)

Если мы будем нажимать на кнопку Button 1, то записи будут переноситься из левого списка в правый (рис. 2 77).

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


OpenAsCIone - открыть DataSet и скопировать данные из другого DataSet

CacheAppend - добавить запись в конец локального буфера.

SwapRecords - обменять записи в локальном буфере.

CacheEdit - задать значения полей у записи в локальном буфере.

Cachelnsert - вставить запись в локальный буфер

CacheModify - общий метод для вставки и изменения записи в локальным буфере

LoadFromFile - заполнить локальный буфер записями из файла.

LoadFromStream - заполнить локальный буфер записями из потока.

SaveToFile - сохранить локальный буфер в файл.

SaveToStream - сохранить локальный буфер в потоке


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







Рис 2.76. Внешний вид запущенного приложения







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

Разработка клиентских приложений СУБД InterBase с использованием технологии Microsoft OLE DB

 Сделать закладку на этом месте книги

Немного истории

 Сделать закладку на этом месте книги

Одним из распространенных заблуждений разработчиков баз данных является мысль, что СУБД InterBase ориентирована исключительно на работу с продуктами компании Borland. И этому способствовало то, что до последнего времени все качественные библиотеки доступа к этому серверу баз данных существовали только для создания приложений на Delphi, C++ Builder или Kylix. Для остальных систем программирования приходилось использовать InterBase API или ODBC. И хотя первое позволяет создавать высокопроизводительные приложения, а второе обладает значительными претензиями на универсальность, оба подхода не в полной мере удовлетворяют требованиям современных программных систем, базирующихся на компонентных технологиях. Поэтому потребность в использовании компонентов доступа к InterBase, универсальных с точки зрения языка программирования, была. И вопрос их реализации заключался только в одном: что именно должны предоставлять эти компоненты и кто решится начать их разработку.

Ответ на первый вопрос дают принципы современной организации масштабируемой архитектуры программного обеспечения, подразумевающие использование компонентов и их группировку в раздельно компилируемые модули. Поэтому компоненты доступа должны обеспечить прозрачную интеграцию компонентов в пределах как одного модуля, так и нескольких. И технология Component Object Model (COM) позволяет без проблем применять эти принципы на практике. Но полноценное использование этой технологии для создания крупных проектов с использованием InterBase осложнялось отсутствием готовой стандартизованной реализации СОМ-объектов доступа к этой СУБД. В результате разработчики программного обеспечения под InterBase вынуждены либо продолжав создавать монолитные приложения, либо самостоятельно решать проблемы совместного использования ресурсов СУБД малосвязанными между собой модулями программы. В первом случае осознанное ограничение возможностей программы экономит время. Во втором, опуская дополнительные трудозатраты на создание компонентов доступа, можно попасть в ловушку, которая в лучшем случае не позволяет сменить сервер базы данных, в худшем приводит к краху всего проекта. Как правило, разработчики это осознают, когда поздно что-либо менять.

Тем не менее попытки создания СОМ-объектов для доступа к InterBase были Наиболее успешной попыткой можно считать библиотеку Visual Database Tools от компании Borland (VDBT). Это VCL-подобные компоненты для Visual Basic, работающие с InterBase через Borland Database Engine. Но библиотека VDBT была готовой реализацией СОМ-объектов доступа к InterBase, а не открытой спецификацией. Поэтому расширению и усовершенствованию не подлежала.

Спецификацию под названием OLE Database (OLE DB), предназначенную для создания компонентов доступа к базам данных, выпустила компания Microsoft, которая курирует и саму СОМ-технологию. Но Open Source InterBase 6 не имел собственного OLE DB-провайдера, поэтому ничего не оставалось, как начать самостоятельную разработку OLE DB for InterBase, известную ныне как IBProvider.

Обзор возможностей IBProvider

 Сделать закладку на этом месте книги

* Возможность работы со всей линейкой СУБД InterBase, начиная с версии 4 х и заканчивая клонами InieiBase 6 - Firebird и Yattil Минимальным условием работы IBProvider является наличие на компьютере клиента динамической библиотеки GDS32.dll от InterBase 4 (см. главу "Состав модулей InterBase" (ч. 4)). IBProvider самостоятельно определяет уровень возможностей сервера (так называемый base level) и клиентской части (т е. возможности GDS32.dll). а также диалект базы данных и автоматически подстраивается под эти параметры.

* Поддержка всех типов данных InterBase. Есть поддержка BLOB-полей (бинарных и текстовых), массивов и типов DECIMAL/NUMERIC (см. главу " Типы данных" (ч. 1)).

* Поддержка storage-объектов для работы с BLOB-полями. Эти объекты могут возвращаться клиенту и приниматься в качестве входящих параметров.

* Практически весь спектр OLE DB-типов. Помимо типов, непосредственно поддерживаемых InterBase, IBProvider способен принимать и возвращать беззнаковые целые числа, булевы значения, строки UNICODE и т. д.

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

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

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

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

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

* Оптимизация работы с оперативной памятью. Во-первых, IBProvider использует две собственные "кучи" (heap) для динамического выделения памяти. Это снижает нагрузку на системную кучу. Во-вторых, IBProvider интенсивно запускает совместно используемые объекты, хранящие информацию только для чтения. Во время работы IBProvider создает глобальный пул (pool) объектов, что приводит к экономии памяти и позволяет уменьшить время создания и инициализации объектов и, таким образом, улучшить общую производительность приложения баз данных.

* Полная поддержка синтаксиса SQL. Также поддерживаются команды для создания/удаления базы данных и явного управления транзакциями.

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

* Полная поддержка параметризованных запросов. Можно использовать именованные и неименованные параметры, самостоятельно или автоматически формировать описания параметров и передавать их значения в обоих направлениях (in-out-параметры).

* Поддержка вызова хранимых процедур (сокращенно - ХП; подробнее о них см главу "Хранимые процедуры" (ч. 1)). Провайдер распознает запросы вида "ехес proc_name", "execute proc_name", "execute procedure proc_name" и возвращает результат работы хранимой процедуры через выходные (out) параметры.

Возможность получения метаданных из базы данных InterBase. Это списки таблиц, колонок, хранимых процедур, индексов, ограничений и т. д. (всего 26 видов метаданных). Помимо CASE-средств и систем построения отчетов, эта информация использ>ется в Microsoft Distributed Query для выполнения гетерогенных запросов к нескольким базам данных под управлением различных (!) SQL-серверов (например, MS SQL) посредством OLE DB-провайдеров.

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

В настоящий момент, оставив позади большой объем работ по созданию OLE DB для InterBase, можно пересмотреть роль и назначение этого драйвера. Вытеснив оригинальную клиентскую часть GDS32.DLL на второй план, IBProvider предоставляет мощный объектно-ориентированный низкоуровневый


убрать рекламу






клиентский API для работы с InterBase. Встраиваясь в приложения баз данных, OLE DB-провайдер способен взять на себя всю работу по организации взаимодействия с сервером базы данных. Предоставление ресурсов для работы с базой данных в виде СОМ-объектов снимает традиционные ограничения, накладываемые на клиентские приложения баз данных. Приложение можно дробить на модули, которые можно создавать с помощью разных систем программирования. Используя сценарии, написанные на VBScript/JScript, в программы можно добавлять логику, которую невозможно реализовать на уровне базы данных. OLE DB является общепризнанным промышленным стандартом доступа к данным, что позволяет легко разворачивать и управлять приложениями, разработанными с использованием IBProvider.

Таким образом, разработка крупных масштабируемых клиентских приложений для InterBase с помощью средств разработки компании Microsoft, а также любых других систем, поддерживающих OLE DB, становится более реальной и доступной, чем можно было себе представить ранее.

Использование IBProvider в клиентских приложениях

 Сделать закладку на этом месте книги

Низкоуровневые прикладные интерфейсы для работы с СУБД (API) обычно не используются в клиентских приложениях из-за большого объема кода, необходимого для подготовки и выполнения SQL-запросов. Это относится и к OLE DB-интерфейсам. Поэтому примеры, демонстрирующие взаимодействие с различными OLE DB-провайдерами непосредственно через их СОМ-интерфейсы, носят исключительно демонстрационный характер и имеют мало общего с реальным программированием приложений баз данных Для решения повседневных задач обычно используют надстройки в виде других СОМ-объектов или библиотек классов, которые существенно упрощают работу с OLE DB. Работать с IBProvider можно двумя основными способами - через стандартные ADODB компоненты и с помощью собственной библиотеки классов для поддержки IBProvider. написанной для компилятора Borland C++ Builder.

Компоненты ADODB

 Сделать закладку на этом месте книги

В настоящее время этот набор компонентов стал промышленным стандартом взаимодействия с OLE DB-провайдерами. ADODB (www.microsoft.com/data) - это весьма удобный высокоуровневый интерфейс, реализующий классическую иерархию объектов для работы с базами данных в виде СОМ-объектов, поддерживающих технолог ию OLE Automation.

Реализация OLE DB-интерфейсов большинства провайдеров не является взаимозаменяемой и больше ориентирована на использование из программ, написанных на C++, что приводит к проблемам совместимости и переносимости приложений между различными OLE DB-провайдерами. Поэтому ADODB-компонеты, сглаживающие различия между разными OLE DB-провайдерами и доступные для использования практически везде - начиная от VisualBasic и заканчивая тем же C++, - лучше подходят для использования в качестве универсальной платформонезависимой основы для приложений баз данных.

Но у ADODB-компонентов имеется ряд недостатков, которые являются обратной стороной достоинств:

* Ограничения на типы данных, накладываемые структурой VARIANT.

* Некоторое снижение производительности за счет того, что создание объектов ADODB производится через инфраструктуру СОМ.

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

* Появляется дополнительный уровень взаимодействия с OLE DB.

Для того, чтобы избежать недостатков ADODB-компонентов, была разработана специализированная библиотека классов C++, которая реализует доступ к OLE DB провайдерам (в том числе и к IBProvider) с максимально возможной эффективностью. Эта библиотека поставляется в составе дистрибутива IBProvider.

Библиотека классов C++ для работы с OLE DB

 Сделать закладку на этом месте книги

Созданная как дополнительный слой ("обертка") над СОМ-объектами, эта библиотека классов обеспечивает более тесную интеграцию с OLE DB-провайдерами. В ней нет всего списка возможностей, который предлагает ADODB, но предоставляемый сервис делает ее более приспособленной для построения независимых и эффективных компонентов, работающих с базами данных через OLE DB.

К основным достоинствам данной библиотеки классов относится:

* автоматическое создание и разрушение объектов;

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

* возможность подключения объекта C++ к уже существующему OLE DB компоненту;

* удобная работа с наборами полей и параметров, значительно более гибкая по сравнению с компонентами ADODB и VCL;

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

Поскольку данная библиотека доступа создавалась специально для использования в больших проектах, её классы значительно уменьшают сложность взаимодействия с OLE DB-провайдером В случае необходимости использовать ADODB (например, для совместной работы модулей проекта, написанных на C++ и на VBScript, в рамках одной транзакции) в библиотеке реализованы механизмы "шлюзования".

Разумеется, существуют еще несколько других библиотек, упрощающих работу с OLE DB-провайдерами. Однако в нижеследующих примерах будут использоваться только компоненты ADODB и библиотека классов C++ для работы с OLE DB. Поэтому, прежде чем приступить к работе над описанными примерами, убедитесь в наличии всех необходимых программных продуктов. Помните, что вы можете скачать все примеры и нужные для их работы программы на сайте поддержки данной книги www.InterBase-world com.

Инсталляция IBProvider

 Сделать закладку на этом месте книги

Перед установкой OLE DB-провайдера убедитесь, что на вашей машине инсталлирована клиентская часть InterBase Для этого на компьютере как минимум, должна находиться GDS32.DLL. Обычно она находится в системном каталоге Windows (System - для 95/98/МЕ, System32 - для NT4/Win2000) Подробнее обустановке клиентской части InterBase см. главу "Установка InterBase - взгляд изнутри" (ч. 4)

В минимальный набор дистрибутива IBProvider входят два модуля: _IBProvider.dll и cw3250mt.dll. Скопируйте оба файла в системный каталог Windows и выполните команду regsvr32 _B3Provider.dll для регистрации провайдера в системе.

Если вы обладаете готовым дистрибутивом IBProvider, то программа инсталляции выполнит все необходимые операции самостоятельно.

Обратите внимание, что при инсталляции провайдера в Windows NT4/Windows 2000 у вас должны быть права на запись в реестр Поэтому операцию регистрации лучше всего выполнять, обладая правами администратора.

После установки провайдера перезагрузка ОС не требуется

Инсталляция ADODB-компонентов

 Сделать закладку на этом месте книги

Компоненты ADO входят в состав свободно распространяемого дистрибутива Microsoft Data Access Components и доступны для скачивания на сайте компании Microsoft - www microsoft com/data. Для написания примеров использовались ADODB-компоненты из дистрибутива версии 2.6.

Примеры использования ADODB

 Сделать закладку на этом месте книги

Для создания примеров работы с IBProvider через ADODB был применен Visual Basic for Application (VBA) из Microsoft Excel 97. Для использования ADODB-компонентов нужно их добавить в список библиотек, употребляемых Visual Basic Для этого:

* Откройте редактор кода Visual Basic (Alt+Fl 1).

* Выберите пункт меню Сервис\Ссылки .

* Найдите в списке строку Microsoft ActiveX Data Objects 2.6 Library и поставьте рядом с ней галочку.

* Закройте окно, нажав кнопку "ОК".

Использование библиотеки классов

 Сделать закладку на этом месте книги

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

* Явно добавить в проект файлы из каталога Lib:

ole_lib\oledb\oledb_client_lib.cpp Основные классы для работы с OLE DB

ole_lib\oledb\oledb_client_base.cpp

ole_lib\oledb\oledb_common.cpp

ole_lib\oledb\oledb_variant.cpp

ole_lib\oledb\oledb_ado_lib.cpp Утилиты стыковки с ADODB

ole_lib\ole_base.cpp

ole_lib\ole_auto.cpp

Win32Lib\win321ib.cpp

structure\util_classes.cpp

util_func.cpp

* Начало каждого срр-файла, включенного в проект, должно выглядеть следующим образом:

ttinclude <_pch_.h> #pragma hdrstop

* Добавить в параметры проекта (опция Conditional defines) макрос INCLUDE_OLEDB_HEADER.

* При использовании в проекте VCL компонент, нужно добавить в параметры проекта макрос _USE_VCL_. В этом случае файл <vcl.h> будет добавлен в проектный csm-файл (файл прекомпилированного заголовка) косвенно из <_pch_ h>.

* Основной каталог include, используемый компилятором C++ Builder, должен содержать заголовочные файлы OLE DB SDK. BCB5 и Free Borland C++ Compiler уже содержат все необходимое. В ВСВЗ нужно добавить эти файлы самостоятельно, используя OLE DB SDK версии не выше 2.1.

Представленная в составе дистрибутива IBProvider библиотека классов является основой для проектов, её использующих. Поэтому предполагается, чго заголовочный файл <_pch_.h> прямо или косвенно включен в каждый срр-файл проекта. Возможность параллельного использования с другими библиотеками осуществляется за счет определения пространств имен. Поддержка библиотеки VCL добавлена изначально. Для поддержки других библиотек потребуется модифицировать <_pch_.h>.

Перенос на другие компиляторы C++ полностью зависит от степени их совместимости с последним стандартом C++ и от сложности перехода на другую реализацию STL.

Примеры использования библиотеки классов

 Сделать закладку на этом месте книги

Для написания и тестирования примеров использовался Borland C++ Builder 3-й версии (с установленным пакетом исправлений, который доступен для скачивания на сайте компании Borland). Библиотека классов самостоятельно конфигурируется под использование компилятора и STL из ВСВ5, поэтому примеры переносятся на Borland C++ Builder 5-й версии без проблем.

В примерах, включенных в текст этого раздела, опускаются этап инициализации СОМ и обработка исключений. Все это, естественно, присутствует в оригинале примеров, доступных для скачивания с сайта поддержки этой книги www.lnterBase-world.com.

Также для изучения технологии использования библиотеки классов для работы с OLE DB из C++ рекомендуется посмотреть примеры из дистрибутива IBProvider.

Тестовая база данных

 Сделать закладку на этом месте книги

Для тестирования использовался Firebird 1.0 и база данных employee.gdb, входящая в дистрибутив этого сервера баз данных. На этом сервере был создан пользователь "gamer" с паролем "vermin"

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

data source=c266:d:\database\employee.gdb;

user=gamer;

password=vermut;

auto_cornmit = true ;

ctype=win!251;

Операционная система

 Сделать закладку на этом месте книги

Все перечисленные компоненты для написания примеров были установлены на одном компьютере, работающем под управлением Windows NT4 Service Pack 5, Internet Explorer 5.

Состав компонентов IBProvider

 Сделать закладку на этом месте книги





Давайте рассмотрим составные части IBProvider. Компоненты, входящие в состав OLE DB-провайдера, делятся на 4 основные группы:

Источник Оанны\ (Data Source). Компоненты этой группы отвечают за инициализацию и управление подключением к базе данных. Здесь же предоставляется интерфейс для создания сессии.

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

Команда (Command). Компонент для подготовки и выполнения SQL-запросов к базе данных Команда выполняется в рамках конкретной сессии, однако в случае работы в режиме автоматического запуска и подтверждения транзакций (autocommit) выполняется в рамках собственной транзакции. Внутри одной сессии может существовать множество команд.

Набор строк (Ronset). Компонент, реализующий интерфейсы навигации по результатам SQL-запроса (результирующему множеству) и доступа к его содержимому.

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

Клиент - это любой фрагмент системного или прикладного кода, использующий интерфейс OLE DB. Сюда могут входить и сами компоненты доступа. Так же, для обозначения этой стороны взаимодействия, будут использоваться термины пользователь и потребитель. Компонентов доступа - это любой программный компонент, предоставляющий интерфейс OLE DB. Параллельно будут использоваться OLE DB- поставщик, OLE DB-провайдер, провайдер и IBProvider. Настройка и определение функциональности компонентов осуществляется через их свойства. Свойства - это атрибуты объекта. Например, свойства набора строк определяют верхний предел объема оперативной памяти для хранения данных, поддержку закладок и потоковую модель. Клиенты устанавливают значения свойств, чтобы потребовать от соответствующего объекта некоторого заданного поведения, и читают свойства, чтобы определить возможности объекта Каждое свойство характеризуется значением, типом, описанием, атрибутом чтения/записи

Свойство идентифицируется GUID (глобальный уникальный идентификатор, представляющий собой структуру длиной 128 бит) и целым числом, представляющим идентификатор свойства. Набор свойств - это совокупность свойств с одним и тем же GUID.

Источник данных

 Сделать закладку на этом месте книги

Создание компонента Data Source является отправной точкой для работы с базой данных через IBProvider. Существует несколько сценариев создания и инициализации компонента доступа. Они отличаются объемом работы, выполняемой в клиентском приложении, библиотекой доступа к OLE DB и самим IBProvider.

Вариант 1. Клиент самостоятельно осуществляет все этапы:

ADODB

Dim en As New ADODB.Connection

cn.Provider = "LCPI.IBProvider.1"

cn.Properties("data source") =

"localhost:d:\database\employee.gdb"

en.Properties("user id") = "gamer"

en.Properties("password") = "vermut"

cn.Open

C++

t_db_data_source cn;

_THROW_OLEDB_FAILED(cn,create("LCPI.IBProvider.1"))

t_db_ob]_props cn_props(/*refresh=*/false);

_THROW_OLEDB_FAILED(cn_props,attach_data_source(en.m_obj, DBPROPSET_DBINITALL))

_THROW_OLEDB_FAILED(cn_props,set("data source" "iocalhost:d:\\database\\employee. gdo")) ;

_THROW_OLEDB_FAILED(cn_props,set("user id","gamer"));

_THROW_OLEDB_FAILED(cn_props,set("password","vermut"));

_THROW_OLEDB_FAILED(en,attach(""));

Вариант 2. Создание и инициализацию выполняет клиентская библиотека:

ADODB

Dim en As New ADODB.Connection

Call en.Open("provider=LCPI.IBProvider.1; data

source=Iocalhost:d:\database\employee.gdb", "gamer", "vermut")

C++

t_db_data_source cn;

_THROW_OLEDB_FAILED(en,attach("provider=LCPI.IBProvider.1;"

"data source=localhost:d:\\database\\employee.gdb;"

"user id=gamer;password=vermut"));

Вариант 3. Провайдер создает клиентская библиотека, инициализацию выполняет сам провайдер:

ADODB

Dim en As New ADODB.Connection

Call en.Open("file name=d:\database\employee.ibp")

C++

t_do_data_source cn;

_THROW_OLEDB_FAILED(cn,

attach!"file name=d:\\database\\employee.ibp"));

//или явно указываем провайдер и файл с параметрами

_THROW_OLEDB_FAILED(en,

attach("provider=LCPI.IBProvider.1;"

"file name=d:\\database\\employee.ibp"));

где "employee.ibp" - обычный текстовый файл, в котором хранится строка подключения вида

data souгсе=Iocalhost:d:\\database\\employee.gdb;

user id=gamer;

password=vermut

Пока OLE DB-провайдер не подключен к базе данных, параметры инициализации будут единственно доступным набором свойств. В IBProvider определены стандартные свойства инициализации и собственные, предназначенные для специанализированной настройки дальнейшей работы с базой данных. (За подробностями обращайтесь к документации по IBProvider.)

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

Пример получения значений информационных свойств:

ADODB

'подключение к базе данных

....

'стандартные свойства

Debug.Print en.Properties("provider version")

Debug.Print en.Properties("provider friendly name")

'специфические свойства

Debug.Print en.Properties("IB Base Level")

Debug.Print en.Properties("IB GDS32 Version")

Debug.Print en Properties("IB Version")

C++

//Подключение к базе данных

//...

t_db_obj_props cn_props(false);

_THROW_OLEDB_FAILED(cn_props,

attach_data_source(cn.m_obi, DBPROPSET_DATASOURCEINFOALL));

..печать всех информационных свойств

for(UINT i = 0; i! =cn_props .GetltemsInContainer () ;+ + i)

{

cout<<cn_props[i].name()<<":"<<print(cn_props[i].value())<<endl

;

}

Компонент Data Source имеет еще несколько возможностей: например, сохранение параметров подключения к базе данных в файле и перечисление до- пус!имы\ символов для названий объектов базы данных. Однако в основном он используется для создания объектов сессий.

Сессия

 Сделать закладку на этом месте книги

Основная функция сессии - установить рамки транзакции с заданными параметрами (подробнее о транзакциях см. главу "Транзакции. Параметры транзакций" (ч 1))

Хотя в ADODB понятие сессии совмещено с понятием источника данных, в OLE DB это два различных объекта. Надо полагать, что основная причина такой иерархии объектов ADODB заключается в архитектуре пула подключений, используемого в серверных приложениях Microsoft. Гораздо проще и эффективнее осуществлять балансировку загрузки на уровне отдельных подключений к базе данных, чем на уровне сессий. Как правило, в SQL-серверах через два раздельных подключения можно осуществлять параллельные запросы к базе данных, а через разные сессии одного подключения такая работа будет осуществляться последовательно. Тем не менее InterBase может одновременно обслуживать несколько транзакций в рамках одного подключения и в данном случае выгодно отличается от большинства других SQL-серверов. Поэтому IBProvider поддерживает возможность создания нескольких объектов сессий, принадлежащих одному источнику данных.

Уровни изоляции транзакции

 Сделать закладку на этом месте книги

В IBProvider реализована поддержка трех уровней изоляции транзакций: READ COMMITTED, REPEATABLE READ (SNAPSHOT), SERIALIZABLE (SNAPSHOT TABLE STABILITY). При работе через ADODB след>ет обратить внимание на значение свойства ADODB.Connection.IsolationLevel. Библиотека классов C++ по умолчанию использует режим REPEATABLE READ. Подробнее об у ровнях изоляции транзакций вы можете узнать в главе "Транзакции. Параметры транзакций" (ч. 1).

Управление транзакциями

 Сделать закладку на этом месте книги

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

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

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

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

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

Автоматические транзакции

 Сделать закладку на этом месте книги

Для разрешения провайдеру самостоятельно управлять транзакциями нужно указать в строке инициализации параметр "auto_commit=true":

Call сn.Open(

"data source=localhost:d:\database\employee.gdb;auto_commit=true",

"gamer", "vermut")

В этом случае все создаваемые объекты сессий для данного источника данных будут способны самостоятельно запускаться и завершать транзакции без явного участия пользователя. Для выборочного разрешения такого режима можно воспользоваться свойством сессии "Session AutoCommit". Если это свойство равно true, то сессия может обслуживать запросы к базе данных без необходимости явного запуска и подтверждения.

По умолчанию автоматические транзакции используют уровень изоляции SNAPSHOT. Если требуется установить другой уровень изоляции, то нужно либо установить параметр инициализации источника данных "auto_commit_level", либо изменить свойство сессии "Autocommit Isolation Levels" в одно из следующих значений:

* 0 х 1000 - READ COMMITED;

* 0 x 10000 - REPEAT ABLE READ. Это режим по умолчанию;

* 0 x 100000 - SERIALIZABLE.

О принципе функционирования автоматических транзакций следует сказать следующее. Автоматическая транзакция не управляется через интерфейс сессии. За её завершение и откат отвечает команда или набор строк внутри этой сессии. Если команда не возвращает набор строк (rowset), то транзакция фиксируется сразу. Если внутри автоматической транзакции появляется набор строк, то транзакция завершается при освобождении этого набора То есть внутри одной сессии. у которой свойство "Session AutoCommit" равно true, может существовать несколько активных транзакций. Естественно, что в этом случае пользователь также может явно управлять запуском и завершением транзакции, принадлежащей сессии. В случае явного запуска транзакции, для дальнейших запросов и операций в рамках этой сессии будет использоваться контекст именно этой транзакции.

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

Управление транзакциями через SQL

 Сделать закладку на этом месте книги

Помимо управления транзакцией через OLE DB-интерфейсы сессии, IBProvider осуществляет специальную поддержку SQL-запросов вида: "SET TRANSACTION...", "COMMIT" и "ROLLBACK". В этом случае будет использоваться транзакция, принадлежащая сессии. Управление транзакциями через SQL-запросы позволяет указывать специфические параметры контекста транзакции, которые не стандартизированы в OLE DB и которые возможно использовать благодаря особенностям InterBase API.

Примеры работы с транзакциями

 Сделать закладку на этом месте книги

Ниже приведены примеры, демонстрирующие способы запуска и завершения транзакций, поддерживаемые OLE DB-провайдером.

ADODB:

явное управление транзакцией:

Dim en As New ADODB.Connection

сn.Provider = "LCPI.IBProvider.1"

Call en.Open(

"data source=localhost:d:\database\employee.gdb",

"gamer","vermut")

'стандартный способ

сn IsolationLevel = adXactRepeatableRead

сn.BeginTrans

'...

'можно указать использование commit retaining

'cn.Attributes = adXactAbortRetaining + adXactCommitRetaining

cn.CoimutTrans

'управление транзакцией через SQL (можно использовать специфику

InterBase)

Dim cmd As New ADODB.Command

cmd.ActiveConnection = en

cmd.CommandText = "set transaction"

cmd.Execute

'...

cmd CommandText = "rollback"

cmd Execute

автоматический запуск транзакций - разрешение и запрещение:

Dim en As New ADODB.Connection

en Provider = "LCPI.IBProvider.1"

'auco_commit=true включение автоматических транзакций

'для всех сессий

Call cn.Open(

"data source=localhost:d:\database\employee.gdb;auto_commit=true",

"gamer", "vermut")

Dim cmd As New ADODB.Command

Dim rs As ADODB Recordset

cmd ActiveConnection = cn

cmd.CommandText = "select * from rdb$database"

Sec rs = cmd.Execute 'транзакция запускается неявно

'...

'и Судет завершена при закрытии результирующего множества

rs Close

'альтернативой auto_commit=true является установка

'(после успешного подключения к база данных)

'свойства сессии Session AutoCommit=true.

'В ADODB это одно и то же,

'поскольку на одно подключение - одна сессия

'через это же свойство можно "выключить" глобальное

'разрешение на автоматический

'запуск транзакции, что дальше и демонстрируется

cn Properties("Session AutoCommit") = False

cmd.CommandText = "select * from rdb$database"

Set rs = cmd Execute

'выдаст олибку "Automatic transaction is disabled"

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

В нижеследующем примере на C++ демонстрируется трюк, который часто используется для моделирования принудительного завершения транзакций. Дело в том, что InterBase реализует мно


убрать рекламу






говерсионную архитектуру данных (подробнее о многоверсионности данных см. главу "Транзакции Параметры транзакций" (ч 1)) Одной из особенностей такой архитектуры является то, что любые транзакции желательно завершать подтверждением (commit), а не откатом (lollback).

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

try

{

t_db_data_source cn;

_THROto_OLEDB_FAILED(en,attacn("provider=LCPI.IBProvxCter.1,"

"data source=localhost:d:\\database\\employee.gdb;"

"user id=gamer;"

"password=vermut"));

t_db_session session;

// метод create перегружен для разных типов аргумента, поэтому

// можно передавать

// как C++ объект (t_db_data_source), так и IUnknown источника

// данных

_THROW_OLEDB_FAILED(session,create(en)) ,

// запуск транзакции

_THROW_OLEDB_FAILED(session,start_transaction() ) ;

// создаем объект для принудительного завершения транзакции

t_auto_mem_fun_l<HRESULT,bool,t_db_session>

_auto_commit_(session, /*commit_retaining=*/false, &t_db_session::commit);

//... теперь, что бы ни произошло, транзакция

// будет "закоммичена"

throw runtime_error("This is my test error");

}

catcn(const exception& exc)

{

cout<< "error:"<<exc.what()<<endl;

}

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

Распределенные транзакции

 Сделать закладку на этом месте книги

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

На практике распределенные транзакции обычно используются в СОМ+ (MTS) и Microsoft Distributed Query. В первом случае существует следующая особенность. Когда компоненты просят у своего окружения предоставить им подключение к базе данных, то на каждый такой запрос создается отдельная пара источник данных - сессия. Связано это с описанным выше принципом работы пула подключений. Если компонентов, обрабатывающих пользовательский запрос, очень много и они работают с одной и той же базой данных с идентичными параметрами подключения, то имеет смысл один раз получить подключение и потом предоставлять его компонентам. Иначе в распределенной транзакции будет использовано множество сессий, каждая из которых соответствует процессу или потоку на сервере базы данных, что может привести к резкому снижению производительности сервера СУБД.

Использование нескольких сессий в ADODB

 Сделать закладку на этом месте книги

Несмотря на то что модель объектов ADODB не предоставляет прямой возможности одновременного использования нескольких сессий с одним источником данных, это ограничение можно обойти. Решение основывается на применение внутреннего интерфейса ADOConnectionConstruction компоненты ADODB.Connection.

void clone_adodb_connection(IDispatch* pCurrentConnection,

IDispatchPtr& spNewConnection)//throw

{

//объявляем типы смарт-указателей для интерфейсов

//конструирования ADODB-подключения

DECLARE_IPTR_TYPE(ADOConnectionConstruction);

DECLARE_IPTR_TYPE(ADOConnectionConstructionl5);

//1 получаем источник данных, привязанный к pCurrentConnection

ADOConnectionConstructionPtr

spConstruct(pCurrentConnection);

ADOConnectionConstruetionl5Ptr

spConstructlS(pCurrentConnection);

if(!spConstruct && !spConstruct15)

t_ole_error::throw_error("Объект - не ADODB.Connection",

E_INVALIDARG);

IUnknownPtr spDataSource;

//берем указатель на OLE DB-источник данных

if((bool)spConstruct15)

spConstruct15->get_DSO(&spDataSource.ref_ptr());

if(!spDataSource && (bool)spConstruct)

spConstruct->get_DSO(&spDataSource.ref_ptr());

if(!spDataSource)

t_ole_error::throw_error(

"ADODB.Connection не инициализирован",E_FAIL);

//2 создаем новую сессию для spDataSource ------------------

IUnknownPtr spNewSession;

IDBCreateSessionPtr spDBCreateSession(spDataSource);

assert((bool) spDBCreateSession);

if(FAILED(spDBCreateSession->CreateSession

(NULL,IID_IUnknown,kspNewSession.ref_ptr())))

{

t_ole_error::throw_error(

"Ошибка создания новой сессии",E_FAIL);

}

assert((bool)spNewSession);

//3 создаем новый экземпляр ADODB.Connection ----------------

IUnknownPtr spUnkNewConnection;

HRESULT hr=SafeCreateInstance("ADODB.Connection",

NULL,CLSCTX_INPROC,IID_IUnknown,

(void**)&spUnkNewConnection.ref_ptr());

if(FAILED(hr))

t_ole_error::throw_error(

"Ошибка создания \"ADODB.Connection\"",hr);

spConstruet = spUnkNewConnection;

spConstruct15=spUnkNewConnection;

if(!spConstructlS && !spConstruct)

throw t_ole_error(

"Ошибка подключения к \"ADODB.Connection\"",E_FAIL);

spDataSource->AddRef(); //ADO не вызывает для них AddRef spNewSession->AddRef();

//при конструировании ADODB.Connection

//пытается установить свои свойства,

//в результате чего, если IBProvider уже подключен

//к базе данных, может произойти ошибка

//тем не менее подключение уже сконструировано

//и вполне работоспособно.

if((bool)spConstructlS) //IID_Connect-on15

{

spConstruct15->WrapDSOandSession(spDataSource,spNewSession);

hr = spUnkNewConnection ->

Querylnterface(IID_ConnectionlS,spNewConnection);

}

else //IID_Connection

{

spConstruct->WrapDSOandSession(spDataSource,spNewSession);

hr = spUnkNewConnection ->

Querylnterface(IID_Connection,spNewConnection);

}

if (FAILED(hr))

t_ole_error::throw_error(

"Ошибка получения IDispatch из ADODB.Connection",hr);

assert((bool)spNewConnection);

//всё — spNewConnection подключен к тому же

//источнику данных, но обладает

//собственной сессией.

}//cione_adodb_connection

Чтение метаданных

 Сделать закладку на этом месте книги

Помимо управления транзакциями, сессия предоставляет еще одну полезную возможность - получение метаданных для базы данных (о метаданных см. главу "Структура базы данных InterBase" (ч. 4)). Поскольку в некоторых системах, например в Microsoft Distributed Query, операция получения метаданных выполняется очень часто, то IBProvider хранит информацию о них в оперативной памяти (т. е. кеширует). Кэширование метаданных можно настраивать для обеспечения оптимального быстродействия. Определить режим кеширования можно через свойство инициализации источника данных "schema_cache" и свойство сессии - "Session Schema Cache". Этим свойствам можно присваивать следующие значения:

* Кэширование запрещено. Данные будут всегда перечитываться.

* Глобальное кеширование на уровне Data Source. Это режим по умолчанию.

* Кеширование на уровне Session.

Если при запросе метаданных сессия содержит явно запущенную транзакцию, то провайдер не будет использовать дополнительную внутреннюю транзакцию для получения данных, а воспользуется уже существующей. Если явно запущенной транзакции нет, то IBProvider автоматически запустит внутреннюю транзакцию с уровнем изоляции, указанной в свойстве сессии "Autocommit Isolation Levels". Для запрещения автоматического запуска провайдером внутренних транзакций для извлечения метаданные необходимо определить в строке инициализации источника данных "inner_trans=false" или установить свойство сессии "Session InnerTrans=false".

Получение и вывод списка таблиц базы данных:

ADODB

Dim cn As New ADODB.Connection

Call cn.Open("file name=d:\database\employee.ibp")

Dim rs As ADODB.Recordset

Set rs = cn.OpenSchema(adSchemaTables)

Cells.Clear

Dim col As Long, row As Long

row = 1

'печать названия колоник

For col = 0 To rs.Fields.Count - 1

Cells(row, col + 1) = rs(col).Name

Next col

'печать содержимого

While Not rs.EOF

row = row + 1

For col = 0 To rs.Fields.Count - 1

Cellsfrow, col + 1) = rs(col).Value

Next col

rs.MoveNext

Wend

Здесь следует обратить внимание на одну особенность. Спецификация OLE DB для некоторых полей таблиц метаданных определяет типы, несовместимые с VARIANT, например UI8. Поэтому при попытке получения значения из этих полей через ADODB может возникнуть ошибка;

C++

try

{

t_db_data_source сn;

_THROW_OLEDB_FAILED(cn,attach("file

name=d:\\database\\employee.ibp"));

t_db_session session;

_THROW_OLEDB_FAILED(session,create(сn));

//библиотека напрямую не поддерживает

//работу с интерфейсом получения

//наборов информационной схемы,

//поэтому напишем необходимый код "в лоб".

IDBSchemaRowsetPtr spSR(session.session_obj());

if(!spSR)

t_ole_error::throw_error(

"query interface [IDBSchemaRowset]",spSR.m_hr);

IUnknownPtr spUnk;

HRESULT hr=spSR->GetRowset(NULL,DBSCHEMA_TABLES,0,NULL,

IID_IUnknown,0,NULL,&spUnk.ref_ptr());

if(FAILED(hr))

t_ole_error::throw_disp_error(hr,"get tables list");

//подключаем полученный набор к курсору

t_db_cursor cursor;

_THROW_OLEDB_FAILED(cursor,attach(spUnk))

//получаем описание полей результирующего

//множества (набора данных)

t_db_row row;

_THROW_OLEDB_FAILED(cursor,describe(row))

//печатаем содержимое набора

while(cursor.fetch(row)==S_OK)

{

for(t_db_row::size_type i=0;i!=row.count;++i)

cout<<row.columns(i).name<<":"<<row[i].as_string<<endl ;

cout<<endl;

}//while

//проверяем причину выхода из цикла

_THROW_OLEDB_FAILED(cursor,m_last_result)

}

catch(const exception& exc){

cout<<"error:"<<exc.what()<<endl;

}

Команда

 Сделать закладку на этом месте книги

Команда используется для выполнения SQL-запросов к базе данных Важно не путать команду, которая является СОМ-объектом, с текстом команды, который представляет собой строку. Обычно команды используют для описания данных, например для создания таблицы и предоставления привилегий, и манипуляции данными, например для обновления и удаления строк Особый случай манитчя- ции данными - создание набора строк (примером служит оператор SQL SELECT).

Спецификация OLE DB определяет гибкий набор интерфейсов для выполнения и обработки результатов SQL-запросов. Определяется так последовательность шагов:

* Создание команды.

* Установка текста запроса.

* Подготовка запроса.

* Подготовка параметров запроса.

* Установка свойств результирующего множества (набора данных).

* Выполнение запроса.

Создание команды

 Сделать закладку на этом месте книги

Как уже было отмечено ранее, объект команды создается объектом сессии.

С помощью отдельной сессии можно создать много команд

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

Примеры создания команд.

ADODB

Dim сn As New ADODB.Connection

сn.Open "file name=d:\database\employee.ibp"

Dim cmd As New ADODB.Command cmd.ActiveConnection = сn

C++

t_db_data_source сn;

_THROW_OLEDB_FAILED(cn, attach(

"file name=d:\\database\\employee.ibp"));

t_db_session session;

_THROW_OLEDB_FAILED(session,create(en));

t_db_command cmdl,cmd2;

//Создаем команду традиционным способом,

//используя объект C++ сессии

_THROW_OLEDB_FAILED(cmdl,create(session));

//Команду также можно создать, обладая

//только указателем на lUnknown OLE DB-сессии.

//Это более подходящий способ для СОМ-объектов

IUnknownPtr spSession(session session_obj());

_THTOW_OLEDB_FAILED(cmd2,create(spSession));

Установка текста команды

 Сделать закладку на этом месте книги

Если команда только что создана, она еще не содержит текста. Поэтому текст SQL-запроса нужно установить

Пример установки текста запроса:

ADODB

cmd.Command.Text = "select x from job"

При этом IBPrivider сбрасывает флаг подготовленности команды и очищает список параметров Как это ни странно, здесь могут происходить ошибки:

* Команда обнаружит смешанное использование параметров - именованных " param" и неименованных, обозначаемых вопросительным знаком ('"?")

* Сбой преобразования SQL-запросов из ODBC-диалекта в вид, пригодный для передачи на сервер

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

Подготовка команды

 Сделать закладку на этом месте книги

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

C++

t_db_row row;

_THROW_OLEDB_FAILED (cmd, prepare("select * from iob",&row))

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

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

Подготовка параметров SQL-запроса

 Сделать закладку на этом месте книги

Многократно выполняемые SQL-запросы, как правило, содержат параметры, представляющие собой переменные в тексте SQL-запроса. IBProvider поддерживает два вида параметров: именованные и неименованные. Перед выполнением параметризованного SQL-запроса команда должна обладать описаниями параметров. Описание параметра — это его тип, имя, направление передачи значения (in-out). Пользователь может самостоятельно сформировать описания параметров или поручить формирование параметров команде.

Явное определение параметров SQL-запроса, несмотря на свою громоздкость при работе через ADODB. обеспечивает более эффективную работу с приложениями, поскольку исключается лишнее обращение к серверу для получения их описаний.

Пример явного определения параметров SQL-запроса:

ADODB

cmd.CommandText="select * from job where job_code=?"

cmd.Parameters.Append cmd.CreateParameter(,adBSTR,adParamlnput)

cmd(0)="Eng"

C++

t_db_row row;

t_db_row param(1);

_THROW_OLEDB_FAILED(cmd2,

prepare("select * from job where job_code=?",&row))

//тип параметра определяется его значением

param[0]="Eng";

param.count=1;

_THROW_OLEDB_FAILED(cmd2,execute(&param));

//Тип параметра задается отдельно от значения,

//в этом случае провайдер выполнит преобразование значения

//в указанный тип.

set_param(param,0,adBSTR,"Eng");

param.count=1;

_THROW_OLEDB_FAILED(cmd2,execute(&param));

Автоматическое определение описаний параметров SQL-запроса позволяет клиентскому приложению перепоручить отслеживание типов параметров InterBase и конвертору типов IB Provider.

Пример явного указания команде сгенерировать описания типов:

ADODB

cmd.CommandText = "select * from job where job_code=?"

cmd.Parameters.Refresh

cmd(0) = "Eng"

Явное указание обновления списка параметров (cmd.Parameters.Refresh) обычно можно опустить. Однако иногда это необходимо. Например, для выполнения такого цикла:

ADODB

Dim cmd As New ADODB.Command

Dim is As ADODB.Recordset

cmd.ActiveConnection = сn

cmd.CommandText = "select * from job where job_code=?"

Dirr i AS_ Long For i = 0 To 10

cmd.Parameters.Refresh

cmd(0) = "Eng"

Set rs = cmd,Execute

'...

'rs.Close

Next i

Вся хитрость заключается в том, что ADODB при выполнении второй итерации будет создавать новую OLE DB-команду, поскольку предыдущая занята обслуживанием результирующего множества SQL-запроса, созданного на первом шаге. Без строки cmd.Parameters.Refresh внутренний список описания параметров новой команды не будет сформирован, хотя коллекция ADODB.Command.Parameters будет содержать элементы. В результате при вызове метода cmd.execute в команду передаются значения параметров, описание которых у нее отсутствует. Принудительное обновление решает эту проблему. Понятно, что создание новой команды снижает производительность описанного выше алгоритма. Поэтому для того, чтобы ADODB могло повторно воспользоваться OLE DB-командой, нужно закрывать результирующее множество (rs.Close).

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

Автоматическая генерация описания параметров:

C++

_THROW_OLEDB_FAILED(cmd2,describe_params(param));

param[0]="Eng";

_THROW_OLEDB_FAILED(cmd2,execute(&param) ) ;

Существует единственное исключение, когда IBProvider обязательно выполнит дополнительный запрос на сервер для получения описания параметров SQL- запроса. Это касается слуиая, когда в параметре передается массив. Для такого типа параметров необходима дополнительная информация об имени таблицы и поля, в которые будут производить запись данных, а также информация о размерности массива. Подробности см. далее в разделе "Работа с массивами".

В вышеприведенных примерах были использованы неименованные параметры, обозначаемые в тексте запроса символом вопросительного знака. Именно такое обозначение параметров поддерживает и сам InterBase. Однако иногда удобно использовать именованные параметры в SQL-запросах:

* Именованный параметр можно многократно указывать в разных частях одного запроса.

* Порядок описания параметров может не соответствовать порядку использования параметров в тексте запроса. Это недопустимо для неименованных параметров.

В ADODB за удобство именованных параметров приходиться "платить" использованием режима автоматической генерации описания параметров (ADODB.Command.Parameters.Refresh). Причина заключается в том, что имя параметра, указываемое в ADODB.Command.CreateParameter, не передается команде. При использовании классов C++ такого ограничения нет - описание параметров можно формировать обоими способами. Еще одним ограничением, ADODB является невозможность использования именованных параметров для BLOB-полей -только неименованные параметры '?'.

Как уже было сказано выше, команда запрещает одновременное использование в тексте запроса именованных и неименованных параметров.

Сам InterBase поддерживает неименованные параметры. Поэтому команда вынуждена заменять в тексте запроса именованные параметры на неименованные параметры. Окончательный текст запроса, используемый для передачи на сервер, доступен через свойство команды "Prepare Stmt"

Пример многократного выполнения параметризованного запроса, содержащего именованный параметр:

ADODB

Dim cmd As New ADODB.Command

Dim rs As ADODB.Recordset

cmd.ActiveConnection = en

cmd.CommandText = "select * from job where job_code=:job_code"

Dim i As Long

For i = 0 To 10

cmd.Parameters.Refresh

cmd("job_code") = "Eng"

Debug.Print cmd.Properties("prepare stmt")

Set rs = cmd.Execute

'...

rs.Close

Next i

Установка свойств результирующего множества

 Сделать закладку на этом месте книги

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

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

ADODB

'разрешить поддержку закладок для

'произвольного позиционирования в наборе

cmd.Properties("Use bookmarks") = True

'Использовать 1 MB памяти для кеширования рядов

cmd.Properties("Memory Usage") = 1024

C++

t_db_obj_props cmd_props(false);

_THROW_OLEDB_FAILED(cmd_props,

attach_command(cmd2.command_obj()));

_THROW_OLEDB_FAILED(cmd_props,set("Use Bookmarks",true));

_THROW_OLEDB_FAILED(cmd_props,set("Memory Usage",1024));

Выполнение команды

 Сделать закладку на этом месте книги

Вызов операции execute является последним этапом выполнения SQL-запроса к базе данных, в ко юром )частв)е] объект команды Первоначальное и почти полное описание всех этапов выполнения этой операции заняло больше двух листов, забитых сухой технической информацией, дочитывая которую забываешь, с чего все началось. Шутка. Поэтому ограничимся коротким списком задач, выполняемых командой при вызове операции выполнения SQL-запроса.

* Проверка параметров. Количество разнообразных ошибок, вылавливаемых на этом этапе, превышает полтора десятка.

* Получение транзакции, в рамках которой будет выполняться SQL-запрос. Это может быть собственная активная транзакция родительской сессии или отдельная автоматически завершаемая транзакция (если таковые разрешены).

* Создание нового дескриптора низкоуровневого запроса, если текущий, принадлежащий команде, обслуживает набор строк, сформированных предыдущим вызовом операции execute. Такая ситуация может произойти при многократных вызовах execute для одного и того же SQL-запроса.

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

* Вызов InterBase API для выполнения запроса.

* Возвращение результата (если таковые создаются) через OUT-параметры или объект набора строк (rowset)

При выполнении SQL-запросов модификации данных (INSERT, UPDATE, DELETE) можно узнать число строк, затронутых запросом:

ADODB

cmd.CommandText =

"insert into project (proj_id,proj_name,proj_desc) " & _

"values(?,?,?)"

cmd(0) = 1001

cmd(l) = "test 1001"

cmd(2) = "test 1001"

Dim RowAffected As Long 'кол-ве вставленных строк

cmd.Execute RowAffected

Debug.Print "insert " & CStr(RowAffected) & " rows"

Набор строк

 Сделать закладку на этом месте книги

Наборы строк - это центральные объекты, которые позволяют всем компонентам доступа к данным OLE DB представлять свои данные в табличной форме. Фактически набор строк - это совокупность строк, состоящих из полей данных. Компоненты доступа к базовым таблицам предоставляют свои данные в форме набор строк. Процессоры запросов (команда) представляют в форме набора строк результаты SQL-запросов. Это позволяет создавать слои объектов, поставляющих и потребляющих данные посредством одного и того же объекта.

СУБД InterBase поддерживает только однонаправленное движение курсора по набору строк, возвращаемому SQL-запросами. Под курсором здесь и далее будет подразумеваться текущая позиция в наборе строк. Этого вполне достаточно для очень широкого круга задач. Положительной стороной однонаправленного обхода наборов строк в InterBase является возможность загрузки приложением большого объема данных без хранения в памяти уже обработанной информации. IBProvider по умолчанию обеспечивает именно такой способ "навигации" по множеству строк.

Хотя понятие курсора и присутствует в OLE DB, его основное назначение заключается в получении идентификаторов строк (HROW), а не самих данных полей строки. С помощью этих идентификаторов клиент может получать интересующие его данные в конкретной строке результирующего набора данных. То есть получив идентификатор строки, пользователь может выполнять многократное чтение полей одной и той же строки. Например, при первом чтении определяется размер данных BLOB-поля, а при втором осуществляется загрузка его содержимого.

Клиент обязан освободить идентификатор строки, когда последний ему уже не нужен. Но может это и не сделать, имитируя с помощью массива идентификаторов строк произвольный доступ к результирующему множеству SQL- запроса. При использовании ADODB этого можно добиться с помощью свойства ADODB.Recordset.CacheSize. В этом случае провайдер начнет осуществлять кеширование данных заблокированных рядов.

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

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

ADODB

cmd.CommandText = "select * from job where job_code=:job_code"

cmd(";job_code") = "Eng"

Set rs = cmd.Execute

'последовательный обход всех строк множества

While Not rs.EOF

rs.MoveNext

Wend

rs.MoveFirst 'Restart

rs.Move 1 'пропускаем первый ряд

'...

rs.MoveFirst 'Restart

rs.Kove 2 'пропускаем первые два ряда

'...

Произвольный доступ к результирующему множеству SQL-запросов IBProvider имитирует за счет кеширования выбранных данных на стороне клиента. Для работы в этом режиме провайдер использует более совершенный компонент управления множеством, реализующий возможности обратной выборки и произвольного перемещения по набору данных, а также возможность "приблизительного" позиционирования. И кроме того, в режиме произвольного доступа набор строк предоставляет закладки строк, с помощью которых клиент может быстро возвращаться к некоторой строке. В некотором смысле закладки строк эквивалентны идентификатору строки (HROW), но гораздо более эффективны и не требуют никаких ресурсов для хранения. Кроме того, при работе через ADODB значение закладки текущей строки можно получить и сохранить для дальнейшего использования (см. ADODB.Recordset.Bookmark), а идентификатор строки - нет.

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

Пример позиционирования курсора набора рядов в случайном порядке.

ADODB

Dim cmd As New ADODB.Command

Dim rs As ADODB.Recordset

cmd.ActiveConnection = cn

cmd.CommandText = "select * from job where job_code=:job_code"

cmd("job_code") = "Eng"

'включ


убрать рекламу






аем поддержку закладок

cmd.Properties("Use Bookmarks") = True

Set rs = cmd.Execute

Dim i As Long

For i = 0 To rs.RecordCount

'нумерация с единицы

rs.AbsolutePosition = CLng(Rnd * rs.RecordCount) + 1

'...

Next i

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

Поэтому набор строк OLE DB-провайдера для InterBase всегда использует собственный механизм контроля над объемом расходуемой памяти для хранения результирующего множества. Он заключается в удержании в указанном объеме памяти только наиболее часто используемых строк и вытеснении остальных строк во временный файл. При этом создание файла откладывается до последнего момента. Такой способ хранения данных выгодно отличает IBProvider от других компонентов доступа, которые основываются на поддержке со стороны ОС и использовании ее файла подкачки.

По умолчанию IBProvider удерживает в памяти 32 строки, независимо от их размера. Часто этого может оказаться недостаточно. Поэтому спецификация OLE DB определяет стандартное свойство набора строк "Memory Usage", которое позволяет отрегулировать верхнюю границу используемой памяти.

0

IBProvider удерживает в памяти 32 ряда. По умолчанию.

1...99

Использовать процент от доступной памяти ОС (как физической гак и файла подкачки)

100...

Использование указанного размера памяти в килобайтах.

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

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

Практическое использование IBProvider

 Сделать закладку на этом месте книги

Работа с BLOB-полями

 Сделать закладку на этом месте книги

IBProvider предоставляет поддержку двум типам BLOB-полей: содержащих текст (SUB_TYPE TEXT) и бинарные данные. При этом доступ к BLOB может быть организован как к данным в памяти, так и к объекту-хранилищу. В любом случае провайдер не хранит сами данные BLOB-поля, а каждый раз загружает их по требованию клиента.

ADODB. Чтение BLOB:

Dim cn As New ADODB.Connection

cn.Open "file name=d:\database\employee.ibp"

cn.BeginTrans

Dim cmd As New ADODB.Command

Dim rs As ADODB.Recordset

cmd.ActiveConnection = cn

'JOB_REQUIREMENT - текстовое BLOB-поле

cmd.CommandTexc = "select nob_requirement from job"

Set rs = cmd.Execute

'доступ к BLOB как к данным в памяти

While Not rs.EOF

'печатаем размер BLOB-поля. обращение к ActualSize

'не производит загрузки самих данных

Debug.Print "size:" & CStr(rs(0).ActualSize)

If IsNull(rs(0)) Then

Debug.Print "NULL"

Else

Debug.Print rs(0)

End If

rs.MoveNext

Wend

Debug.Print "******************"

rs.MoveFirst

'чтение порциями по 40 байт

Dim seg As Variant, str As String

Const seg_size = 40

While Not rs.EOF

'пропускаем пустые BLOB-поля

If (Not IsNulKrs(0))) Then

str = ""

Do

seg = rs(0).GetChunk(seg_size)

'когда данных нет - GetChunk возвращает NULL

If IsNull(seg) Then Exit Do

str = str + seg

Debug.Print "get chunk: " & Len(seg) & " bytes"

Loop While True

Debug.Print ">" & CStr(rs(0).ActualSize) & " - " & Len(str)

Debug.Print ">" & str

End If

rs.MoveNext

Wend

cn.CommitTrans

ADODB. Запись BLOB-поля:

Dim cn As New ADODB.Connection

cn.Open "file name=d:\database\employee.ibp"

cn.BeginTrans

Dim cmd As New ADODB.Command

Dim rs As ADODB.Recordset

cmd.ActiveConnection = cn

'JOB_REQUIREMENT - текстовое BLOB-поле

cmd.CommandText = "select * from job"

Set rs = cmd.Execute

Dim upd_cmd As New ADODB.Command

upd_cmd.ActiveConnection = cn

upd_cmd.CommandText = _

"update job set job_requirement=? " & _

"where job_code=? and job_grade=? and " & _

"job_country=?"

upd_cmd.Parameters.Refresh

Dim RowAffected As Long

While Not rs.EOF

If (Not IsNull(rs("job_requirement"))) Then

upd_cmd(0) = UCase(rs("job_requirement"))

upd_cmd(l) = rs("job_code")

upd_cmd(2) = rs("job_grade")

upd_cmd(3) = rs("job_country")

upd_cmd.Execute RowAffected

Debug.Print "affect:" & CStr(RowAffected)

End If

rs.MoveNext

Wend

'отменяем все изменения в базе данных

cn.RollbackTrans

Работа с BLOB-полями через TBProvider на C++ также прозрачна. Хотя можно написать более интересные алгоритмы, например подстановка объекта-хранилища, полученного из результирующего множества, в качестве параметра в команду, привязанную к другой базе данных. Подробности см. в примерах из дистрибутива IBProvider и спецификации OLEDB - "BLOB's and OLE Objects".

Работа с массивами

 Сделать закладку на этом месте книги

Встроенная поддержка массивов является одним из основных пунктов списка достоинств InterBase как SQL сервера баз данных. И одновременно массивы возглавляют список его невостребованных возможностей. В практике сильная потребность в использовании массивов возникала только один раз. Эта работа была связана с анализом накапливаемой информации, касающейся функционирования торговой организации. Тогда основным препятствием оказалось отсутствие готового решения для работы с массивами InterBase из VBA (MS Office).

Поэтому в IBProvider была реализована поддержка массивов с предоставлением высокоуровневого представления этого несомненно полезного типа данных InterBase.

Особенности реализации поддержки массивов

 Сделать закладку на этом месте книги

* OLEDB-спецификация для представления типа данных "массив" использует структуру SAFEARRAY. Эта же структура употребления для управления массивами в Visual Basic.

* Элементы массивов не могут содержать NULL. Это ограничение связано с тем, что InterBase не поддерживает тип VARIANT.

* Все типы строк, хранящиеся в массивах, обрабатываются сервером как Си- строки, т. е. заканчивающиеся нулем. IBProvider исходит именно из такого способа хранения и не использует собственных символов типа \п для определения конца строки.

* Провайдер предоставляет полную поддержку для преобразования массивов из одного типа в другой. И пользуется ею по умолчанию, поскольку VB не понимает структуру SAFEARRAY, содержащую данные, несовместимые с VARIANT. Вообще говоря, наверное, можно было бы всегда возвращать массивы, содержащие VARIANT, но удалось обойтись без этой крайности. Отключить конвертирование массивов можно с помощью свойства инициализации источника данных (строка подключения) или набора строк "array_vt_type", установив его значение в false.

* Как и BLOB-поля, провайдер не хранит и не кеширует данные массива. Информация каждый раз загружается с сервера.

* Если при чтении массивов от пользователя не требуется никакой помощи, то для записи массивов провайдеру может потребоваться дополнительная информация. Дело в том, что запись массива, как и BLOB-поля, производиться отдельным обращением к InterBase API. Для этой операции требуется иметь описание, содержащее имя таблицы и имя колонки, в которую производится запись, тип элемента и сведения о размерности. Провайдер способен самостоятельно определить эту информацию только при работе с сервером InterBase 6.x и выше. Для работы с InterBase 4.x и InterBase 5.x IBProvider вводит нестандартное расширение, позволяющее определять в тексте запроса параметры вида "параметр.таблица.колонка". Этот синтаксис может быть использован как для именованных, так и для неименованных параметров:

update job set language_req=?.job.language_req

update job set language_req=:param.job.language_req

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

* Клиент не имеет возможности определить интересующее его подмножество массива. Провайдер всегда возвращает массив целиком. Это ограничение OLEDB, а не InterBase.

* При записи массива можно передавать подмножество. Но нужно учитывать, что массив всегда пересоздается. Поэтому в случае выполнения "UPDATE...", можно потерять предыдущую информацию из неуказанных элементов.

Пример чтения массивов.:

ADODB

Dim en As New ADODB.Connection

cn.Open "file name=d:\database\employee.ibp"

Dim cmd As New ADODB.Command, rs As ADODB.Recordset

cmd.ActiveConnection = cn

cmd. CornmandText = "select * from proj_dept_budget"

Set rs = cmd.Execute

Dim qhc As Variant ' QUART_HEAD_CNT

Dim i As Long

While Not rs.EOF

If IsNull(rs("quart_head_cnt")) Then

Debug.Print "NULL"

Else

qhc = rs("quart_head_cnt")

For i = LBound(qhc, 1) To UBound(qhc, 1)

Debug.Print "qhc[" & CStr(i) & "]=" & CStr(qhc(i))

Next i

End If

rs.MoveNext

Debug.Print "-------------------"

Wend

Пример записи массивов (InterBase 5.6):

ADODB

Dim en As New ADODB.Connection

cn.Open "file name=d:\database\employee.ibp"

Debug.Print en.Properties("IB Version")

cn.BeginTrans

Dim cmd As New ADODB.Command, rs As ADODB.Recordset

cmd.ActiveConnection = cn

cmd CommandText = "select * from proj_dept__budget"

Set rs = cmd.Execute

Dim upd_cmd As New ADODB.Command

upd_cmd.ActiveConnection = cn

upd_cmd.CommandText = _

"update proj_dept_budget " & _

"set quart_head_cnt=:a pro]_aept_budget.quart_head_cnt " & _

"where year=:year and proj_id=:proj_id and dept_no=:dept_no"

upd_cmd.Parameters.Refresh

Dim qhc As Variant ' QUAD_HEAD_CNT

Dim i As Long, RowAffected As Long

While Not rs.EOF

If Not IsNull(rs("quart_head_cnt")) Then

qhc = rs("quart_head_cnt")

For i = LBound(qhc, 1) To UBound(qhc, 1)

qhc(i) = 10 * qhc(i)

Next i

upd_cmd("a") = qhc

upd_cmd("year") = rs("year")

upd_cmd ("proj_id" ) = rs ("proj_id")

upd_cmd("dept_no") = rs("dept_no")

upd_cmd.Execute RowAffected ' транзакционные изменения

Debug.Print ">" & CStr(RowAffected)

End If

rs.MoveNext

Debug.Print -------------------

Wend

en.CommitTrans

Работа с хранимыми процедурами

 Сделать закладку на этом месте книги

Хранимые процедуры делятся на две категории - селективные (процедуры- выборки) и исполняемые.

Принцип работы с селективной хранимой процедурой, возвращающей ре- з>льтат своей работы в виде набора строк, очень похож на выполнение обычного SQL-запроса "SELECT..." содержащего параметры.

Вызов селективной процедуры "SUB_TOT_BUDGET"

ADODB

'вспомогательная функция конвертирования VARIANT в строку

'с поддержкой NULL

Function my_cstr(s As Variant) As String

If (IsNull(s)) Then

my_cstr = "NULL"

Else

my_cstr = CStr(s)

End If

End Function

Sub sproc_select()

Dim cn As New ADODB.Connection

cn.Open "file name=d:\database\employee.ibp"

cn.BeginTrans

Dim cmd As New ADODB.Command

cmd.ActiveConnection = cn

cmd.CommandText = "select * from SUB_TOT_BUDGET(?)"

cmd(0) = 100

Dim rs As ADODB.Recordset

Set rs = cmd.Execute

Dim col As Long

While Not rs.EOF

Debug.Print "----------------"

For col = 0 To rs.Fields.Count - 1

Debug.Print CStr(col) & ":" & rs.Fields(col).Name & " - " &

my_cstr(rs(col})

Next col

rs.MoveNext

Wend

cn.CommitTrans

End Sub

Вызов исполняемой ХП отличается от использования селективной ХП в том плане, что применяют SQL-запрос EXECUTE PROCEDURE... и получают результат работы через out-параметры. IBProvider различает только вызов исполняемой ХП, анализируя сигнатуру SQL-запроса. Наряду с SQL-выражением EXECUTE PROCEDURE..., непосредственно поддерживаемого InterBase, в тексте команды можно указывать EXECUTE ... и ЕХЕС ... IBProvider распознает в этих командах попытку вызова исполняемой ХП и автоматически приводит текст SQL-запроса к совместимому с InterBase. Для получения результата работы исполняемой ХП нужно либо самостоятельно описать out-параметры, либо попросить команду сформировать эти описания самостоятельно (ADODB Command Paiameters.Refresh). Основными правилами здесь являются:

* В тексте запроса out-параметры не упоминаются

* Описание out-параметров после in-параметров.

* Не обязательно определять все out-параметры. При работе через ADODB. это могут быть первые out-параметры из всего списка (пропуски не допускаются). При прямой работе с OLE DB-командой можно указывать имена интересующих выходящих параметров ХП, тем самым получать out-параметры в любой комбинации.

Тестовая база данных employee.gdb не содержит готовых примеров исполняемых ХП, поэтому для следующего примера будет определена своя собственная простейшая хранимая процедура.

Определение исполнимой хранимой процедуры:

SQL

create procedure sp_calculate_values(x integer,у integer)

returns(valuel integer,value2 varchar(64))

as

begin

valuel=x+y;

value2=x-y;

end

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

ADODB

Sub sproc_exec()

Dim en As New ADODB.Connection

cn.Open "file name=d:\database\employee.ibp"

cn.BeginTrans

Dim cmd As New ADODB.Command

cmd.ActiveConnection = cn

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

cmd.CommandText = "exec sp_calculate_values(:xl,:x2)"

cmd('x1") =200

cmd("x2") = 100

cmd.Execute

Debug.Print "outl=" & CStr(cmd("valuel"))

Debug.Print "out2=" & CStr(cmd("value2"))

' явное определение параметров

cmd.CommandText = "execute sp_calculate_values(?,?)"

cmd.Parameters.Append cmd.CreateParameter(, adlnteger,

adParamlnput, , 200)

cmd.Parameters.Append cmd.CreateParameter(, adlnteger,

adParamlnput, , 300)

cmd.Parameters.Append cmd.CreateParameter("vl", adlnteger,

adParamOutput)

cmd.Parameters.Append cmd.CreateParameter("v2", adBSTR,

adParamOutput)

cmd.Execute

Debug.Print "vl=" & CStr(cmd("vl"))

Debug.Print "v2=" & CStr(cmd("v2"))

сn.CommitTrans

End Sub

При явном определении параметров можно попробовать испытать мрован к'р на "прочность", задав некорректный порядок перечисления параметров. Например, сначала out-параметры, потом in-параметры.

Создание СОМ-объектов для работы с базой данных

 Сделать закладку на этом месте книги

Самым эффективным применением технологии OLEDB является создание и использование специализированных компонентов для работы с базой данных. Этот подход изначально начал применяться для серверов приложений (application servers). Однако ничто не мешает реализовывать re же самые принципы при создании обычного приложения базы данных. В этом случае, помимо обычных преимуществ компонентной технологии, исчезают типичные проблемы, связанные с передачей и разделением ресурсов сервера баз данных между несколькими модулями клиентского приложения. Для малосвязанных модулей достаточно разделять одно подключение, в случае использования сервисных компонентов может потребоваться совместная работа в контексте одной транзакции. Поскольку эти ресурсы представлены в виде СОМ-объектов, то для корректной работы требуется только правильно управлять их счетчиком ссылок. Однако для опытного программиста, использующего СОМ-технологии в реальной работе, это не проблема.

Исходя из накопленного опыта создания таких компонентов и их использования из программ, написанных на C++, VBA и VBScript, можно порекомендовать следующую структуру СОМ-объектов:

* Дуальный (dual) интерфейс автоматизации, через который выполняется основное взаимодействие с объектом. Этот же интерфейс предоставляет свойство Connection для того, чтобы устанавливать и получать подключение, используя ADODB-компоненты. Как уже было сказано ранее, ADODB.Connection одновременно является и источником данных и сессией.

* Обычный интерфейс (наследующий ILJnknown) для инициализации компонента посредством указателя на ITJnknown сессии.

* Внутренняя работа с базой данных осуществляется через низкоуровневые интерфейсы OLEDB посредством классов C++. Таким образом, компонент изолируется от ADODB и обеспечивает более производительное функционирование собственных алгоритмов.

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

В качестве поддержки совместного использования ADODB и OLEDB в одном проекте инструментальная библиотека представляет две утилиты:

* construct_adodb_connection - создание ADODB подключения на базе существующего источника данных и сессии;

* get_adodb_session - получение OLEDB-сессии, обслуживаемой ADODB-подключением.

Несмотря на открывающиеся в связи с использованием IBProvider перспективы, связанные с дроблением ваших приложений для InterBase на модули, главное не переусердствовать. Не стоит делать компоненты, предназначенные для коллекций, с собственным механизмом чтения и записи. Помните, что любой использующий команды объект делает как минимум 4-5 обращений к серверу:

* Создание

* Подготовка.

* Выполнение.

* Выборка результата.

* Разрушение

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

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

Ниже приводиться пример СОМ-объекта, который подключается к сессии и используется для того, чтобы получить значение генератора (см. главу "Таблицы Первичные ключи и генераторы" (ч. 1)). Здесь мы ограничимся лишь IDL-описанием двух интерфейсов и реализацией их методов. Помимо этого, используются возможности инструментальной библиотеки из дистрибутива IBProvider I.6.2. В реальном случае этот код, конечно же, лучше оформить в виде обычного класса. Тогда можно исключить одновременную поддержку ADODB и OLEDB. Кроме того, в данном примере не оптимизирована работа метода GenID для случая повторного использования подготовленной команды, для случая многократного вызова метода с идентичными аргументами.

IDL-описание интерфейсов:

////////////////////////////////////////////////////////////

//interface IDBSessionObject

// уcтановка/получение рабочей OLEDB-сессии объекта

[

object,

uuid(98E5AB40-333E-llD6-AC8F-OOAOC907DB93),

pointer_default(unique)

]

interface IDBSessionObject:IUnknown

{

HRESULT SetDBSession([in] lUnknown* pSession);

HRESULT GetDBSession([out]lUnknown** ppSession);

};//interface IDBSessionObject

/////////////////////////////////////////////

//interface IDBGenID

// интерфейс получения значения генератора

[

object,

uuid(98E5AB41-333E-llD6-AC8F-OOAOC907DB93),

dual,

oleautomation,

pointer_default(unique),

nonextensible

]

interface IDBGenID:IDispatch

{

[propput]

HRESULT Connection([in]IDispatch* pConnection);

[propget]

HRESULT Connection([out,retval]IDispatch** ppConnection);

HRESULT Convert([in]BSTR GenName,

[in]LONG Count,

[out,retval]LONG* pResult);

};//interface IDBGenID

Реализация методов установки сессии:

//m_spADODBConnection - член класса,

// содержащий указатель на ADODB-подключение

//m_spSession - член класса,

// содержащий указатель на используемую OLEDB-сессию

//m_Cmd - команда (t_db_command) получения значения генератора

//IDBSessionObject interface ------------------------------

HRESULT _stdcall TDBGenID::SetDBSession(lUnknown* pSession)

{

::SetErrorlnfo(0,NULL);

HRESULT hr=S_OK;

_OLE_TRY_

{

//освобождаем ADODB connection

m_spADODBConnection.Release();

m_spSession=pSession;

//инициализируем объекты взаимодействия с базой данных

m_Cmd destroy();

}

_OLE_DIS P_CATCHES_

return hr;

}//SetDBSession

HRESULT _stdcall TDBGenID::GetDBSession(lUnknown** ppSession)

{

::SetErrorlnfo(0,NULL);

return m_spSession.CopyTo(ppSession);

}//GetDBSession

//IOC2_ObjectLoader interface -----------

HRESULT _stdcall TDBGenID::put_Connection

(IDispatch* pConnection)

{

::SetErrorInf0(0,NULL);

HRESULT hr=NOERROR;

_OLE_TRY_

{

IDispatchPtr spConnection(pConnection) ; //блокируем в памяти

//освобождаем текущие подключения

SetDBSession(NULL);

if(pConnection) {

IUnknownPtr spDBSession;

get_adodb_session(pConnection,spDBSession); //throw

if(SUCCEEDED(hr=SetDBSession(spDBSession)))

m_spADODBConnection=pConnection;

}//pConnection!=NULL

}

_OLE_DISP_CATCHES_

return hr;

}//put_Connection

HRESULT _stdcall TDBGenID::get_Connection

(IDispatch** ppConnection)

{

::SetErrorlnfо(0,NULL);

if(ppConnection==NULL)

return E_POINTER;

*ppConnection=NULL;

HRESULT hr=S_OK;

_OLE_TRY_

{

if(!m_spADODBConnection && (bool)m_spSession)

{

IGetDataSourcePtr spGetDataSource(m_spSession);

if(i spGetDataSource)

t_ole_error::throw_error

("query IGetDataSource interface",spGetDataSource.m_hr);

IUnknownPtr spDataSource;

if(FAILED(hr=get_data_source(spGetDataSource,spDataSource)))

t_ole_error::throw_error("Получение источника данных",hr);

IDBPropertiesPtr spDBProperties(spDataSource);

if(!spDBProperties)

t_ole_error::throw_error

("query IDBProperties interface",spDBProperties.m_hr);

construct_adodb_connection(spDBProperties,m_spSession,

m_spADODBConnection);//throw

}//if - создание ADODB-объекта

hr=m_spADODBConnection.CopyTo(ppConnection);

}

_OLE_DISP_CATCHES_

return hr;

}//get_Connection

Реализация метода получения значения генератора:

HRESULT _stdcall TDBGenID::GenID(BSTR GenName,LONG Count,

LONG* pResult)

{

::SetErrorlnfо(0,NULL);

if(pResult==NULL)

return E_POINTER;

HRESULT hr=S_OK;

_OLE_TRY_

{

if(!m_spSession)

throw runtime_error("Объект неинициализирован");

if(!m_Cmd.is_created())

_THROW_OLEDB_FAILED(m_Cmd,create(m_spSession));

structure::str_formatter stmt

("select gen_id(%l,%2) from rdb$database");

t_db_row row(1);

_THROW_OLEDB_FAILED (m_Cmd, prepare ( stmt«GenName«Count, &row) )

_THROW_OLEDB_FAILED(m_Cmd,execute(NULL));

if(m_Cmd.fetch(row)==S_OK)

*pResult=row[0].as_integer;

else

{

//проверим причину сбоя получения данных

_THROW_OLEDB_FAILED(m_Cmd,m_last_result)

throw runtime_error("Получено пустое множество");

}

}

_OLE_DIS P_CATCHES_

return hr;

}//GenID

Использование скриптов в клиентских приложениях базы данных InterBase

 Сделать закладку на этом месте книги

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

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

* Проверка достоверности данных при сохранении.

* Контроль прав на чтение и изменение данных.

* Правила движения документов.

* Настраиваемый пользовательский интерфейс.

* Печать выходных форм.

* Встроенные отчеты.

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

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

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

Компоненты ADODB изначально приспособлены для использования в ActiveX-сценариях. Впрочем, для передачи в сценарий соединения с базой данных ADODB.Connection с выделенной сессией могут потребоваться некоторые усилия. Решение этой задачи было приведено в разделе описания сессии.

Компоненты OLEDB нельзя непосредственно использовать в сценариях. Но их можно "обернуть" в ADODB-компоненты и таким образом использовать в коде сценария. Подробности можно посмотреть в примерах, входящих в дистрибутив IBProvider.

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

Использование пула подключений к базе данных

 Сделать закладку на этом месте книги

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


убрать рекламу






м пула подключений не реализуется самим OLE DB-провайдером. От последнего требуется только корректно обрабатывать уведомления о помещении в пул и обеспечивать многопоточный доступ к компонентам. IBProvider реализует оба требования, поэтому клиенту предлагается только провести корректную инициализацию источника данных.

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

"OLE DB Services=-l;free_threading=true"

Параметр "OLE DB Services=-l" указывает ADODB на необходимость использования пула подключений. Параметр "free_threading=true" устанавливает внутренний флаг, объявляющий поддержку многопоточного доступа.

Для демонстрации работы пула ниже приведен простой пример, выполняющий в цикле инициализацию и закрытие источника данных. Для запрещения пула подключений присвойте "OLE DB Services" нулевое значение. Замеры производительности проводятся очень грубо - в секундах, но этого оказалось достаточно, чтобы увидеть преимущества пула подключений. По истечении 60 с с момента добавления в пул неиспользуемые источники данных освобождаются и производиться отключение от сервера базы данных.

Для того чтобы подробно изучить процесс функционирования пула подключений, следует воспользоваться информацией на сайте компании Microsoft или посмотреть в документацию по OLE DB SDK (см. "Resource Pooling").

ADODB:

Dim en As New ADODB.Connection

Dim cnt As Long

Dim start As Date, total As Date

total = Time

For cnt = 1 To 10

start = Time

cn.Provider = "LCPI.IBProvider.1"

cn.Properties("OLE DB Services") = -1

cn.Properties("free_threading") = True

cn.Open "data source=localhost:d:\database\employee.gdb;", _

"gamer", "vermut"

Dim cmd As New ADODB.Command

cmd.ActiveConnection = cn

cmd.CommandText = "select count(*) from job"

cn.BeginTrans

cmd.Execute

cn.CommitTrans

cn.Close

Debug.Print ">" & CStr(CDate(Time - start))

'можно сделать задержку чуть больше 60 с,

'чтобы понаблюдать за освобождением

'инициализированного источника данных и

'потерю подключения к базе данных

'Application.Wait Time + CDate("О:1:05")

'Debug.Print "disconnect"

'Application.Wait Time + CDate("0:0:15")

Next cnt

Debug.Print "total:" & CStr(CDate(Time - total))

'освобождение последнего объекта, использующего

'пул подключений, приводит к уничтожению всех

' инициализированных источников данных

Set cn = Nothing

Распределенные запросы

 Сделать закладку на этом месте книги

Помимо определения самой спецификации OLE DB, Microsoft активно применяет ее в своих разработках, связанных с управлением данными. И одной из самых потрясающих разработок этой категории является Microsoft Distributed Query - программный компонент, входящий в состав MS SQL, позволяющий делать SQL-запросы к нескольким источникам данных с использованием OLE DB-провайдеров. И хотя возможность обращения в одном запросе сразу к нескольким источникам данных так же доступна и в BDE, процессор распределенных SQL-запросов, реализованный Microsoft, несомненно, представляет собой более мощный и более совершенный механизм для этих целей. Далее будут перечислены основные моменты и принципы использования IBProvider в распределенных запросах с применением MS SQL 7.

* MS Distributed Query потребовал полной стабильности в описании метаданных. Для этого пришлось реализовать в IBProvider полную поддержку всех типов InterBase и обеспечить совпадение описания метаданных в наборах информационной схемы с описанием колонок результирующих множеств.

* Из-за скрупулезной сверки данных результирующих множеств с описанием их метаданных не допускается усечение хвостовых пробелов полей типа CHAR. По умолчанию усечение производится. Чтобы запретить эту операцию, в строке подключения к базе данных нужно указать свойство инициализации источника данных "truncate_char=false".

* Процессор распределенных запросов не поддерживает массивы, поэтому не стоит их выбирать в результирующее множество.

* Несовпадение диапазона дат MS SQL и InterBase приводит к тому, что нельзя выбирать даты до 1 января 1753 года.

* При работе с 3-м диалектом подключения нужно соблюдать регистр символов имени объекта базы данных, независимо от того, квотировано оно или нет Дело в том, что процессор запросов начинает повсеместно использовать двойные кавычки для имен объектов базы данных независимо от того, хотите вы этого или нет. Для подключения 1-го диалекта квотированные имена не используются, поэтому большие и маленькие символы в названии объектов базы данных не различаются.

Регистрация базы данных к процессору распределенных запросов MS SQL 7:

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

* Откройте "SQL Server Enterprise Manager" (консоль управления MS SQL серверами) и подключитесь к интересующему вас серверу.

* Перейдите на "SecurityVLinked Servers" дерева элементов конфигурирования сервера. В контекстном меню этого элемента выберите пункт "New Linked Server..."

* В открывшемся диалоге нужно указать параметры подключения к базе данных InterBase через IBProvider. Для этого:

* В поле "Linked Server" укажите имя, которое будет использоваться в SQL- запросах для идентификации нашей базы данных. Например, "ГВ_ЕМР".

* В выпадающем списке "Provider Name" выберите "LCPI OLE DB Provider for InterBase".

* Нажмите кнопку Options и установите галочку напротив пункта "Allow InProcess". Закройте это окно, нажав кнопку ОК.

* В поле "Data Source" нужно указать путь к базе данных. Например, main:e:\database\employee.gdb.

* В поле "Provider string" указываются остальные параметры подключения:

"use id=gamer, password=vermut; free_threading=true; truncate_char=false".

* Закройте диалог ввода параметров подключения.

Если все параметры были введены правильно, то, перейдя в дереве на элемент "SecurityYLinked Servers\IB_EMP\Tables", можно посмотреть на список таблиц базы данных "employers.gdb". Если при указании параметров подключения была допущена ошибка, связанный сервер нужно удалить (выбрав в его контекстном меню "Удалить") и повторить операцию регистрации с самого начала.

После того как источник данных был подключен в качестве связанного сервера к MS SQL, можно начать эксперименты с SQL-запросами. Для этого нужно открыть SQL Server Query Analyzer. Это можно сделать из меню "Tools" консоли управления MS SQL-серверами или через меню группы программ для работы с MS SQL, которое доступно через кнопку "Пуск" панели задач Windows.

В поле ввода SQL-запросов Query Analyzer можно вводить как одиночный текст запроса, так и группу запросов, используя точку с запятой в качестве разделителя. При наборе запросов нужно использовать синтаксис MS SQL, а не InterBase, поскольку последний переходит в ранг обычного носителя данных взаимодействие с которым осуществляется через OLE DB-интерфейсы и очень простые SQL-запросы.

Как уже было описано ранее, любая операция с данными базы InterBase, требует наличия активной транзакции. Поэтому самым первым SQL-запросом будет команда запуска транзакции:

MS SQL

BEGIN TRANSACTION;

После этого можно вводить SQL-запросы на выборку данных из источника данных IВ_ЕМР:

MS SQL

SELECT EMP.* FROM IB_EMP...EMPLOYEE EMP WHERE

EMP.JOB_CODE='Eng';

где IВ_ЕМР...EMPLOYEE представляет собой составное имя объекта, которое полностью идентифицирует его расположение в пространстве имен сервера баз данных MS SQL.

Теперь можно зарегистрировать любую другую базу данных InterBase, выполнив действия по аналогии с вышеописанными. В том числе никто не запрещает зарегистрировать несколько раз одну и ту же базу данных под разными псевдонимами, например ПЗ_ЕМР и Ю_ЕМР_1. Конечно, в качестве связанного сервера может использоваться любой другой OLE DB-поставщик данных, который поддерживает необходимую для процессора запросов функциональность.

Пример нахождения записей описания служащих источника данных Ш_ЕМР_1, отсутствующих в базе данных Ш_ЕМР:

MS SQL

SELECT EMP1.*

FROM IB_EMP_1...EMPLOYEE EMP1

WHERE NOT EXISTS(SELECT * FROM IB_EMP... EMPLOYEE EMP

WHERE EMP1.FIRST_NAME=EMP.FIRST_NAME AND

EMP1.LAST_NAME=EMP.LAST_NAME)

Если IB_ЕМР и IB_ЕМР_1 являются псевдонимами одной и той же базы, то результирующее множество этого SQL-запроса должно быть пустым.

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

Поскольку в настоящий момент IB Provider (версия 1.6.2) не реализует OLE DB-интерфейсы модификации результирующих множеств, использование процессора запросов для выполнения запросов "INSERT ...", "UPDATE ...", "DELETE..." невозможно.

Заключение

 Сделать закладку на этом месте книги

В этой главе рассмотрены способы разработки клиентских приложений для InterBase 6.x и его клонов с помощью технологии OLEDB. Показаны основные подходы к разработке приложений баз данных InterBase с использованием таких популярных средств разработки компании Microsoft, как Visual Basic и Visual C++. Мощные и гибкие возможности, предоставляемые IBProvider, опровергают распространенное мнение, что InterBase - это сервер, с которым можно работать только с помощью средств разработки от компании Borland.

Создание CGI-приложений под ОС Linux с использованием InterBase API

 Сделать закладку на этом месте книги

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

* Чтение переменных, специфичных для WWW (разбор переменных html-форм и т. д.).

* Подключение к базе данных.

* Некоторое количество выборок и вставок (возможно, с исполнением хранимых процедур).

* Отключение от базы данных.

* Отправка данных клиенту.

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

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

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

CGI-приложение является по сути прослойкой между Интернетом и базой данных, поэтому особое внимание следует уделять корректной работе с памятью и данными, которые приложение будет получать из Интернента. Самый лучший вариант - считать, что изначально будут посылаться некорректные данные, поэтому придется разработать эффективную защиту от "замусоривания".

При работе на уровне API дополнительный разбор текстовых переменных не производится - они попадают в базу данных в том виде, в каком хранятся в памяти скрипта. Таким образом, можно, например, хранить текст в различных кодировках в полях таблиц с CHARACTER SET NONE. Также не возникает проблем с одинарными или двойными кавычками.

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

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

Специфика CGI-приложений предполагает, что в качестве SQL-сервера хорошо проявит себя архитектура SuperServer, которая обеспечивает хорошую производительность на множестве маленьких запросов при относительно небольшом объеме занимаемой памяти.

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

Давайте рассмотрим написание CGI-приложения под ОС Linux с использованием языка C/C++, который производит откомпилированный код и избавляет от необходимости распространять приложения в исходных кодах, как в случае Perl или РНР. Все примеры в данной главе написаны на C/C++, однако легко могут быть переписаны под FPC/PERL/PHP.

В комплект поставки InterBase и его клонов входит заголовочный файл ibase.h с описанием функций InterBase API, констант, макросов и т. д., доступных разработчику клиентских приложений InterBase на C/C++. Написание приложений с использованием чистого API может показаться непривычно громоздким из-за высокой детализации кода приложения. Зная работу с базой данных на уровне InterBase API, можно довольно легко написать свою обертку вокруг API- функций или воспользоваться уже существующими (например, IBProvider из предыдущей главы).

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

Параметры запроса - это переменные, которые передаются серверу (входящие) или возвращаются в качестве результата (исходящие). В примерах эти переменные носят соответственно названия isqlda и osqlda.

Большинство IB API-функций возвращают так называемый STATUS_VECTOR - массив целых чисел (long). Анализируя этот массив, можно узнать, произошел ли вызов функции успешно, и если нет-то по какой причине.

В качестве параметров используется особая структура, XSQLDA - extended SQL Descriptor Area. Она позволяет \знать (или передать) всю необходимую информацию о каждом конкретном параметре, а также сами данные в структуре SQLVAR - SQL Variable. В силу изложенного выше при написании CGI- приложений интерес представляют лишь несколько полей из этих структур:

Структура XSQLDA, описывающая параметры запросов

Поле

Тип (для языка С)

Описание

version

short

Версия XSQLDA-структуры; должна быть установлена в соответствии с версией используемой клиентской библиотеки. Для текущих версий InterBase это значение соответствует SQDA_VERSION1

sqln

short

Количество элементов массива sqlvar (иными словами, количество передаваемых и/или получаемых переменных). Это поле должно быть обязательно заполнено для входящих и исходящих параметров до передачи данных запроса серверу

sqld

short

Указывает количество входящих или исходящих параметров. Обычно это поле для исходящих параметров инициализируется библиотекой InterBase. 0 в этом поле означает, что запрос не является выборкой (non-SELECT query)

Структура SQLVAR, описывающая панные параметров структуры XSQLDA

Поле

Тип (для языка С)

Описание

sqldata

char*

Указатель на данные, которые необходимо передать серверу

sqltype

short

Информация о типе передаваемых данных. Для полей, допускающих SQL-значение NULL- используется тип, на единицу больший, чем базовый. Например, для типа SQL INTEGER используется значение SQL_LONG, если NULL не допускается, и тип SQLJ.ONG+1, если в значении поля допускается NULL

sqllen

short

Размер памяти, занимаемой переменной, на которую указывает sqldata

sqlind

short *

Это поле - индикатор того, является ли значение передаваемой переменной NULL (в этом случае это поле должно быть равно единице) или нет (в этом случае значение этого поля 0). Заполнение этого поля является обязательным, если переменная может принимать значение NULL. Таким образом, если вы хотите передать значение NULL серверу и в sqltype указан не базовый тип (например, SQLJ.ONG+1), то инициализировать это поле обязательно; если же указан, например тип SQL_LONG, то это поле можно не инициализировать - сервер проигнорирует его значение. При чтении данных из результата это поле указывает, является ли полученное значение NULL или отличается от него. Анализировать необходимо именно это поле - результат сравнения переменной sqldata с пустым значением может быть абсолютно непредсказуемым

Подробнее все поля этих структур описаны в [7] и представляют интерес > только при написании действительно сложных приложений или библиотек.

Теперь, когда даны необходимые определения, можно непосредственно по* I знакомиться с методами DSQL-программирования. Их всего 4: I

Нет переменных ни на входе ни на выходе. В этом случае обе структуры * SQLDA (isqlda, osqlda) должны быть инициализированы NULL (или любым пустым указателем в используемом вами языке - например, nil в Паскале) в вызове функций isc_dsql_execute, isc_dsql_execute2 и т. д. Обычно это запросы, которые удаляют записи с жестко заданными свойствами.

* Есть переменные как на входе, так и на выходе; в этом случае нужно инициализировать соответствующие структуры.

* Нет переменных на выходе, но есть на входе; в этом случае osqlda = NULL, a isqlda требуется инициализировать.

* Нет переменных на входе, но есть на выходе; в этом случае isqlda = NULL, a osqlda требуется инициализировать.

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

Пример 1. Запрос без параметров

 Сделать закладку на этом месте книги

Предположим, у нас есть таблица следующей структуры:

CREATE TABLE BOOKS (

B_ID INTEGER NOT NULL,

B_INDEX CHAR(16) NOT NULL,

B_NAME VARCHAR(80) NOT NULL,

B_AUTHOR VARCHAR(SO) NOT NULL,

B_ADDED TIMESTAMP DEFAULT 'now' NOT NULL,

B_THEME VARCHAR(60) NOT NULL);

Для того чтобы отобразить результат запроса "select b_id. b_index, b_name. b_authoi, b_added, b_theme from books" в виде следующей таблицы (см. Рис. 3.1):







Рис 3.1. Результат выполнения SQL-запроса, представленный в браузере в виде HTML- таблицы

понадобится следующий скрипт: (examplel.с)

#include <ibase.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <time.h>

// Эта структура предназначена для хранения переменных типа SQL_VARYING

#define SQL_VARCHAR(len) struct {short vary_length; char

vary_string[(len)+1];}

int main (void){

/ / Константы, необходимые для работы с базой данных - инициализируйте их в

// соответствии с реальным путем к базе, пользователем и паролем

char *dbname = "localhost:/var/db/demo.gdb";

char *uname = "sysdba";

char *upass = "masterkey";

char *query = "select b_id, b_index, b_name, b_author, b_added,

b_theme from books";

/ / Переменные для работы с базой данных

isc_db_handle db_handle = NULL;

isc_tr_handle transaction_handle = NULL;

isc_stmt_handle statement_handle=NULL;

char dpb_buffer[256], *dpb, *p;

short dpb_length;

ISC_STATUS status_vector[20] ;

XSQLDA *isqlda, *osqlda;

long fetch_code;

short

o_ind[20]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0, 0,0);

/ / Остальные переменные

int i = 0;

long b_id;

char b_index[17];

SQL_VARC HAR(100) b_name;

SQL_VARCHAR(100) b_author;

SQL_VARCHAR(100) b_theme;

ISC_TIMESTAMP b_added;

struct tm added_time;

char decodedTime[100];

Здесь наше приложение начинает вывод стандартного HTML-документа:

printf("Content-type:

text/plain\n\n<html><body><center><b>Example Nr l</b><br>SELECT

without input parameters</centerxbr>") ;

а затем подключается к базе данных - здесь два этапа

// создаем так называемый database parameter buffer, необходимый

/ / для подключения к базе данных

dpb=dpb_buffer;

*dpb++ = isc_dpb_versionl;

*dpb++ = isc_dpb_user_name;

*dpb++ = strlen(uname);

for(p = uname; *p; ) *dpb++ = *p++;

*dpb++ = isc_dpb_password;

*dpb++ = strlen(upass);

for (p=upass; *p;) *dpb++ = *p++;

dpb_length = dpb dpb_buffer;

// Подключаемся к базе

isc_attach_database(

status_vector,

strlen(dbname),

dbname,

&db_handle,

dpb_length,

dpb_buffer) ;

Далее идет стандартная для большинства API-функций проверка и анализ результата:

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector) ;

return(1);

}

Если подключение к базе данных произошло удачно, начинается транзакция:

if (db_handle){

isc_start_transaction(

status_vector,

&transaction_handle,

1,

&db_handle,

0,

NULL);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(1);

}

}

Далее инициализируются структуры, которые будут заполняться результатами запроса:

osqlda = (XSQLDA *)malloc(XSQLDA_LENGTH(6));

osqlda -> version = SQLDA_VERSION1;

osqlda -> sqln = 6;

osqlda->sqlvar[0].sqldata = (char *)&b_id;

osqlda->sqlvar[0].sqltype = SQL_LONG;

osqlaa->sqlvar[0].sqlind = &o_ind[0];

osqlda->sqlvar[1].sqldata = (char *)&b_index;

osqlda->sqlvar[1].sqltype = SQL_TEXT;

osqlda->sqlvar[1].sqlind = &o_ind[l];

osqlaa->sqlvar[2].sqldata = (char *)&b_name;

osqlda->sqlvar[2].sqltype = SQL_VARYING;

osqlda->sqlvar[2J .sqlind = ko_ind[2];

osqlda->sqlvar[3].sqldata = (char *)&b_author;

osqlda->sqlvar[3].sqltype = SQL_VARYING;

osqlda->sqlvar[3].sqlind = &o_ind[3];

osqlda->sqlvar[4].sqldata = (char *)&b_added;

osqlda->sqlvar[4].sqltype = SQL_TIMESTAMP;

osqlda->sqlvar[4].sqlind = &o_ind[4];

osqlda->sqlvar[5].sqldata = (char *)&b_theme;

osqlda->sqlvar[5].sqltype = SQL_VARYING;

osqlda->sqlvar[5].sqlind = &o_ind[5];

А вот здесь, собственно, и начинается подготовка к исполнению запроса сервером:

isc_dsql_allocate_statement(

status_vector,

&db_handle,

&statement_handle);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(1);

}

isc_dsql_prepare(

status_vector,

&transaction_handle,

&scatement_handle,

0,

query,

SQL_DIALECT_V6,

osqlda);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(1);

}

isc_dsql_execute2(

status_vector,

&transaction_handle,

&statement_handle,

1,

NULL,

NULL);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector) ;

return(1);

}

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

printf("<center><table border=0 bgcolor=black cellpadding=l

cellspacing=l><tr align=center bgcolor=#999999> <td>Book

ID</tr> <td>CODE</tr> <td>TITLE</tr> <td>AUTHOR</tr>

<td>ADDED</tr> <td>THEME</tr> </tr>");

После исполнения запроса сервер готов к передаче данных. "Доставкой" данных !лнимаегся функция isc_dsql_tetch()'

while((fetch_code = isc_dsql_fetch(

status_vector,

&statement_handle,

1,

osqlda))= = 0) {

Для строковых переменных требуется корректно установить длину, так как размер возвращаемых данных не всегда соответствуем максимально возможному, и если этого не сделать, то вместе с реальными данными можно получить' "мусор" из памяти или остатки предыдущих строк:

b_index[osqlda->sqlvar[1].sgllen]= ' \0 ' ;

b_name.vary_string[b_name.vary_length] = '\0' ;

b_author.vary_string[b_author.vary_length]='\0';

b_theme.vary_string[b_theme.vary_length]='\0';

Структуру типа TIMESTAMP, как и структуры DATE/TIME, перед выводом в документ можно преобразовать в строковый тип в нужном формате. Для этого сначала она декодируется в структуру tm, а затем в строку:

isc_decode_timestamp(&b_added,&added_time);

strftime(decodedTime,sizeof(decodedTime),"%d-%b-%Y

%H:%M",&added_t ime);

printf("<tr bgcolor=white><td>%i</td> <td>%s</td> <td>%s</td>

<td>%s</td> <td>%s</td> <td>%s</td> </tr>",

b_id,

b_index,

b_name vary_strxng,

b_author.vary_string,

decodedTime,

b_theme.vary_string);

}

После вывода всех данных необходимо завершить документ:

printf ("</table></center></body></html>") ;

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(1);

}

free(osqlda);

isc_dsql_free_statement(

status_vector,

&statement_handle,

DSQL_drop);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

recurn(l);

}

Затем завершить транзакцию и отключиться от базы данных:

if (transaction_handle){isc_commit_transaction(status_vector,

&transaction_handle);}

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(1),

}

if (db_handle) isc_detach_database(status_vector, &db_handle);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector) ;

return(1);

}

return(0);

}// end of main

Обратите внимание на маленькую разницу в работе с переменными SQLJVARYING и SQL_TEXT (это соответственно VARCHAR и CHAR языка SQL). Разница в том, что если в базе данных хранится меньше символов, чем максимально возможно для столбца (например, объявлено CHAR(16), а хранится строка "12345"), то сервер добавит N пробелов в конец строки, где N является' разницей между максимально возможным количеством символов и реально хра-1 нящимся в поле таблицы. Тип SQL_VARYING свободен от этого недостача,< однако при получении данных нужно учитывать, что только определенное зна-1 чение символов является реально полученными; остальное количество - это| случайные данные из памяти компьютера, на котором исполняется скрипт. Для | удобства работы с этим типом обычно определяют структуру, где член структу- \ ры SQL_VARCHAR vary_length указывает размер полученной строки,^ a vary_string собственно содержит строку.

Если запрос гарантированно возвращает одно значение (например, одиночный SELECT или вызов хранимой процедуры), то использовать функцию; isc_dsql_fetch() нет необходимости, вместо этого в параметр функции! isc_dsql_execute2() можно подставить значение osqlda переменной Работа с ти-1 пами SQL DATE и TIME абсолютно не отличается от работы с переменнымиii типа TIMESTAMP - всего лишь используются другие функции для преобразо-" вания: isc_decode_sql_date() и isc_decode_sql_time().


убрать рекламу






j

Пример 2. Запрос с параметрами

 Сделать закладку на этом месте книги

Теперь рассмотрим пример исполнения запроса с параметрами - вызов xpaнимой процедуры, которая просто вставит данные из формы в эту же таблицу.| Принципиально этот пример практически ничем не отличается от вышеприведенного, за исключением того что в нем появляются две дополнительные части - одна разбирает переменные HTML-формы, другая (если переменные переданы) исполняет процедуру.

Вот текст этой ХП.

create procedure InsertData (b_index char(16),

b_name varchar(80),

b_author varchar(80),

b_theme varchar(60))

returns (result_code integer)

as

begin

insert into books (

B_ID,B_INDEX,B_NAME, B_AUTHOR,B_ADDED,BJTHEME)

values(0,.b_index, :b_name, :b_author, 'now', :b_theme);

result_code = 0;

when any

do begin

result_code=-l;

end

end

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

Текст скрипта второго примера выглядит так :

#include <ibase.h>

#include <stdio.h>

#include <stdlib.h>

#include <scring.h>

#include <time.h>

#include "cgic.h"

#define SQL_VARCHAR(len) struct {short vary_length; char

vary_string[(len)+1];}

Вот здесь некоторое отличие: используемая для разбора переменных www- библиотека заменяет стандартную функцию main:

int cgiMain (void){ '

char *dbname = "localhost:/var/db/demo.gdb";

char *uname = "sysdba";

char "upass = "masterkey";

char *qaery = "select b_id, b_index, b_name, b_author, b_added,

b_theme from books";

На месте неизвестных входящих параметров - знаки вопроса:

char *SPCall = "execute procedure insertdata (?,?,?,?)";

isc_db_handle db_handle = NULL;

isc_tr_handle transaction_handle = NULL;

isc_stmt_handle statement_handle=NULL;

char dpb_buffer[256], *dpb, *p;

short dpb_length;

ISC_STATUS status_vector[20];

XSQLDA *isqlda, *osqlda;

Long fetch_code;

Short

o_ind[20]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);

int i = 0;

int hDisplayed=0;

int formVarErr;

int ExecSP=l;

long res_code;

long b_id;

char b_index[17];

SQL_VARCHAR(100) b_name;

SQL_VARCHAR(100) b_author;

SQL_VARCHAR (100) b_theme;

ISC_TIMESTAMP b_added;

struct tm added_time;

char decodedTime[100];

char form_b_index[17],

form_b_name[81],

form_b_author[81],

form_b_theme[61];

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

printf("Content-type:

text/plain\n\n<htmlxbody><center><b>Example Nr 2</b><hr

width=\%></centerxbr>&nbsp;&nbsp;&nbsp;Part 1: Executable SP

demo with both types of parameters.<br>");

if(strcmp(cgiRequestMethod,"POST")==0){

formVarErr =

cgiFormStringNoNewlines("b_index",form_b_index,17);

if (formVarErr!=cgiFormSuccess){

printf("<br><b>Error: Book index missed or too long</b>");

ExecSP=0,

}

formVarErr = cgiFormStringNoNewlines("b_name",form_b_name,81);

if {formVarErr!=cgiFormSuccess){

printf ("<brxb>Error: Book name missed or too long</b>");

ExecSP=0;

}

formVarErr =

cgiFormStringNoNewlines("b_author",form_b_author,81);

if (formVarErr!=cgiFormSuccess){

printf ("<br><b>Error: Book author missed or too long</b>");

ExecSP=0;

}

formVarErr =

cgiFormStringNoNewlines("b_theme",form_b_theme,61);

if (formVarErr!=cgiFormSuccess){

printf("<br><b>Error: Book theme missed or too long</b>");

ExecSP=0;

}

}

else{

ExecSP=0;

printf ("<brxi>Procedure execution skipped</i> - REQUEST_METHOD

must be POST");

}

Заметьте: если специфика приложения требует того, чтобы определенные www-переменные обязательно присутствовали и удовлетворяли определенным условиям (например, учетные данные пользователя могут быть защищены контрольной суммой), то именно в этом месте приложение производит решение о целесообразности продолжения работы. 1

После разбора переменных идет непосредственно работа с базой данных:

dpb=dpb_buffеr;

*dpb++ = isc_dpb_versionl;

*dpb++ = isc_dpb_user_name;

*dpb++ = strlen(uname);

for(p = uname; *p;)

*dpb++ = *p++;

*dpb++ = isc_dpb_password;

*dpb++ = strlen(upass);

for (p=upass; *p;)

*dpb++ = *p++;

dpb_length = dpb- dpb_buffer;

isc_attach_database(

status_vector,

strlen(dbname),

dbname,

&db_handle,

dpb_length,

dpb_buffer);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(1};

}

if (db_handle){

isc_start_transaction(

status_vector,

&transaction_handle,

1,

&db_handle,

0,

NULL);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(l);

}

}

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

if(ExecSP){

printf("<br><i>Attempt to call SP with the following

parameters:

' %s' , '%s' , ' %s' , '%s'</i>.....",form_b_index,form_b_name,form_b_a

uthor,form_b_theme);

Как можно видеть, принципиально инициализация структур для входящих параметров не сильно отличается от инициализации исходящих параметров из первого примера:

isqlda = (XSQLDA *)malloc(XSQLDA_LENGTH(4));

isqlda->version = SQLDA_VERSION1;

isqlda->sqln = 4;

isqlda->sqld = 4;

isqlda->sqlvar[0] .sqldata = (char *)&form_b_index;

isqlda->sqlvar[0].sqltype = SQL_TEXT;

isqlda->sqlvar[0].sqllen = strlen(form_b_index);

isqlda->sqlvar [1] . sqldata = (char * ) &f orm_b_name,

isqlda->sqlvar[1].sqltype = SQL_TEXT;

isqlda->sqlvar[1].sqllen = strlen(form_b_name);

isqlda->sqlvar[2].sqldata = (char *)&form_b_author;

isqlda->sqlvar[2].sqltype = SQL_TEXT;

isqlda->sqlvar[2].sqllen = strlen(form_b_author);

isqlda->sqlvar[3].sqldata = (char *)&form_b_theme;

isqlda->sqlvar[3].sqltype = SQL_TEXT;

isqlda->sqlvar[3].sqllen = strlen(form_b_theme);

osqlda = (XSQLDA *)malloc(XSQLDA_LENGTH(1));

osqlda -> version = SQLDA_VERSION1;

osqlda -> sqln = 1; osqlda -> sqld = 1;

osqlda->sqlvar[0].sqldata = (char *)&res_code;

osqlda->sqlvar[0].sqltype = SQL_LONG;

osqlda->sqlvar[0].sqllen = sizeof(long);

osqlda->sqlvar[0].sqlind = &o_ind[0];

Вызов ХП происходит сразу, без предварительной подготовки (подробнее об этом будет рассказано несколько ниже):

isc_dsql_exec_immed2(

status_vector,

&db_handle,

&transaction_handle,

0,

SPCall,

SQL_DIALECT_V6,

isqlda,

osqlda};

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector) ;

return(1);

}

if(res_code==0){

printf(" <t»successfly</b>.");

}

else{

printf(" <b>failed with result_code=%i</b>.",res_code);

}

free(isqlda);

free(osqlda);

}

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

printf ("<brxbr>&nbsp; &nbsp; &nbsp; Part 2: Select without input

parameters<br>");

osqlda = (XSQLDA *)malloc(XSQLDA_LENGTH(6));

osqlda -> version = SQLDA_VERSION1;

osqlda -> sqln = 6;

osqlda->sqlvar[0].sqldata = (char *)&b_id;

osqlda->sqlvar[0].sqltype = SQL_LONG;

osqlda->sqlvar[0].sqlind = &o_ind[0];

osqlda->sqlvar[1].sqldata = (char *)&b_index;

osqlda->sqlvar[1].sqltype = SQL_TEXT;

osqlda->sqlvar[1].sqlind = &o_ind[l];

osqlda->sqlvar[2].sqldata = (char *)&b_name;

osqlda->sqlvar[2].sqltype = SQLJVARYING;

osqlda->sqlvar[2].sqlind = &o_ind[2];

osqlda->sqlvar[3].sqldata = (char *)&b_author;

osqlda->sqlvar[3].sqltype = SQL_VARYING;

osqlda->sqlvar[3].sqlind = &o_ind[3];

osqlda->sqlvar[4].sqldata = (char *)&b_added;

osqlda->sqlvar[4].sqltype = SQL_TIMESTAMP;

osqlda->sqlvar[4].sqlind = &o_ind[4];

osqlda->sqlvar[5].sqldata = (char *)&b_theme;

osqlda->sqlvar[5].sqltype = SQL_VARYING;

osqlda->sqlvar[5].sqlind = &o_ind[5];

isc_dsql_allocate_statement(

status_vector,

&db_handle,

&statement_handle);

if (stacus_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return{1);

}

isc_dsql_prepare(

status_vector,

&transaction_handle,

&statement_handle,

0,

query,

SQL_DIAL,ECT_Y6,

osqlda);

if (status_vector [ 0] == 1 && status_vector[1]) {

isc_print_status(status_vector) ;

return(1);

}

Заметьте, что если бы запрос гарантированно возвращал одну запись, то надобность в вызове функции isc_dsql_fetch()oтпaлa бы, а на месте последних двух пустых значений в вызове нижеследующей функции были бы соответственно isqlda и osqlda:

isc_dsql__execute2 (

status_vector,

&transaction_handle,

&statement_handle,

1,

NULL,

NULL);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status (status_vector);

return(1);

}

while((fetch_code = isc_dsql_fetch(

status_vector,

&staternent .handle,

1,

osqlda))==0)

{

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

if (!hDisplayed){

hDisplayed=l;

printf ("<brxbr><center><table border=0 bgcolor=black

cellpadding=l cellspacing=l><tr align=center bgcolor=#999999>

<td>Book ID</tr> <td>CODE</tr> <td>TITLE</tr> <td>AUTHOR</tr>

<td>ADDED</tr> <cd>THEME</tr> </tr>");

}

Далее идет уже знакомая по предыдущему примеру обработка текстовых переменных:

b_index[osqlda->sqlvar[1].sqllen]='\0';

b_name.vary_string[b_name.vary_length]='\0';

b_author.vary_string[b_author.vary_length]='\0';

b_theme.vary_string[b_theme.vary_length]='\0';

и преобразование TIMESTAMP в удобочитаемое выражение:

isc_decode_timestamp(&b_added,&added_time);

strftime(decodedTime,sizeof(decodedTime),"%d-%b-%Y

%H: %M", &added_t ime) ;

printf("<tr bgcolor=white><td>%i</td> <td>%s</td> <td>%s</td>

<td>%s</td> <td>%s</td> <td>%s</td> </tr>",

b_id,

b_index,

b_name.vary_string,

b_author.vary_string,

decodedTime,

b_theme.vary_string);

}

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(1);

}

free(osqlda);

isc_dsql_free_statement(

status_vector,

&statement_handle,

DSQL_drop);

if (scacus_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector) ;

return(1);

}

Завершаем транзакцию и отключаемся от базы данных:

if (transaction_handle){

isc_commit_transaction(status_vector, &transaction_handle); }

if (status_vector[0] == 1 && status_vector[1]){

isc_pnnt_status (status_vector) ;

return(1);

}

if (db_handle) isc_detach_database(status_vector, &db_handle);

if (status_vector[0] == 1 && status_vector[1]){

isc_print_status(status_vector);

return(1);

}

Завершаем документ HTML-формой для ввода данных:

if(hDisplayed){

printf("</table></center><br>");

}

else{

printf ("<br><brxbr><center><b>Table is

empty</b></center><brxbr><br>") ;

}

printf ( "<centerxtable border=0 width=%><tr

align=right><td><form name ='demo ' method=POST

action= ' example2 ' xtable border=0>") ;

printf("<tr><td align=left>Book index</td><td>

<input type=text name='b_index' size=16 maxlength=16></td></tr>") ;

printf("<tr><td align=left>Book name</td><td>

<input type=text name= ' b_name ' size=16 maxlength=80x/tdx/tr>");

printf ("<tr><td align=lef t>Book author</td><td>

<input type=text name='b_author' size=16 maxlength=80></td></tr>");

printf("<tr><td align=left>Book theme</td><td>

<input type=text name='b_theme' size=16 maxlength=60></td></tr>");

printf ("<trxtd colspan=2 align=right>

<input type= ' submit' value='Add data'></td></tr>"};

printf ("</table></form></td></tr></table></center></body></html>");

return(0);

}// end of main

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







Рис 3.2. Результат вставки записи

После вставки какой-нибудь записи в таблицу при помощи формы справа снизу результат будет следующим (см. рис 3.3).







Рис 3.3. Результаты выполнения CCI-приложения

В этом приложении читается несколько переменных www-окружения, которые проверяются на ненулевую длину и передаются в качестве входящего параметра запроса. Переменные www-окружения читаются при помощи функций библиотеки CGIC (ее можно загрузить с сайта https://www.boutell.com/cgic), однако вы можете воспользоваться любой удобной вам библиотекой. В качестве результата анализируется переменная res_code, и в зависимости от ее значения сообщается результат вызова процедуры. Неизвестные входные параметры, участвующие в запросе, передаются следующим образом: на местах неизвестных параметров ставится знак вопроса (?), а в соответствующей этому параметру переменной XSQLDA определяется тип и данные для передаваемой переменной, причем допускается смешивать родственные типы (int-smallint, char-varchar и т д ) При внимательном изучении примера видно, что запрос, выбирающий данные из таблицы, вызывается несколько отличным методом, нежели запрос вызывающий ХП. Первый запрос, состоит из следующих этапов:

* Резервирование ресурсов, требуемых для запроса библиотекой доступа к InterBase, - вызов функции isc_dsql_allocate_statement().

* Подготовка запроса к исполнению сервером - именно на этом этапе выдаются сообщения об ошибках в синтаксисе - вызов функции isc_dsql_prepare().

* Исполнение запроса сервером - вызов функции isc_dsql_execute2() (или isc_dsql_execute(), отличие этих функций в том, что вызов isc_dsql_execute() непременно требует вызова функции isc_dsql_fetch(), если существуют исходящие параметры). На этом этапе могут быть выданы сообщения о несоответствии типов и/или количества элементов XSQLDA.

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

* Освобождение ресурсов, занятых на этапе 1, - вызов isc_dsql_free_statement(). Инициализация структур XSQLDA обычно происходит до п. 2.

Второй запрос исполняется сразу, минуя все вышеперечисленные пункты, кроме п. 3. В данном случае используется функция isc_dsql_exec_immed2(). Отличие данных методов заключается в том, что в случае первого запроса можно воспользоваться пп. 3 и 4 внутри цикла - это дает преимущество в скорости, при исполнении одного и того же запроса, но с разными значениями параметров, так как синтаксис уже проверен и запрос сразу исполняется. Метод, реализованный в исполнении второго запроса, обычно применяется при однократном вызове хранимых процедур или при исполнении команд, которые разрешается исполнять только в этих функциях (например, CREATE DATABASE разрешается использовать только в вызове функции isc_dsql_execute_immediate()).

Так как в случае "немедленного" исполнения запроса не происходит его предварительного анализа, то необходимо инициализировать переменную sqld структуры XSQLDA, которая используется для получения результата (в данном случае это делает строка osqlda -> sqld =1). Если не проинициализировать эту переменную, то в качестве результата вызова функции isc_dsql_exec_immed2 будет получена ошибка Message length error... По той же причине, необходимо явно указывать размер памяти, который будет занимать переменная, хранящая результат, - в данном случае это sizeof(long). Если этого не сделать, то последствия будут непредсказуемы, - в лучшем случае вы не получите ничего на выхо