Системы визуального программирования C++ Builder и Delphi содержат ряд компонент, специально ориентированных на работу с базами данных. Использование этих компонент позволяет быстро создавать приложения, работающие с базами данных. Кроме того, большая часть этих компонент построена таким образом, что обеспечивает максимальную переносимость программ, позволяя им работать (с минимальными изменениями, а в некоторых случаях вообще без изменений) с различными базами данных. Сразу, правда, следует оговориться, что данные системы ориентированы на работу под управлением Windows / Windows NT.

ОРГАНИЗАЦИЯ ДОСТУПА К ОБЪЕКТАМ БАЗЫ ДАННЫХ Основными компонентами для доступа к объектам произвольных баз данных в C++ Builder и Delphi являются TDatabase, TSession, TTable и TQuery. Для работы с InterBase можно также использовать специализированные компоненты TIBTable, TIBQuery, TIBDatabase, TIBTransaction. Перечисленные компоненты составляют только часть возможных средств для работы с базами, но их достаточно для рассмотрения всех основных возможностей работы с базой данных.

Компоненты TDatabase, TTable и TQuery ориентированы на работу с произвольными базами данными, так что доступ к базам данных в них осуществляется не прямо, а через средства Borland Database Engine (BDE). Это является, с одной стороны, достоинством, обеспечивая переносимость программ, с другой - недостатком, поскольку часть возможностей InterBase, к счастью незначительная, оказывается недоступной.

Компоненты TIBTable, TIBQuery, TIBDatabase, TIBTransaction прямо ориентированы на работу с InterBase, обеспечивая реализацию всех его возможностей, но при этом более острой становится проблема переносимости программ для работы с другими СУБД.

Использование средств BDE при работе с InterBase

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

Основные свойства TDatabase

AliasName Алиас базы данных, устанавливаемый средствами BDE Administrator. Алиас обеспечивает настройку BDE на работу с базой данных. Указание алиаса предназначено для настройки приложения па работу с конкретной базой.

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

Connected Указывает, что связь с базой данных, алиас которой задан свойством AliasName, установлена, если его значение есть true, или не установлена, если его значение - false. Установка свойства Connected в true эквивалентна выдаче SQL-команды CONNECT, установка свойства Connected в false эквивалентна выдаче SQL-команды DISCONNECT.

InTransaction Принимает значение true, если транзакция, связанная с базой данных активна, иначе - false.

При работе с BDE приложение может стартовать только одну транзакцию на каждую присоединенную базу данных. Однако для одной и той же базы можно установить несколько объектов TDatabase, имеющих один и тот же алиас (AliasName), но различные имена баз (DatabaseName). В этом случае они будут трактоваться, как разные базы и в каждой из них будет стартована своя транзакция. Такой, может быть, несколько искусственный прием позволяет иметь в приложении несколько параллельно работающих транзакций в одной базе.

Translsolation Задает уровень изоляции для транзакций базы данных, управляемых BDE. При работе с BDE допустимы следующие три уровня: tiDirtyRead, tiReadCommitted и tiRepeatableRead. Поскольку InterBase уровень tiDirtyRead не поддерживает, то tiDirtyRead автоматически заменяется на tiReadCommitted. Реально возможны два уровня -tiReadCommitted и tiRepeatableRead, которым соответствуют уровни изоляции ReadCommitted и Snapshot в InterBase.

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

Наиболее важным для наших целей является объект TQuery.

Основные свойства TQuery

SQL Содержит текст команды на SQL, подлежащей выполнению.

Params Содержит список параметров запроса. При выполнении запроса значения параметров подставляются в выражение SQL, после чего запрос компилируется. Это позволяет динамически формировать запросы, ^митируя параметры, хотя сам SQL их и не поддерживает.

Prepared Признак готовности запроса к выполнению: Ргера-

red=true - запрос подготовлен, Prepared=false - нет. Установка Prepared=true (или вызов метода Ргераге()) вызывает компиляцию запроса; последнее существенно при многократно используемых запросах с параметрами.

DatabaseName Содержит имя базы данных, с которой работает запрос. Если объект TDatabase не создавался явно, то он будет создан по умолчанию со свойствами AliasName и DatabaseName, принимающими значение указанное в свойстве DatabaseName объекта TQuery. Если объекты TDatabase созданы явно (или по умолчанию, но ранее), то объект TQuery связывается с TDatabase по значениям полей DatabaseName. Свойство AliasName объекта TDatabase при этом может принимать другое значение.

Active Указывает, открыт (Active=true) или нет (Active=false) запрос. Применяется только к запросам, содержащим SQL-команду SELECT. Установка Active в true или false соответственно открывает или закрывает запрос (можно также использовать методы Ореп() и Close()).

Использование объектов TQuery, TDatabase при работе с InterBase

Сначала создаются сами объекты TQuery, TDatabase. Объект TDatabase может явно и не создаваться, в этом случае он будет все равно создан со свойствами по умолчанию.

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

Для выполнения запроса на выборку (команда SELECT) необходимо либо установить свойство объекта Active в true, либо вызвать метод Ореп().

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

Для выполнения других команд SQL (текст их должен быть помещен в свойство SQL-объекта) необходимо вызвать метод ExecSQL().

Рассмотрим теперь, какие команды SQL доступны в приложении, использующем перечисленные объекты, а какие - нет. Недоступные для помещения в TQuery SQL-команды выделены курсивом.

ALTER DATABASE ALTER DOMAIN ALTER EXCEPTION ALTER INDEX ALTER PROCEDURE ALTER TABLE ALTER TRIGGER BASED ON BEGIN DECLARE SECTION CLOSE

CLOSE (BLOB)

COMMIT CONNECT CREATE DATABASE CREATE DOMAIN CREATE EXCEPTION CREATE GENERATOR CREATE INDEX CREATE PROCEDURE CREATE ROLE CREATE SHADOW CREATE TABLE CREATE TRIGGER CREATE VIEW DECLARE CURSOR DECLARE CURSOR (BLOB)

DECLARE external function

DECLARE FILTER

DECLARE STATEMENT DECLARE TABLE DELETE DESCRIBE DISCONNECT DROP DATABASE

DROP DOMAIN DROP EXCEPTION DROP EXTERNAL FUNCTION

DROPFILTER DROP INDEX DROP PROCEDURE DROPROLE DROP SHADOW DROPTABLE DROP TRIGGER DROP VIEW END DECLARE SECTION EVENT INIT EVENT WAIT EXECUTE

EXECUTE IMMEDIATE EXECUTE PROCEDURE FETCH FETCH (BLOB)

GRANT

INSERT

INSERT CURSOR (BLOB) OPEN

OPEN (BLOB)

PREPARE

REVOKE

ROLLBACK

SELECT

SET DATABASE

SET GENERATOR

SETNAMES

SET STATISTICS

SET TRANSACTION

UPDATE

WHENEVER

Из приведенного перечня видно, что "потери" относятся почти исключительно к командам работы с курсорами, которые реализуются с точки зрения приложения несколько иначе и, на мой взгляд, удобнее. Единственные команды, которые остаются действительно недоступными _ это команды обработки событий. Кроме того, управление транзакциями несколько ограничено, а именно, нельзя стартовать поименованную транзакцию. И еще, с базой данных в приложении может быть связана только одна транзакция. Последнее ограничение, впрочем, легко обходится объявлением нескольких объектов TDatabase с разными значениями OatabaseName и одинаковыми AliasName. Каждый из объектов имеет свою транзакцию, хотя физически они работают с одной базой.

При работе с данными отдельной таблицы можно использовать объект ТТаЫе. В этом случае просмотр данных таблицы и их изменение с точки зрения приложения реализуются максимально просто. Приложение видит табличный файл, просматривает его и вносит изменения в отдельные поля, оставляя в стороне механизм такой работы. С точки зрения реализации объект ТТаЫе не представляет собой чего-либо нового. Фактически при работе с ним выдается множество SQL-запросов к базе, как на чтение, так и на запись. Объект только предоставляет пользователю определенный сервис. Это важно при практическом программировании, но несущественно для понимания механизмов доступа к базе из приложений на C++Builder и Delphi.

Использование средств InterBase Expressприра-боте с In terBase

InterBase Express (IBX) представляет собой набор компонентов, которые обеспечивают средства доступа к данным в базах данных InterBase. Этот набор включает

TIBDatabase

TIBQuery

TIBSQT

TIBDatabaselnfo

TIBTransaction

TIBDataSet

TIBUpdateSQL

TIBEvents

TIBTable

TIBStoredProc

TIBSQLMonitor

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

Хотя компоненты IBX в основном аналогичны BDE-компонентам, ТеМ не менее, имеется ряд отличий. Приведем эти отличия для основных компонентов IBX.

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

DatabaseName Имя базы данных. Для локального подключения -диск, путь и имя файла базы данных.

Connected Указывает, установлена ли связь с базой данных, заданной свойством DatabaseName (значение true или false). Установка свойства Connected в true эквивалентна выдаче SQL-команды CONNECT, установка свойства Connected в false эквивалентна выдаче SQL-команды DISCONNECT.

Имя пользователя и пароль можно сохранить в свойстве Params компонента TIBDatabase:

Пример 10.22

User_name=sysdba

Password=masterkey

В отличие от Borland Database Engine, IBX управляет транзакциями с помощью отдельного компонента - TIBTransaction. Это позволяет отделить транзакции от подключений к базам данных, так что можно реализовать преимущества InterBase при двухфазном завершении транзакций при работе с несколькими базами.

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

DefaultDatabase Задает базу данных, для которой запускается транзакция. В случае работы с несколькими базами список баз данных формируется с использованием метода AddDatabase().

Active Указывает, активна (Active=true) или нет (Active=false) транзакция. Задание Active=true стартует транзакцию.

Params Содержит список параметров транзакции. В качестве параметров задается уровень изоляции, обработка конфликтов.

Например, для уровня изоляции SNAPSHOT можно задать

concurrency

nowait.

Для уровня изоляции READ COMMITTED можно задать

read_committed

rec_version

nowait.

Компонент TIBQuery позволяет выполнить любую InterBase-оманДУ DSQL. Рассмотрим основные свойства объекта.

SQL Содержит текст команды на SQL, подлежащей выполнению (аналогично свойству SQL-объекта TQuery).

' Params Содержит список параметров запроса. При выполнении запроса значения параметров подставляется в выражение SQL, после чего запрос компилируется. Это позволяет динамически формировать запросы, имитируя параметры, хотя сам SQL их и не поддерживает (аналогично свойству Params объекта TQuery).

Prepared Признак готовности запроса к выполнению:

prepared=true - запрос подготовлен, Prepared=false - нет. Установка Prepared=true (или вызов метода Ргераге()) вызывает компиляцию запроса; последнее существенно при многократно используемых запросах с параметрами (аналогично свойству Prepared объекта TQuery).

Database Идентифицирует базу данных, указывая на компонент TIBDatabase с которой работает запрос. При работе с BDE для ссылки на базу данных используется в большей мере свойство DatabaseName (свойство Database доступно только для чтения), а в IBX такого свойства просто нет.

Active Указывает, открыт (Active=true) или нет (Active-false) запрос. Применяется только к запросам, содержащим SQL-команду SELECT. Установка Active в true или false соответственно открывает или закрывает запрос (можно также использовать методы Ореп() и Close()). Свойство Active аналогично свойству Active объекта TQuery.

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

При использовании TIBQuery для выборки данных результаты выборки доступны, в отличие от TQuery, только для чтения. Для коррекции данных, полученных с помощью TIBQuery, следует использовать объекты типа TIBUpdateSQL.

Компонент TIBDataSet, как и TIBQuery, позволяет выполнить любую InterBase-команду DSQL. Помимо этого данный объект позволяет корректировать считанные по SELECT данные. Рассмотрим основные свойства объекта.

Вместо свойства SQL, описывающего действия с базой в объектах TIBQuery и Tquery, объекты TIBDataSet содержат пять свойств: SelectSQL, RefreshSQL, DeleteSQL, InsertSQL, ModifySQL. Они предназначены для хранения соответствующих SQL-команд. Таким образом, Данный объект содержит, практически, полный набор средств для обработки данных.

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

Пример 10.23

SelectSQL SELECT BOOKNM, unikey from ТВООК

RefreshSQL

SELECT BOOKNM, uni key from TBOOK WHERE unikey = : unikey ModifySQL

UPDATE TBOOK SET BOOKNM = :BOOKNM WHERE unikey = :01d_unikey

DeleteSQL

DELETE FROM TBOOK WHERE unikey = :01d_unikey InsertSQL

INSERT INTO TBOOK (unikey, BOOKNM) VALUES (:unikey,

•.BOOKNM)

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

Database Связывает регистрируемые события с конкретной базой данных. Свойство Database указывает на объект TIBDatabase.

Events Задает список контролируемых событий. Список может содержать до 15 событий.

Registered Указывает, что были зарегистрированы события (Registered = true) или нет (Registered=false), перечисленные в свойстве Events.

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

По существу, объект TIBEvents реализует группу команд SQL EVENT INIT, EVENT WAIT.

Компонент TIBTable в основном аналогичен BDE-компоненту TTable. Компоненты TIBStoredProc, TIBStoredProc, TIBUpdateSQL также аналогичны соответствующим компонентам TStoredProc, TstoredProc и TUpdateSQL. С точки зрения анализа работы с InterBase они носят вспомогательный характер.

ОСНОВНЫЕ ЭТАПЫ РАЗРАБОТКИ ПРИЛОЖЕНИЙ Рассмотрим типичный фрагмент приложения, работающего с базой данных. Фрагмент включает экранную форму с размещенным на ней табачным документом, в котором осуществляется просмотр и редактирование информации, получаемой из базы данных.

Данные для документа выбираются из базы по запросу. Для формулировки и обработки запроса используется объект TQuery. SQL для сохранения изменений (обновления, модификации, удаления) записывается в объекте TUpdateSQL.

Для визуализации данных используется объект TDBGrid. Удобство навигации обеспечивается с помощью объекта TDBNavigator.

Связи между объектами TQuery, TDBGrid, TDBNavigator реализуются с помощью объекта TDataSource.

Внешний вид формы во время проектирования представлен на рис.

10.1. Объекты TQuery, TDataSource, TUpdateSQL являются невизуальными и потому во время выполнения их нет на форме.

Форма с размещенными на ней компонентами

Рис. 10.1. Форма с размещенными на ней компонентами:

- верхний ряд - DataSource 1, Query 1, UpdateSQL 1;

- средняя полоса - DBNavigatorl;

- нижнее окно - DBGrid 1

По умолчанию размещенным объектам присваиваются имена, включающие имя объекта и его порядковый номер на форме. При желании их можно задать и явно. В нашем случае в этом нет необходимости, поэтому объекты получат имена DataSourcel, Query], UpdateSQLl, DBGridl DBNavigatorl.

Размещение компонентов на форме осуществляется выбором соответствующих объектов из палитры инструментов и помещением их на форму. Размеры и расположение визуальных компонентов осуществляются их перетаскиванием и растяжением «мышью».

Теперь можно перейти к настройке приложения на работу с базой.

Прежде всего, установим связи между объектами. Для указания связи визуальных компонент с источником данных необходимо задать их свойство с DataSource. В нашем случае DataSourcel. Удобнее всего задавать свойства, использую инспектор объектов Object Inspector (рис. 10.2).

Инспектор объектов для DBGridl с выбранным полем DataSource

Рис. 10.2. Инспектор объектов для DBGridl с выбранным полем DataSource.

Установим свойство DataSource объектов DBGridl и DBNavigatorl в DataSourcel.

Теперь свяжем DataSourcel с реальными данными, выбираемыми из базы- В нашем случае - Query 1, Связь осуществляется заданием свойства pataSet объекта DataSourcel в Query 1.

Поскольку данные предполагается корректировать, то необходимо связать Queryl с объектом, содержащим SQL для корректировки -UpdateSQLl. В TUpdateSQL предусмотрена запись трех команд SQL. Команды могут быть, вообще говоря, любыми. Их запуск осуществляется по инициативе прикладной программы. Стандартно предполагается их вызов ддя команд удаления - Delete (DeleteSQL), вставки - Insert (InsertSQL), обновления - Update (ModifySQL).

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

• Только для чтения.

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

• Для корректировки с записью результатов с помощью явно ука занных команд SQL. В этом случае никаких ограничений на TQuery нет, но за это приходится платить необходимостью явного описания команд обновления. В Query данные при этом могут копиться, как в локальной таблице, вне прямой связи с данными в базе. Это позволяет проводить запись изменений пакетом, например, при полном завершении корректировки. На необходимость того, что данные должны копиться, указывает свойство CashedUpdate: CashedUpdate=true - должны копиться,

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

Рассмотрим подробнее последний вариант, как наиболее мощный и гибкий.

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

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

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

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

Схема связей объектов

Рис. 10.3. Схема связей объектов.

BeforeDelete

Beforeinsert

BeforePost

AfterDelete

Afterinsert

AfterPost

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

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

Рассмотрим подробнее обработку этих событий и ее особенности от момента возникновения события «до» или «после».

Пример 10.24

v0id_fastcall TForml: .-QuerylBeforelnsert (TDataSet

♦DataSet)

{

if (. . .) // Проверка условия допустимости вставки

{Abort(); // Вставка недопустима

return;

}

. . . // Вставка допустима, выполняем

// подготовку к вставке данных }

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

События Beforeinsert, Afterinsert являются результатом попытки добавить строку в набор данных (в нашем случае - Queryl). Какие действия пользователя или приложения могут вызвать подобную ситуацию?

• Нажатие в навигаторе (DBNavigatorl) кнопкиЩ.

• Нажатие клавиши Ш , когда курсор находится в последней строке набора данных при его просмотре (в DBGridl).

• Вызов приложением метода Append объекта класса TDataSet (Queryl-» Append();).

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

Следующее событие также связано с добавлением новой строки. Пример 10.25

void_fastcall TForml::QuerylAfterInsert(TDataSet

‘DataSet)

{

// Заполнение полей в новой строке // FieldByName(". . .") адресует имя поля,

// заданное параметром, конструкции AsString, . . .

// описывают тип возвращаемого или присваиваемого // значения

Queryl->FieldByName ( * . . . * ]i ->AsString=" . .

Queryl->FieldByName (" . „ . * ]i ->AsInteger=567 ;

Queryl->FieldByName(". . .")->AsFloat=567.54;

}

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

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

По факту фиксации в наборе внесенных изменений возникают события BeforePost (непосредственно перед фиксацией) и AfterPost (сразу же после нее).

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

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

Пример 10.26

void_fastcall TForml::QuerylBeforePost(TDataSet *DataSet)

{

// Заполнение полей в измененной строке // FieldByName (". . .") адресует имя поля,

// заданное параметром, конструкции AsString, . . .

// описывают тип возвращаемого или присваиваемого // значения

Queryl->FieldByName(". . .")->AsString=". . .";

Queryl->FieldByName(". . .*)->AsInteger=567;

Queryl->FieldByName(". . .")->AsFloat=567.54;

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

}

Пример 10.27

void_fastcall TForml::QuerylAfterPost(TDataSet *DataSet)

{

// Обработка результатов внесенных изменений

// Обработка результатов коррекции, например // подготовка запросов для загрузки информации // в базу данных }' * ’

Рассмотрим подробнее обработку удаления.

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

• Нажатием в навигаторе (DBNavigatorl) кнопки [-].

• Вызовом приложением метода Delete объекта класса TDataSet (Queryl-» Delete();).

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

Пример 10.28

Схема обработки удаления:

void_fastcall TForml::QuerylBeforeDelete(TDataSet

*DataSet) .

{

if(. . .) // Проверка условия допустимости удаления (Abort (); // Удаление недопустимо return;

}

. . . // Удаление допустимо, выполняем

// подготовку к удалению данных }

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

Пример 10.29

Обработчик события AfterDelete:

void_fastcall TForml::QuerylAfterDelete(TDataSet

*DataSet)

{ ,

// Расчеты на оставшейся части набора данных.

>

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

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

• Внесение изменений сразу же после ввода данных в каждой строке.

• Внесение изменений после ввода данных в целый документ. Проиллюстрируем эти две схемы примерами.

ВНЕСЕНИЕ ИЗМЕНЕНИЙ СРАЗУ ПОСЛЕ ВВОДА ДАННЫХ В КАЖДОЙ СТРОКЕ Обработка вставки Обработку вставки данных в базу имеет смысл проводить только после их добавления в рабочий корректируемый набор (событие Afterlnsert), поскольку до выполнения самой вставки их просто нет.

Пример 10.30

v0id_fastcall TForml::QuerylAfterlnsert(TDataSet

*DataSet)

{

// Заполнение полей в новой строке // FieldByName(". . .") адресует имя поля,

// заданное параметром, конструкции AsString, . . .

// описывают тип возвращаемого или присваиваемого // значения

Queryl->FieldByName<". . . ")->AsString=". .

Queryl->FieldByName(". . .*)->AsInteger=567;

Queryl->FieldByName ( " . . . * ) ->AsFloat=567 . 54 ;

UddateSQLl->Apply(uklnsert);

)

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

- тогда необходимо выдать команду Insert, или модификации - тогда необходимо выдать команду Update. Для определения того, является ли строка новой, можно использовать хранимые данные, например, если первичный ключ является автоинкрементным, то у «новой» в соответствующем поле будет 0, а у «старой» - ненулевое значение. Кроме того, состояние набора «вставка строки» можно запомнить в рабочей переменной, можно также воспользоваться свойством State редактируемого объекта (в нашем случае Queryl). В последнем случае надо, правда, соблю дать некоторую осторожность, чтобы правильно отслеживать траекторию изменений. Пример единой точки обработки занесения данных в базу рассмотрим ниже.

Пример 10.31

void_fastcall TForml::QuerylBeforelnsert(TDataSet

*DataSet) ■

{

if(. . .) // Проверка условия допустимости вставки

{Abort(); // Вставка недопустима

return;

}

. . . // Вставка допустима, выполняем

// подготовку к вставке данных )

//------------------

void_fastcall TForml::QuerylAfterlnsert(TDataSet

*DataSet)

{

// Заполнение полей в новой строке // FieldByName (". . .") адресует имя поля,

// заданное параметром, конструкции AsString, . . .

// описывают тип возвращаемого или присваиваемого // значения

Queryl->FieldByName(". . .*')->AsString=". . Queryl->FieldByName(*. , , *')->AsInteger=567 ; Queryl->FieldByName(“. . , *')->AsFloat = 5 67.54;

Queryl->Tag=l; // Выполнена вставка, записи в базу //не было

void_fastcall TForml::QuerylBeforePost(TDataSet *DataSet)

{

// Заполнение полей в измененной строке // FieldByName (”. . .") адресует имя поля,

// заданное параметром, конструкции AsString, . . .

// описывают тип возвращаемого или присваиваемого // значения

Queryl->FieldByName(". . . " )->AsString=" . .

Queryl->FieldByName(". . .")->AsInteger=567;

Queryl->FieldByName(". . .")->AsFloat=567.54;

j) Обработка результатов вставки, например // подготовка запросов для загрузки информации // в базу данных

}

//------------------

void_fastcall TForml: QuerylAfterPost (TDataSet *DataSet)

{

// Обработка результатов внесенных изменений

// Обработка результатов вставки, например // подготовка запросов для загрузки информации'

//в базу данных

UpdateSQL->Apply((Queryl->Tag)?uklnsert:ukModify);

Queryl->Tag=0;

}

Обработка замены Собственно замена данных в базе сосредоточивается в обработчиках событий BeforePost, AfterPost. Основная схема показана в приведенном выше примере. Единственное, что стоит добавить, так это то, что в некоторых случаях стоит предварительно проверять, произошли в обрабатываемом наборе реальные изменения или нет.

Обработка удаления Удаление данных в базе наиболее естественно проводить, как уже отмечалось выше, в обработчике события BeforeDelete.

Пример 10.32

void_fastcall TForml: .-QuerylBeforeDelete (TDataSet

*DataSet)

{

if(. . . ) // Проверка условия допустимости удаления

{Abort (); // Удаление недопустимо return;

}

• . . // Удаление допустимо, выполняем

// подготовку к удалению данных UpdateSQL->Apply(Queryl->Tag)?ukDelete);

}

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

Общую схему обработки успешности записи можно проиллюстрировать следующим примером.

Ппимеп 10.33

try {

TDatabase *DB=Queryl->Database;

// лучше запомнить указатель на объект Database,

// описывающий базу данных однократно при ее / / открытии

DB->StartTransaction();

UpdateSQLl->Apply(ukModify);

DB->Commit () ;

}catch (const Exception &E) {

int i ;

AnsiString ErrMes=E.Message,

DiagMes="CTpoKa документа. . .";

// Выделение части сообщения, порождаемого Exception //на русском языке (коды кириллицы < 0 ) ; если такой // части нет, то сохраняется полный текст

for(i=l;(i<=ErrMes.Length())&&(ErrMes[i]>'\0'); i++) ;

if(i<=ErrMes.Length())

ErrMes=ErrMes.substring(i,ErrMes.Length());

ShowMessage(ErrMes);

DB->Rollback();

Application->MessageBox(DiagMes.c_str(),

"Предупреждение",16);

}

ВНЕСЕНИЕ ИЗМЕНЕНИЙ ПОСЛЕ ВВОДА ДАННЫХ В ЦЕЛЫЙ ДОКУМЕНТ Если документ необходимо либо записать в базу целиком, либо не записывать вообще, то приведенная выше схема является неудачной. В этом случае следует копить изменения, а затем записывать их одним пакетом.

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

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

Пример 10.33

static struct MFields {AnsiString a,b,c;

MFields *d;

} ;

static struct {AnsiString a,b,c;} WField;

static struct MList {MFields *p;

Add(AnsiString x, AnsiString y, AnsiString z)

{MFields *q=new MFields; q->a=x; q->b=y; q->c=z; q->d=p;

p=q;

};

int Get(void)

{if(P)

{MFields *q=p;

WField.a=p->a;

WField.b=p->b;

WField.c=p->c; p=p->d; delete q; return 1;

}else return 0;

};

MList(){p=0;};

~MList()

{MFields *q; while(p)

{q=p; p=p->d; delete q;

};

};

};

MList wl;

При начале работы, например при запуске формы (событие OnShow), выдаем

wl.p=0;

В обработчик события BeforeDelete вписываем команду вида:

wl.Add(Queryl->FieldByName("Fieldl")->AsString, Queryl->FieldByName("Field2")->AsString, Queryl->FieldByName("Field3•)->AsString);

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

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

Пример 10.34

void_fastcall TForml::QuerylBeforeDelete(TDataSet

*DataSet)

{

if(. . .) // Проверка условия допустимости удаления { // Удаление допустимо

Queryl->FieldByName("PDELETE")->AsInteger=l;

}Abort();

}

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

При работе с объектом UpdateSQL следует помнить, что при стандартном использовании параметры запросов на обновления выбираются из объекта Query, который ссылается на данный UpdateSQL. Этих данных иногда бывает недостаточно. Тогда для записи удобнее использовать отдельный объект Query, параметры SQL которого можно задавать программно. Кроме того, можно непосредственно в программе формировать текст SQL-запроса как символьную строку, помещать ее в свойство SQL-> Text объекта Query и выдавать команду на исполнение.

0>имер 10.35 ^siString s, si, s2;

g="UPDATE ”+sl+s2;

Queryl->SQL->Text=s;

Queryl->ExecSQL() ;

ОДНОВРЕМЕННАЯ РАБОТА С НЕСКОЛЬКИМИ НАБОРАМИ ДАННЫХ Все, что рассматривалось выше, относилось к случаю, когда работа ведется с одной выборкой данных. Этого во многих случаях недостаточно. Упомянем о некоторых средствах для связывания нескольких наборов.

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

Пусть набор В детализирует данные набора А. Рассмотрим пример.

Для редактирования на экран выводятся две табличные формы DBGridl и DBGrid2 с соответствующими навигаторами DBNavigatorl и DBNavigator2. DBGridl связан с Queryl (точнее, связи DBGridl DataSourcel -> Queryl и DBNavigatorl -> DataSourcel -•> Queryl). DBGrid2 связан с Query2 (точнее, связи DBGrid2 -» DataSourcel Query2 и DBNavigator2 -> DataSource2 -> Query2).

Queryl соответствует набору A. Query2 соответствует набору В. Наборы данных связаны по значениям полей: поле F1 набора А соответствует полю G1 набора В, поле F2 набора А соответствует полю G2 набора В.

Внешний вид формы представлен на рис. 10.4.

Для указания связи наборов в свойстве SQL объекта Query2 в конструкции WHERE команды SELECT записывается:

WHERE G1=:F1 AND G2=:F2

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

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

Форма с размещенными на ней компонентами для пары связанных наборов

Рис. 10.4. Форма с размещенными на ней компонентами для пары связанных наборов.

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

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

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

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

При непосредственном использовании объектов, ориентированных на InterBase (TIBDatabase, TIBTransaction, TIBTable, TIBQuery, TIBDataSet, TIBStoredProc, TIBSQL, TIBUpdateSQL), логика прикладной программы практически не меняется. Различия состоят только в несколько больших возможностях по обработке транзакций и некоторых нюансах в составе свойств объектов и, поскольку они прямо ориентированы на InterBase, естественно, несколько большей скорости выполнения. В силу аналогичности этих объектов отдельно рассматривать работу с ними представляется нецелесообразным.

10.1. разработка приложений на базовом языке | Введение в InterBase | Глава 11


Введение в InterBase



Новости за месяц

  • Октябрь
    2019
  • Пн
  • Вт
  • Ср
  • Чт
  • Пт
  • Сб
  • Вс