Text
                    

Российская Ассоциация Издателей компьютерной литературы В.В.Фаронов, П.В.Шумаков Delphi 5 Руководство разработчика баз данных Москва Издательство «Нолидж» 2000
ББК 32.973.26-018.1 Ф 24 УДК 004.438 Delphi Фаронов В.В., Шумаков П.В. Delphi 5. Руководство разработчика баз данных - М.: «Нолидж», 2000. - 640., ил. В книге описывается применение системы программирования Delphi (версия 5) для разработки программ, предназначенных для создания и обслуживания баз данных. В большей своей части книга рассчитана на профессиональных программистов, занимающихся созданием корпоративных приложений, знакомых со средой Delphi и языком Object Pascal. Тем не менее, в начальных главах приводятся основные понятия, связанные с базами данных, что позволяет использовать книгу и программистам, не имевшим ранее опыта разработки такого рода приложений. ISBN 5-89251-071-9 Borland® - зарегистрированная торговая марка корпорации Inprise Corporation. Delphi™ - торговая марка корпорации Inprise Corporation. Excel™ - торговая марка корпорации Microsoft Corporation. Inprise® - зарегистрированная торговая марка корпорации Inprise Corporation Microsoft® - зарегистрированная торговая марка корпорации Microsoft Corporation. MS-DOS™ - торговая марка корпорации Microsoft Corporation. Oracle™ - торговая марка корпорации Oracle Corporation. Windows™ - торговая марка корпорации Microsoft Corporation. Word™ - торговая марка корпорации Microsoft Corporation. ActiveX™ - торговая марка корпорации Microsoft Corporation © Фаронов В.В., Шумаков П.В., 2000. © Нолидж, 2000. Scan & Edit: Gunpowder, 2006
Оглавление ОТ АВТОРОВ..................................................19 ЧАСТЬ 1. ОБЩИЕ ПРИНЦИПЫ РАЗРАБОТКИ ПРИЛОЖЕНИЙ БАЗ ДАННЫХ В СРЕДЕ DELPHI 5.....................................21 ГЛАВА 1. ВВЕДЕНИЕ В БАЗЫ ДАННЫХ.............................23 1.1. ТЕРМИНОЛОГИЯ.........................................23 1.2. ПЕРВИЧНЫЕ КЛЮЧИ И ИНДЕКСЫ............................24 1.3. РЕЛЯЦИОННЫЕ ОТНОШЕНИЯ МЕЖДУ ТАБЛИЦАМИ................25 1.3.1. Отношение один-ко-многим..........................25 1.3.2. Отношение один-к-одному...........................26 1.3.3. Отношение многие-ко-многим........................26 1.3.4. Связи между записями одной таблицы................27 1.4. ССЫЛОЧНАЯ ЦЕЛОСТНОСТЬ................................28 1.5. ИНДЕКСЫ..............................................30 1.6. НОРМАЛИЗАЦИЯ ТАБЛИЦ ПРИ ПРОЕКТИРОВАНИИ БАЗЫ ДАННЫХ............................................31 1.6.1. Первая нормальная форма.........................31 1.6.2. Вторая нормальная форма.........................33 1.6.3. Третья нормальная форма.........................35 1.6.4. Нормализация - за и против........................37 1.7. ПОНЯТИЕ ТРАНЗАКЦИИ...................................39 1.8. ТЕРМИНОЛОГИЯ РАСПРЕДЕЛЕННЫХ СУБД.....................40 1.8.1. Технология СОМ....................................41 1.8.2. Технология CORBA..................................42 1.8.3. СОМ или CORBA?....................................44 ГЛАВА 2. НЕФОРМАЛЬНОЕ ЗНАКОМСТВО С ПРОГРАММИРОВАНИЕМ БД..............................45 2.1. ПОСТАНОВКА ЗАДАЧИ....................................45 2.2. СОЗДАНИЕ ПСЕВДОНИМА БАЗЫ ДАННЫХ......................46 2.3. СОЗДАНИЕ ТАБЛИЦ БАЗЫ ДАННЫХ..........................47 2.3.1. Объявление полей..................................47 2.3.2. Изменение языкового драйвера......................50 2.3.3. Запоминание таблицы...............................51 2.3.3. Определение индексов..............................51 2.3.5. Определение ссылочной целостности между таблицами.53 2.4. СОЗДАНИЕ ПРОСТЕЙШЕГО ПРИЛОЖЕНИЯ......................54
4 Оглавление 2.5. СОЗДАНИЕ ПРИЛОЖЕНИЯ ДЛЯ РАБОТЫ С ДВУМЯ ТАБЛИЦАМИ.... 55 2.6. НАСТРОЙКА ПОЛЕЙ И СМЕНА АКТИВНОГО ИНДЕКСА.........58 2.7. ОПРЕДЕЛЕНИЕ ВИЗУАЛЬНЫХ КОМПОНЕНТОВ ДЛЯ РАБОТЫ С ПОЛЯМИ....................................61 2.8. ФОРМИРОВАНИЕ НАБОРА ДАННЫХ ИЗ НЕСКОЛЬКИХ ТАБЛИЦ....64 ГЛАВА 3. СРЕДСТВА DELPHI.................................66 3.1. ОБЩИЙ ОБЗОР СРЕДСТВ...............................66 3.2. ОСОБЕННОСТИ ПРОГРАММ ДЛЯ РАБОТЫ С БД..............67 3.3. ПОДДЕРЖИВАЕМЫЕ В DELPHI ТИПЫ БД...................69 3.3.1. Локальные и файл-серверные БД..................69 3.3.2. Клиент-серверные БД............................70 3.3.3. Архитектура с сервером приложений..............71 ГЛАВА 4. ОБЗОР КОМПОНЕНТОВ ДЛЯ РАБОТЫ С БАЗАМИ ДАННЫХ..........................................74 4.1. НЕКОТОРЫЕ ВАЖНЫЕ КЛАССЫ...........................74 4.2. НЕВИЗУАЛЬНЫЕ КОМПОНЕНТЫ...........................74 4.2.1. Страница Data Access...........................74 4.2.2. Страница MIDAS.................................75 4.2.3. Страница ADO...................................76 4.2.4. Страница InterBase.............................76 4.2.5. Страница Decision Cube.........................77 4.3. ВИЗУАЛЬНЫЕ КОМПОНЕНТЫ.............................77 4.3.1. Страница Data Controls..........................77 4.3.2. Страница QReport................................78' 4.3.3. Страница Decision Cube..........................79 4.4. МОДУЛИ ДАННЫХ......................................79 4.5. ИЕРАРХИЯ КЛАССОВ ДЛЯ ПРЕДСТАВЛЕНИЯ ДАННЫХ И ДОСТУПА К НИМ.........................................83 ГЛАВА 5. РАБОТА С ПОЛЯМИ..................................86 5.1. ОБЗОР СВОЙСТВ, МЕТОДОВ И СОБЫТИЙ...................86 5.1.1. Свойства........................................86 5.1.2. Методы..........................................90 5.1.3. События.........................................91 5.2. ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ-ПОЛЕЙ......................91 5.3. ТИПЫ ПОЛЕЙ........................................93 5.4. ОБРАЩЕНИЕ К ЗНАЧЕНИЮ ПОЛЯ.........................94 5.5. ПРОВЕРКА ПРАВИЛЬНОСТИ ВВЕДЕННОГО В ПОЛЕ ЗНАЧЕНИЯ...95 5.6. ФОРМИРОВАНИЕ ТЕКСТОВОГО ПРЕДСТАВЛЕНИЯ ПОЛЯ.........96 5.7. СОЗДАНИЕ ВЫЧИСЛЯЕМЫХ ПОЛЕЙ.........................97 5.8. СОЗДАНИЕ ПОЛЕЙ ВЫБОРА ДАННЫХ (LOOKUP-ПОЛЕЙ)........98 5.8.1. Поля выбора данных..............................98 5.8.2. Использование полей выбора......................99
Оглавление 5 5.9. ОБЗОР ПОЛЕЙ TXXXFIELD..................................103 5.9.1. Строковые поля.....................................103 5.9.2. Целочисленные поля.................................103 5.9.3. Вещественные поля..................................104 5.9.4. Логические поля....................................105 5.9.5. Поля даты и времени................................105 5.9.6. Поля для хранения значений произвольных форматов...106 ГЛАВА 6. РАБОТА С НАБОРАМИ ДАННЫХ.............................108 6.1. ОБЗОР СВОЙСТВ, МЕТОДОВ И СОБЫТИЙ.......................108 6.1.1. Свойства...........................................108 6.1.2. Методы.............................................111 6.1.3. События..'..........................................113 6.2. ОСНОВНЫЕ ПРИЕМЫ РАБОТЫ С НД.............................115 6.2.1. Открытие и закрытие НД..............................115 6.2.2. Программный доступ к записям........................115 6,3. НАВИГАЦИЯ ПО НАБОРУ ДАННЫХ..............................116 6.3.1. Общие положения....................................116 6.3.2. Порядок следования записей.........................117 6.3.3. Последовательная навигация по записям..............117 6.3.4. Использование закладок.............................119 6.4. ПОИСК ЗАПИСЕЙ В НАБОРАХ ДАННЫХ..........................119 6.4.1. Метод Locate.......................................119 6.4.2. Метод Lookup.......................................121 6.5. ФИЛЬТРАЦИЯ ЗАПИСЕЙ.....................................123 6.5.1. Свойство Filter.......................•............123 6.5.2. Событие OnFilterRecord..............................125 6.5.3. Навигация в неотфильтрованном НД между записями, удовлетворяющими фильтру..................................125 6.6. ПОЛУЧЕНИЕ ИНФОРМАЦИИ О ПОЛЯХ ,..........................126 6.6.1. Свойство FieldDefs............................... 126 6.6.2. Свойства FieldCount и Fields.......................129 6.6.3. Свойство DefaultFields и метод ClearFields....... 131 6.6.4. Способы обращения к полям набора данных............131 6.7. БЛОКИРОВКА ТАБЛИЦ В МНОГОПОЛЬЗОВАТЕЛЬСКОМ РЕЖИМЕ......................................................131 6.8. СИНХРОНИЗАЦИЯ СОДЕРЖИМОГО НАБОРОВ ДАННЫХ...............132 6.8.1. Синхронизация содержимого наборов данных в одном приложении........................................133 6.8.2. Синхронизация содержимого наборов данных в разных приложениях......................................133 6.9. ОБРАБОТКА ОШИБОК СМЕНЫ СОСТОЯНИЯ НАБОРА ДАННЫХ......................................................135 6.10. ОГРАНИЧЕНИЯ НА ЗНАЧЕНИЯ ПОЛЕЙ.........................135
6 Оглавление ГЛАВА 7. РАБОТА С КОМПОНЕНТОМ TTABLE.........................137 7.1. ОБЗОР СВОЙСТВ И МЕТОДОВ................................137 7.1.1. Свойства..........................................137 7.1.2. Методы............................................138 7.2. РАБОТА С ИНДЕКСАМИ.....................................140 7.2.1. Получение информации об индексах..................140 7.2.2. Установка текущего индекса........................143 7.2.3. Добавление нового индекса.........................143 7.2.4. Удаление существующего индекса....................144 7.2.5. О составных индексах..............................144 7.3. ИСКЛЮЧИТЕЛЬНЫЙ ДОСТУП К НД............................145 7.3.1. Установка приоритетного доступа в многопользовательском режиме.... 145 7.3.2. Удаление записей и таблиц.........................146 7.3.3. Создание новой таблицы............................146 7.4. ПОИСК ЗАПИСЕЙ В НД....................................147 7.4.1. Обзор методов.....................................147 7.4.2. Установка значений для поиска.....................147 7.4.3. Точный поиск................-.....................148 7.4.4. Неточный поиск....................................149 7.4.5. Инкрементальный локатор...........................152 7.4.6. Поиск по части текущего индекса...................153 7.5. ФИЛЬТРАЦИЯ ЗАПИСЕЙ....................................155 7.5.1. Обзор методов.....................................155 7.5.2. Использование SetRange............................155 7.5.3. Методы SetRangeStart, SetRangeEnd, ApplyRange.....157 7.5.4. Метод CancelRange.................................158 7.5.5. Методы EditRangeStart, EditRangeEnd...............161 7.5.6. Свойство KeyExclusive.............................162 7.5.7. Фильтрация по составному индексу..................163 7.5.8. Фильтрация по частичному соответствию.............165 7.5.9. Фильтрация по части составного индекса............166 7.5.10. Ограничения возможностей фильтрации..............166 7.6. СОВМЕЩЕНИЕ КУРСОРОВ ДВУХ НД...........................166 ГЛАВА 8. РАБОТА С КОМПОНЕНТОМ TQUERY.........................168 8.1. ОБЩИЕ СВЕДЕНИЯ........................................168 8.2. ИСПОЛЬЗОВАНИЕ SQL BUILDER.............................169 8.2.1. Окно SQLB.........................................169 8.2.2. Работа с SQLB.....................................170 8.3. ОБЗОР СВОЙСТВ И МЕТОДОВ...............................174 8.3.1. Свойства..........................................174 8.3.2. Методы............................................175 8.4. ВЫПОЛНЕНИЕ СТАТИЧЕСКИХ ЗАПРОСОВ.......................175 8.5. ОТКРЫТИЕ И ЗАКРЫТИЕ TQUERY............................176 8.6. ИЗМЕНЯЕМЫЕ НАБОРЫ ДАННЫХ..............................176
Оглавление 7 8.7. ВЫПОЛНЕНИЕ ПАРАМЕТРИЧЕСКИХ ЗАПРОСОВ................181 8.7.1. Понятие параметрического запроса...............181 8.7.2. Формирование параметрического запроса..........182 8.7.3. Программная установка значений параметров......182 8.7.4. Методы Prepare и Unprepare.....................184 8.7.5. Указание значения NULL для параметров...........184 8.7.6. Передача параметров через свойство DataSource..185 8.8. ДИНАМИЧЕСКИЕ ЗАПРОСЫ..............................186 8.9. ПОЛУЧЕНИЕ АГРЕГИРОВАННЫХ ЗНАЧЕНИЙ.................189 8.10. СОРТИРОВКА В ОБРАТНОМ ПОРЯДКЕ....................190 ГЛАВА 9. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDBGRID.................191 9.1. ОБЗОР СВОЙСТВ, МЕТОДОВ И СОБЫТИЙ..................191 9.1.1. Свойства.......................................191 9.1.2. Методы.........................................193 9.1.3. События........................................193 9.2. ПОНЯТИЕ СТОЛБЦОВ TDBGRID..........................193 9.3. ДИНАМИЧЕСКИЕ СВОЙСТВА СТОЛБЦОВ....................195 9.4. СОЗДАНИЕ И ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ-СТОЛБЦОВ........196 9.4.1. Работа с редактором столбцов...................196 9.4.2. Изменение свойств столбцов во время выполнения программы........................................... 197 9.4.3. Пустые столбцы.................................200 9.4.4. Формирование списка возможных значений столбца.201 9.4.5. Изменение порядка следования столбцов..........203 9.5. УПРАВЛЕНИЕ ОТОБРАЖЕНИЕМ ДАННЫХ.........'..........204 9.6. ПЕРЕМЕЩЕНИЕ МЕЖДУ ЯЧЕЙКАМИ........................208 9.7. РАБОТА С КОМПОНЕНТОМ TDBCTRLGRID..................208 ГЛАВА 10. ВИЗУАЛЬНЫЕ КОМПОНЕНТЫ ДЛЯ РАБОТЫ С ТЕКУЩЕЙ ЗАПИСЬЮ НАБОРА ДАННЫХ..........................212 10.1. КОМПОНЕНТ TDBTEXT................................212 10.2. КОМПОНЕНТ TDBEDIT................,...............212 10.3. КОМПОНЕНТ TDBCHECKBOX............................213 10.4. КОМПОНЕНТ TDBRADIOGROUP..........................214 10.5. СПИСОЧНЫЕ КОМПОНЕНТЫ.............................215 10.5.1. Компонент TDBListBox..........................215 10.5.2. Компонент TDBComboBox.........................216 10.5.3. Компонент TDBLookupComboBox...................217 10.5.4. Компонент TDBLookupListBox....................218 10.6. КОМПОНЕНТ TDBMEMO................................218 10.7. КОМПОНЕНТ TDBRICHEDIT............................222 10.7.1. Назначение компонента.........................222 10.7.2. Свойства, методы и события....................222 10.7.3. Работа с абзацем..............................224
8 Оглавление ГЛАВА 11. КОМПОНЕНТ TSESSION...........................227 11.1. НАЗНАЧЕНИЕ КОМПОНЕНТА..........................227 11.2. РАБОТА С УСТАНОВКАМИ BDE.......................227 11.3. ЧТЕНИЕ ИМЕН ТАБЛИЦ БАЗЫ ДАННЫХ.................228 11.4. РАБОТА С БАЗАМИ ДАННЫХ...........................229 11.4.1. Список объявленных БД........................229 11.4.2. Хранение соединения с неактивной БД..........229 11.4.3. Работа с активными БД......................230 11.5. УПРАВЛЕНИЕ ПАРОЛЬНОЙ ЗАЩИТОЙ................. 230 11.5.1. Работа с паролями для таблиц Paradox.........230 11.5.2. Средства TSession для работы с паролями......231 11.6. ОПРЕДЕЛЕНИЕ СЛУЖЕБНЫХ КАТАЛОГОВ СЕССИИ.........232 11.7. СОБЫТИЕ АКТИВИЗАЦИИ СЕССИИ.....................232 ГЛАВА 12. КОМПОНЕНТ TDBCHART......................... 233 12.1. СОЗДАНИЕ ГРАФИКА...............................233 12.2. ДОБАВЛЕНИЕ СЕРИИ В ГРАФИК И УСТАНОВКА ЕЕ СВОЙСТВ.237 12.2.1. Добавление серии...........................237 12.2.2. Выбор источника данных................... 238 12.2.3. Определение функций........................241 12.3. ДОБАВЛЕНИЕ СЕРИИ ВО ВРЕМЯ ПРОГОНА ПРОГРАММЫ............................................242 12.4. СВОЙСТВА, МЕТОДЫ И СОБЫТИЯ КЛАССА TCHARTSERIES.........................................243 12.4.1 .Свойства...................................243 12.4.2. Методы.....................................245 12.4.3. События....................................246 ГЛАВА 13. ВВЕДЕНИЕ В ПОСТРОЕНИЕ ОТЧЕТОВ................247 13.1. КОМПОНЕНТЫ ДЛЯ ПОСТРОЕНИЯ ОТЧЕТОВ..............247 13.2. КОМПОНЕНТ TQUICKREP............................248 13.2.1. Свойства.....................................249 13.2.2. Методы.....................................252 13.2.3. События....................................253 13.3. КОМПОНЕНТ TQRBAND..............................254 13.4. СОЗД ХНИЕ ПРОСТЕЙШЕГО ОТЧЕТА.....................- 256 13.5. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TQREXPR...............258 13.6. ИСПОЛЬЗОВАНИЕ TQRBAND ДЛЯ ПРЕДСТАВЛЕНИЯ ЗАГОЛОВКОВ СТОЛБЦОВ..................................261 13.7. ИСПОЛЬЗОВАНИЕ TQRBAND ДЛЯ ПОКАЗА ЗАГОЛОВКА И ПОДВАЛА СТРАНИЦЫ...................................262 13.8. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TQRSYSDATA............262 13.9. ГРУППИРОВКИ ДАННЫХ.............................263 13.10. МНОЖЕСТВЕННАЯ ГРУППИРОВКА ДАННЫХ,.............266
Оглавление 9 13.11. ПОСТРОЕНИЕ ОТЧЕТА ГЛАВНЫЙ-ДЕТАЛЬНЫЙ..............267 13.12. ПОСТРОЕНИЕ КОМПОЗИТНОГО ОТЧЕТА...................270 13.13. ДРУГИЕ ВОЗМОЖНОСТИ..........................................271 13.13.1. Использование дочерних полос.................271 13.13.2. Компонент TQRStringsBand.....................272 ГЛАВА 14. ВВЕДЕНИЕ В РАЗРАБОТКУ СИСТЕМ ПРИНЯТИЯ РЕШЕНИЙ_______________________________________________ 274 14.1. ПОНЯТИЕ МНОГОМЕРНЫХ ДАННЫХ...................................274 14.2. МЕТАКУБ (МНОГОМЕРНЫЙ КУБ).........................274 14.3. ДИНАМИЧЕСКАЯ ФИКСАЦИЯ КОЛИЧЕСТВА ИЗМЕРЕНИЙ........276 14.4. ОБЗОР КОМПОНЕНТОВ DELPHI ДЛЯ РАЗРАБОТКИ СИСТЕМ ПРИНЯТИЯ РЕШЕНИЙ.....................................278 14.5. ПРИМЕР МНОГОМЕРНЫХ ДАННЫХ.....................г..............278 14.6. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONQUERY...........279 14.7. РЕДАКТОР МНОГОМЕРНОГО ЗАПРОСА................................279 14.8. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONCUBE............280 14.9. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONSOURCE..........283 14:10. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONGRID......................283 14.10.1. Создание сетки многомерных данных............283 14.10.2. Раскрытие и закрытие данных по измерениям средствами компонента TDecisionGrid...........................284 14.10.3. Показ промежуточных сумм................... 285 14.10.4. Исключение и включение измерения в состав показываемых в сетке данных................................... 286 14.10.5. Установка свойств показа данных по отдельному измерению.286 14.10.6. Свойства и события, управляющие поведением TDecisionGrid в целом............................................287 14.11. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONPIVOT..........290 14.11.1. Создание переключателя активных измерений....290 14.11.2. Изменение способа показа данных по измерению.291 14.11.3. Фиксация значения в измерении.....'..........292 14.11.4. Свойства, управляющие отображением компонента TDecisionPivot.... 292 ЧАСТЬ 2. СОЗДАНИЕ КЛИЕНТ-СЕРВЕРНЫХ ПРИЛОЖЕНИЙ..................................................295 ГЛАВА 15. ВВЕДЕНИЕ В ТЕХНОЛОГИЮ КЛИЕНТ-СЕРВЕР________________________297 15.1. АРХИТЕКТУРЫ ФАЙЛ-СЕРВЕР И КЛИЕНТ-СЕРВЕР...........297 15.2. SQL-СЕРВЕР INTERBASE И ЕГО ОСНОВНЫЕ КОМПОНЕНТЫ....299 15.3. INTERBASE: НЕКОТОРЫЕ ТЕХНИЧЕСКИЕ ХАРАКТЕРИСТИКИ...300 15.4. ФИЗИЧЕСКАЯ ОРГАНИЗАЦИЯ БАЗЫ ДАННЫХ INTERBASE......301 15.5. DELPHI И INTERBASE................................303 15.6. ВОПРОСЫ СОЕДИНЕНИЯ С УДАЛЕННЫМ СЕРВЕРОМ...........304
10 Оглавление ГЛАВА 16. ПРИМЕР СОЗДАНИЯ УДАЛЕННОЙ БД И ПРОГРАММЫ ДЛЯ РАБОТЫ С НЕЙ.......................305 16.1. ОПИСАНИЕ БД «УЧЕТ ТОВАРОВ НА СКЛАДЕ».......305 16.2. СОЗДАНИЕ БД И ЕЕ ПСЕВДОНИМА................306 16.3. СОЗДАНИЕ ТАБЛИЦ БД.........................308 16.4. СОЗДАНИЕ ПРОГРАММЫ ДЛЯ ВВОДА ДАННЫХ..........................310 16.4.1. Соединение с БД из программы.............................310 16.4.2. Создание формы дЛя ввода данных..........................311 16.4.3. Создание триггеров для поддержания каскадных воздействий.312 16.6.4. Использование генератора и хранимой процедуры для присвоения уникального значения столбцу первичного индекса........................................315 ГЛАВА 17. СОЗДАНИЕ БАЗЫ ДАННЫХ....................317 17.1. ОПЕРАТОР CREATE DATABASE...................317 17.2. МНОГОФАЙЛОВАЯ БД...........................318 17.3. ОПРЕДЕЛЕНИЕ ПАРОЛЯ.........................318 17.4. УКАЗАНИЕ РАЗМЕРА СТРАНИЦЫ БД...............318 17.5. УКАЗАНИЕ НАЦИОНАЛЬНОЙ КОДИРОВКИ СИМВОЛОВ...319 ГЛАВА 18. ТИПЫ СТОЛБЦОВ INTERBASE.................320 18.1. ОБЗОР ТИПОВ ДАННЫХ INTERBASE...............320 18.2. ЦЕЛОЧИСЛЕННЫЕ ЗНАЧЕНИЯ.....................321 18.3. ВЕЩЕСТВЕННЫЕ ЗНАЧЕНИЯ......................321 18.4. ФИКСИРОВАННО-ДЕСЯТИЧНЫЕ ЗНАЧЕНИЯ...........321 18.5. ЗНАЧЕНИЯ ТИПА ДАТЫ.........................322 18.6. СИМВОЛЬНЫЕ ТИПЫ ДАННЫХ.....................323 18.7. ЗНАЧЕНИЯ ТИПА BLOB.........................324 18.8. СТОЛБЦЫ-МАССИВЫ............................325 18.9. СОВМЕСТИМОСТЬ ТИПОВ........................325 ГЛАВА 19. СОЗДАНИЕ ДОМЕНОВ........................326 19.1. ПОНЯТИЕ ДОМЕНА............................326 19.2. ОГРАНИЧЕНИЯ НА ЗНАЧЕНИЯ СТОЛБЦОВ, АССОЦИИРОВАННЫХ С ДОМЕНОМ.......................327 19.3. ИЗМЕНЕНИЕ ОПРЕДЕЛЕНИЯ ДОМЕНА..............328 ГЛАВА 20. СОЗДАНИЕ ТАБЛИЦ.........................330 20.1. ОБЩИЙ ФОРМАТ ОПЕРАТОРА CREATE TABLE........330 20.2. СТОЛБЦЫ ВЫЧИСЛЯЕМЫХ ЗНАЧЕНИЙ...............330 20.3. ОГРАНИЧЕНИЯ ЦЕЛОСТНОСТИ....................331 20.4. ПЕРВИЧНЫЙ КЛЮЧ.............................331 20.5. УНИКАЛЬНЫЙ КЛЮЧ............................332
Оглавление 11 20.6. ВНЕШНИЙ КЛЮЧ И ОПРЕДЕЛЕНИЕ ССЫЛОЧНОЙ ЦЕЛОСТНОСТИ.........................................332 20.7. ИМЕНОВАНИЕ ССЫЛОЧНОЙ ЦЕЛОСТНОСТИ.........................335 20.8. ТРЕБОВАНИЯ К ЗНАЧЕНИЯМ СТОЛБЦОВ..........................336 20.9. ИЗМЕНЕНИЕ ОБЪЯВЛЕНИЯ ТАБЛИЦЫ.............................340 20.10. ИЗМЕНЕНИЕ АТРИБУТОВ СТОЛБЦА.............................341 20.11. УДАЛЕНИЕ ТАБЛИЦЫ........................................342 ГЛАВА 21. РАБОТА С ИНДЕКСАМИ.....................................343 21.1. ЛОГИЧЕСКОЕ РАЗДЕЛЕНИЕ НА КЛЮЧИ И ИНДЕКСЫ.................343 21.2. НЕОБХОДИМОСТЬ СОЗДАНИЯ ИНДЕКСОВ..........................343 21.3. СОЗДАНИЕ ИНДЕКСА.........................................344 21.4. УЛУЧШЕНИЕ ПРОИЗВОДИТЕЛЬНОСТИ ИНДЕКСА.....................345 21.4.1. Перестройка индекса...................................345 21.4.2. Повторное вычисление показателя «полезности» индекса..346 21.5. УДАЛЕНИЕ СУЩЕСТВУЮЩЕГО ИНДЕКСА...........................346 ГЛАВА 22. КОМПОНЕНТ TDATABASE....................................347 ГЛАВА 23. ОПЕРАТОР SELECT........................................351 23.1. ПРОСТЕЙШИЙ ВИД ОПЕРАТОРА SELECT (SELECT...FROM)..........351 23.2. ИСПОЛЬЗОВАНИЕ ПРЕДЛОЖЕНИЯ WHERE..........................352 23.2.1. Сравнение значения столбца с константой...............352 23.2.2. Внутреннее соединение таблиц..X.......................353 23.3. ИСПОЛЬЗОВАНИЕ ПСЕВДОНИМОВ ТАБЛИЦ.........................355 23.4. ПРЕДЛОЖЕНИЕ ORDER BY - ОПРЕДЕЛЕНИЕ СОРТИРОВКИ...356 23.5. УСТРАНЕНИЕ ПОВТОРЯЮЩИХСЯ ЗНАЧЕНИЙ........................357 23.6. РАСЧЕТ ЗНАЧЕНИЙ ВЫЧИСЛЯЕМЫХ СТОЛБЦОВ.....................358 23.7. АГРЕГАТНЫЕ ФУНКЦИИ.......................................359 23.8. ГРУППИРОВКА ЗАПИСЕЙ......................................359 23.9. ПРЕДЛОЖЕНИЕ HAVING - НАЛОЖЕНИЕ ОГРАНИЧЕНИЙ НА ГРУППИРОВКУ ЗАПИСЕЙ..............................361 23.10. ЗАДАНИЕ СЛОЖНЫХ УСЛОВИЙ ПОИСКА..........................362 23.10.1. Использование логических выражений...................363 23.10.2. Сравнение столбца с результатом вычисления выражения.363 23.10.3. Использование BETWEEN................................364 23.10.4. Использование IN.....................................364 23.10.5. Использование STARTING...............................365 23.10.6. Использование CONTAINING.............................365 23.10.7. Использование функции UPPER..........................366 23.10.8. Использование LIKE...................................366 23.10.9. Использование функции CAST...........................367 23.11. ИСПОЛЬЗОВАНИЕ ПОДЗАПРОСОВ...............................369 23.12. ВЛОЖЕНИЕ ПОДЗАПРОСОВ....................................370
12 Оглавление 23.13. ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ ИСПОЛЬЗОВАНИЯ ПОДЗАПРОСОВ, ВОЗВРАЩАЮЩИХ ЕДИНИЧНОЕ ЗНАЧЕНИЕ..........371 23.13.1. Использование EXISTS.........................371 23.13.2. Использование SINGULAR.......................371 23.14. ИСПОЛЬЗОВАНИЕ ПОДЗАПРОСОВ, ВОЗВРАЩАЮЩИХ МНОЖЕСТВО ЗНАЧЕНИЙ..................................372 23.14.1. Использование ALL, SOME......................372 23.14.2. Использование HAVING и агрегатных функций для вложенных подзапросов.........................................373 23.15. ВНЕШНИЕ СОЕДИНЕНИЯ............................ 374 23.16. UNION - ОБЪЕДИНЕНИЕ РЕЗУЛЬТАТОВ ВЫПОЛНЕНИЯ НЕСКОЛЬКИХ ОПЕРАТОРОВ SELECT........................378 23.17. ИСПОЛЬЗОВАНИЕ IS NULL..........................379 23.18. ИСПОЛЬЗОВАНИЕ ОПЕРАЦИИ СЦЕПЛЕНИЯ СТРОК.........379 23.19. РАБОТА С РАЗНЫМИ БД В ОДНОМ ЗАПРОСЕ.............380 ГЛАВА 24. ДОБАВЛЕНИЕ, ИЗМЕНЕНИЕ, УДАЛЕНИЕ ЗАПИСЕЙ.......381 24.1. ОПЕРАТОР INSERT..................................381 24.1.1. Явное указание списка значений...............381 24.1.2. Указание значений при помощи оператора SELECT.382 24.2. ОПЕРАТОР UPDATE..................................383 24.3. ОПЕРАТОР DELETE..................................384 ГЛАВА 25. РАБОТА С ПРОСМОТРАМИ VIEW......................385 25.1. ПОНЯТИЕ ПРОСМОТРА КАК ВИРТУАЛЬНОЙ ТАБЛИЦЫ........385 25.2. СПОСОБЫ ФОРМИРОВАНИЯ ПРОСМОТРОВ..................385 25.3. УКАЗАНИЕ СТОЛБЦОВ ПРОСМОТРА В ОПЕРАТОРЕ CREATE VIEW..........................................386 25.4. ОБНОВЛЯЕМЫЕ И НЕОБНОВЛЯЕМЫЕ ПРОСМОТРЫ............386 25.5. ИСПОЛЬЗОВАНИЕ CHECK OPTION.......................387 25.6. КОМПОНЕНТЫ DELPHI И ИСПОЛЬЗОВАНИЕ ПРОСМОТРОВ....387 ГЛАВА 26. РАБОТА С ХРАНИМЫМИ ПРОЦЕДУРАМИ.................388 26.1. ПОНЯТИЕ ХРАНИМОЙ ПРОЦЕДУРЫ.......................388 26.2. СОЗДАНИЕ ХРАНИМОЙ ПРОЦЕДУРЫ......................388 26.3. АЛГОРИТМИЧЕСКИЙ ЯЗЫК ХРАНИМЫХ ПРОЦЕДУР...........389 26.3.1. Объявление локальных переменных...............389 26.3.2. Операторные скобки BEGIN ... END..............389 26.3.3. Оператор присваивания.........................390 26.3.4. Оператор IF ... THEN ... ELSE.................390 26.3.5. Оператор SELECT...............................390 26.3.6. Оператор FOR SELECT ... DO....................391 26.3.7. Оператор SUSPEND..............................391 26.3.8. Оператор WHILE... DO..........................392 26.3.9. Оператор EXIT.................................393
Оглавление 13 26.3.10. Оператор EXECUTE PROCEDURE............................393 26.3.11. Оператор POSTJEVENT...................................394 26.4. ВЫЗОВ ПРОЦЕДУР ВЫБОРА В ПРИЛОЖЕНИИ КЛИЕНТА.................394 26.5. ОБРАЩЕНИЕ К ПРОЦЕДУРАМ ДЕЙСТВИЯ. КОМПОНЕНТ TSTOREDPROC..........................................396 26.6. ИЗМЕНЕНИЕ И УДАЛЕНИЕ ХРАНИМЫХ ПРОЦЕДУР..............400 ГЛАВА 27. РАБОТА С ТРИГГЕРАМИ..............................401 27.1. СОЗДАНИЕ ТРИГГЕРОВ..................................401 27.2. ОПРЕДЕЛЕНИЕ ЗАГОЛОВКА ТРИГГЕРА......................402 27.3. ЗНАЧЕНИЯ OLD И NEW..................................402 27.4. ОБЕСПЕЧЕНИЕ КАСКАДНЫХ ВОЗДЕЙСТВИЙ...................403 27.5. ВЕДЕНИЕ ЖУРНАЛА ИЗМЕНЕНИЙ...........................404 27.6. ИСПОЛЬЗОВАНИЕ ТРИГГЕРОВ ДЛЯ РЕАЛИЗАЦИИ БИЗНЕС-ПРАВИЛ.. 405 27.7. ИЗМЕНЕНИЕ И УДАЛЕНИЕ ТРИГГЕРА..................... 409 ГЛАВА 28. ИСПОЛЬЗОВАНИЕ ГЕНЕРАТОРОВ........................410 ГЛАВА 29. ТРАНЗАКЦИИ.......................................412 29.1. ОТКАТ ИЗМЕНЕНИЙ И ЦЕЛОСТНОСТЬ БД...........................412 29.2. ПОНЯТИЕ ТРАНЗАКЦИИ..................................412 29.3. УРОВНИ ИЗОЛЯЦИИ ТРАНЗАКЦИЙ: ПРИЛОЖЕНИЕ КЛИЕНТА.............414 29.3.1. Уровни изоляции транзакций......................414 29.3.2. Установка уровней изоляции транзакций в Delphi..415 29.4. СВОЙСТВО UPDATEMODE И ОБНОВЛЕНИЕ ЗАПИСЕЙ............415 29.5. ЯВНО И НЕЯВНО СТАРТУЕМЫЕ ТРАНЗАКЦИИ.................417 29.6. УПРАВЛЕНИЕ ТРАНЗАКЦИЯМИ НА SQL-СЕРВЕРЕ INTERBASE...........418 ГЛАВА 30. КЭШИРОВАННЫЕ ИЗМЕНЕНИЯ...........................420 30.1. ИСПОЛЬЗОВАНИЕ КЭШИРОВАННЫХ ИЗМЕНЕНИЙ................420 30.2. ПОДТВЕРЖДЕНИЕ КЭШИРОВАННЫХ ИЗМЕНЕНИЙ................420 30.2.1. Использование метода ApplyUpdates компонента TDatabase.421 30.2.2. Использование метода ApplyUpdates набора данных.421 30.3. ВИДИМОСТЬ ИЗМЕНЕННЫХ ЗАПИСЕЙ........................422 30.4. ОБРАБОТЧИК СОБЫТИЯ ONUPDATEERROR....................422 30.4.1. Использование обработчика OnUpdateError.........422 30.4.2. Использование параметра UpdateKind..............423 30.4.3. Использование параметра UpdateAction............423 30.4.4. Использование параметра Е.......................426 30.4.5. Свойства OldValue и NewValue....................428 30.5. КОМПОНЕНТ TUPDATESQL................................428 30.5.1. Назначение компонента...........................428 30.5.2. Работа с компонентом на этапе конструирования...428 30.5.3. Выполнение SQL-операторов.......................430
14 Оглавление ГЛАВА 31. РАБОТА С СОБЫТИЯМИ......................................431 31.1. ПОНЯТИЕ СОБЫТИЯ...........................................431 31.2. КОМПОНЕНТ TIBEVENTALERTER.................................431 31.3. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TIBEVENTALERTER..................432 ГЛАВА 32. ФУНКЦИИ, ОПРЕДЕЛЯЕМЫЕ ПОЛЬЗОВАТЕЛЕМ..........................434 32.1. ПОНЯТИЕ ФУНКЦИИ, ОПРЕДЕЛЯЕМОЙ ПОЛЬЗОВАТЕЛЕМ....................434 32.2. РАЗРАБОТКА DLL И UDF В DELPHI.............................434 32.2.1. Общие положения.......................................434 32.2.2. Совместимость типов параметров........................435 32.2.3. Особенности использования в UDF параметров типа PChar.435 32.2.4. Особенности использования в UDF параметров типа даты и времени..............................................436 32.3. ОБЪЯВЛЕНИЕ UDF............................................436 32.4. ПРИМЕР СОЗДАНИЯ DLL С НЕСКОЛЬКИМИ UDF И ОБЪЯВЛЕНИЯ ИХ В БД.....................................437 ГЛАВА 33. ОПРЕДЕЛЕНИЕ БИЗНЕС-ПРАВИЛ...............................439 33.1. РАЗМЕЩЕНИЕ БИЗНЕС-ПРАВИЛ..................................439 33.2. РЕАЛИЗАЦИЯ БИЗНЕС-ПРАВИЛ НА СЕРВЕРЕ.......................439 33.2.1. Ограничения значения столбца записи...................439 33.2.2. Запрет добавления записей в просмотре.................443 33.2.3. Использование триггеров для поддержания ссылочной целостности............................................443 33.3. РЕАЛИЗАЦИЯ БИЗНЕС-ПРАВИЛ В ПРИЛОЖЕНИИ КЛИЕНТА..................................................445 33.3.1. Реализация бизнес-правил в компонентах типа «набор данных».446 33.3.2. Свойство Constrained (компонент TQuery)...............448 33.3.3. Свойство Constraints..................................448 33.3.4. Реализация бизнес-правил в компоненте TField..........449 33.3.5. Другие способы реализации бизнес-правил...............450 33.4. ИСПОЛЬЗОВАНИЕ СЛОВАРЯ ДАННЫХ ДЛЯ ОПРЕДЕЛЕНИЯ АТРИБУТОВ ПОЛЕЙ..........................................451 ГЛАВА 34. ОПТИМИЗАЦИЯ РАБОТЫ С БД.................................455 34.1. ОПТИМИЗАЦИЯ СТРУКТУРЫ БД..................................455 34.1.1. Нормализация таблиц: теория и практика.....................455 34.1.2. Частичная зависимость структуры данных от методов доступа к ним..........................................456 34.1.3. Физические характеристики БД..'.......................457 34.2. ОПТИМИЗАЦИЯ ЗАПРОСОВ......................................458 34.2.1. Оптимальная структура индексов........................458 34.2.2. «Полезность» индексов.................................458 34.2.3. Просмотр плана выполнения запросов....................459 34.2.4. Целесообразность создания индексов....................460
Оглавление 15 34.2.5. Частичное использование составного индекса........461 34.2.6. Многопоточность поиска по OR и IN.................462 34.2.7. Уменьшение общего количества индексов.............462 34.3. Оптимизация клиентских приложений...................463 34.3.1. Минимизация соединений с БД.......................463 34.3.2. Использование TQuery..............................463 34.3.3. Перенос тяжести вычислительной работы на сервер....464 ГЛАВА 35. ПОДДЕРЖКА ORACLE8................................ 465 35.1. ПОЧЕМУ ORACLE8........................................465 35.2. НЕКОТОРЫЕ НОВЫЕ ВОЗМОЖНОСТИ ORACLE8...................466 35.3. СРЕДСТВА DELPHI ДЛЯ ПОДДЕРЖКИ ORACLE8.................467 35.3.1. Класс TObjectField и его наследники...............467 35.3.2. Изменения в классе TDBDataSet.....................469 35.3.3. Компонент TNestedTable............................470 ЧАСТЬ 3. ТРЕХЗВЕННАЯ АРХИТЕКТУРА И РАСПРЕДЕЛЕННЫЕ ПРИЛОЖЕНИЯ.....................................471 ГЛАВА 36. ВВЕДЕНИЕ В ТРЕХЗВЕННУЮ АРХИТЕКТУРУ.................473 36.1. ОБЗОР ТРЕХЗВЕННОЙ АРХИТЕКТУРЫ.........................473 36.1.1. Преимущества трехзвенной архитектуры..............473 36.1.2. Программная реализация............................474 36.1.3. Пример реализации трехзвенной архитектуры.........476 36.1.4. Библиотека типов..................................479 36.2. ИНТЕРФЕЙСЫ............................................480 36.2.1. Создание и использование интерфейсов..............480 36.2.2. Объекты Автоматизации и интерфейс IDispatch.......484 36.2.3. Стандартные интерфейсы............................486 ГЛАВА 37. КОМПОНЕНТЫ DELPHI ДЛЯ ТРЕХЗВЕННОЙ АРХИТЕКТУРЫ..................................................490 37.1. КОМПОНЕНТЫ ДЛЯ СОЗДАНИЯ СЕРВЕРА ПРИЛОЖЕНИЙ.............490 37.1.1. TRemoteDataModule.................................490 37.1.2. TMTSDateModule....................................491 37.1.3. TCorbaDataModule..................................493 37.1.4. TDataSetProvider..................................494 37.2. КОМПОНЕНТЫ ДЛЯ СОЗДАНИЯ КЛИЕНТСКОГО ПРИЛОЖЕНИЯ.................................................498 37.2.1. TClientDataSet....................................498 37.2.2. TDCOMConnection...................................505 37.2.3. TCorbaConnection..................................505 37.2.4. TSocketConneCtion.....г...........................507 37.2.5. TSimpleObjectBroker...............................508 37.2.6. TWebConnection....................................508
16 Оглавление ГЛАВА 38. ОБЩИЕ ВОПРОСЫ СОЗДАНИЯ ТРЕХЗВЕННЫХ ПРИЛОЖЕНИЙ.................................................509 38.1. СОЗДАНИЕ СЕРВЕРА ПРИЛОЖЕНИЙ...........................509 38.1.1. Последовательность создания сервера приложений....510 38.1.2. Управление данными................................511 38.1.3. Выполнение запроса на получение данных...........513 38.1.4. Сервер приложений как брокер ограничений..........516 38.1.5. Выполнение запроса на обновление данных..........517 38.1.6. Обработка ошибок обновления.......................518 38.1.7. Событие OnDataRequest.............................519 38.1.8. Расширение интерфейса сервера приложений..........520 38.1.9. Поставка сервера приложений......................521 38.2. СОЗДАНИЕ КЛИЕНТСКОГО ПРИЛОЖЕНИЯ.......................523 38.2.1. Особенности клиента в трехзвенной архитектуре.....523 38.2.2. Связь COM/DCOM....................................524 38.2.3. Связь CORBA.......................................524 38.2.4. Связь с помощью сокетов...........................525 38.2.5. Управление связью.................................525 38.2.6. Раннее и позднее связывание с интерфейсом сервера.525 38.2.7. Обновление и освежение данных.....................526 38.2.8. Отложенная обработка и регулирование количества записей в пакете данных.........................................528 38.2.9. Работа с файлами данных...........................529 38.3. ИСПОЛЬЗОВАНИЕ ВЛОЖЕННЫХ ТАБЛИЦ ВНД ГЛАВНЫЙ-ДЕТАЛЬНЫЙ.......................................530 38.4. СОЗДАНИЕ ПОВТОРНО ИСПОЛЬЗУЕМЫХ СЕРВЕРОВ ПРИЛОЖЕНИЙ................................................532 38.5. СОЗДАНИЕ КЛИЕНТСКОГО ПРИЛОЖЕНИЯ КАК КОМПОНЕНТА ACTIVEX....................................533 ЧАСТЬ 4. НОВЫЕ ВОЗМОЖНОСТИ DELPHI 5.............................537 ГЛАВА 39. РАБОТА С ADO.......................................539 39.1. ТЕСТОВАЯ ПРОГРАММА....................................540 39.2. УСТАНОВЛЕНИЕ СВЯЗИ С ADO..............................545 39.2.1. Структура строки связи............................545 39.2.2. Формирование строки связи.........................545 39.3. ОСОБЕННОСТИ ИСПОЛЬЗОВАНИЯ КОМПОНЕНТОВ ADO.............550 39.3.1. Базовые объекты ADO...............................551 39.3.2. Связной компонент TADOConnection..................553 39.3.3. Компонент TADOCommand.............................557 39.3.4. Общие свойства компонентов-наборов................560 39.3.5. Компонент TADODataSet.............................571 39.5.6. Компонент TADOTable...............................572 39.5.7. Компонент TADOQuery............................. 573
Оглавление 17 39.5.8. Компонент TADOStoredProc...............................574 39.5.9. Компонент TRDSConnection и создание сервера приложений.574 ГЛАВА 40. ТЕХНОЛОГИЯ INTERBASE EXPRESS............................582 40.1. ПРОСТОЙ ПРИМЕР.............................................582 40.2. КОМПОНЕНТЫ ДЛЯ РЕАЛИЗАЦИИ IBX..............................585 40.2.1. Класс TIBBase..........................................585 40.2.2. Компонент TIBDatabase..................................586 40.2.3. Компонент TIBTransaction...............................589 40.2.4. Компонент TIBTable.....................................592 40.2.5. Компонент TIBQuery.....................................593 40.2.6. Компонент TIBStoredProc................................593 40.2.7. Компонент TIBUpdateSQL.................................594 40.2.8. Компонент TIBDataSet...................................594 40.2:9. Компонент TIBSQL.......................................597 40.2.10. Компонент TIBDatabaselnfo.............................598 40.2.11. Компонент TIBSQLMonitor...............................600 40.2.11. Компонент TIBEvents...................................600 ПРИЛОЖЕНИЯ...........................................................601 Ш. РАБОТА С УТИЛИТОЙ BDE ADMINISTRATOR............................601 П1.1. СОЗДАНИЕ ПСЕВДОНИМА БД.....................................601 П1.1.1 Создание псевдонима STANDARD............................602 П1.1.2. Создание псевдонима INTRBASE...........................602 П1.2. УСТАНОВКИ ПАРАМЕТРОВ ДРАЙВЕРА...........-..................604 П1.2.1. Paradox................................................605 П1.2.2. InterBase..............................................605 Ш.З. СИСТЕМНЫЕ СТАРТОВЫЕ УСТАНОВКИ...............................606 П1.4. УСТАНОВКИ ФОРМАТОВ.........................................606 П1.4.1. Параметры формата даты.................................606 П 1.4.2. Параметры формата времени........................... 607 П 1.4.3. Параметры числового формата...........................607 П1.5. СОХРАНЕНИЕ КОНФИГУРАЦИИ....................................608 С ЧЕМ СВЯЗАНО П2. РАБОТА С УТИЛИТОЙ SQL EXPLORER..................609 П2.1. НАЗНАЧЕНИЕ SQL EXPLORER....................................609 П2.2. ГЛАВНОЕ МЕНЮ...............................................610 П2.2.1. Опция Object...........................................610 П2.2.2. Опция Dictionary.......................................610 П2.2.3. Ойция Edit.............................................610 П2.2.4. Опция View.............................................611 П2.2.5. Опция Options..........................................611 П2.3. РАБОТА С УТИЛИТОЙ..........................................612 П2.3.1. Управление псевдонимами................................612 П2.3.2. Просмотр/редактирование таблиц.........................612
18 Оглавление П2.3.3. Создание и выполнение запросов........612 П3.3.4. Просмотр метаданных...................613 ПЗ. РАБОТА С УТИЛИТОЙ WISQL......................616 П3.1. УСТАНОВКА НАБОРА СИМВОЛОВ ТЕКУЩЕЙ СЕССИИ.616 П3.2. СОЗДАНИЕ БД И СОЕДИНЕНИЕ С НЕЙ...........616 П3.4. ВЫПОЛНЕНИЕ SQL-ОПЕРАТОРОВ................617 П3.5. ПОДТВЕРЖДЕНИЕ И ОТКАТ ИЗМЕНЕНИЙ..........617 П3.6. ПРОСМОТР СТРУКТУРЫ КОМПОНЕНТОВ БД........618 П3.7. ВЫПОЛНЕНИЕ SCRIPT-ФАЙЛОВ.................619 П4. ОПИСАНИЕ БИБЛИОТЕКИ IB_UDF.DLL...............622 ЛИТЕРАТУРА.........................................627 АЛФАВИТНЫЙ УКАЗАТЕЛЬ...............................630
х От авторов В книге детально рассматривается применение системы программирования Delphi для разработки приложений баз данных, т.е. программ, предназначенных для создания и изменения электронных хранилищ информации - баз данных (БД). Предполагается, что читатель уже знаком со средой Delphi и использующимся в ней языком Object Pascal, причем не обязательно с новейшей на сегодня версии Delphi 5: в книге не рассматриваются особенности визуального программирования и языка Object Pascal, как и особенности операционных систем Windows 3.x или Windows 32 (здесь и далее этим ^термином обозначаются 32-разрядные операционные системы Windows 95/98/NT/2000) - за информацией по этим вопросам обратитесь к соответствующей литературе, например, к книгам [24], [14]. Настоящее издание во многом использует материал книги [25], который был дополнен и изменен с учетом изменений в версиях Delphi 4 и 5. Для тех, кто уже знаком с версией 4, ниже кратко перечисляются нововведения версии 5, которые в той или иной мере описаны в этой книге: • поддержка Microsoft Data Access (средств доступа к данным корпорации Microsoft), и в том числе технологий OLE DB и ADO', • поддержка технологии InterBase Express (1ВХ)\ как и средства ADO, основное назначение IBX — создание клиентских мест, способных обращаться к данным без BDE\ • расширение VCL за счет включения многих новых компонентов, в том числе для поддержки новых технологий ADO, IPX; • новая редакция модулей данных, имеющих средства графического отображения реляционных данных между отдельными таблицами; • усиленные средства построения многозвенных приложений. На сегодня Delphi является одним из самых распространенных средств создания приложений баз данных для корпоративных применений. Простота и естественность языка, ориентация системы на разработку именно такого рода приложений, наконец, эффективность (большая производительность и относительно небольшие размеры) создаваемых с ее помощью программ сделали Delphi незаменимым средством разработки различного рода клиентских мест, т.е. программ для доступа к БД. На программистов, создающих клиентские места, и рассчитана эта книга. В основе материала книги лежит фирменная документация по Delphi 5 и личный опыт авторов* . Она построена, скорее, по принципу справочника, но не учебника: ее главы и части относительно мало связаны друг с другом, и излагаемый в них материал не строится по принципу «от простого к сложному». Мы систематизировали материал по конкретным проблемам (не обязательно - крупным), с которыми разработчик может столкнуться в ходе реального программирования. В начале большинства глав кратко перечисляются свойства, методы и события того или иного компонента, а затем идет комментарий, уточняющий и иллюстрирующий особенности использования соответствующих средств Delphi. Поскольку компоненты Delphi наследуют свойства 1 С учетом того, что используется именно версия 5, некоторые компоненты и классы, доступные в более ранних версиях, в ней не описываются. Точно так же авторы изменили некоторые практические советы и примеры создания приложений.
20 От авторов своих предков, материал не дублируется. Например, целая глава посвящена классу TField. Изложенные в ней сведения уже не повторяются при описании многочисленных компонентов-потомков этого базового класса. Вторая часть книги посвящена клиент-серверным приложениям, основанным на использовании промышленных серверов баз данных. Для эффективной работы разработчикам таких приложений необходимо знание языка структурированных запросов SQL. Многие главы этой части содержат детальный анализ конструкций языка на примере промышленного сервера InterBase, входящего в поставку Delphi Enterprise. Поскольку диалект SQL для InterBase почти полностью соответствует общепринятому стандарту SQL92, этот материал будет полезен и для разработчиков, использующих другие S^Z-серверы (MS SQL Server, Oracle, DB2 и др.). В отдельной главе очень кратко рассматриваются особенности сервера Oracle8, поскольку Delphi 5 - инструментальное средство, поддерживающее нововведения этой версии популярного сервера баз данных. В третьей части описываются новейшие технологии создания распределенных систем, т.е. программ, отдельные части которых располагаются на разных машинах (подчас разнесенных на большие расстояния), но тем не менее работают как единое целое. В главах этой части подробно анализируются особенности использования технологий COM/DCOM, MTS, OLEnterprise, CORBA. Насколько нам известно, в русскоязычной (в том числе и переводной) литературе эти вопросы в должной мере еще не освещались (за исключением книги [5], материалы которой существенно учитывались при редактировании соответствующих разделов). В отличие от книги [24] в настоящее издание включена отдельная часть 4, в которой сосредоточены обширные материалы по новым технологиям доступа к данным (без BDE), которые, как нам представляется, вызовут особый интерес у опытных программистов. Авторы будут благодарны читателям за возможные замечания и предложения, которые просим направлять в адрес нашего издательства по электронной почте: knowledge@readus. ги. 31 января 2000 г.
Часть 1 ОБЩИЕ ПРИНЦИПЫ РАЗРАБОТКИ ПРИЛОЖЕНИЙ БАЗ ДАННЫХ В СРЕДЕ DELPHI 5
Глава 1 ВВЕДЕНИЕ В БАЗЫ ДАННЫХ 1.1. ТЕРМИНОЛОГИЯ Базами данных (БД) называют электронные хранилища информации, доступ к ко- торым осуществляется с помощью одного или нескольких компьютеров. Обычно БД создается для хранения и доступа к данным, содержащим сведения о некоторой пред- метной области, то есть некоторой области человеческой деятельности или области реального мира. Системы управления базами данных (СУБД) - это программные средства, предна- значенные для создания, наполнения, обновления и удаления баз данных. Различают три основных вида СУБД: промышленные универсального назначения, промышлен- ные специального назначения и разрабатываемые для конкретного заказчика. Специа- лизированные СУБД создаются для управления базами данных конкретного назначе- ния - бухгалтерские, складские, банковские и т. д. Универсальные СУБД не имеют четко очерченных рамок применения, они рассчитаны «на все случаи жизни» и, как следствие, достаточно сложны и требуют от пользователя специальных знаний. Как специализированные, так и универсальные промышленные СУБД относительно деше- вы, достаточно надежны (отлажены) и готовы к немедленной работе, в то время как заказные СУБД требуют существенных затрат, а их подготовка к работе и отладка занимают значительный период времени (от нескольких месяцев до нескольких лет). Однако в отличие от промышленных заказные СУБД в максимальной степени учиты- вают специфику работы заказчика (того или иного предприятия), их интерфейс обыч- но интуитивно понятен пользователям и не требует от них специальных знаний. В зависимости от расположения СУБД различают локальные и распределенные (удаленные) СУБД. Все части локальной СУБД размещаются на компьютере пользо- вателя базы данных. Если к одной БД обращаются несколько пользователей одновре- менно, каждый пользовательский компьютер должен иметь свою копию локальной СУБД. В отличие от этого значительная часть программно-аппаратных средств рас- пределенной СУБД централизована и находится на одном достаточно мощном ком- пьютере (сервере), в то время как компьютеры пользователей несут относительно небольшую часть СУБД, которая называется клиентом. Локальные СУБД могут рабо- тать в сети, но могут и не использовать ее, в то время как распределенные (клиент- серверные) СУБД обязательно работают в компьютерной сети. Заметим, что местона- хождение собственно базы данных никак не влияет на специфику СУБД: в локальных СУБД сама БД может располагаться как на компьютере пользователя, так и на уда- ленном сетевом компьютере (файл/сервере). Безусловным достоинством клиент- серверных систем является возможность централизованного управления доступом к БД. В таких системах база данных в значительной мере защищена как от случайных, так и намеренных искажений, в них проще реализовать целостность и непротиворечи- вость данных. Единицей хранящейся в БД информации является таблица. Каждая таблица пред- ставляет собой совокупность строк и столбцов, где строки соответствуют экземпляру
24 Глава 1 объекта, конкретному событию или явлению, а столбцы - атрибутам (признакам, ха- рактеристикам, параметрам) объекта, события, явления. На рис. 1.1. приведен пример таблицы, в которой содержатся сведения об отпуске товаров со склада. Столбцы пред- ставляют собой такие параметры, как дата отпуска товара, наименование товара, на- именование покупателя, количество единиц отпущенного товара. Каждая строка со- держит сведения о конкретном событии - отпуске товара покупателю. В терминах БД столбцы таблицы называются полями, а ее строки - записями. Дата Товар Покупатель Отпущено (ед.) 10.12.99 Сахар Геракл, ТОО 100 10.12.99 Сахар Геракл, ТОО 100 12.12.99 Сахар Пищеторг, ЗАО 2000 12.12.99 Макароны Пищеторг, ЗАО 300 14.12.99 Сахар Геракл, ТОО 200 15.12.99 Дрожжи База № 28 100 Рис. 1.1. Пример таблицы «Отпуск товаров». Между отдельными таблицами БД могут существовать связи. Например, информа- ция о покупателе в предыдущей таблице может дополняться в другой: Покупатель Адрес Телефон Геракл, ТОО 107005, Москва, 2-я Бауманская ул., 12 273-00-14 Пищеторг, ЗАО 105066, Москва, Измайловский б-р, 18/11 165-18-99 База № 28 274088, Хотьково МО, ул. Лесная, 1 17-54 Рис. 1.2. Пример таблицы «Покупатель». Базы данных, между отдельными таблицами которых существуют связи, называ- ются реляционными (от relation — связь, отношение). Связанные отношениями таблицы взаимодействуют по принципу главная (master) - подчиненная (detail). В нашем примере таблица «Отпуск товаров» - главная, а таб- лица «Покупатель» - подчиненная. Главную таблицу часто называют родительской, а подчиненную - дочерней. Одна и та же таблица может быть главной по отношению к одной таблице БД и дочерней по отношению к другой. 1.2. ПЕРВИЧНЫЕ КЛЮЧИ И ИНДЕКСЫ В каждой таблице БД может существовать первичный ключ - поле или набор по- лей, однозначно идентифицирующий запись. Значение первичного ключа в таблице БД должно быть уникальным, то есть в таблице не должно существовать двух или более записей с одинаковым значением первичного ключа. Первичные ключи облегчают установление связи между таблицами. В таблице «Покупатель» таким ключом является одноименное поле. Установив связь по пер- вичному ключу, мы можем выяснить, что, например, 10.02.99 со склада было отпу- щено 100 единиц товара «Сахар» покупателю «Геракл, ТОО», офис которого распо- ложен по адресу: 107005, Москва, 2-я Бауманская ул., 12 (телефон для связи 273-00- 14). Поскольку первичный ключ должен быть уникальным, для него могут использо- ваться не все поля таблицы. В приведенном примере название покупателя вряд ли может быть уникальным, поэтому поле «Покупатель» не может использоваться в ка- честве первичного ключа. Значительно более редким является совпадение телефонов у
Введение в базы данных 25 двух разных покупателей, поэтому поле «Телефон» в большей степени подходит на роль первичного ключа. Если в таблице нет полей, значения в которых уникальны, для создания первичного ключа в нее обычно вводят дополнительное числовое поле, зна- чениями которого СУБД может распоряжаться по своему усмотрению. Если, напри- мер, в таблицу «Покупатель» добавить поле «№№», то связанные таблицы выглядели бы так: Дата Товар Покупатель Отпущено (ед.) 10.12.99 Сахар 1 100 10.12.99 Сахар 1 100 12.12.99 Сахар 2 2000 - 12.12.99 Макароны 2 300 14.12.99 Сахар 1 200 15.12.99 Дрожжи 3 100 и №№ Покупатель Адрес Телефон 1 Геракл, ТОО 107005, Москва, 2-я Бауманская ул., 12 273-00-14 2 Пищеторг, ЗАО 105066, Москва, Измайловский б-р, 18/11 165-18-99 3 База № 28 274088, Хотьково МО, ул. Лесная, 1 17-54 Теперь в таблице «Отпуск товаров» в поле «Покупатель» указывается значение первичного ключа, построенного по полю «№№» таблицы «Покупатель», что позволя- ет установить однозначную связь между таблицами. Вторичные ключи устанавливаются по полям, которые часто используются при поиске и сортировке данных: вторичные ключи (см. п. 1.5) помогут системе значи- тельно быстрее найти нужные данные. В отличие от первичных ключей поля для ин- дексов могут содержать неуникальные значения - в этом, собственно, и заключается главная разница между первичными и вторичными ключами. 1.3. РЕЛЯЦИОННЫЕ ОТНОШЕНИЯ МЕЖДУ ТАБЛИЦАМИ 1.3.1. Отношение один-ко-многим Таблица «Товары» Таблица «Отпуск товаров» Товар Ед. изм. Цена ед. Товар Дата Кол-во (ед.) Сахар КГ 10 Сахар 10.12.99 100 Макароны кг 10 х Сахар 12.12.99 200 Куры кг 30 Сахар 14.12.99 50 Фанта бут.2 л 20 Макароны 10.12.99 1000 Макароны 11.12.99 500 Фанта 10.12.99 2000 Фанта 12.12.99 3000 Рис. 1.3. Связь один-ко-многим.
26 Глава 1 Как видно из рис. 1.3, одной записи из родительской таблицы «Товары» может со- ответствовать несколько записей в дочерней таблице «Отпуск товаров». Обратите внимание на глагол может: он означает, что такая возможность - потенциальная и что в родительской таблице могут быть записи, для которых в данный момент нет записей в дочерней таблице (например, товар «Куры»), Различают две разновидности связи один-ко-многим: в первом случае выдвигается жесткое требование, согласно которому всякой записи в родительской таблице долж- ны соответствовать записи в дочерней таблице; во втором случае подобное требование не носит жесткого характера и подразумевается (как в описанном выше случае), что некоторые записи в родительской таблице могут не иметь связанных с ними записей в дочерней таблице. Связь один-ко-многим является самой распространенной для реляционных баз дан- ных. Она позволяет моделировать иерархические структуры данных. 1.3.2. Отношение один-к-одному Отношение один-к-одному имеет место, когда одной записи в родительской табли- це соответствует одна запись в дочерней таблице (рис. 1.4.). Таблица «Сотрудники» Таблица «Информация о сотрудниках» № ФИО Должность Отдел 1 Иванов И.И. инженер 10 2 Петров П.П. бухгалтер 20 3 Васин В.В. прораб 10 № Год рожд. Кол-во детей ... 1 1950 3 2 1952 1 3 1960 2 Рис. 1.4 Связь один-к-одному. Данное отношение встречается много реже, чем отношение один-ко-многим. Его используют, если не хотят, чтобы таблица БД «распухала» от второстепенной инфор- мации. Связь один-к-одному приводит к тому, что для чтения связанной информации в нескольких таблицах приходится производить несколько операций чтения, что замед- ляет получение нужной информации. Кроме того, базы данных, в состав которых вхо- дят таблицы со связью один-к-одному, не могут считаться полностью нормализован- ными (о нормализации см. ниже). Подобно связи один-ко-многим связь один-к-одному может быть жесткой и неже- сткой. 1.3.3. Отношение многие-ко-многим На рис. 1.5 показаны таблицы, состоящие в отношении многие-ко-многим. Каждой учебной группе соответствует несколько преподавателей. Каждый преподаватель мо- жет вести, во-первых, несколько разных предметов и, во-вторых, преподавать в раз- ных группах.
Введение в базы данных 27 Таблица «Учебные группы и дисциплины» Таблица «Преподаватели» ТРУП -па Предмет № пре- пода- вателя № пре- пода- вателя ФИО Кафе- дра ПС-1 Программирование 10 10 Краснов Ю.Б. ТИ-1 ТИ-1 Программирование 12 12 Володин В.Н. ТИ-1 ПС-1 Теория систем 10 62 Булгаков В.М. РИО РТ-2 Философия 62 78 Гноенский Л.С. ТИ-1 ПС-1 Социология 62 85 Подушкин М.А. ЭИ-1 Рис. 1.5. Связь многие-ко-многим. Некоторые СУБД не поддерживают связи многие-ко-многим на уровне индексов и ссылочной целостности (см. следующий подраздел), хотя и позволяют реализовывать ее в таблицах неявным образом. Считается, что БД всегда можно перестроить так, чтобы любая связь многие-ко-многим была заменена на одну или более связей один-ко-многим. 1.3.4. Связи между записями одной таблицы Между записями одной таблицы также могут существовать связи, то есть одни за- писи могут ссылаться на другие. Пусть в реляционной БД необходимо хранить древовидную структуру произволь- ного уровня, например, структуру организации (рис. 1.6.) Департамент автоматизации Техническое управление Отдел сетевого оборудования Ремонтный отдел АТС Управление программными системами Отдел эксплуатации Информационная группа Административная группа Диспетчерское бюро Отдел разработки Рис. 1.6. Структура организации. В этом случае можно создать таблицу (рис. 1.7), в которой каждому подразделению организации соответствует одна запись. Эта запись ссылается на запись, соответствую- щую подразделению более высокого уровня, в которое входит данное подразделение. И только в записи о подразделении самого высокого уровня нет подобной ссылки. № подраз- деления Название подразделения № подразделения предыдущего уровня 1 Департамент автоматизации 2 Техническое управление 1
28 Глава 1 3 Управление разработки и эксплуатации про- граммных систем 1 4 Отдел сетевого оборудования 2 5 Ремонтный отдел 2 6 АТС 2 7 Отдел эксплуатации 3 8 Отдел разработки 3 9 Информационная группа 7 10 Административная группа 7 11 Диспетчерское бюро 10 Рис. 1.7. Табличное представление структуры организации. 1.4. ССЫЛОЧНАЯ ЦЕЛОСТНОСТЬ Рассмотрим наиболее часто встречающуюся в базах данных связь один-ко-многим. Соответствующие таблицы показаны на рис. 1.8. Как можно заметить, дочерняя и родительская таблицы связаны между собой по общему полю «Товар». Назовем это поле полем связи. Таблица «Товары» Товар Ед. изм. Цена ед. Сахар кг ю Макароны кг 10 Куры кг 30 Фанта бут.2 л 20 Таблица «Отпуск товаров» Товар Дата Кол-во (ед.) -Сахар 10.12.99 100 .Сахар 12.12.99 200 £ахар 14.12.99 50 Макароны 10.12.99 1000 Макароны 11.12.99 500 Фанта 10.12.99 2000 Фанта 12.12.99 3000 Рис. 1.8. Связанные таблицы базы данных. Возможны два вида изменений, которые приведут к утере связей между записями в родительской и дочерней таблицах: • изменение значения поля связи в записи родительской таблицы без изменения значений полей связи в соответствующих записях дочерней таблицы; • изменение значения поля связи в одной из записей дочерней таблицы без соответ- ствующего изменения значения полей связи в родительской и дочерней таблицах. Рассмотрим первый случай. На рис. 1.9 показано изменение значения поля «Товар» с «Сахар» на «Рафинад» в таблице «Товары». В таблице «Отпуск товаров» значение поля связи «Сахар» осталось прежним. В результате: • в дочерней таблице «Отпуск товаров» для товара «Рафинад» (таблица «Товары») нет сведений о его отпуске со склада; • некоторые записи таблицы «Отпуск товаров» содержат сведения об отпуске то- вара «Сахар», о котором нет информации в таблице «Товары».
Введение в базы данных 29 Таблица «Товары» Таблица «Отпуск товаров» Товар Ед. изм. Цена ед. % 4 Товар Дата Кол-во, ед. Рафинад КГ 10 кСахар 10.12.99 100 Макароны кг 10 Сахар 12.12.99 200 Куры кг 30 'Сахар 14.12.99 50 Фанта бут.1 л 20 • * > ^Макароны 10.12.99 1000 . % Макароны 11.12.99 500 % Фанта 10.12.99 2000 Фанта 12.12.99 3000 Рис. 1.9. Нарушение целостности базы данных: записи с товаром «Сахар» (таблица «Отпуск товаров») не имеют родительской записи. Рассмотрим второй случай. Пусть в одной из записей таблицы «Отпуск товаров» значение поля связи «Сахар» изменилось на «Рафинад» (рис. 1.10). В результате: • в дочерней таблице «Отпуск товаров» недостоверны сведения об отпуске со склада товара «Сахар» (таблица «Товары»); • одна из записей таблицы «Отпуск товаров» содержит данные об отпуске товара «Рафинад», сведения о котором отсутствуют в таблице «Товары». Таблица «Товары» Таблица «Отпуск товаров» Товар Ед. изм. Цена ед. Товар Дата Кол-во, ед. Сахар КГ 10 Рафинад 10.12.99 100 Макароны кг 10 ^*^^^^**^* Сахар 12.12.99 200 Куры кг 30 Сахар 14.12.99 50 Фанта бут.1 л 20 Макароны 10.12.99 1000 Макароны 11.12.99 ' 500 Фанта 10.12.99 2000 Фанта 12.12.99 3000 Рис. 1.10. Нарушение целостности базы данных: запись с товаром «Рафинад» (таблица «Отпуск това- ров») не имеет родительской записи. И в первом, и втором случае мы наблюдаем нарушение целостности базы данных; это означает, что хранящаяся в ней информация становится недостоверной. СУБД обычно блокирует действия, которые нарушают целостность связей между таблицами, т.е. нарушают ссылочную целостность. Когда говорят о ссылочной цело- стности, имеют в виду совокупность связей между отдельными таблицами во всей БД. Нарушение хотя бы одной такой связи делает информацию в БД недостоверной. Чтобы предотвратить потерю ссылочной целостности, используется механизм кас- кадных изменений. Он состоит в обеспечении следующих действий: • при изменении поля связи в записи родительской таблицы следует синхронно изменить значения полей связи в соответствующих записях дочерней таблицы; • при удалении записи в родительской таблице следует удалить соответствующие записи в дочерней таблице. Изменения или удаления в записях дочерней таблицы при одновременном измене- нии (удалении) записи родительской таблицы называются каскадными изменениями и каскадными удалениями.
30 Глава 1 Существует другая разновидность каскадного удаления: при удалении родитель- ской записи в записях дочерних таблиц значения полей связи обнуляются. Эта разно- видность применяется редко, т.к. дочерние таблицы в этом случае будут содержать избыточные данные, например, сведения о товаре, которого нет на складе. Обычно для реализации ссылочной целостности в дочерней таблице создают внеш- ний ключ, в который входят поля связи дочерней таблицы. Этот ключ для дочерней таблицы является первичным и поэтому по составу полей должен совпадать с первич- ным ключом родительской таблицы или реже - с частью первичного ключа. 1.5. ИНДЕКСЫ По определениям ключей (первичных или вторичных, см. п. 1.2) СУБД автоматиче- ски строит индексы, которые представляют» собой механизмы быстрого доступа к хранящимся в таблицах данным. Сущность индексов состоит в том, что они хранят отсортированные значения ин- дексных полей (т.е. полей, по которым построен индекс) и указатель на запись в таб- лице. Например, пусть имеется таблица, показанная на рис. 1.11. Порядковый Дата прихода Наименование Количество, № записи товара товара кг 1 10.12.1999 Сахар 10 2 12.12.1999 Картофель 50 3 12.12.1999 Свекла 20 4 14.12.1999 Сахар 50 5 14.12.1999 Свекла 10 6 16.12.1999 Сливы 4 Рис. 1.11. Физическая структура таблицы. С логической точки зрения ее индексы выглядят так, как показано на рис. 1.12. Индекс по дате прихода товара Индекс по наименованию товара Индекс по количе- ству Дата прихода № за- писи Наименование товара № записи Количе- ство № записи 10.12.1999 1 Картофель 2 4 6 12.12.1999 2 Сахар 1 10 1 12.12.1999 3 Сахар 4 10 5 14.12.1999 4 Свекла 3 20 3 14.12.1999 5 Свекла 5 50 2 16.12.1999 6 Сливы 6 50 4 Рис. 1.12. Логическая структура индексов. Обратите внимание: значения полей в индексе («Дата прихода», «Наименование товара», «Количество») сортируются по возрастанию, что существенно ускоряет поиск нужных значений. Если, например, нужно выбрать все записи с наименованием товара «Свекла», нет нужды просматривать всю таблицу. Достаточно найти в индексе, построенном по столбцу «Наименование товара», первый указатель на запись, содер- жащую товар «Свекла», и считать из таблицы эту запись, а затем повторить то же для всех иных указателей в индексе на записи с товаром «Свекла». Поскольку значения
Введение в базы данных 31 полей отсортированы, поиск первого указателя осуществляется специальными мето- дами быстрого поиска и реализуется значительно быстрее, чем если бы поиск шел по неотсортированным полям таблицы. Такой метод доступа к записям таблицы называ- ется индексно-последовательным, потому что: • поиск ведется по индексу, а не по таблице; • доступ начинается с первой строки, удовлетворяющей условию запроса или его части; • строки в индексе, начиная с первой найденной записи, просматриваются после- довательно. В том случае если в условия запроса входят поля, по которым не построено индексов, ищется иной пригодный индекс, а если такого индекса нет, производится последователь- ный перебор записей таблицы БД (в некоторых СУБД, например, использующих техно- логию ADO, в этом случае может быть построен вспомогательный индекс). 1.6. НОРМАЛИЗАЦИЯ ТАБЛИЦ ПРИ ПРОЕКТИРОВАНИИ БАЗЫ ДАННЫХ При проектировании структуры новой БД определяют сущности (объекты, явления) предметной области, которые должны найти свое отражение в базе данных. Анализ пред- метной области обычно осуществляется на основании известных сведений о ней с учетом целей проектирования программной системы. В результате анализа создается проект БД. Отметим, что существуют специальные программные комплексы (так называемые CASE- средства), которые в значительной мере автоматизируют процесс создания проекта БД. Заметим, что введенная в Delphi 5 реализация модуля (см. п. 4.4) данных позволяет по- строить реляционные связи между таблицами, и хотя эти модули не заменяют CASE- средства, они в известной мере облегчают процесс восприятия и анализа сложной БД. Процесс проектирования БД в немалой степени зависит от опыта и интуиции раз- работчика, т.е. является творческим, однако некоторые его моменты можно формали- зовать. Одной из таких формализаций является требование, согласно которому реля- ционная база данных должна быть нормализована. Процесс нормализации имеет своей целью устранение избыточности данных и заключается в приведении к третьей нор- мальной форме (ЗНФ)1. 1.6.1. Первая нормальная форма Первая нормальная форма (1 НФ) требует, чтобы каждое поле таблицы БД было неделимым и не содержало повторяющихся групп. Неделимость поля означает, что содержащиеся в нем значения не должны делиться на более мелкие. Например, если в поле «Подразделение» содержится название факультета и название кафедры, требование неделимости не соблюдается и необходимо выделить на- звание факультета или кафедры в отдельное поле; поле, содержащее фамилию, имя и отчество, следует разделить на три поля - отдельно для фамилии, имени и отчества и т.д. Повторяющимися являются поля, содержащие одинаковые по смыслу значения. Например, если требуется получить статистику продаж четырех товаров по месяцам, можно создать поля для хранения данных о продаже по каждому товару (рис. 1.13): 1 Существует несколько нормальных форм - 1НФ, 2НФ, ЗНФ, 4НФ..5НФ, нормальная форма Бойса- Кодда (БКНФ). При практической разработке баз данных важны первые трн - 1НФ, 2НФ, ЗНФ.
32 Глава 1 СТАТИСТИКА-ПРОДАЖ Год Месяц_________________ Товар1 Товар2 ТоварЗ Товар4 Рис. 1.13. Повторяющиеся группы. Замечание. Дефис в заголовке таблицы не является обязательным требовани- ем именования таблиц БД; просто таблицы именуются именно таким образом в использованной авторами программе формирования диаграмм базы данных. Однако что делать, если товаров не 4, а 104, и как быть, если количество товаров заранее не известно? Повторяющиеся группы следует устранить, сохранив в таблице единственное поле «Товар» (рис. 1.14). В результате получим запись, содержащую информацию о статистике продаж по одному товару, но этот товар может быть лю- бым: для 4 товаров будем иметь 4 записи, для 104 товаров - 104 записи и т. д. СТАТИСТИКА-ПРОДАЖ Год Месяц_________________ Товар Рис. 1.14. Устранение повторяющихся групп. Рассмотрим пример приведения к первой нормальной форме. Пусть необходимо автоматизировать процесс отпуска товаров со склада по накладной, примерный вид которой показан на рис. 1.15. Сведем имеющиеся данные в одну таблицу. Приводя ее к первой нормальной фор- ме, учтем, что впоследствии будет необходимо производить анализ продаж по горо- дам. Поэтому из поля «Адрес» (допускающего толкование как делимого поля) выде- лим часть данных (город) в отдельное поле «Город». Накладная № 123 Дата 10.01.97 Покупатель ТОО "Геракл" Адрес г. Москва, ул. Стромынка, 20 Отпущен товар Количество Ед. изм. Цена ед. изм. Общая СТОИМОСТЬ Тушенка 10000 банки 25 25 000 Сахар 50 КГ 10 5 000 Макароны 300 кг 10 3 000 Итого 33 000 Рис. 1.15. Примерный вид накладной.
Введение в базы данных 33 Известно, что каждый покупатель может закупить в один день различное количе- ство товаров, однако, чтобы не создавать повторяющихся групп, фиксируем факт от- пуска каждого товара в отдельной записи. В результате получим таблицу, показанную на рис. 1.16. ОТПУСК-ТОВАРОВ-СО-СКЛАДА I I ________________________I Дата Покупатель 'город ' Адрес |Товар ! Ед_измерения |Цена_за_ед_изм 'Отпущено_ед 'Общая_стоимость |Номер накладной I______~_______________________ Рис. 1.16. Таблица без составных полей и циклических групп. 1.6.2. Вторая нормальная форма Вторая нормальная форма (2НФ) требует, чтобы все поля таблицы зависели от первичного ключа, то есть чтобы первичный ключ однозначно определял запись и не был избыточен. Те поля, которые зависят только от части первичного ключа, должны быть выделены в составе отдельных таблиц. Продолжим рассмотрение описанного выше примера. Для приведения к 2НФ вы- делим поля, которые входят в первичный ключ. Дата накладной и номер накладной по отдельности не могут уникально определять запись, поскольку они будут одинаковы для всех записей, относящихся к одной и той же накладной (напомним, что одна на- кладная в таблице рис. 1.16 представляется несколькими записями). Поэтому введем в первичный ключ поле «Товар». При этом исходим из предположения, что по одной накладной может быть отпущено одно наименование конкретного товара, то есть не может иметь место ситуация, когда отпуск одного и того же товара оформляется в накладной двумя строками, что повлекло бы за собой две одинаковые записи в табли- це «Отпуск товаров со склада». На рис. 1.17 показана структура таблицы после выделения полей в составе первич- ного ключа (эти поля отчеркнуты от прочих полей линией и располагаются в верхней части структуры таблицы).
34 Глава 1 ОТПУСК-ТОВАРОВ-СО-СКЛАДА Дата Покупатель 'Номер_накладной [товар______________________ 'Город Адрес . Ед_измерения Цена_за_ед_изм Отпущено_ед Общая_стоимость Рис. 1.17. Таблица с избыточным первичным ключом. Нетрудно увидеть, что созданный нами первичный ключ является избыточным: поле «Номер накладной» однозначно определяет дату и покупателя. Для данной накладной не может быть никакой иной даты и никакого иного покупателя. Поле «Товар» в комбина- ции с номером накладной, напротив, однозначно идентифицирует запись. ОТПУСК-ТОВАРОВ-СО-СКЛАДА Номер_накладной Товар_____________________ Дата Покупатель (Город Адрес Ед_измерения Цена_за_ед_изм Отпущено_ед Общая_стоимость Рис. I.I8. Таблица с неизбыточным первичным ключом, но еще не приведенная к 2НФ. После уточнения состава полей в первичном ключе получим таблицу со структу- рой, показанной на рис. 1.18. Первое требование 2НФ выполнено, чего не скажешь о втором, гласящем, что зна- чения всех полей записи должны однозначно зависеть от совокупного значения пер- вичного ключа и не должна иметь место ситуация, когда некоторые поля зависят от части первичного ключа. В таблице на рис. 1.18 поля «Единица измерения», «Цена за единицу измерения» зависят от значения поля «Товар», входящего в первичный ключ. Поэтому выделяем эти поля в самостоятельную таблицу «Товары» и определяем связь: поскольку один товар может присутствовать во многих накладных, таблицы «Товары» и «Отпуск товаров со склада» находятся в связи один-ко-многим (рис. 1.19.).
Введение в базы данных 35 ТОВАРЫ Товар I Ед_измерения Цена за ед изм | ।___~___z__z____! ОТПУСК-ТОВАРОВ-СО-СКЛАДА Номер_накладной 'I 4 ^Товар (FK)___________________________ Дата Покупатель Город । Адрес Отпущено_ед |0бщая_стоимость | Рис. 1.19. Выделение таблицы «Товар». После анализа структуры таблицы можно заметить, что значение поля «Покупатель» никоим образом не зависит от пары значений «Номер накладной», «Товар», а зависит только от значения поля «Номер накладной». Поэтому данное поле и зависящие от его значения поля «Город», «Адрес» выделяем в таблицу «Покупатели» (рис. 1.20). Анализируя далее структуру таблицы «Отпуск товаров со склада», обнаруживаем, что одно из оставшихся полей - «Дата» - зависит только от значения поля «Номер накладной». Поэтому выделяем дату и номер накладной в самостоятельную таблицу «Накладные» (рис. 1.21). Установим связи между таблицами. Один покупатель может встречаться во многих накладных. Поэтому между таблицами «Покупатели» и «Накладные» имеется связь один-ко-многим по полю «Покупатель». Одной накладной может соответствовать несколько товаров. Поэтому между таблицами «Накладные» и «Отпуск товаров со склада» имеется связь один-ко-многим по полю «Номер накладной» (рис. 1.22). 1.6.3. Третья нормальная форма Третья нормальная форма (ЗНФ) требует, чтобы в таблице не имелось транзитив- ных зависимостей между неключевыми полями, то есть чтобы значение любого поля, не входящего в первичный ключ, не зависело от значения другого поля, также не вхо- дящего в первичный ключ.
36 Глава 1 ТОВАРЫ ^-Товар_______ Ед_измерения Цена_за_ед_изм ПОКУПАТЕЛИ Покупатель Город Адрес I I ОТПУСК-ТОВАРОВ-СО-СКЛАДА х______Ф Номер_накладной [Товар (FK)________________ Дата Отпущено_ед Общая_стоимость Рис. 1.20. Выделение таблицы «Покупатели». ТОВАРЫ ПОКУПАТЕЛИ г Товар_________ ' Ед_измерения Цена_за_ед_изм Покупатель Город Адрес НАКЛАДНЫЕ Номер_ накладной Дата ОТПУСК-ТОВАРОВ-СО-СКЛАДА ~<Товар (FK) Отпущено_ед Обща'я_стоимость Рис. 1.21. Выделение таблицы «Накладные».
Введение в базы данных 37 ТОВАРЫ ПОКУПАТЕЛИ Рис. 1.22. Связи между таблицами. Продолжим рассмотрение примера. Можно увидеть, что в таблице «Отпуск това- ров со склада» имеется зависимость значения поля «Общая стоимость» от значения поля «Количество». Значение поля «Общая стоимость» может вычисляться как значе- ние поля «Количество», умноженное на значение поля «Цена за единицу измерения» из таблицы «Товары» (из записи с таким же значением поля «Товар»). Поэтому поле «Общая стоимость» из таблицы «Отпуск товаров со склада» удаляем. В результате получаем нормализованную базу данных, структура которой приводится на рис. 1.23. Замечание. В таблице «Покупатели» значение поля «Адрес» зависит от зна- чения поля «Город», поскольку в разных городах могут оказаться улицы с одинаковыми названиями и, соответственно, дома с одинаковыми номерами (вспомним известный кинофильм «Ирония судьбы, или С легким паром»). Од- нако такой зависимостью можно пренебречь, поскольку поле «Адрес» в нашем случае носит чисто информационный характер и не должно входить в условия запросов самостоятельно. Вообще говоря, на практике не всегда возможно по- лучить идеально нормализованную БД. Часто к этому и не стремятся - по причинам, изложенным в следующем разделе. 1.6.4. Нормализация - за и против Нормализация таблиц БД призвана устранить из них избыточную информацию. Как видно из приведенных выше примеров, таблицы нормализованной БД содержат
38 Глава 1 только один элемент избыточных данных - это поля связи, присутствующие одновре- менно у родительской и дочерних таблиц. Поскольку избыточные данные в таблицах не хранятся, экономится дисковое пространство. ТОВАРЫ ПОКУПАТЕЛИ Рис. 1.23. Нормализованная база данных. Однако у нормализованной БД есть и недостатки, прежде всего практиче- ского характера. Чем шире число сущностей, охватываемых предметной обла- стью, тем из большего числа таблиц будет состоять нормализованная БД. Базы данных в составе больших систем, управляющих жизнедеятельностью крупных организаций и предприятий, могут содержать сотни связанных между собою таблиц. Поскольку порог человеческого восприятия не позволяет одновремен- но анализировать большое число объектов с учетом их взаимосвязей, можно утверждать, что с увеличением числа нормализованных таблиц уменьшается целостное восприятие базы данных как системы взаимосвязанных данных. Поэтому при разработке и эксплуатации крупных систем нередки ситуации, когда каждый сотрудник представляет себе процессы, протекающие только в части системы. Известны случаи эволюционного создания таких систем, прин- ципы функционирования которых впоследствии признавались вышедшими за границы понимания. Другим недостатком нормализованной БД является необходимость считывать свя- занные данные из нескольких таблиц при выполнении одного запроса. Например, пусть для рассмотренной выше БД требуется выдать отчет, в котором для каждой накладной указан покупатель и его реквизиты (город и адрес). Для этого необходимо каждую запись в таблице «Накладные» объединить по названию покупателя (поле
Введение в базы данных 39 связи) с соответствующей записью из таблицы «Покупатели». Операции такого объе- динения подразумевают поиск и позиционирование в таблице «Покупатели» и могут выполняться достаточно медленно, особенно когда одна из таблиц имеет большой объем, данные в базе данных и на диске фрагментированы и т. д. Замечено, что не- нормализованные или не вполне нормализованные данные отыскиваются быстрее, если они хранятся в одной таблице, по сравнению со случаем поиска данных в одной или более связанных таблицах. Подобное ускорение тем заметнее, чем больше число записей в связанных таблицах. Таким образом, при работе с данными большого объема приходится искать ком- промисс между требованиями нормализации (то есть логичности данных и экономии места на носителях информации) и необходимостью улучшения быстродействия сис- темы. 1.7. ПОНЯТИЕ ТРАНЗАКЦИИ Под транзакцией понимается воздействие на БД, переводящее ее из одного цело- стного состояния в другое. Воздействие выражается в изменении данных в таблицах базы. Если одно из изменений, вносимых в БД в рамках транзакции, завершается неус- пешно, должен быть произведен откат к состоянию базы данных, имевшему место до начала транзакции. Следовательно, все изменения, внесенные в БД в рамках транзакции, либо одновременно подтверждаются, либо не подтверждается ни одно из них. Поясним сказанное примером. Рассмотренную выше нормализованную БД, со- держащую сведения об отпуске товаров со склада, дополним двумя таблицами (рис. 1.24): • «Статистика по товару» — содержит свейения о суммарном отпуске каждого то- вара со склада, начиная с начала года; • «Статистика по покупателю» - содержит сведения о суммарном отпуске това- ров каждому покупателю, начиная с начала года. Тогда транзакция, связанная с добавлением в БД сведений о расходе товара со склада, будет состоять из следующих операций: • добавление записи в таблицу «Отпуск товаров»; • поиск записи по данному товару в таблице «Статистика по товару» и увеличе- ние значения поля «Всего отпущено товара» на значение «Отпущено ед.»; если запись по такому товару в таблице «Статистика по товару» отсутствует, она должна быть добавлена; • поиск записи по данному покупателю в таблице «Статистика по покупателю»; вычисление стоимости отпущенного товара и увеличение на это значение поля «Всего отпущено»; если запись по покупателю в таблице «Статистика по поку- пателю» отсутствует, она должна быть добавлена. Если в рамках транзакции произошел сбой при выполнении одной из этих опера- ций, необходимо отменить результаты выполнения всех других операций, иначе информация в БД будет недостоверной. В этом случае говорят об «откате» транзакции. Выше мы рассмотрели ссылочную целостность таблиц БД и такие механизмы ее осуществления, как правильные каскадные воздействия на записи в дочерних таблицах
40 Глава 1 при изменении или удалении записи в родительской таблице. Приведенный пример по- казывает нам другой вид целостности - смысловую (семантическую) целостность БД. Требование смысловой целостности определяет, что данные в БД должны изменяться таким образом, чтобы не нарушались сложившиеся между ними смысловые связи. Дей- ствительно, если в случае отпуска товаров информация о расходе товара не будет учтена в соответствующей записи таблицы «Статистика по товару», но при этом будет учтена в соответствующей записи таблицы «Статистика по покупателю», произойдет нарушение достоверности данных, хотя ссылочная целостность базы данных не будет нарушена. Нарушение достоверности данных в этом случае может быть легко проверено: сумма количеств расхода всех товаров из таблицы «Статистика по товару», умноженных на соответствующие цены за единицу товара, должна сойтись с суммой отпуска по всем покупателям из таблицы «Статистика по покупателю». СТАТИСТИКА-ПО-ТОВАРУ СТАТИСТИКА-ПО-ПОКУПАТЕЛЮ Рис. 1.24. Добавление таблиц для накапливания статистики. 1.8. ТЕРМИНОЛОГИЯ РАСПРЕДЕЛЕННЫХ СУБД В этом разделе в общих чертах описываются специфичные термины, которые ис- пользуются в книге и касаются взаимодействия компонентов и программ. Такое взаи-
Введение в базы данных 41 модействие неизбежно для распределенных СУБД, поэтому, если вы собираетесь раз- рабатывать только локальные СУБД, можете пропустить этот раздел. Однако общая тенденция развития СУБД показывает, что распределенные системы станут преобла- дающими в ближайшие 3—5 лет. Например, известная исследовательская фирма Gartner Group заявляет, что к 2000 году 40% работников офисов станут мобильными или надомными работниками, т.к. ежегодно продается около 30 млн. ПК типа ноутбук. С вероятностью 70% она же прогнозирует, что доля широкополосных (оптоволоконных) каналов связи возрастет к 2001 году до 30% при одновременном уменьшении стоимости коммуникационных услуг в 2 раза. Все это позволяет сделать вывод, что завтрашняя СУБД - это в основном распределенная СУБД. На сегодняшний день существуют две параллельно развивающиеся и конкури- рующие технологии взаимодействия объектов и программ: COM (Component Object Model - компонентная модель объектов) корпорации Microsoft и CORBA (Common Object Require Broker Architecture - архитектура с поставщиком требуемых общих объектов) независимой группы OMG. Основные принципы этих технологий и исполь- зующиеся в них термины описываются ниже. 1.8.1. Технология СОМ Технология СОМ разрабатывается корпорацией Microsoft и предназначена для то- го, чтобы одна программа (клиент) смогла заставить работать объект, являющийся частью другой программы (частью сервера), так, как если бы этот объект был частью клиента, причем обе программы в общем случае могут быть расположены на разных компьютерах (в том числе находящихся в разных частях света), написаны на разных языках и исполняться под управлением разных операционных систем. Более того, сами компьютеры могут быть разного типа - например, /ВЛ/-совместимый ПК и рабо- чая станция SUN. Ключевым аспектом СОМ является так называемый интерфейс. Интерфейс имеет уникальный идентификатор и набор параметров, описывающих методы, события и свойства общего объекта. Идентификатор интерфейса IID (Interface Identifier) является частным случаем GUID (Global Unique Identifier - глобально уникальный идентифика- тор). Уникальность GUID такова, что если бы все существующие в мире ЭВМ заня- лись генерацией GUID, частота совпадения двух GUID была бы не чаще одной раз в 10000 лет. В состав Windows 32 включены функции, генерирующие GUID. Параметры интерфейса в общем случае описывают некоторый класс с идентификатором CLSID (Class ID реализуется как GUID), т.е. типы и имена используемых в нем полей, коли- чество и типы параметров обращения к доступным методам и свойствам, имена мето- дов и свойств и т. д. Получив интерфейс внешнего СОЛ/-объекта, клиент может его использовать так же, как свои собственные объекты. Любой СОЛ/-объект имеет ин- терфейс lUnknow, с помощью которого он может получить доступ к основному ин- терфейсу объекта. Сервер СОМ представляет собой исполняемую программу или DLL, содержащую один или несколько объектов СОМ. В зависимости от местоположения клиента и сервера возможны три варианта: • клиент и сервер располагаются на одной машине и запускаются в одном про- цессе (именно так взаимодействует программа Delphi с компонентами ActiveX); в этом случае сервер представляет собой DLL;
42 Глава 1 • клиент и сервер располагаются на одной машине, но запускаются в разных про- цессах (например, таблицы Excel вставлены в документ Word); в этом случае сервер представляет собой программу; • клиент и сервер располагаются на разных машинах; сервером может быть как программа, так и DLL; используется распределенный вариант СОМ, который называется DCOM (Distributed COM). В первом случае клиент с помощью интерфейса объекта непосредственно обраща- ется к методам объекта в своем собственном адресном пространстве (рис. 1.25). Рис. 1.25. Взаимодействие клиента и сервера в одном процессе. Если сервер запускается в другом процессе или на другой машине, между объек- том и клиентом располагаются два посредника - Proxy (уполномоченный) и Stub (заглушка) (рис. 1.26). Клиент помещает параметры вызова в стек и обращается к ме- тоду интерфейса объекта. Однако это обращение перехватывает Proxy, упаковывает параметры вызова в пакет СОМ и направляет его в Stub другого процесса, возможно, на другой машине. Stub распаковывает параметры, помещает их в стек и делает вызов нужного метода объекта. Таким образом, метод объекта выполняется в собственном адресном пространстве процесса сервера. 1.8.2. Технология CORBA Подобно СОМ в CORBA активно используется интерфейс объекта. Главным отли- чием CORBA от СОМ является интегрированный в нее слой, реализующий доступ к удаленным объектам. В соответствии с этой технологией схема взаимодействия клиента и сервера вы- глядит следующим образом (рис. 1.27). На машине клиента создаются два объекта-посредника: Stub (заглушка) и ORB (Object Require Broker - брокер требуемого объекта). Stub выступает как полномочный представитель объекта: с помощью интерфейса объекта клиент обращается к Stub так, как если бы это был сам объект.
Введение в базы данных 43 Получив вызов метода, Stub транслирует этот вызов объекту ORB, который посы- лает в сеть широковещательное сообщение. На это сообщение откликается один из объектов Smart Agent («умный» агент), установленный в сетевом окружении клиента (как в локальной сети, так и в Internet). Smart Agent моделирует сетевой каталог, в котором зарегистрированы известные ему серверы объектов. Он отыскивает нужный сетевой адрес сервера и передает запрос объекту ORB на машине сервера. Заметим, что обмен данными между ORB (клиента и сервера) и Smart Agent осуществляется с использованием специального протокола UDP, который более бережно использует сетевые ресурсы, чем протокол TCP. Через BOA (Basic Object Adapter — базовый объ- ектный адаптер) данные получает особый объект сервера, который называется Skeleton (основа). Skeleton помещает параметры вызова в стек адресного пространства объекта и реализует собственно вызов. Роль объекта ВОА заключается в фильтрации обращений к объекту сервера: с по- мощью его методов сервер через Skeleton может объявить некоторые свои поля и свойства доступными только для чтения или вовсе срытыми от данного клиента. (Поскольку в рамках технологии данные, которыми обмениваются клиент и сервер, рассматриваются просто как цепочки байт, клиент должен поместить в буфер вызова свой авторизованный ключ в системах, защищенных от «посторонних» клиентов.)
44 Глава 1 «Изюминкой» CORBA является способ описания интерфейса объекта. Для этих целей разработан специальный язык IDL (Interface Definition Language - язык описания интерфей- са), очень напоминающий язык C++. После описания интерфейса в терминах этого языка компилятор IDL автоматически создает объекты Stub и Skeleton. Обмен информацией об интерфейсе между разработчиками осуществляется в терминах языка высокого уровня, в то время как компилятор описания интерфейса переводит его текст в машинные инструкции конкретного компьютера (клиента или сервера). В результате достигается высокая степень независимости обмена данными от аппаратных средств клиента и пользователя. Для реализации технологии в сетевом окружении клиента должен существовать хотя бы один Smart Agent. Если обмен данными осуществляется в локальной сети офиса, Smart Agent устанавливается на головную машину (на клиент-сервер или ма- шину с SQL-сервером), а при обмене данными по Internet - на одном из ее узлов. При создании сервера осуществляется автоматическая регистрация объектов в одном или нескольких Smart Agent. Таким образом, Smart Agent «знает», по каким сетевым адре- сам расположены его серверы. Это позволяет системе повысить свою надежность: если в одном из серверов произошел сбой, Smart Agent повторит вызов и при повтор- ном сбое переключится на другой сервер. 1.8.3. СОМ или CORBA? Как уже говорилось, обе технологии стремительно развиваются, поэтому выносить окончательную оценку каждой из них еще рано. Но создавать распределенные систе- мы нужно уже сейчас, поэтому ниже проводится небольшое сравнение этих техноло- гий так, как они выглядят на момент написания книги - во второй половине 1999 года. СОМ — это детище Microsoft, в этом и заключаются ее главные плюсы и минусы. На СОМ базируются такие технологии Microsoft, как OLE, Automation OLE, ActiveX, ADO. Весь интерфейс Windows 98 построен на СОМ. Таким образом, спектр-примене- ния СОМ значительно шире, чем у CORBA. СОМ идеально приспособлена для работы с любыми версиями Windows, и поэтому ее можно рекомендовать для распределенных офисных систем, оснащенных компьютерами с этой ОС. CORBA значительно сложнее СОМ как в понимании деталей ее работы, так и в практической реализации. Однако ее спецификации разрабатывает отраслевой коми- тет OMG (Object Management Group - группа управления объектами), который пред- ставляет собой объединение многих фирм и пользователей, поэтому она значительно лучше справляется с работой в кросс-платформенной среде, а ее надежность позволяет распределенным системам работать 24 часа в сутки, 7 дней в неделю.
Глава 2 НЕФОРМАЛЬНОЕ ЗНАКОМСТВО С ПРОГРАММИРОВАНИЕМ БД Давно известно, что лучше один раз увидеть, чем сто раз услышать. Поэтому в этой главе вам предлагается попрактиковаться в создании простейшего приложения, работающего с базами данных, пройдя при этом все этапы - от создания псевдонима БД до запуска разработанного приложения. Если вы никогда не проектировали в Delphi приложений для работы с базами дан- ных, приобретенный опыт поможет вам понять, что построение таких приложений в Delphi максимально упрощено. Есть и еще одно следствие выполнения описываемых упражнений: читая в последующих разделах о средствах Delphi для разработки при- ложений баз данных, вы будете встречать уже знакомые названия и понятия. 2.1. ПОСТАНОВКА ЗАДАЧИ Требуется создать приложение, предназначенное для учета поступающих на склад товаров. База данных состоит из двух таблиц: «Товары» и «Приход товаров». Таблицы будем создавать и хранить в формате СУБД Paradox. Отношение между таблицами - один-ко-многим, то есть одному товару в таблице «Товары» может соответствовать более одной записи в таблице «Приход товаров». Рабочая структура таблиц приведена ниже: Таблица «Товары» Название поля Смысл Тип Длина Tovar Название товара Строка 20 Edlzm Единица измерения Строка 10 Zena Цена за единицу измерения Целочисленный Таблица «Приход товаров» Название поля Смысл Тип Длина N_Prih Номер прихода Автоинкремент Tovar Название товара Строка 20 DatPrih Дата прихода Дата Kolvo Количество Целочисленный Для таблицы «Товары» первичный ключ будет построен по полю Tovar, а для таблицы «Приход товаров» - по полю N_Prih. Для реализации ссылочной целостности в таблице «Приход товаров» необходимо построить внешний ключ по полю Tovar. Для сортировки записей при их выводе в приложении необходимо создать индекс по полям DatPrih, Tovar.
46 Глава 2 2.2. СОЗДАНИЕ ПСЕВДОНИМА БАЗЫ ДАННЫХ При работе с таблицами локальных БД (в число которых входят таблицы СУБД Paradox и dBase) сама база данных размещается в каталоге на диске и хранится в виде набора файлов. Для хранения одной таблицы создается отдельный файл. Такие же отдельные файлы создаются для хранения индексов таблицы и мемо-полей. Для раз- мещения нашей БД потребуется не более 10 Кбайт дисковой памяти: пожертвуйте ими и создайте каталог PROBA на диске С. Обращение к БД из утилит и программы осуществляется по псевдониму базы данных. Псевдоним должен быть зарегистрирован в файле конфигурации конкретного компьюте- ра при помощи утилиты BDE Administrator. Присвоим псевдоним PROBA создаваемой БД. Для этого запустите утилиту BDE Administrator (проще всего это сделать с помощью главного меню Windows: Пуск | Программы | Borland Delphi 5 | BDE Administrator). Выбе- рите в главном меню окна утилиты элемент Object | New. В появившемся окне (рис. 2.1) оставьте тип создаваемой БД без изменений (STANDARD) и нажмите кнопку ОК. Рис. 2.1. Окно выбора типа драйвера базы данных. В левом поле окна администратора БД вы увидите строку с именем STANDARD 1. Измените это имя на PROBA. В правом поле указаны параметры БД. Измените параметр PATH, который указы- вает маршрут доступа к каталогу, в котором располагается БД. Можно ввести путь вручную, но лучше воспользоваться средствами администратора: для этого нужно щелкнуть по полю PATH и нажать на появившуюся в правом углу поля кнопку zJ. Затем следует выбрать каталог с: \proba и нажать кнопку ОК (рис. 2.2). Рис. 2.2. Окно параметров псевдонима базы данных.
Неформальное знакомство с программированием БД 47 Теперь необходимо запомнить определение псевдонима. Для этого в левом окне ад- министратора БД необходимо щелкнуть по имени псевдонима правой кнопкой мыши и в дополнительном меню выбрать опцию Apply. В появившемся диалоговом окне (в нем спрашивается, собираемся ли мы запоминать изменения для псевдонима) нажмите кноп- ку ОК. Закройте окно утилиты BDE Administrator-, создание псевдонима завершено и к нему можно обращаться из других утилит и приложений. Однако каталог, на который ссылается псевдоним БД, еще пуст. Необходимо создать в нем таблицы базы данных. 2.3. СОЗДАНИЕ ТАБЛИЦ БАЗЫ ДАННЫХ 2.3.1. Объявление полей Для создания таблиц базы данных необходимо запустить утилиту Database Desktop (DBD). После запуска утилиты установим умалчиваемый псевдоним. Для этого нужно выбрать элемент главного меню File | Working Directory и в выпадающем списке Aliases выбрать имя псевдонима PROBA, после чего нажать кнопку ОК. Для создания таблицы БД нужно выбрать элемент главного меню File | New | Table. В появившемся окне Create Table оставьте без изменения тип создаваемой таблицы (Paradox 7) и нажмите кнопку ОК. После этого появится окно определения структуры таблицы БД (рис. 2.3). Рис. 2.3. Утилита Database Desktop: окно определения структуры таблицы БД. Каждая строка таблицы соответствует полю. Назначения столбцов: • Field Name - имя поля; • Туре - тип поля; • Size - размер поля (для строковых полей, поскольку иные поля подразумевают размер, определяемый типом поля);
48 Глава 2 • Key - содержит звездочку «*», если поле входит в состав первичного ключа; ес- ли в первичный ключ входит несколько полей, они должны определяться в той последовательности, в которой они присутствуют в первичном ключе; кроме того, все поля, входящие в состав первичного индекса, должны определяться перед иными полями, то есть первыми определяться в списке полей. Определим поля, входящие в таблицу «Товары». Введите название поля Tovar в столбец Field Name. Чтобы определить тип поля, щелкните по столбцу Туре и нажмите клавишу пробела. В ответ раскроется список типов полей (рис. 2.4). Рис. 2.4. Выбор типа поля. Ниже дается краткая характеристика типов полей для таблиц Paradox: Тип поля и обозначение Хранимые значения Alpha A Символьные значения длиной до 255 символов. Number N Числовые значения с плавающей точкой в диапазоне -10зи7...+10М. Точ- ность до 15 значащих цифр. Money $ Аналогичен типу Number, но предназначен для хранения денежных сумм. Число знаков после запятой по умолчанию равно двум. При пока- зе значения выводится знак денежной единицы. Short S Целочисленные значения в диапазоне -32 768 ... 32 767. Longlnteger I Целочисленные значения в диапазоне -2 147 483 648 ... 2 147 483 647. BCD # Числовые значения, в том числе и дробные, в двоично-десятичном фор- мате. Обеспечивает исключительную точность при работе с большим числом знаков в дробной части. Применяется в вычислениях, где важна точность (финансовые, научные приложения). Для проведения вычис- лений требует больше времени, чем для числовых полей иных типов. Date D Значения даты. Time T Значения времени. Timestamp @ Значения даты и времени. Memo M Строковые значения длиной более 255 символов. Максимальная длина не ограничена. От 1 до 240 символов могут храниться вместе с таблицей БД; остальные хранятся в виде Мелю-файла (расширение .МВ)
Неформальное знакомство с программированием БД 49 Formatted Memo F Форматированный текст произвольной длины, в ко юром отдельные фрагменты текста могут использовать разные шрифты, цвет и стили. Graphic Fields G Графические изображения в форматах BMP, PCX, T1F, GIF, EPS, кото- рые при хранении преобразуются в формат BMP. Хранятся отдельно от основной таблицы БД. OLE 0 Информация в форматах, поддерживаемых технологией OLE {Object Linking and Embedding) корпорации Microsoft. Logical L Логические значения (True, False). Autoincrement ± Автоинкрементное поле. Значения доступны только для чтения. Обычно - ключевое поле в составе первичного ключа. При добавлении новой записи значение поля вычисляется автоматически таким образом, чтобы в одной и той же таблице не было одинаковых значений. Значения поля из удаленных записей повторно не используются. Binary В Двоичные значения произвольной длины. Должны интерпретироваться приложениями пользователя. Хранятся в отдельных от основной табли- цы Л/В-файлах. Bytes Y Произвольные двоичные значения, интерпретируемые приложениями пользователя, длиной от 1 до 240 байт. Хранятся вместе с таблицей БД. Чтобы определить тип поля Tovar, выберите Alpha и затем в столбце Size укажите значение 20. В столбце Key поместите звездочку, означающую, что данное поле вхо- дит в состав первичного ключа. Для этого нажмите любой символ на клавиатуре. По- вторное нажатие любого символа снимает отметку в столбце Key. Введите определения остальных столбцов таблицы «Товары» (рис. 2.5). В Create Paradox 7 Table: ( Untitled ) Right-click or press Spacebar to choose a field type Define | Table properties: (Validity Checks Г 1. Required Field 2. Minimum value: Г”------------ 3. Maximum value: 4. Default value: 5. Picture: Assisi... Borrow., 'I Savers [ C&nc I I Help Puc. 2.5. Определение структуры таблицы «Товары».
50 Глава 2 Для каждого поля определим требование обязательного его заполнения значением. Для этого, переходя от поля к полю, включите переключатели Required Field. Другие поля служат для наложения ограничений на значение поля: • Minimum value - определяет минимальное значение поля; • Maximum value — определяет максимальное значение поля; • Default value - определяет значение поля по умолчанию; • Picture - определяет шаблон изображения поля. Отсутствие значения в одном из полей означает отсутствие ограничений на значе- ние поля. 2.3.2. Изменение языкового драйвера Поскольку в структуре нашей таблицы используются символьные поля (Tovar и Edlzm), значения которых, скорее всего, будут составлять строки русского текста (кириллицы), необходимо установить соответствующий языковый драйвер, чтобы русский текст отображался на экране без искажений. Щелкните по списку Table Properties и выберите в раскрывшемся списке продолжение Table Language, после чего щелкните по кнопке Modify. Если в появившемся после этого окне Table Language (рис. 2.6) указан драйвер, отличный от Pdox ANSI Cyrillic, раскройте список этого окна и выберите именно этот драйвер, после чего закройте окно кнопкой ОК. Restructure Paradox 5 0 for Windows Table: Tovary.db Eield roster: | Field Name J Type Size Key T able properties: | Table Language о vat Edlzm Zena Define.. j Modify... | 20 10 I A A I 1 U 2 3 Enter a field name up to 25 characters long Г Pack Table | Save As... I Cancel | Help Save Puc. 2.6. Выбор языкового драйвера.
Неформальное знакомство с программированием БД 51 На заметку. Процедуру установки нужного языкового драйвера следу- ет проводить до создания индексов по символьным полям и установле- ния по ним ссылочной целостности (см. ниже). 2.3.3. Запоминание таблицы Чтобы запомнить созданную таблицу на диске, нажмите кнопку Save As и в поя- вившемся окне укажите имя Tovary. Поскольку вы работаете в Windows 32, имя таб- лицы в отличие от имен полей могло бы состоять из русских букв: Товары. При же- лании вы можете именно так назвать таблицу без малейшего ущерба для дальнейшего выполнения нашего примера. Однако следует помнить о том, что в реальной работе вам, возможно, потребуется перейти к системам клиент-сервер. Все промышленные серверы не принимают кириллицу в именах таблиц, поэтому вам придется русские имена заменять на латинские и вносить дополнительные изменения в уже созданные вами программы. В момент сохранения в каталоге C:\PROBA будет создан табличный файл tovary . DB и файлы индексов с соответствующими расширениями. 2.3.3. Определение индексов Определите структуру таблицы «Приход товаров» (рис. 2.7). Рис. 2.7. Структура таблицы Prihod. Всем полям назначьте атрибут Required (требование обязательного существования значения у поля на момент его запоминания в БД), кроме поля N_Prih-., поскольку это поле автоинкрементное, заполнение его значением производится автоматически при запоминании новой записи.
52 Глава 2 Напоминание. Как и для предыдущей таблицы, установите для табли- цы Prihod языковый драйвер Pdox ANSI Cyrillic. Создадим индекс по полям DatPrih, Tovar. Для этого в комбинированном спи- ске Table Properties (в правом верхнем углу окна) выберите элемент Secondary Indexes. Чтобы определить новый индекс, нажмите кнопку Define. В появившемся диалого- вом окне в поле Fields содержится список полей определяемой нами таблицы. Поле Index Fields предназначено для хранения полей, входящих в создаваемый индекс. Что- бы скопировать конкретное поле из списка Fields в список Index Fields, нужно нажать кнопку с изображением правой стрелки. Последовательность добавления полей в спи- сок важна, она определяет порядок чередования полей в списке. После того как вы поместили нужные поля в список Index Fields (рис. 2.8), нажмите кнопку ОК. Рис. 2.8. Определение полей, входящих в состав индекса. В появившемся диалоговом окне запрашивается имя индекса. Введите имя Sort_DatPrich_Tovar и нажмите ОК. Не рекомендуется составлять название индекса только из имен полей, поскольку такой способ именования индексов используется автоматически при создании ссы- лочной целостности между таблицами (см. следующий раздел). Как видно на рис. 2.9, после добавления нового индекса его имя появилось в спи- ске индексов. Впоследствии, щелкнув по имени индекса, мы можем его удалить (кнопка Erase) или изменить (кнопка Modify). Сохраните созданную таблицу под именем Prihod.
Неформальное знакомство с программированием БД 53 Рис. 2.9. Список индексов, определенных для таблицы Prihod. 2.3.5. Определение ссылочной целостности между таблицами Таблицы Tovary и Prihod находятся в отношении один-ко-многим, то есть с одной записью в таблице Tovary может быть связано несколько записей по приходу того же товара в таблице Prihod. В качестве поля связи выступает поле Tovar, присутствую- щее в обеих таблицах. Определим ссылочную целостность между данными таблицами. Ссылочная цело- стность в Paradox определяет, во-первых, связь между таблицами, а во-вторых, вид каскадных воздействий. Откройте таблицу Prihod (элемент меню File | Table | Open) и затем выберите ре- жим изменения структуры таблицы {Table | Restructure). В выпадающем списке Table Properties выберите элемент Refrential Integrity и нажмите кнопку Define. В появив- шемся диалоговом окне (рис. 2.10) в списке Fields показаны поля таблицы Prihod, а в списке Tables - таблицы базы данных PROBA. Рис. 2.10. Окно определения ссылочной целостности. Сначала укажем поле связи для дочерней таблицы Prihod'. выберите в списке Fields поле Tovar и нажмите кнопку с изображением стрелки вправо. Название Tovar будет записано в поле Child Fields (поле внешнего ключа дочерней таблицы).
54 Глава 2 Выберите в списке Tables таблицу Tovary и нажмите кнопку с изображением стрелки влево. В поле Parents Key (ключ родительской таблицы) будут показаны поля из первичного ключа таблицы Tovary. В данном случае это поле Tovar. Переключатели Update rules определяют вид каскадных воздействий на таблицу Prihod при изменении значения поля связи в таблице Tovary или при удалении в ней записи: • Cascade - разрешены каскадные изменения и удаления подчиненных записей в дочерней таблице; • Prohibit - запрещены изменения полей связи или удаление записи в родительской таблице, если для данной записи есть связанные записи в дочерней таблице. Оставьте умалчиваемый выбор Cascade и нажмите кнопку ОК - DBD запросит имя ссылочной целостности (в Paradox ссылочные целостности именуются). Введите имя, например, TovaryPrihodlntegrity, и нажмите кнопку ОК. Теперь имя созданной ссы- лочной целостности будет помещено в список. Сохраните изменения в таблице Prihod (кнопка Save) и заново войдите в режим ре- структуризации [Table | Restructure). В выпадающем списке Table properties выберите элемент Secondary Indexes (индексы таблицы, кроме индекса, построенного по опреде- лению первичного ключа). В списке индексов вы увидите новый индекс с именем Tovar. Этот индекс построен автоматически по неявному определению внешнего клю- ча при создании ссылочной целостности. Выйдите из режима реструктуризации (кнопка Cancel) и закройте окно DBD. 2.4. СОЗДАНИЕ ПРОСТЕЙШЕГО ПРИЛОЖЕНИЯ Создайте в каталоге C:\PROBA подкаталог АРР. В нем будут храниться разрабо- танные вами программы. Запустите Delphi. По умолчанию Delphi при своем запуске создает пустую форму для нового приложения. Воспользуемся ею. В палитре компонентов на странице Data Access выберите мышью компонент Е [Table), щелкните на нем мышью и затем щелкните мышью на форме. После этого изображение компонента останется в форме. В окне Инспектора Объектов установите в свойство DatabaseName (псевдоним БД) значение proba при помощи выпадающего списка или вручную. Таким же образом установите значение tovary . db в свойство TableName (имя таблицы БД). Для свойст- ва Active установите значение True. В этот момент произойдет реальное связывание компонента Table (он по умолчанию имеет имя Tablet) с таблицей tovary . DB. Расположите на форме компонент [DataSource). Он служит в качестве связую- щего звена между невизуальными компонентами (в данном случае Table 1) и визуаль- ными компонентами, которые мы добавим в форму позднее. Поэтому компоненты DataSource часто называют источниками данных. Установите в свойство DataSet (имя набора данных) компонента значение Table!, выбрав его из выпадающего списка. Расположим в форме компонент DBGrid, взяв его из палитры компонентов на странице Data Controls. Установите в свойство DataSource компонента значение Data- Sourcel (это имя, присвоенное по умолчанию созданному нами перед этим компонен- ту DataSource), выбрав его из выпадающего списка. Компонент DBGrid служит для отображения записей набора данных в табличной форме. Вид разрабатываемой формы представлен на рис. 2.10.
Неформальное знакомство с программированием БД 55 Рис. 2.10. Вид формы на этапе разработки. Сохраните форму и проект на диске и запустите программу. Работающая програм- ма открывает вам непосредственный доступ к данным в таблице Tovary. Для добавления записи нужно нажать на клавиатуре клавишу Insert или, находясь на последней записи набора данных, клавишу смещения курсора вниз. Таблица автомати- чески перейдет в режим добавления новой записи. После ввода значений в поля записи запомнить запись в наборе данных можно, перейдя на другую запись при помощи кла- виш управления курсором. Отказаться от запоминания записи можно, нажав кнопку Esc. Для изменения записи следует переместить указатель текущей записи в нужное ме- сто и изменить значения там, где это необходимо. Набор данных автоматически пе- рейдет в режим редактирования. Для удаления записи следует установить на нее указатель текущей записи и нажать Ctrl+Del. На рис. 2.11 показан вид окна программы в момент добавления в таблицу Tovary новой записи. Рис. 2.11. Добавление новой записи в таблицу Tovary. 2.5. СОЗДАНИЕ ПРИЛОЖЕНИЯ ДЛЯ РАБОТЫ С ДВУМЯ ТАБЛИЦАМИ Покажем, как в одной форме можно связать два набора данных (главный и подчи- ненный) так, чтобы в подчиненном наборе всегда показывались записи, соответст- вующие текущей записи в главном наборе.
56 Глава 2 Поместите на форму еще одну пару компонентов ТТаЫе (с именем ТаЫе2) и TDataSource (имя DataSource2) для работы с таблицей Prihod. С помощью свойств DatabaseName и TableName настройте компонент Table2 на работу с таблицей prihod. DB и установите в его свойство Active значение True, а с помощью свойства DataSet компонента DataSource2 свяжите его с компонентом Table2. Разместите на форме компонент TDBGrid (имя DBGrid2) и установите в его свойство DataSource значение DataSource2. Чтобы пояснить, содержимое каких таблиц отображают компо- ненты DBGrid, поместите над каждым из них по компоненту TLabel и установите в их свойствах Caption значения Товары и Приход товаров (рис. 2.12). Откройте обе таб- лицы, поместив в их свойства A dive значения True. Рис. 2.12. Форма с главным и подчиненным наборами данных во время разработки. Если сейчас запустить программу на исполнение, обе таблицы будут независимы друг от друга. Чтобы содержимое подчиненного набора соответствовало выбору запи- си в главном наборе, подчиненную таблицу нужно связать с главной. Для этого рас- кройте список выбора в свойстве MasterSource таблицы Table2 и выберите единствен- ное имеющееся в нем значение DataSource 1. Затем щелкните по правой части строки MasterFields в окне Инспектора Объектов и по появившейся в ней кнопке с тремя точками, чтобы раскрыть окно редактора связей. Раскройте список Availablelndexes в верхней части этого окна и выберите индекс Tovar - в окошке Detail Fields появится имя поля связи Tovar. Выберите это же поле в окошке Master Fields и нажмите кнопку Add- связь установлена (см. рис. 2.13).
Неформальное знакомство с программированием БД 57 Рис. 2.13. Окно редактор связей. Закройте окно редактора связей и запустите приложение. Ввод и изменение данных в таблице Prihod можно производить с помощью компонента DBGrid2 (рис. 2.14). При этом перемещение указателя в главной таблице приводит к автоматической смене информации в отображаемых данных подчиненной таблицы. Рис. 2.14. Заполнение таблш^ГPrihod.
58 Глава 2 На заметку. Поскольку таблица Prihod связана с таблицей Tovary, на нее накладываются два ограничения: во-первых свойство IndexName (IndexFieldName) не должно ссылаться на какой-либо индекс, в котором поле связи не является ведущим (если это требование нарушить, связь будет разрушена и таблицы окажутся независимыми, а если при этом тип данных в индексном поле будет отличаться от ожидаемого типа по- ля связи, возникнет исключение с сообщением о несовместимости ти- пов); во-вторых, при редактировании (вводе) записей дочерней таблицы пользователь не может изменять поле связи или вводить в него данные (при вставке новой записи это поле формируется автоматически). В на- шем примере, кроме того, также автоматически формируется значение инкрементного поля NPrih. 2.6. НАСТРОЙКА ПОЛЕЙ И СМЕНА АКТИВНОГО ИНДЕКСА Наша простенькая программа имеет три недостатка. Во-первых, в ней показан ав- тоинкрементный столбец N_Prih, который необходим для обеспечения уникальности записей в таблице Prihod. Он не несет никакой смысловой нагрузки, поэтому его луч- ше не показывать. Во-вторых, дата в поле DatPrih отображается в кратком формате, поэтому год 2000 представлен двумя нулями, что непривычно и сразу бросается в глаза: год нужно показывать в полном формате. И, наконец, в-третьих, названия коло- нок в сетках DBGrid соответствуют названиям полей таблиц и представлены латин- скими буквами - эти названия следует заменить на русские. Все эти задачи решаются созданием для наших таблиц объектов-полей и их соответствующей настройкой. Дважды щелкните по компоненту ТаЫе2 в окне формы или щелкните по нему пра- вой кнопкой мыши и в дополнительном меню выберите элемент Fields Editor. На эк- ране появится пустое окно редактора полей (рис. 2.15, а). Щелкните по нему правой кнопкой мыши и выберите элемент меню Add Fields. Будет показан список всех полей таблицы Prihod.DB. Выделите (при помощи мыши и клавиши Shift) все поля, кроме N_Prih (рис. 2.15, б) и нажмите кнопку ОК. Теперь список редактора полей будет включать все отмеченные поля (рис. 2.15, в). Для каждого добавленного поля Delphi автоматически создает объект класса TField' (поле набора данных) и назначает ему уникальное имя, получающееся сцепле- нием имени набора данных (Table!) и имени поля. Так, компонент TField, соответст- вующий полю Tovar, получит имя Table2Tovar. Имена автоматически созданных объ- ектов-полей вы найдете в объявлении класса TForml в окне кода. Если какое-то поле (в нашем случае N Prih) не включено в список полей, оно становится невидимым для программы и не появляется в соответствующем наборе данных. Совет. Некоторые СУБД, в том числе Paradox, позволяют использовать в названиях полей кириллицу. Наш совет - ни в коем случае не делайте этого! Во-первых, русскоязычные названия полей сильно мешают при формировании -запросов, а рано или поздно вам придется такие за- просы делать (названия полей в этом случае приходится обрамлять апо- строфами). Во-вторых, относительно семантически понятные автоимена 1 Точнее - одного из специализированных потомков этого класса.
Неформальное знакомство с программированием БД 59 объектов-полей, создаваемые редактором, превращаются в совершенно бессмысленные имена типа TablelField, TablelFieldl и т.д. а) б) в) Рис. 2.15. Работа с редактором полей: а) пустой список редактора полей; б) добавление полей; в) заполненный список. Для каждого вновь созданного объекта-поля изменим название соответствующей колонки. Для этого поочередно щелкните мышью в списке редактора по каждому полю и в окне Инспектора Объектов в свойство DisplayLabel введите его русское на- звание: Товар, Дата прихода. Количество. Чтобы дата отображалась в полном формате, щелкните по полю DatPrih в списке редактора, затем в Инспекторе Объектов дважды щелкните по событию OnGetText и напишите такой его обработчик: procedure TForml.Table2DatPrihGetText(Sender: TField; var Text: String; DisplayText: Boolean); begin Text := FormatDateTime('dd.mm.yyyy',Table2DatPrih.Value) end; С помощью объектов-полей мы смогли бы переименовать и столбцы сетки DBGridl, но мы покажем, как можно сделать это по-другому. Изменим параметры компонента DBGridl так, чтобы названия его столбцов содержали русские наименова- ния. Для этого щелкните по компоненту правой кнопкой мыши и выберите в локаль- ном меню опцию Columns Editor. На экране появится окно редактора столбцов компо- нента (рис. 2.16, а). Для изменения характеристики столбцов нужно перейти от неявно определяемых столбцов к явно определяемым - щелкните по кнопке St на инструмен- тальной панели окна редактора (рис. 2.16, б). Чтобы изменить заголовок столбца, нужно щелкнуть по нему в списке полей редактора столбцов и в Инспекторе Объек- тов раскрыть список свойства Title (для этого нужно либо дважды щелкнуть по имени
60 Глава 2 свойства мышью, либо щелкнуть по нему правой кнопкой и выбрать Expand). В эле- менте Caption этого списка содержится заголовок столбца. Измените соответствую- щим образом заголовки и затем закройте редактор столбцов. 1 Editing DBGndl Columns а) б) Рис. 2.16. Окно редактора столбцов: а) пустой список столбцов; б) заполненный список. Теперь окно работающей программы приобретет вид, показанный на рис. 2.17. Рис. 2.17. Окно программы после исправлений. Отсортируем записи в наборе данных ТаЫе2 по дате прихода товара. Для этого нам прежде всего придется разорвать связь один-ко-многим между таблицами, так как
Неформальное знакомство с программированием БД 61 индекс Sort_Dat_Prih не содержит поле связи Tovar. Закройте работающую програм- му, щелкните по ТаЫе2, в Инспекторе Объектов уберите ссылки из свойств MasterSource и MasterField, раскройте список в свойстве Table2.IndexFieldNames и выберите значение DatPrih;Tovar. После этого еще раз вызовите редактор столбцов DBGrid2 и при помощи мыши «перетащите» столбец DatPrih так, чтобы он предшест- вовал столбцу Tovar. Вид окна модифицированной программы показан на рис. 2.17. Рис. 2.17. Сортировка по дате. Теперь вы можете перемещаться по таблицам и изменять их независимо друг от друга. 2.7. ОПРЕДЕЛЕНИЕ ВИЗУАЛЬНЫХ КОМПОНЕНТОВ ДЛЯ РАБОТЫ С ПОЛЯМИ Улучшим интерфейс нашей программы так, чтобы к полям записи в наборе данных ТаЫе2 можно было обращаться не только из сетки компонента DBGrid2, но и с помо- щью дополнительных визуальных компонентов. Поместите на форму два компонента TDBEdit (страница Data Controls палитры компонентов): компонент DBEditl под столбцом «Дата прихода», а компонент DBEdit2 под столбцом «Количество» (рис. 2.18). Чтобы связать новые компоненты с полями, поместите в их свойства DataSource значение DataSource2 и в свойства DataField соответственно DatPrih и Kolvo.
62 Глава 2 Рис. 2.18. Форма с визуальными компонентами для работы с полями текущей записи. Для доступа к полю Tovar нам нужен более сложный компонент, который позволял бы вводить в это поле только те значения, которые уже введены в поле Tovar таблицы tovary.DB, и никакие другие. Для этой цели под столбцом «Товар» компонента DBGrid2 разместите компонент TDBLookupComboBox. Измените умалчиваемые зна- чения следующих свойств этого компонента так: Свойство Значение Свойство Значение DataSource DataSource2 ListField Tovar DataFiled Tovar KeyField Tovar Listsource DataSourcel Теперь на форме есть специальные компоненты для доступа к полям записи. Чтобы обращение к полям шло только с помощью этих компонентов, запретим доступ к по- лям из компонента DBGrid2'. установите в его свойство ReadOnly значение True. Добавьте к форме 5 компонентов TButton (страница Standard палитры компонен- тов). Измените имена этих компонентов (свойство Name) соответственно на Insert- Button, EditButton, DeleteButton, PostButton, EditButton. Измените надписи на кнопках (свойство Caption) соответственно на «Добавить», «Изменить», «Удалить», «Запом- нить», «Отменить».
Неформальное знакомство с программированием БД 63 Напишите такой обработчик события OnClick для кнопки InsertButton: procedure TForml.InsertButtonClick(Sender: TObject); begin if Table2.State = dsBrowse then Table2.Insert; end; Если набор данных Table2 находится в режиме просмотра dsBrowse, метод Insert переводит его в состояние добавления записи dslnsert. Ввод значений полей осущест- вляется в компонентах DBEditl, DBLookupComboBoxl, DBEdit2. Для кнопки EditButton: procedure TForml.EditButtonClick(Sender: TObject); begin if Table2.State = dsBrowse then Table2.Edit; end; Метод Edit переводит набор данных Table2 в состояние добавления записи dsEdit. Редактирование значений полей осуществляется в компонентах DBEditl, DBLookup- ComboBoxl, DBEdit2. Для кнопки DeleteButton: procedure TForml.DeleteButtonClick(Sender: TObject); begin if Table2.State = dsBrowse then if MessageDlg('Подтвердите удаление записи', mtConfirmation,[mbYes, mbNo],0) = mrYes then Table2.Delete; end; Если набор данных Table2 находится в режиме просмотра записей dsBrowse, вызы- вается диалоговое окно MessageDlg с предложением подтвердить удаление записи. Если пользователь нажимает кнопку Yes, текущая запись в наборе данных Table2 уда- ляется методом Delete. Для кнопки PostButton: procedure TForml.PostButtonClick(Sender: TObject); begin if Table2.State in [dslnsert,dsEdit] then Table2.Post; end; Если набор данных находится в режиме добавления новой записи или редактиро- вания, вызывается метод Post набора данных ТаЫе2, который запоминает текущее состояние записи в таблице БД. После запоминания набор данных автоматически пе- реводится в режим просмотра dsBrowse. Для кнопки CancelButton: procedure TForml.CancelButtonClick(Sender: TObject); begin if Table2.State in [dslnsert,dsEdit] then Table2.Cancel; end;
64 Глава 2 Если набор данных находится в режиме добавления новой записи или редактиро- вания, вызывается метод Cancel, который отменяет сделанные изменения и переводит набор данных в режим просмотра dsBrowse. Запустите программу. При добавлении новой записи или при корректировке суще- ствующей в поля можно заносить значения, используя компоненты DBEditl, DBEdit2 и путем выбора из списка значений в компоненте DBLookupComboBoxl. То же проис- ходит при изменении записи. При удалении записи появляется диалоговое окно, пока- занное на рис. 2.19. Рис. 2.19. Окно подтверждения удаления записи. 2.8. ФОРМИРОВАНИЕ НАБОРА ДАННЫХ ИЗ НЕСКОЛЬКИХ ТАБЛИЦ В этом разделе иллюстрируется создание набора данных из нескольких таблиц с помощью компонента TQuery. Создайте новое приложение. Расположите на форме компоненты TQuery, TData- Source (страница Data Access палитра компонентов) и TDBGrid (страница Data Controls). Установите в свойство Query 1 .DatabaseName значейие PROBA. Свяжите компонент DataSourcel с Query! (свойство DataSource!.DataSet), a DBGrid! с DataSource! (свойство DBGrid!.DataSource). Раскройте редактор свойства SQL компонента Query! и введите такой текст SQL- запроса: SELECT Р.DatPrih, Р.Tovar, P.Kolvo, T.Zena, (P.Kolvo * T.Zena) As Stoim FROM Tovary T, Prihod P WHERE T.Tovar = P.Tovar ORDER BY P.DatPrih, P.Tovar Закройте редактор кнопкой OK. Как видно из текста запроса, набор данных собирается из двух таблиц: tovary . db и PRIHOD.DB (FROM Tovary T, Prihod P). При этом соединяются записи, имеющие одинаковое значение поля TOVAR (where Т.Tovar - Р.Tovar). Часть SELECT Р.DatPrih, Р.Tovar, P.Kolvo, T.Zena, (P.Kolvo * T.Zena) As Stoim тек- ста запроса определяет выбираемые поля, причем создается вычисляемое поле Stoim со значением P.Kolvo * T.Zena. Чтобы выполнить запрос, установите в свойство Queryl.Active значение True (рис. 2.20).
Неформальное знакомство с программированием БД 65 ! Forml -|П|Х I sail2.! | Tovar Kolvo Zena Stoim I А Сахар 50 10 500 Q uery1aiaSoutce13KapoHbi 100 10 1000 20. 30. 12.99 Сахар 100 10 1000 12.99 Макароны 200 10 2000 30.12.99 П епси-ко ла 80 6 480 02.01.00 П епси-ко ла 300 6 1800 20.01.00 Макароны 150 10 1500 30.01.00 П епси-ко ла 100 6 600 03.02.00 Сахар 120 10 1200 Ж | Рис. 2.20. Соединение данных из разных таблиц в одном наборе данных. Если вы запустите программу, то обнаружите, что изменить каким-либо образом отображаемые в DBGridl данные нельзя, так как они получены в результате выполне- ния оператора SELECT языка SQL, который объединил данные из двух таблиц. Описа- ние языка SQL приводится во второй части книги.
Глава 3 СРЕДСТВА DELPHI 3.1. ОБЩИЙ ОБЗОР СРЕДСТВ В состав Delphi 5 Enterprise входят следующие средства для разработки и эксплуа- тации приложений, использующих базы данных. BDE (Borland Database Engine) - машина баз данных Borland Представляет собой набор £)££-библиотек, обеспечивающих низкоуровневый дос- туп к локальным и клиент-серверным БД. Должна устанавливаться на каждом компь- ютере, который использует приложения для работы с БД, написанные на Delphi (за исключением облегченных клиентов в трехзвенной архитектуре - см. п. 3.3.3). SQL Links Драйверы для работы с удаленными серверами данных, такими как Sybase, MS SQL Server, Oracle. Для работы с «родным» S^-cePBePOM InterBase устанавливать SQL Links нет необходимости. Доступ к таблицам локальных СУБД типа Paradox и dBase также осуществляется BDE напрямую, без использования SQL Links. BDE Administrator Утилита для установки псевдонимов (имен) баз данных, параметров БД и драйве- ров баз данных на конкретном компьютере. При работе с БД из приложения, создан- ного с помощью Delphi, доступ к базе данных производится по ее псевдониму. Пара- метры определяемой псевдонимом БД действуют только для этой БД; параметры, установленные для драйвера БД, действуют для всех баз данных, использующих драй- вер. Кроме того, в утилите BDE Administrator можно произвести установку таких об- щих для всех БД параметров, как формат даты и времени, форматы представления числовых значений, используемый языковый драйвер и т. д. Database Desktop (DBD) Средство для создания, изменения и просмотра БД. Эта утилита прежде всего ори- ентирована на работу с таблицами локальных СУБД. В ряде случаев может использо- ваться и для работы с таблицами распределенных СУБД. Например, с помощью DBD можно с некоторыми ограничениями создавать и просматривать таблицы баз данных, работающих под управлением промышленных серверов InterBase, MS SQL Server, Oracle и некоторых других. DBD предоставляет программисту возможность сформи- ровать запрос к БД методом QBE (Query By Example - запрос по образцу). SQL Explorer Универсальная утилита, совмещающая многие функции BDE Administrator и DBD. С ее помощью можно создавать и просматривать псевдонимы БД, просматривать структуры и содержимое таблиц БД, формировать запросы к БД на языке SQL, созда- вать словари данных (шаблоны полей таблиц). SQL Monitor Средство для трассировки выполнения S^"3anPOCOB-
Средства Delphi 67 VisiBroker Комплекс программных средств фирмы VisiGen (ныне входит в состав Inprise} для поддержки технологии CORBA. Datapump Средство для перемещения данных между БД различных типов (например, при пе- реходе от локальных систем к распределенным). Data Dictionary Словарь данных. Предназначен для хранения атрибутов полей таблиц БД отдельно от самих БД. Информация о полях может использоваться различными приложениями. Data Module Невизуальные компоненты DataModule применяются для централизованного хра- нения наборов данных в приложении, работающем с БД. Одним из главных удобств использования Data Module является возможность связывания с каждым набором данных правил по управлению данными - бизнес-правил. Бизнес-правила определяют реакцию системы на добавление, изменение, удаление данных и реализуют блокиров- ку действий, которые могут разрушить ссылочную или смысловую целостность БД. Невизуальные компоненты для работы с БД Невизуальные компоненты Delphi служат для соединения приложения с таблицами БД в локальных и распределенных системах. Они расположены на страницах Data Access и Midas палитры компонентов (на странице Data Access собраны компоненты как для локальных, так и для распределенных систем, а на странице Midas - только для распределенных систем). С помощью невизуальных компонентов осуществляется подключение к базам данных, формирование запросов к ним, манипулирование таб- лицами, создание клиентов и серверов в трехзвенной архитектуре. Визуальные компоненты для работы с БД Визуальные компоненты предназначены для визуализации записей наборов данных или их отдельных полей. Эти компоненты расположены на странице Data Controls палитры компонентов. Они служат основным инструментом разработки пользователь- ского интерфейса доступа к данным. Компоненты для построения отчетов На странице QReport палитры компонентов размещены компоненты для построе- ния отчетов. Отчеты играют важную роль: на основании запросов к БД они создают для пользователя нужные ему документы. 3.2. ОСОБЕННОСТИ ПРОГРАММ ДЛЯ РАБОТЫ С БД Характерной особенностью созданных с помощью Delphi программ для работы с БД1 является непременное использование в них BDE, которая осуществляет роль свя- зующего моста между программой и БД* 2. BDE берет на себя всю низкоуровневую ' Поскольку в этой книге речь идет исключительно о программах, разработанных с помощью Delphi и предназначенных для работы с базами данных, условимся в дальнейшем для краткости называть их просто программами, опуская остальные определения. 2 В Delphi 5 есть средства для работы с данными без £DE (см. 4-ю часть книги).
68 Глава 3 работу по обеспечению клиентской программы нужными ей данными, поэтому в са- мом общем случае взаимодействие программы с данными происходит так, как показа- но на рис. 3.1. Рис. 3.1. Общая схема взаимодействия программы с данными. BDE не является частью программы. В зависимости от типа СУБД она может раз- мещаться на машине клиента или сервера. Лицензионное соглашение разрешает вам устанавливать это программное средство бесплатно вместе с распространяемыми вами программами. Поскольку сама BDE содержится в нескольких каталогах и должна ре- гистрироваться в реестре Windows 32, создание дистрибутивных носителей распро- страняемых программ становится'не совсем простым делом. Существенно упростить эту процедуру помогает утилита InstallShield Expreess for Delphi, поставляемая на одном компакт-диске с Delphi. Программа может использовать низкоуровневый интерфейс функций API BDE для непосредственного обращения к данным, однако обычно между ней и BDE располага- ется слой компонентов, существенно упрощающих разработку программ. Как уже говорилось, невизуальные компоненты осуществляют непосредственную работу с BDE, и три из них (TTable, TQuery и TStoredProc) служат наборами данных, в то время как визуальные компоненты отображают поставляемые им данные и служат для соз- дания удобного интерфейса пользователя. Между наборами данных и визуальными компонентами обязательно располагаются компоненты TDataSource, играющие роль клапанов, открывающих или закрывающих потоки данных, которыми обмениваются источники с визуальными компонентами (рис. 3.2). В версии Delphi 5 появились средства, позволяющие работать с данными без BDE, - это ActiveX Data Objects {ADO) и InterBase Express (IPX). Особенности использова- ния новых средств описываются в заключительных главах книги.
Средства Delphi 69 Рис. 3.2. Взаимодействие с использованием компонентов. 3.3. ПОДДЕРЖИВАЕМЫЕ В DELPHI ТИПЫ БД 3.3.1. Локальные и файл-серверные БД В локальных и файл-серверных БД, как это следует из названия, базы данных мо- гут располагаться на машине клиента (локальные БД) или на сетевом файл-сервере (файл-серверные БД) (рис. 3.3). Ясно, что локальный вариант реально может обеспечить лишь однопользователь- ский режим доступа к данным. Этот вариант в корпоративной работе практически нс встречается, так как в нем чрезвычайно трудно синхронизировать содержимое не- скольких копий БД, количество которых (копий) должно равняться количеству одно- временно работающих с данными пользователей. Рис. 3.3. Типы БД: а) локальная; б) файл-серверная.
70 Глава 3 В файл-серверных БД данные располагаются на сетевом файл-сервере, который может быть доступен одновременно нескольким пользователям, поэтому к таким БД возможен многопользовательский режим доступа. Данные в БД хранятся в единствен- ном экземпляре, а каждый клиент в каждый момент времени работает с некоторой локальной копией этих данных, причем управление данными целиком возлагается на клиентские программы. Именно они должны заботиться о синхронизации локальных копий данных на каждом клиентском месте с содержимым основной (и единственной) базы данных. В обоих случаях BDE располагается на машине клиента и вместе с программой об- разует локальную СУБД, количество копий которой равно количеству пользователей. В таких системах, в принципе, возможен удаленный доступ, если используемая в них сеть позволяет пользователю находиться на удалении от офиса - в другом здании или в другом городе (вариант разветвленных локальных сетей, региональные служебные сети, наконец, сети с выходом в Internet). 3.3.2. Клиент-серверные БД Архитектура файл-сервер неэффективна по крайней мере в двух отношениях. 1. При выполнении запроса к БД, расположенной на файловом сервере, в дейст- вительности происходит запрос к локальной копии данных на компьютере пользователя. Поэтому перед выполнением запроса данные в локальной копии в полном объеме обновляются из реальной БД. Так, если таблица БД состоит из 10 000 записей, а для выполнения запроса нужно только 10 записей, все равно клиенту передаются все 10 000 записей. Таким образом, не нужно иметь слиш- ком много пользователей и запросов от них, чтобы серьезно загрузить сеть, что, конечно же, не может не сказаться на ее быстродействии. 2. Целостность БД обеспечивается клиентскими программами. Это потенциаль- ный источник ошибок, нарушающих физическую и логическую целостность данных, поскольку различные клиенты могут производить контроль целостно- сти по-разному или не проводить такого контроля вовсе. Намного эффективнее управлять БД из единого места и по единым законам. Поэтому безопасность при работе в архитектуре файл-сервер невысока и всегда присутствует элемент неопределенности. В такой архитектуре трудно обеспечить секретность данных и их защиту: таблицы хранятся на сервере в виде обычных файлов, поэтому любой, кто имеет доступ к каталогу (каталогам) сетевого сервера, где хранится БД, может изменять таблицы, копировать их, заменять и т. д. В архитектуре клиент-сервер между BDE и базой данных появляется важное про- межуточное звено - сервер БД (специальная программа, управляющая базой данных, см. рис. 3.4). Клиент формирует запрос к серверу на языке запросов SQL (Structured Query Language — структурированный язык запросов), являющемся промышленным стандар- том для реляционных БД. 5^-сеРвеР обеспечивает интерпретацию запроса, его вы- полнение, формирование результата и выдачу этого результата клиенту. При этом ресурсы клиентского компьютера не участвуют в физическом выполнении запроса: клиентский компьютер лишь отсылает запрос к серверной БД и получает результат, после чего интерпретирует его необходимым образом и предоставляет пользователю.
Средства Delphi 71 Так как клиентскому приложению посылается результат выполнения запроса, по сети передаются только те данные, которые в действительности нужны клиенту. В итоге снижается нагрузка на сеть. Кроме того, SQL-сервер, если это возможно, оптимизиру- ет полученный запрос таким образом, чтобы он был выполнен за минимально воз- можное время. Все это повышает быстродействие системы и снижает время ожидания результата запроса. Клиент Клиент Клиент Клиент Рис. 3.4. Клиент-серверная архитектура. При выполнении запросов сервером существенно повышается степень безопасно- сти данных, поскольку правила целостности данных определяются на сервере и явля- ются едиными для всех приложений, использующих эту БД. В результате исключается возможность определения противоречивых правил поддержания целостности. Мощ- ный аппарат транзакций, поддерживаемый З^Л-серверами, блокирует одновременное изменение одних и тех же данных различными пользователями и предоставляет воз- можность откатов к первоначальным значениям при внесении в БД изменений, закон- чившихся аварийно. Данные в серверной БД обычно физически хранятся на диске в виде одного боль- шого файла, что в сочетании с назначаемыми каждому пользователю паролями и при- вилегиями существенно повышает защиту данных от намеренной порчи и хищений. В архитектуре клиент-сервер используются промышленные серверы данных типа Oracle, Gupta, Informix, Sybase, MS SQL Server, DB2, InterBase и ряд других. 3.3.3. Архитектура с сервером приложений Развитие идей архитектуры клиент-сервер привело к появлению трехзвенной архи- тектуры доступа к базам данных (в литературе ее также называют многозвенной архи- тектурой, N-tier или multi-tier архитектурой). Архитектура клиент-сервер - двухзвенная: первым звеном в ней является про- грамма клиента, а вторым - сервер БД и сама БД.
72 Глава 3 В трехзвенной архитектуре создается вспомогательная программа, в которую включаются все компоненты-наборы данных, бывшие ранее (в двухзвенной архи- тектуре) собственностью клиентских приложений, а также вспомогательные компо- ненты TDatabase и TSession. Затем эта программа регистрируется в качестве СОМ- или CORBA-сервера, после чего она становится сервером приложений. Теперь кли- ентские машины могут не иметь BDE, а клиентские программы уже не включают в себя громоздкие коды компонентов-наборов и многих других вспомогательных компонентов. Для получения доступа к серверным данным они обращаются к уда- ленному (т.е. находящемуся на другой машине) серверу приложений, который и реализует необходимый обмен данными (рис. 3.5). Заметим, что на рис. 3.5 слева показан вариант размещения сервера приложений на машине сервера БД. Такой вариант наиболее популярен, т. к. снижает загрузку сети и гарантирует одновремен- ную работу обоих серверов. Вообще говоря, это не обязательно: сервер приложений может располагаться на любой сетевой машине, оснащенной BDE. Разумеется, в этом случае каталог его размещения должен быть доступным другим сетевым ма- шинам, а сама машина сервера приложений должна быть включена в период работы с сервером данных. Более того, в этой архитектуре можно использовать файл- серверный вариант размещения данных (на рис. - справа). Рис. 3.5. Архитектура с сервером приложений.
Средства Delphi 73 С помощью словарей БД можно перенести в компоненты-источники и связанные с ними поля часть бизнес-правил, касающуюся различных ограничений на значения вводимых данных. В этом случае неправильные данные будут отвергаться сервером приложений и не будут передаваться в сервер БД. В клиентской программе, которая в этой архитектуре называется облегченным или тонким клиентом (thin client), размещается клиентский набор данных, представляю- щий собой копию части данных из БД. Все изменения, которые пользователь вносит в данные, изменяют эту локальную копию и могут до нужной поры не передаваться в БД (режим отложенной обработки данных). Кроме того, при работе с громоздкими таблицами можно потребовать от сервера приложений передавать в локальный набор записи таблицы порциями, достаточными для одновременного отображения на экране клиента. Все эти меры существенно снижают загрузку сети и, следовательно, умень- шают время ожидания результата запроса.
Глава 4 ОБЗОР КОМПОНЕНТОВ ДЛЯ РАБОТЫ С БАЗАМИ ДАННЫХ 4.1. НЕКОТОРЫЕ ВАЖНЫЕ КЛАССЫ Описываемые ниже компоненты не представлены в палитре компонентов Delphi. Хотя они и произошли от класса TComponent и, следовательно, формально считаются компонентами, именно поэтому далее в книге они называются просто классами. Класс Назначение TDataSet TCustomADO- DataSet TBDEDataSet TDBDataSet TIBCustom- DataSet Являются предками активно используемых в приложениях компонентов- источников. Определяют ряд свойств и методов, наследуемых и частично переопределяемых компонентами TTable, TQuery и TStoredProc, а также компонентами-надстройками ADO. TDataSet определяет свойства и мето- ды для работы с БД, независимые от BDE. Многие из них являются абст- рактными или виртуальными. TBDEDataSet, наоборот, определяет ряд свойств и методов, зависящих от используемой BDE. TCustomADODataSet инкапсулирует свойства, методы и события, типичные для компонентов ADO. TIBCustomDataSet служит непосредственным родителем для компо- нентов, реализующих технологию InterBase Express (IBX). TColumn Столбец компонента TDBGrid, обладающий свойствами и методами, кото- рые позволяют управлять его поведением. TField Реализует поле ИД. Помимо полей, физически определенных в ТБД и включенных в состав конкретного НД, компонент TField создается для каждого вычисляемого поля или поля, возвращающего значение из друго- го НД (lookup-mne), а также для результатов вычисления выражений и агрегатных функций в 5£)£-запросах. Предоставляет набор свойств, мето- дов и событий, посредством которых можно управлять поведением поля. Собственно TField есть родительский класс для дочерних компонентов, реализующих поля конкретных типов (TStringField, TIntegerField и т. д.). TFieldDefs Позволяет получить информацию о полях, определенных в составе запи- сей текущей ТБД. TIndexDefs Позволяет получить информацию об индексах, определенных для текущей ТБД, и об индексных полях для текущего индекса. Реализует поиск индек- са по группе индексных полей. 4.2. НЕВИЗУАЛЬНЫЕ КОМПОНЕНТЫ 4.2.1. Страница Data Access Компоненты этой страницы используются как в локальных, так и в распределен- ных СУБД.
Обзор компонентов для работы с базами данных 7 5 Компонент Назначение TBatchMove Реализует операции над группой записей или таблиц. ТDataBase Активно используется при работе в архитектуре клиент-сервер. Позволяет соединяться с удаленной БД и управлять параметрами соединения, полу- чать информацию о БД, получать информацию об открытых НД и о дос- тупных таблицах БД. TDataSource Служит промежуточным звеном для соединения визуальных компонентов с компонентами-источниками. Позволяет устанавливать некоторые пара- метры НД, устанавливать состояние НД, отслеживать изменения в НД. TNestedTable Реализует вложенные таблицы. Вложенная таблица является полем для таблицы-владельца. TQuery Реализует набор данных, источником для которого являются одна или несколько таблиц БД. Структура записи НД состав НД определяются 5£Д-запросом. Используется для групповых операций обновления, добав- ления или удаления в ТБД, а также может выполнять любые другие дейст- вия, предусмотренные реализацией языка SQL для той СУБД, с которой работает TQuery. TSession Содержит информацию о текущем сеансе работы с БД. Позволяет опреде- лить список доступных БД, открывать, отыскивать и закрывать БД, управ- лять параметрами сеанса. TStoredProc Используется в архитектуре клиент-сервер для доступа к хранимым про- цедурам, расположенным на сервере БД. Хранимые процедуры кодируют- ся с помощью особого процедурного языка, хранят, как правило, часто употребляемые запросы к БД и могут разделяться между различными приложениями. Компонент TStoredProc наряду с компонентами ТТаЫе и TQuery является набором данных, поскольку может возвращать множество записей из одной или нескольких физических таблиц БД. TTable Реализует набор данных, источником для которого является одна таблица БД. Содержит множество методов, свойств и событий, посредством кото- рых программа оперирует с данными. TUpdateSQL Используется совместно с TQuery для обновления измененных данных в реальных таблицах БД, если созданный компонентом TQuery НД предна- значен только для чтения. 4.2.2. Страница MIDAS Компоненты этой страницы используются для создания распределенных СУБД на основе трехзвенной архитектуры (MIDAS — Multi-tier Distributed Application Services — обслуживание многозвенных распределенных приложений). Компонент Назначение TClientData- Set Локальный набор данных для клиента. TCorbaCon- nection Используется в клиентской части для создания соединения с удаленным сервером по технологии CORBA. TDataSet- Provider Брокер данных, расположенный на сервере приложений. Используется для соединения сервера приложения с приложением клиента.
76 Глава 4 TDCOMConnec- tion Используется в клиентской части для создания соединения с удаленным сервером по технологии DCOM. TSimple- ObjectBroker Предоставляет клиенту список доступных серверов. Дает возможность переключения на другой сервер при сбое или выгрузке сервера, с которым шла работа. TSocetCon- nection Используется в клиентской части для создания соединения с удаленным сервером по технологии WinSoc. TWebCon- nection Используется в клиентской части для создания соединения с удаленным сервером по протоколу HTTP. 4.2.3. Страница ADO Компоненты этой страницы являются надстройками над базовыми объектами ADO, поставляются в варианте Enterprise и устанавливаются, если в момент развертывания Delphi 5 принято соответствующее решение (т.е. по выбору). Компонент Назначение TADOCon- nection Этот компонент осуществляет связь остальных компонентов с ADO. С по- мощью своих свойств и методов он может осуществлять тонкую настройку соединения, обеспечивать необходимый уровень изоляции транзакций, управлять транзакциями и т. д. TADOCommand Этот компонент предназначен в основном для реализации 5££-запросов, не возвращающих никаких данных (подмножество Data Definition Language - язык определения данных языка структурированных запросов SQL). К пред- ложениям DDL относятся практически все, которые не начинаются зарезер- вированным словом SELECT. TADODataSet Этот компонент обеспечивает доступ к одной или нескольким ТБД с помо- щью запроса типа SELECT. Компонент рассчитан на возврат набора данных, поэтому его нельзя использовать для выполнения подмножества операторов DDL. TADOTable Этот компонент является прямым аналогом популярного 5£>Е-компонента TTable. TRDSCon- nection Обеспечивает связь с сервером приложений. TADOSto- redProc Компонент TADOStoredProc предназначен для исполнения хранимой проце- дуры сервера БД. TADOQuery В отличие от TADOCommand этот компонент преимущественно предназна- чен для получения набора записей из одной или нескольких таблиц БД. На самом деле фактически он целиком повторяет функциональность компонен- та TQuery, т. к. в него включен специфичный метод ExecSQL, с помощью которого компонент может выполнять предложения DDL языка SQL. 4.2.4. Страница InterBase Компоненты этой страницы служат для непосредственной (без BDE) связи клиент- ского приложения с сервером InterBase 5.5. Многие из них являются функциональны- ми аналогами компонентов из п. 4.2.1.
Обзор компонентов для работы с базами данных 77 Компонент Назначение TIBDatabase Осуществляет соединение с БД. TIBDatabase- Info Возвращает служебную информацию о БД (количество распределенных кэшей, количество страниц для чтения или записи и т. п.). TIBDataSet Реализует выполнение запроса SELECT к БД. TIBEvents Регистрирует некоторые события на сервере. В момент, когда событие про- исходит, срабатывает его обработчик OnEventAlert. TIBQuery Реализует выполнение запроса SELECT к БД. TIBSQL Реализует выполнение запроса к БД с максимальной скоростью. Не может использоваться с элементами Data Controls. TIBSQLMonitor Используется для мониторинга запросов. TIBStoredProc Выполняет хранимую процедуру. TIBTable Представляет набор данных из одной таблицы БД. TIBTransac- tion Реализует механизм транзакций на сервере. TIBUpdateSQL Обновляемый 5'(9£^зап£ос;__ 4.2.5. Страница Decision Cube На этой странице собраны компоненты для разработки систем принятия решений. Компонент Назначение TDecisionCube Осуществляет многомерное представление данных для систем принятия решений. TDecisionQuery Разновидность TQuery для работы с TDecisionCube. TDecision- Source Разновидность TDataSource для работы с TDecisionCube. 4.3. ВИЗУАЛЬНЫЕ КОМПОНЕНТЫ 4.3.1. Страница Data Controls Эта страница содержит визуальные компоненты обшего назначения. Компонент Назначение TDBChart Реализует графическое представление данных, хранящихся в компонентах типа «набор данных». TDBCheckBox Обеспечивает просмотр и изменение значения логического поля текущей записи НД. TDBComboBox Применяется для тех же целей, что и TDBListBox, но список - «выпадающий». TDBCtrlGrid Показывает содержимое нескольких записей одновременно в однотипных наборах визуальных компонентов. TDBEdit Обеспечивает просмотр и изменение значения поля текущей записи НД. Поле может быть любого типа, кроме МЕМО и BLOB. TDBGrid Показывает содержимое полей НД в «табличном» виде, когда записям соответствуют строки, полям - столбцы. TDBImage Позволяет просматривать графические поля и загружать в них изображения.
78 Глава 4 TDBLookupCombo Box Применяется для тех же целей, что и TDBLookupListBox, но список - ком- бинированного вида («выпадающий»). TDBLookupListB ox Применяется для'выбора значения поля из списка значений, который фор- мируется из определенного столбца другого набора данных. TDBMemo Позволяет просматривать и корректировать значение МЕМО-тпя в режи- ме текстового редактора. TDBListBox Применяется, когда нужно выбрать значение поля из предустановленного списка значений. Значения показываются в виде строк в списке фиксированного размера. TDBNavigator Позволяет осуществлять навигацию по записям НД, переводить НД в состояние вставки, изменения, добавления записи, запоминать изменения. TDBRadioGroup Обеспечивает возможность выбора значения для поля, которое может содержать фиксированное число вариантов значений. Значения показыва- ются в виде зависимых кнопок. TDBRichEdit Имеет то же назначение, что и TDBMemo, но позволяет работать с текстом формата RTF, включающим разные шрифты, графику и пр. TDBText Показывает значение поля текущей записи НД. 4.3.2. Страница QReport На этой странице собраны многочисленные компоненты для создания отчетов. Компонент Назначение TQRBand Основной инструмент формирования отчета. Создает заголовок отчета, верхний и нижний колонтитулы и собственно основную страницу. TQRChart Служит для размещения в отчете диаграмм, построенных по взятым из НД данным. TQRChildBand Используется для расширения основной полосы TBand. TQRComposite- Report Этот компонент облегчает создание отчетов, манипулирующих выборками данных из множества НД. TQRCSVFilter Компонент для экспорта отчета при его печати в формат CSV. TQRDBImage В отличие от TQRImage графические данные для этого компонента берутся из НД. TQRDBRichText Служит для отображения текстовых данных из НД в формате RTF. TQRDBText Используется для вывода в отчете текстовой информации из НД. TQRExpr Обеспечивает типичные для отчета вычисления - суммирование данных, вычисление среднего значения, поиск максимума и т. п. TQRGroup Используется совместно с компонентом Query для создания отчета типа главный-детальный. TQRHTMLFilter Компонент для экспорта отчета при его печати в формат HTML. TQRImage Этот компонент служит для размещения в отчете произвольной графической информации. TQRLabel Обеспечивает вывод в отчете статического текста, например, заголовков колонок. TQRMemo Обеспечивает вывод в отчете произвольного многострочного текста. TQRPreview Обеспечивает просмотр образа напечатанного отчета с возможностью дей- ствительной печати документа. TQRRichTfext В отличие от TQRMemo использует текст в формате RTF. TQRShape С помощью этого компонента можно поместить в отчет простейшую графи ческую фигуру - прямоугольник, окружность, линию и т. п. - TQRSubDetail Импортирует в отчет данные из подчиненного ИД.
Обзор компонентов для работы с базами данных 79 TQRSysData Используется для вывода разнообразной системной информации - текущей даты, времени, номера печатаемой страницы, общего количества записей и т. п. TQRTextFilter Компонент для экспорта отчета при его печати в формат текстовых строк. TQuickRep • Определяет поведение и характеристики отчета в целом. После помещения на форму превращает ее в отчет. Служит контейнером для размещения остальных компонентов отчета. 4.3.3. Страница Decision Cube Компонент Назначение TDecisionGraph Представляет в графическом виде многомерные данные. TDecisionGrid Разновидность компонента TDBGrid для табличного представления мно- гомерных данных. TDecisionPivot Реализует двухмерную проекцию многомерных данных. 4.4. МОДУЛИ ДАННЫХ Как уже говорилось в п. 3.1, модули данных представляют собой невизуальные (т. е. невидимые в работающей программе) контейнеры для размещения невизуальных же компонентов доступа к данным. Модули данных впервые появились в версии 2 и ши- роко использовались для обеспечения централизованного доступа к компонентам- наборам, их полям, бизнес-правилам и т. п. В конце концов часто они служили удоб- ным местом размещения глобальных переменных, типов и подпрограмм. В версии 4 роль модулей данных возросла за счет того, что многочисленные экс- перты создания трехзвенных приложений использовали специальные варианты этих модулей для реализации серверов приложений. Ниже описываются дополнительные возможности этих инструментов, доступные в версии 5. Создание нового модуля данных представляет собой тривиальную задачу: щелкни- те по пиктограмме D панели инструментов Delphi или выберите File | New. В обоих случаях будет вызвано окно репозитория Delphi. На его закладке New разыщите пик- тограмму Data Module, щелкните по ней и закройте окно репозитория кнопкой ОК - к вашей программе будет добавлен очередной модуль данных (рис. 4.1). Рис. 4.1. Окно модуля данных.
80 Глава 4 Замечание. В репозитории есть отдельная закладка Data Modules с единственной пиктограммой Customer Data. Не следует использовать этот модуль, т. к. он соз- дан для демонстрационного примера и уже содержит некоторые компоненты дос- тупа К БД DBDEMOS. Заметим также, что стандартное имя модуля выбрано слишком длинным. Как пока- зывает практика, имя модуля данных часто используется в качестве префикса в име- нах таблиц или их полей, например: if DataModulel.tbBookName.AsString= ... Разумеется, излишний лаконизм вреден, но, согласитесь, запись if DM.tbBookName.AsString = ... не только не теряет своей выразительности, но и экономит вам при наборе десяток лишних ударов по клавиатуре (и сколько при наборе длинной программы?). Наш со- вет: выбирайте для модулей данных по возможности короткие имена. Все окно модуля поделено на две части: левое содержит дерево описания всех раз- мещенных в модуле компонентов, а правое - поле с закладками. Закладка Components служит для размещения собственно компонентов, и в этом смысле функциональность ее окна ничем не отличается от функциональности модулей данных предыдущих вер- сий, в то время как закладка Data Diagram открывает окно формирования диаграмм данных. Диаграммы данных позволяют наглядно отобразить существующие между наборами данных реляционные связи. Вот какие связи (рис. 4.2.) можно сформировать в этом окне для уже упоминавшихся таблиц модуля Customer Date (закладка репози- тория Data Modules). Рис. 4.2. Пример отображения реляционных связей.
Обзор компонентов для работы с базами данных 81 Вставлять компоненты в модуль можно как с помощью закладки Components, так и методом перетаскивания компонентов на дерево в левой части окна модуля данных. При этом если компонент из палитры перетащен и брошен1 на какой-либо компонент, уже имеющийся на дереве, он автоматически становится по отношению к нему “дочерним”. Например, если на таблицу Table! бросить источник DataSource!, в свой- ство DataSet источника будет помещена ссылка на Table! (рис. 4.3). Рис. 4.3. Пример связывания компонентов. Если на псевдоним DBDEMOS (см. выше, рис. 4.3) положить еще одну таблицу, в ее свойство DatabaseName будет автоматически помещен этот псевдоним и т. д. При формировании дерева компоненты, для которых еще не определены ключевые свойст- ва, выделяются красным. В нашем случае красная окантовка с таблицы Table! будет снята, если мы свяжем ее с конкретной ТБД. С помошью мыши положение узлов на дереве можно менять, обеспечивая: • замену набора данных для источника; • изменение сессии для базы данных; • перенос набора данных из одной БД в другую (разумеется, физическое положе- ние НД в этом случае останется без изменения). Окно с закладкой Data Diagram используется для формирования и отображений связей четырех типов: • связь от родителя к дочернему компоненту (линия со сплошной стрелкой, ини- циируется кнопкой ®о); • главный-детальный (линия с концевыми квадратиками, инициируется кнопкой W); • типа lookup (линия с изображением глаза на конце, инициируется кнопкой ^>); • подразумеваемая связь (линия с галочкой, инициируется кнопкой ^с>). Реляционные связи главный-детальный и lookup могут формироваться как обыч- ными способами, так и средствами модуля данных, остальные связи - только средст- вами модуля. 1 Термин “перетащить-и-бросить” в данном случае неточен. Процедура реализуется таким образом. Вна- чале, как обычно, выбирается нужный компонент, причём после выбора можно отпустить кнопку мышн. Затем указатель мыши позиционируете^ на нужную часть дерева и нажимается левая кнопка мыши.
82 Глава 4 Если между двумя или несколькими таблицами уже установлены связи главный- детальный или lookup, то при «перетаскивании» этих таблиц из дерева в qkho Data Diagram соответствующие таблицы автоматически вычерчиваются с нужными связя- ми Например, на рис. 4.2 между таблицами orders и items создана связь главный- детальный по полю OrderNo на компонентном уровне (для этого в ITEMS создан ин- декс по этому полю, а в свойства MasterSource и MasterField таблицы ITEMS помеще- ны соответствующие ссылки). При перетаскивании этих таблиц в поле Data Diagram модуль данных учел это и вычертил соответствующую связь. Однако, если бы этой связи не существовало, ее можно было бы создать средствами модуля следующим образом. 1. Начните новый проект и поместите в него модуль данных. 2. На окно с закладкой Components поместите компонент TDatabase, в окне Ин- спектора Объектов установите в его свойство AliasName значение DBDEMOS, а в свойство DatabaseName - DMDEMO. Заметим, что операции с компонентами TDatabase проще осуществлять «стандартным» методом, чем перетаскиванием мышью. 3. Перетащите мышью на вновь созданный узел дерева две таблицы и в окне Ин- спектора Объектов установите в их свойства TableName значения ORDERS и items соответственно. 4. Перетащите на таблицу orders компонент TDataSource (рис. 4.4, а). 5. Перетащите с дерева в окно Data Diagram обе таблицы примерно так, как пока- зано на рис. 4.4, б (orders является в этой связи главной, и поэтому ее следует разместить выше items). Заметим, что это «перетаскивание» осуществляется обычным для метода Drag&Drop приемом: с удержанием и последующим от- пусканием мыши. а) б) Рис. 4.4. Между таблицами пока еще не сформирована связь главный-детальный. Eft 5. Щелкните по кнопке —слева в окне Data Diagram, чтобы начать формирова- ние связи, подведите указатель мыши к нижней части прямоугольника ORDERS (в этот момент указатель должен принять форму крестика), нажмите левую кнопку мыши и чертите линию к нижнему прямоугольнику items (для страхов- ки лучше немного пересечь верхнюю границу прямоугольника), после чего от- пустите кнопку мыши. 6^ В этот момент программа проверит, существует ли индекс в таблице items, и, если существует, раскроет окно, показанное на рис. 4.5, а, в котором вам следу- ет установить связь по индексным полям OrderNo двух таблиц. ’ Р л я IJ .
Обзор компонентов для работы с базами данных 83 Field Link Designei а) б) Рис. 4.5. Установление индексной связи двух таблиц: а) выбор поля связи; б) отображение связи. После установления связи в окне Data Diagram появится соответствующая линия связи (рис. 4.5, б). Таким же образом (с помощью кнопки "тк) формируется «просмотровая» связь loookup. На заметку. Связи, установленные между компонентами, не зависят от способа установления связи. Если щелкнуть по линии связи и вызвать локальное меню, с помощью продолжения Remove relationship можно удалить отображаемую в окне Data Diagrams связь, при этом будут уничтожены (обнулены) и нужные свойства в соответствующих компонентах, обеспечивающие эту связь. На диаграммах можно отображать не только реляционные связи, но также и произ- вольные комментарии (рис. 4.6). Рис. 4.6. Примеры других связей. 4.5. ИЕРАРХИЯ КЛАССОВ ДЛЯ ПРЕДСТАВЛЕНИЯ ДАННЫХ И ДОСТУПА К НИМ Ниже приводится иерархия классов, использующихся при работе с БД. Все они яв- ляются потомками класса TComponent, родительскими классами которого являются не
84 Глава 4 показанные в этой схеме классы TObject -> TPersistent -> TComponent. Цифрами обо- значен иерархический уровень класса относительно TComponent. TComponent 1 TSession 1 TDatabase 1 'TDataSource 1 TDataSet 1 2 TCustomADODataSet 1 2 TIBCustoDataSet 1 2 TBDEDataSet 12 3 TDBDataSet 12 3 4 TTable 12 3 4 TQuery 12 3 4 TStoredProc 1 2 TClientDataSet 1 TField 1 2 TBlobField 12 3 TGraphicField 12 з TMemoField 1 2 TBooleanField 1 2 TBinaryField 12 2 TBytesField 12 3 4 TVarBytesField 1 2 TDateTimeField 12 3 TDateField 12 3 TTimeField 1 2 TNumericField 12 3 TBCDField 12 3 TFloatField 12 з 4 TCurrencyField 12 3 TIntegerField 12 з TAutoIncField 12 3 TSmalllntField 12 3 TWordField 1 2 TStringField 1 TCustomRemoteServer 1 2 TDispatchConnection 12 3 TCOMConnection 12 3 4 TDCOMConnection 1 2 3 4 5 TRemot^eServer 12 3 4 5 TMidasConnection 12 .3 TCORBAConnection 12 3 TSocetConnection 12 3 TOLEnterpriseConnection 1 TCustomProvider 1 2 TBaseProvider 12 3 TDataSetProvider 12 3 TProvider 1 TCustomObjectBroker 1 2 TSimpleObjectBroker
Обзор компонентов для работы с базами данных 85 1 TDataModule 1 2 TRemoteDataModule 12 з TCORBADataModule 12 з TMTSDataModule Напомним, что классы-потомки наследуют все поля, методы и свойства своего ро- дительского класса, поэтому в книге не повторяется та часть информации-, которая рассмотрена при описании родительского класса. Например, в одном из последующих разделов достаточно подробно описывается родительский класс TField (поле набора данных), его методы, свойства и события. Эта информация уже не повторяется при описании дочерних классов TBlobField, TBooleanField и др.
Глава 5 РАБОТА С ПОЛЯМИ Класс TField позволяет обращаться к полям таблиц баз данных (ТБД). Каждый на- бор данных — не важно, создан ли он с помощью ТТаЫе, ТQuery или TStoredProc, — состоит из записей, а те, в свою очередь, состоят из полей. Таким образом, в составе записи имеется минимум одно поле. 5.1. ОБЗОР СВОЙСТВ, МЕТОДОВ И СОБЫТИЙ 5.1.1. Свойства Свойство Назначение type TAlignment = (taLeftJustify, taRightJustify, taCenter); property Alignment: TAlignment; Определяет выравнивание значения поля при его отображении: taLeftJustify - прижать влево; taRightJustify - прижать вправо; taCenter - центрировать. property AsBoolean: Boolean; Приводит значение поля к логическому типу. property AsCurrency: Currency; Приводит значение поля к типу Currency. property AsDateTime: TDateTime; Приводит значение поля к типу дата-время. property AsFloat: Double; Приводит значение поля к типу Double. property Aslnteger: Integer; Приводит значение поля к типу Integer. property AsString: String; Приводит значение поля к типу String. property AsVariant: Variant; Приводит значение поля к типу Variant. property AttributeSet: String; Содержит имя набора атрибутов поля в словаре данных (см. ниже примечание 1). type TAutoRefreshFlag = (arNone, arAutoInc, arDefault); property AutoGenerateValue: TAutoRefreshFlag; Указывает, будет ли значение поля создаваться самой БД (автоинкрементное поле - arAutoInc или поле с умалчиваемым значением - arDefault) property Calculated: Boolean; Содержит True, если значение поля вычисляет- ся в обработчике события OnCalcFields. property CanModify: Boolean; Содержит True, если значение поля можно изменять. property ConstraintErrorMessage.: String; Содержит сообщение, которое будет появлять- ся при нарушении ограничений на значение поля. property CurValue: Variant; Содержит текущее значение с учетом возмож- ных изменений, внесенных другими пользова- телями. property Customconstraint: String; Содержит .^/.-строку, определяющую ограни- чения на значения данных (см. ниже примеча- ние 2). property DataSet: TDataSet; Связанный с полем набор данных (тип TDataSet рассматривается в гл. 6). property “DataSize: word; Содержит размер данных.
Работа с полями 87 type TFieldType = (ftUnknown, ftString, ftSmallint, ftlnteger, ftWord, ftBoolean, ftFloat, ftCurrency, ftBCD, ftDate, ftTime, ftDateTime, ftBytes, ftVarBytes, ftAutoInc, ftBlob, ftMemo, ftGraphic, ftFmtMemo, ftParadoxOle, ftDBaseOle, ftTypedBinary, ftCursor, ftFixedChar, ftWideString, ftLargeint, ftADT, ftArray, ftReference, ftDataSet, ftOraBlob, ftOraClob, ftVariant, ftlnterface, ftIDispatch, ftGuid);); property DataType: TFieldType; Определяет тип данных: ftUnknown - неизвест- ный тип; ftString - строковый тип; ftSmallint — тип Smalllnt;ftlnteger - тип Integer; ftWord - тип Word', ftBoolean - логический тип; ftFloat - ве- щественный тип; ftCurrency - денежный тип; ftBCD - двоично-десятичные данные;/Юа/е - дата;ftTime — время; ftDateTime — дата-время; ftBytes - строка байтов фиксированной длины; ftVarBytes - строка байтов переменной длины; ftAutoInc - автоинкрементное поле; ftBlob - двоичное поле переменной длины; ftMemo - многострочный текст; ftGraphic - графическое изображение^^тМ/ето - форматированный текст; ftParadoxOle - поле ParadoxOle; ftDBaseOle - поле DBaseOle;ftTypedBinary — типизированное двоичное тк>п&\ ftCursor-со- стояние курсора НД для сохраняемой процеду- ры Oracle', ftFixedChar - строка символов фик- сированной длины: ftWideString - строка двух- байтных символов; ftLargelnt - длинное целое значение; ftADT - данные абстрактного типа; ftArray - массив; ftReference - ссылочный тип; ftDataSet - набор данных; ftOraBlob - поле BLOB в таблице Oracle 8;ftOraClob - поле CLOB в таблице Oracle 8;, ftVariant — поле содержит значение типа вариант, ftlnterface - ссылка на интерфейс IUnknown; ftlDispatch- ссылка на интерфейс Dispatch, ftGuid—гло- бально-уникальный идентификатор (см. ниже примечание 3). property DefaultExpression: String; Содержит 5£?£-выражение, определяющее умалчиваемое значение (т. е. если пользователь не ввел значение). property DisplayLabel: String; Содержит текст заголовка столбца в TDBGrid. property DisplayName: String; Содержит имя поля: совпадает с DisplayLabel, если это свойство определено, в противном случае совпадает с FieldName. property DisplayText: String; Содержит текст, который подготовлен обра- ботчиком события OnGetText для отображения значения в визуальных компонентах. property Displaywidth: Integer; Содержит количество символов свойства DisplayText; после умножения на среднюю ширину символа определяет ширину колонки таблицы. property EditMask: String; Определяет маску, которая будет использо- ваться для фильтрации и форматирования ввода значения (см. ниже примечание 4). property EditMaskPtr: String; Открывает доступ только-для-чтения к свой- ству EditMask для защиты маски от случайных изменений.
88 Глава 5 type TFieldKind = (fkData, fkCalcu- lated, fkLookup, fklnternalCalc, fkAggregate) ; property FieldKind: TFieldKind; Определяет тип поля: fkData - обычное поле; fkCalculated - вычисляемое поле; fkLookup - Lookup-тюле; fklnternalCalc - внешнее вычис- ляемое поле (см. ниже); fkAggregate - агрегат- ное поле. property FieldName: String; Имя поля, если не задано свойство DisplayLabel. property FieldNo: Integer; Содержит порядковый номер поля в наборе данных (нумерация с 1). Используется при непосредственном обращении к API BDE. property FullName: String; Определяет префикс для FieldName в состав- ных полях. property HasConstraints: Boolean; Содержит True, если на значение поля наложе- ны ограничения. Не учитывает ограничения, вводимые маской EditMask. property ImportedConstraint: String; Содержит .^/.-предложение, определяющее реализуемые в сервере БД ограничения на значение поля. property Index: Integer; Содержит индекс поля; определяет порядок, в котором поля показываются в TDBGrid. property IsIndexField: Boolean; Содержит True, если поле индексное. property IsNull: Boolean; Содержит True, если не определено значение поля. property KeyFields: String; Список индексных полей текущего набора для связи с LookupDataSet. property Lookup: Boolean; Содержит True, если тип поля ftLookup. Введе- но для совместимости с ранними версиями Delphi. property Lookupcache: Boolean; Разрешает/запрещает кэширование значений записей для lookup-тюлей. property LookupDataSet: TDataSet; Определяет НД, в котором ищутся значения полей LookupKeyFields. property LookupKeyFields: String; Список индексных полей в LookupDataSet, кото- рые должны соответствовать полям KeyFields. property LookupList: TLookupList; Список значений для lookup-тюлей. property LookupResultField: String; Значение, выбранное из списка LookupList. property NewValue: Variant; Новое значение поля (до завершения транзак- ции). property Offset: Word; Содержит количество байт, на которое было увеличено кэшируемое BLOB-тюле. property OldValue: Variant; Старое значение поля. property Origin: String; Содержит имя поля в физической ТБД. property ParentField: TObjectField; Указывает родительское поле во вложенном поле. Содержит NIL, если текущее поле не является частью сложного поля. type TProviderFlag = (pflnUpdate, pflnWhere, pflnKey, pfHidden); TProviderFlags = set of TProviderFlag; property ProviderFlags: TProviderFlags; Указывает, как использовать это поле при пересылке серверу пакета записей для обнов- ления: pflnUpdate- поле-появляется в предло- жении UPDATE и, следовательно, может об- новляться; pflnWhere - поле появляется в пред- ложении WHERE и, следовательно, использу-
Работа с полями 89 ется для поиска обновленных записей; pflnKey - поле используется для поиска записей в слу- чае возникновения ошибки обновления; pfHidden - поле включено в пакет для обеспе- чения уникальности записей и не показывается пользователю. property Readonly: Boolean; Содержит True, если поле предназначено толь- ко для чтения. property Required: Boolean; Если содержит True, значение каждой записи поля не может быть пустым. property Size: Word; Текущая длина записи в полях с переменной длиной записи. property Text: String; Текст, которым отображается значение записи в режиме редактирования. В остальных режи- мах запись отображается содержимым DisplayText. type TFieldChars = set of Char; property validChars: TFieldChars; Определяет набор символов, которые могут использоваться при вводе значения поля. property Value: Variant; Содержит текущее значение поля. property Visible: Boolean; Разрешает/запрещает показ поля в визуальных компонентах. Примечание 1. Набор атрибутов поля в словаре данных определяет вид, в котором данные отображаются в визуальных компонентах на этапе разработки программы, а также ограничения и умалчиваемые значения. Каждый набор имеет имя, определяемое свойством AttributeSet, что позволяет использовать его в других полях. Примечание 2. Ограничения, определяемые свойством CustomConstraint, записы- ваются в виде строк типа х> О and ХсЮО. Примечание 3. Новые типы полей ftCursor, ftFixedChar, JlWideChar, ftLargelnt, ftADT, ft Array, ftReference, ftOraBlob и ftOraClob введены для поддержки промышлен- ного сервера БД Oracle8 (подробнее см. гл. 35). Новые типы ftVariant, ftinterface, ftIDispatch и ftGuid используются в технологии ADO. Примечание 4. Маска состоит из трех частей, отделенных друг от друга символом «;». Первая часть задает собственно маску ввода, вторая — это символ «О» или «1», определяющий, включается ли в форматируемое значение результат наложения маски на исходный текст или только исходный текст («О» - исходный текст). В третьей части указывается символ, который в окне редактора будет стоять в полях, предназначенных для ввода символов (текст в редакторе может содержать символы маски; например, для ввода семизначного номера телефона текст в начале ввода может выглядеть так: (095)ХХХ-ХХ-ХХ где доступные для ввода пользователя поля указаны символом «X» - последним сим- волом в шаблоне). Маска состоит из описателей полей ввода, специальных символов и литералов. Описатель указывает, какой именно символ может ввести пользователь в данное поле (описатель всегда описывает поле единичной длины, т.е. рассчитанное на ввод одного символа). Литерал вставляется в текст, показываемый в окне редактора, но при вводе
90 Глава 5 курсор перескакивает через литерал и не дает пользователю возможности изменить его. Литералами считаются любые символы, кроме описателей полей и специальных символов, а также любой символ, которому предшествует символ «\». Специальные символы формируют дополнительные указания редактору. Описатели полей ввода представлены в следующей таблице: Сим- вол Поле... Сим- вол Поле... L должно содержать букву С может содержать любой символ 1 может содержать букву 0 должно содержать цифру А должно содержать букву или цифру 9 может содержать цифру а может содержать букву или цифру # может содержать цифру, «+»,«-» с должно содержать любой символ Специальные символы: Символ Значение \ Следующий символ - литерал. Позволяет вставить в маску литералы из символов описателей полей ввода и специальных символов. : На это место вставляется символ-разделитель Windows для часов, минут, секунд. / На это место вставляется символ-разделитель Windows для полей даты. г Разделитель частей шаблона. 1 Подавляет все ведущие пробелы. 1 > Все следующие за ним поля ввода преобразуют буквы к заглавным. < Все следующие за ним поля ввода преобразуют буквы к строчным. о Отменяет преобразовани^укв. Примеры: Маска Вид в редакторе Ввод пользователя Результат (095)000-0000;0;х (095)ххх-хххх 1234567 1234567 (095)000-0000;1;х (095)ххх-хххх 1234567 (095)123-4567 (095)\0\00-0000; 1; . (095)00...... 12345 (095)001-2345 5.1.2. Методы Метод Назначение procedure Assign(Source: TPersistent); Копирует значение поля из Source в собствен- ное свойство Value. Поля должны быть со- вместимых типов. procedure AssignValue(const Value: TVarRec) ; Преобразует вариант Value с помощью метода AsXXXX и помещает результат в собственное свойство Value. procedure Clear; Очищает значение поля. constructor Create(AOwner: TComponent); override; Создает и инициирует объект-поле. destructor Destroy; override; Уничтожает объект-поле. function FocusControl; Передает фокус ввода первому визуальному компоненту, связанному с полем.
Работа с полями 91 function GetData(Buffer: Pointer): Boolean; Помещает в буфер Buffer неотформатирован- ное значение поля. Используйте DataSize для определения требуемых размеров буфера. Метод не может копировать BLOB-поля. function GetParentComponent: TComponent; override; Возвращает ParentField или, если таблица не является вложенной, DataSet. Используется при работе с потоками. function HasParent: Boolean; override; Возвращает True, если поле - вложенное. class function IsBlob: Boolean; virtual; Возвращает True, если поле хранит BLOB- значения. function IsValidChar(Inputchar: Char): Boolean; virtual; Возвращает True, если символ InputChar мо- жет присутствовать в текстовом представле- нии значения поля. procedure RefreshLookupList; Заново создает список LookupList. procedure SetData(Buffer: Pointer); Копирует данные из Buffer в свойство Value. procedure SetFieldType(Value: TFieldType); Устанавливает новый тип данных. В TField ничего не делает - он предназначен для пере- крытия в дочерних классах. procedure Validate(Buffer: Pointer); Создает событие On Validate для проверки ^отформатированных данных из буфера Buffer. 5.1.3. События Событие С чем связано type TFieldNotifyEvent = procedure (Sender: TField) of object; property OnChange: TFieldNotifyEvent; Возникает после изменения данных и успеш- ной записи их в буфер. type TFieldGetTextEvent = procedure(Sender: TField; var Text: String; DisplayText: Boolean) of object- property OhGetText: TFieldGetTextEvent; Обработчик этого события готовит текст для свойств DisplayText и Textl Умалчиваемый обработчик помещает в переменную Text значение Value.AsString. Параметр DisplayText имеет значение True, если текст предназначен для свойства DisplayText. type TFieldSetTextEvent = procedure(Sender: TField; const Text: String) of object; property OnSetText: TFieldSetTextEvent; Возникает при записи данных из параметра Text в свойство Text. type TFieldNotifyEvent = procedure (Sender: TField) of object- property OnValidate: TFieldNotifyEvent; Возникает после изменения значения до запи- си в буфер. Обработчик этого события должен проверить правильность введенных данных. 5.2. ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ-ПОЛЕЙ В Delphi имеется возможность использовать при работе с НД или все поля, опреде- ленные в ТБД на текущий момент, или только часть существующих полей. Использо- вание только части полей дает то преимущество, что значения неиспользуемых полей
92 Глава 5 не могут быть случайно изменены, поскольку неиспользуемые поля считаются неиз- вестными. По умолчанию для каждого компонента-набора доступны все поля НД. К полю в этом случае можно обращаться с помощью метода FieldByName компонента-набора: function FieldByName(const FieldName: String): TField или через его свойство property Fields[Index]: TField. Для удобства работы с конкретным полем можно создать объект-поле класса TField или одного из его специализированных потомков TStringField, TIntegerField, TBlobField и т. д. Объект-поле создается на этапе конструирования программы с помощью редак- тора полей. Если определен объект-поле, получить доступ к полю можно по имени этого объекта. Редактор полей присваивает объектам-полям имена путем сцепления имени источника данных и имени поля. Если, например, источник Tablel связан с НД, у кото- рого определен объект для поля Town, объекту будет присвоено имя TablelTown, и мож- но использовать прямое обращение к нему. Например, эти операторы эквивалентны: Tablel.FieldByName(1 Town').AsString := ’Москва'; Tablel.Fields[0].AsString := 'Москва'; TalplelTown.AsString := 'Москва'; Совет. Поскольку создание объектов-полей широко используется на практике, имеет смысл увеличить семантику автоимени и называть ком- поненты-наборы именами связанных с ними таблиц. Например, назвав ТТаЫе именем связанной с ним таблицы Books, получим автоимена типа BooksAuthor, BooksYear, BooksPages и т. п. Если к одной физической таб- лице обращаются разные клмпоненты-наборы, их имена можно допол- нять суффиксами. Например, tbBooks, quBooks, spBooks — соответственно для таблицы, запроса или хранимой процедуры. Определить во время выполнения приложения, используются ли для набора дан- ных все поля по умолчанию или только их часть, можно при помощи свойства property DefaultFields: Boolean. Значение True указывает, что используются поля по умолчанию; False — что исполь- зуются поля, определенные при помощи редактора полей. Если хотя бы для одного поля НД создан объект-поле, все поля НД, для которых такие объекты не определены, становятся недоступными. К «несуществующим» полям обратиться из данного НД нельзя. Такие попытки будут возбуждать исключительные ситуации с сообщением Field <имя> not found. Вновь вернуться к использованию всех полей НД можно только на этапе конструиро- вания программы, удалив в редакторе полей все определенные ранее объекты или добавив с его помощью объекты для недостающих полей. Замечание. Если нужно иметь доступ к полю, но не показывать его значе- ний в компонентах, визуализирующих данные (например, в компоненте TDBGrid), в свойство Visible этого поля следует поместить значение False.
Работа с полями 93 Для вызова редактора полей нужно дважды щелкнуть по установленному на форме компоненту-источнику или щелкнуть по нему правой кнопкой мыши и выбрать Fields Editor. Предварительно источник должен быть связан с нужной ТБД: в ТТаЫе должны быть определены свойства DatabaseName и TableName, в TQuery - DatabaseName и SQL (т. е. это Йоле должно содержать текст 5()£-запроса, который создаст НД), в TStoredProc - DatadaseName и StoredProcName. Чтобы добавить объекты-поля, щелкните по окну редактора правой кнопкой и выбе- рите Add Fields (рис. 5.1). В появившемся списке будут выделены поля, для которых еще не созданы объекты. Вы можете выбрать любую комбинацию полей в списке Add Fields и нажать кнопку ОК - для выбранных полей будут созданы объекты, а окно редактора будет содержать список этих полей. Чтобы удалить определение какого-либо объекта, нужно выбрать соответствующее поле в окне редактора и нажать клавишу Delete. Add Fields Available fields’ BCODE В NAME В PER BQUAN BBUY БОРТ BROZN BCHANGE В TYPE BSIGN BMEMD BSQUAN BUSES.. OK I Cancel Help a) 6) Puc. 5.1. Работа с редактором полей: а) добавление полей; б) список полей, для которых созданы объекты. Если необходимо изменить свойства конкретного поля или написать обработчик для какого-либо связанного с ним события, необходимо в редакторе полей выбрать нужное поле и, используя Инспектор Объектов, установить значение в свойство или определить обработчик события. 5.3. ТИПЫ ПОЛЕЙ Поля в таблицах баз данных различаются по типу - символьные, целочисленные, логические, BLOB-поля и т.д. Для каждого типа данных есть соответствующий спе- циализированный потомок. Собственно TField есть родительский тип, определяющий базовые свойства и методы для своих потомков — типизированных полей. Иерархия классов полей такова: TField TBlobField TGraphicField Большой двоичный объект. Графическое поле (работает с содержимым BLOB-поля как с графическим изображением)
94 Глава 5 TMemoField TBooleanField TBinaryField TBytesField TDateTimeField TDateField TTimeField TNumericField TBCDField TFloaTField Л/еио-поле (интерпретирует BLOB-поле как большой текст). Логическое поле. Нетипизированное двоичное поле. Байтовые значения фиксированной длины. TVarBytesFieid Байтовые значения переменной длины. Дата и время. Только дата. Только время. Числовые значения. BCD-значения. Вещественные значения. TCurrencyFieid Денежные значения. TintegerFieid Целочисленные значения. TAutoincFieid Автоинкрементное целочисленное значение. TSmaiiintFieid Типа Smalllnt. TWordFieid Типа Word. TStringFieid Типа String. 5.4. ОБРАЩЕНИЕ К ЗНАЧЕНИЮ ПОЛЯ К значению поля можно обратиться при помощи свойств Value и AsXXXX. Свойство property Value: Variant возвращает значения следующих типов: Значение Кто возвращает Variant Все компоненты String TStringFieid, TBlobField Longlnt TAutoincFieid, TintegerFieid, TSmaiiintFieid, TWordFieid Double TBCDField, TCurrencyFieid, TFloatField Boolean TBooleanField TDateTime TDateField, TDateTimeField, TTimeField Существуют следующие свойства AsXXXX для приведения типов полей: property property property property property Aslnteger: Integer; property AsString: String; property AsVariant: Variant; AsBoolean: Boolean; AsCurrency: Currency; AsDateTime: TDateTime; AsFloat: Double; Каждое из этих свойств приводит значение поля к соответствующему типу данных, означенному в названии свойства. Например, если TablelNumber - поле TintegerFieid, для приведения его к типу String нужно воспользоваться свойством AsString’. Editl.Text := TablelNumber.AsString; Разумеется, тип поля должен быть совместимым с типом данных, к которому приво- дится значение поля. Например, если TablelSumma — поле TFloatField, попытка при- вести его к несовместимому типу Boolean приведет к ошибке. В приводимой ниже таблице показана совместимость значений полей разных типов.
Работа с полями 95 Обозначения: = - типы равнозначны; + - преобразование возможно; + RI - преобразование возможно, округление до ближайшего целого; ? - преобразование происходит, если возможно; часто зависит от формата показа (свойство Display Format)-, x - преобразование не разрешено; memo - имеет значение для Мето-тля. Тип поля AsString Aslnteger AsFloat AsDateTime AsBoolean TStringField = 2 ? 2 TIntegerField + = + X X TSmallinTField + = + X X TWordField + = + X X TFloatField + + RI = X X TCurrencyField + + RI = X X TBCDF,ield + + RI = X X TDateTimeField + X CM. 1 = X TdateField 9 X CM. 1 = X TTimeField 2 X см. 2 — X TBooleanField cm. 3 X X X = TBytesField + X X X X TVarBytesField + X X X X TBlobField memo X X X X TMemoField memo X X X X TGraphicField memo X X X X Примечания. 1. Преобразование даты к числу дней с 01.01.0001. 2. Преобразование времени делением на 24 часа. 3. Преобразование в строку True или False. 5.5. ПРОВЕРКА ПРАВИЛЬНОСТИ ВВЕДЕННОГО В ПОЛЕ ЗНАЧЕНИЯ Проверить введенное в поле значение на его соответствие некоторым ограничени- ям или условиям можно в обработчике события OnValidate. Это событие наступает при изменении значения поля вручную или программно до выполнения метода Post набора данных, который запоминает изменения БД. Поэтому, если полю присвоено неверное значение, выполнение метода Post можно предотвратить, вызвав метод Abort или возбудив исключительную ситуацию. Пусть, например, строковое поле Tablel Company не должно содержать символа «@». Тогда можно использовать такой обработчик события OnValidate'. procedure TForml.TablelCompanyValidate(Sender: TField); begin if pos('@', TablelCompany.AsString) > 0 then begin ShowMessage(’Обнаружен символ @ 1 ') ; Abort; end; end; или procedure TForml.TablelCompanyValidate(Sender: TField); begin if pos('@',TablelCompany.AsString) > 0 then raise Exception.Create('Неверное значение'); end;
96 Глава 5 Для проверки введенного значения может быть использовано и другое событие - OnSetText. Подобно событию OnValidate оно возникает при изменении значения поля, однако на момент события новое значение полю не присвоено, и, если этого не сде- лать программно в обработчике события, оно останется прежним. Пусть, например, значение поля TablelPURPRICE типа TFloatField не должно превышать 100. Тогда обработчик события будет выглядеть так: procedure TForml.TablelPUR_PRICESetText(Sender: TField; const Text: String); var Tmp: Real; begin Tmp := StrToFloat(Text); if Tmp > 100 then ShowMessage('Ошибочное значение') else TablelPUR_PRICE.Value := Tmp; end; 5.6. ФОРМИРОВАНИЕ ТЕКСТОВОГО ПРЕДСТАВЛЕНИЯ ПОЛЯ Часто требуется некоторое дополнительное преобразование для более точного или более правильного представления хранящегося в поле значения. Для этих целей слу- жит обработчик события OnGetText объекта-поля. Например, все денежные данные в БД хранятся в «старом» масштабе цен (с тремя лишними нулями). После деноминации нужно либо пересчитать все денежные поля во всех таблицах (это не всегда возможно, т. к. на практике при создании СУБД в 90-х годах таким полям часто присваивался целочисленный тип), либо - что значительно проще - изменить вид значения в обработчике события OnGetText. В следующем об- работчике не только устраняются лишние нули, но и вставляются разделители тысяч для удобства чтения длинных сумм: procedure TForml.tbBooksPriceGetText(Sender: TField; var Text: String; DisplayText: Boolean); begin Text := FloatToStrF(tbBooksPrice.AsFloat/1000, ffNumber, 10, 2) end; Другой пример. Поле Sex в таблице Clients имеет логический тип. Обработчик вы- водит текст Муж или Жен в зависимости от значения поля: procedure TForml.tbClientsSexGetText(Sender: TField; var Text: String; DisplayText: Boolean); begin if tbClientsSex.Value then Text := 'Муж' else Text := 'Жен' end; Заметим, что обработчики выполняются только в работающей программе, поэтому на этапе конструирования вы не сможете увидеть результат их работы.
Работа с полями 97 5.7. СОЗДАНИЕ ВЫЧИСЛЯЕМЫХ ПОЛЕЙ Создать вычисляемое поле, значения которого не существуют в реальных ТБД, а вычисляются по значениям других полей, можно с помощью редактора полей. Для его вызова нужно дважды щелкнуть по компоненту-источнику или вызвать его локальное меню (правой кнопкой мыши) и выбрать Field Editor. Вся работа с редактором полей ведется с помощью его локального меню. Для создания вычисляемых полей использу- ется опция New Field этого меню. В диалоговом окне New Field (рис. 5.2) нужно выбрать переключатель Calculated, ввести имя создаваемого поля (Name), выбрать его тип (Туре) и для строковых полей указать длину (Size). Рис. 5.2. Параметры вычисляемого поля в окне добавления нового поля. Для нового поля будет создан компонент TField. Чтобы вычислить значение нового поля, необходимо определить обработчик собы- тия OnCalcFields для компонента-источника, к которому оно принадлежит. Например, для НД Tablet, ассоциированного с ТБД «Сотрудники», необходимо поместить в вычисляемое поле TabletVychisl значение «Да», если в поле TablellnYaz (знание иностранных языков) этой записи содержится True. В противном случае поле Tablei Vychisl должно остаться пустым (рис. 5.3): procedure TGridForm.TablelCalcFields(DataSet: TDataSet); begin if TablellnYaz.Value then TablelVychisl.AsString := 'Да' else TablelVychisl.AsString := ' end;
98 Глава 5 Рис. 5.3. Пример отображения вычисляемого поля Vychisl. Событие OnCalcFields возникает всякий раз, когда курсор (указатель записи) пере- мещается в НД от записи к записи (например, после выполнения методов Next, Last и т. д. или при движении по записям в TDBGrid вручную). Это событие возникает и при инициализации НД (после открытия), а также после фильтрации записей в НД, что, впрочем, также связано с изменением местоположения указателя записи. Кроме того, если свойство AutoCalcFields набора данных содержит True, событие OnCalcFields наступает также и при модификации значений невычисляемых полей в режимах dslnsert и dsEdit данного НД или НД, реляционно с ним связанного (когда установлены ограничения целостности в самой ТБД, а не тогда, когда они подразуме- ваются). Обработчик события OnCalcFields содержит реализацию алгоритма определения значения вычисляемого поля или группы полей. Необходимо помнить, что в этом обработчике значение может быть присвоено только вычисляемому полю и не может - полю, определенному в структуре таблицы БД. 5.8. СОЗДАНИЕ ПОЛЕЙ ВЫБОРА ДАННЫХ (LOOKUP-ПОЛЕЙ) 5.8.1. Поля выбора данных Кроме обычных полей и вычисляемых полей, в Delphi имеется возможность созда- ния полей выбора данных (lookup-полей). Поле выбора данных одного НД содержит значения из другого набора данных, свя- занного по ключу с НД, к которому принадлежит это поле. Будем называть первый НД родительским, а второй дочерним. Сразу же уточним, что поля выбора и специализи- рованные компоненты выбора TDBLookupListBox, TDBLookupComboBox используют схожий механизм, основанный на реляционной связи двух НД, однако компоненты выбора не создают новых полей ни в одном наборе данных. Для определения поля выбора данных необходимо создать новое поле в редакторе полей и выбрать переключатель Lookup (рис. 5.4).
Работа с полями 99 Рис. 5.4. Параметры поля выбора данных в окне добавления нового поля. После выбора переключателя становятся доступными элементы группы Lookup definition, с помощью которых устанавливаются параметры связи наборов данных. Название Назначение DataSet Определяет имя родительского НД. Key Fields Определяет список ключевых полей родительского НД. По этим полям построен индекс для связи родительского НД с дочерним. Если в индексе имеется несколько полей, они разделяются точками с запятыми. Lookup Fields Определяет список ключевых полей дочернего НД. По этим полям постро- ен индекс для связи дочернего НД с родительским. Если в индексе имеется несколько полей, они разделяются точками с запятыми. Result Field Поле дочернего НД, возвращаемое в качестве результата. Необходимо следить, чтобы типы вновь создаваемого поля и поля результата совпадали. Перечисленным элементам окна New Field редактора полей соответствуют свойст- ва LookupDataSet, KeyFields, LookupKeyFields и LookupResultField класса Tfield. 5.8.2. Использование полей выбора Поля выбора могут использоваться для показа дополнительной информации из до- чернего НД при навигации по родительскому набору, если наборы связаны отношени- ем один-к-одному, или для выбора значения записи дочернего НД из списка всех воз- можных значений родительского набора при связи один-ко-многим. Пусть существует ТБД «Сотрудники», которая включает в себя поля «Табельный номер» (TabNum), «ФИО» (FIO), «Должность» (Doljnost) и «Ученая степень» (UchStepen). С ней ассоциирован НД Tablel (рис. 5.5).
100 Глава 5 Рис. 5.5. Набор данных, ассоциированных с ТБД «Сотрудники». Существует также ТБД «Информация о сотрудниках», которая включает в себя ФИО сотрудника (поле FIO), год рождения (GodRojd) и семейное положение (SemPoloj). С ней ассоциирован НД DataModulel. Tablet (рис. 5.6). ТБД «Сотрудники» и «Информация о сотрудниках» связаны по индексу, образованному полями FIO. Требуется при просмотре в ТБД «Сотрудники» выводить год рождения данного со- трудника ниже компонента TDBGrid, в DBTextl. Вызываем редактор полей для Tablet и выбираем переключатель Lookup (рис. 5.7). Рис. 5.6. Набор данных, ассоциированных с ТБД «Информация о сотрудниках». Затем устанавливаем значения свойств так, как показано на рисунке 5.7.
Работа с полями 101 Рис. 5.7. Добавление поля выбора данных GR. Таким образом, нами определен новый объект-поле TablelGR типа TStringField, источником данных для которого служит поле GodRojd ТБД «Информация о сотруд- никах», из той ее записи, у которой значение поля FIO совпадает со значением поля FIO соответствующей записи ТБД «Сотрудники». Далее размещаем в форме компоненты Labell и DBTextl. Устанавливаем Label 1.Caption = 'Год рождения' и связываем DBTextl с полем TablelGR. Теперь для текущей записи сотрудника в DBGridl компонент DBTextl отображает его год рождения (рис. 5.8). ДШЕЗ лГ Списочный состав кафедры ТИ-1 Таб. N ФИО Должность Уч. Степень 31 123456 Ванина Е.С. доцент К.Т.Н 234324 Сивый Х.М. профессор Д.Ф.-М.Н 345213 Харин П.Б. ассистент 345678 Базаренкова Н.С. ассистент К.Т.Н ► 354234 Суровое В.Д. профессор Д.З.Н. 1 432123 НайрымбвМ.С. инженер К.Т.Н 765423 Лавочкина С.З. доцент К.Т.Н. 876534 Айга Ю.З. зав. лаб. К.Т.Н. 923433 Ванилинов К.Г. ассистент К.Т.Н. ▼ | Г од рождения: 1930 Рис. 5.8. Показ года рождения сотрудника с помощью поля выбора данных. Поля выбора данных могут использоваться для автоматической установки нового значения в другое поле НД.
102 Глава 5 Рассмотрим пример. Имеется родительская ТБД «Сотрудники», состоящая из по- лей TabNum (табельный номер, уникальное), FIO (ФИО), KodDol (код должности), и дочерняя ТБД «Оклады», состоящая из полей KodDol (код должности, уникальное), Doljnost (должность), Oklad (оклад). Таблицы находятся в отношении один-ко-многим. Полем связи является KodDol. В ТБД «Сотрудники» построен индекс по полю TabNum, в ТБД «Оклады» - по полю KodDol. Требуется при отображении таблицы «Сотрудники» вместо кода должности показывать ее название из таблицы «Оклады», а при изменении должности автоматически изменять ее код. Расположенная на форме Table! соответствует ТБД «Сотрудники», a Table2 - ТБД «Оклады». Добавим в набор данных Table! поле выбора данных Doljnost (рис. 5.9) Теперь в работающей программе (рис. 5.10) отображается должность сотрудника, а при вводе в это поле нового значения автоматически раскрывается список выбора, и после выбора автоматически обновляется код должности (на рисунке для наглядности показано поле KodDol, которое в реальной программе нужно сделать невидимым). Рис. 5.9. Добавление поля выбора Doljnost. «Г Forml Таб. номер ФИО Код должности Должность 123456 Ванина Е.С. 2; Доцент ► 123457 Сивый Х.М. 1 Профессор v) 123458 Харин П.Б. 3 123459 Багаринова Р.С. 3 Доцент Ассистент 123460 Суровев В.Д. 1 Зав. лаб. 123461 Найрымов М.С. 5 Инженер Лаборант 234567 Лавочкина С.З. 2 345678 Айга Ю.З. 4 Зав. лаб. zJ Рис. 5.10. Комбинированный список выбора должности.
Работа с полями 103 5.9. ОБЗОР ПОЛЕЙ TXXXFIELD 5.9.1. Строковые поля Поле TStringField предназначено для хранения строк длиной до 255 символов. Свойство property Transliterate: Boolean указывает, следует ли производить преобразование символов в кодировку ANSI при чтении строк из реальной ТБД и при записи строк обратно в ТБД в том случае, если строки в ТБД хранятся не в ЛЛ57-кодировке или содержат расширенные ASCII-симво- лы. Когда в свойство установлено значение True, для преобразования используются функции AnsiToNative и NativeToAnsi. Для работы с TStringField предназначены компоненты TDBLabel и TDBEdit. Поле TMemoField предназначено для хранения множества строк неопределенной длины. Специфичные свойства TStringField'. property BlobSize: Integer; Содержит размер поля в байтах. property Transliterate: Boolean; См. свойство TStringField-Transliterate. Специфичные методы: procedure Clear; Очищает поле. procedure LoadFromFile(const FileName: String); Загружает содержимое поля из файла. procedure LoadFromStream (Stream: TStream); Загружает содержимое поля из потока. procedure SaveToFile(const FileName: String); Сохраняет содержимое поля в файле. procedure SaveToStream(Stream: TStream); Сохраняет содержимое поля в потоке. Для работы с мемо-полями используются компоненты TDBMemo-н TDBRichEdit. 5.9.2. Целочисленные поля Целочисленные поля применяются для хранения целых чисел различной длины: TintegerFieid от -2 147 483 648 до +2 147 483 647 TSmaiiintFieid от -32 768 до +32 767 TWordFieid от 0 до 65 535 Свойства property MaxValue: Longlnt и property MinValue: Longlnt определяют максимальное и минимальное значение поля. По умолчанию содержат 0, что означает отсутствие ограничений. Если MaxValue > MinValue и при вводе в поле вводи- мое значение вышло из указанного диапазона, возникает исключительная ситуация. Свойство property DisplayFormat: String осуществляет форматирование данных перед их показом во всех режимах, кроме ре- жима редактирования, для которого форматирование осуществляет свойство property EditFormat: String.
104 Глава 5 Оба свойства унаследованы полями от общего родителя TNumericField. Для описа- ния формата в них могут использоваться следующие символы: Спецификаторы форматирования чисел 0 Определяет поле для цифры. Если в данной позиции форматируемое число имеет значащую цифру, она выводится, если нет - выводится 0. # Определяет поле для цифры. Если в данной позиции форматируемое число имеет значащую цифру, она выводится, если нет - ничего не выводится. ПоЛе для разделителя целой и дробной частей числа. Если в строке больше одной точки, вторая и все следующие точки игнорируются. г Каждая группа из трех цифр будет отделяться от соседней слева цифры разделите- лем тысяч. Положение и количество запятых в строке формата не имеет значения. Е+, е+, Е- , е- Признак представления числа в научном формате. В этом случае число представля- ется мантиссой и десятичным порядком, между которыми стоит символ Е. Специ- фикаторы Е+ и е+ предписывают выводить знак + перед неотрицательным деся- тичным порядком, при использовании Е- и е- знак + перед порядком не выводится. Разделитель спецификаторов формата для положительного, отрицательного и нуле- вого числа. Любые другие символы строки Format, а также заключенные в апострофы или ка- вычки специальные символы-спецификаторы помещаются в выходную строку без преобразования: для Value = л* 1000 спецификаторы #, рубля дадут строку 3 142 рубля. С помощью символа «;» можно задавать различные формы представления положительного, отрицательного и нулевого числа. При этом набор спецификаторов без «;» относится ко всем числам; если используется один символ «;», то специфика- торы слева от него применяются для форматирования положительных чисел, справа - отрицательных, а вывод нулей подавляется; при использовании двух символов «;» первый набор - для положительных, второй - для отрицательных и третий - для нуле- вых чисел. Например, спецификаторы '#;Нуль' для Value = -1 выведут Нуль, а для Value = 0 ничего не выведут, в то время как спецификаторы ' #; 00;Нуль' для Value = -1 выведут 01, а для Value = 0 - строку Нуль. Спецификаторы ' #;; Нуль' для Value = -1 выведут -1, т. к. в этом случае спецификатор для отрицательных чисел не задан и для их вывода будет использован спецификатор положительных чисел, в то время как спецификаторы ' #; ; Нуль' подавляют вывод отрицательных чисел. 5.9.3. Вещественные поля Вещественные поля используются для хранения вещественных чисел: TFloatField TCurrencyField TBCDField числа от 5, 0 * 10"324 до 1,7 * 10+3°8 с точностью 15-16 цифр; аналогично TFloatField, но в денежном формате; вещественные десятичные числа в двоично-десятичном виде с фиксированным количеством разрядов после точки; Delphi не поддерживает тип BCD, поэтому при работе с полем ис- пользуется тип Currency, что определяет до 20 значащих цифр и до 4 цифр после запятой; применяется только в табли- цах Paradox.
Работа с полями 105 Свойство property Precision: Integer; определяет количество знаков после десятичной точки (по умолчанию 15). Это свой- ство не может иметь значение меньше 2. Свойства MaxValue, MinValue, DisplayFormat и EditFormat аналогичны по назначе- нию таким же свойствам целочисленных полей. 5.9.4. Логические поля Поля TBooleanField предназначены для хранения логических значений. Их свойство property Displayvalues: String определяет формат отображения данных, например: True;False, Да;Нет и т. п. Если свойство не определено, значения отображаются как True и False, в противном случае часть строки до первого символа «;» используется для представления значения Истина, а остальная часть - для представления значения Ложь. TDateTimeField TDateField TTimeField 5.9.5. Поля даты и времени Содержит значения даты и времени в формате TDateTime. Содержит значения даты в формате TDate. Содержит значения времени в формате TTime. Их свойство property Displayvalues: String предназначено для форматирования значений, отображаемых в визуальных компонен- тах. Строка DisplayValues может содержать такие символы-спецификаторы (в приме- рах показаны строки для времени 19 часов 45 минут и даты 8 июня 1998 года): Спецификаторы формата даты-времени С Отображает сначала дату в формате дд.мм.гг, затем через пробел время в формате ччмм. сс: 08.06.98 19:45. d Отображает день без ведущего нуля: 8. ' dd Отображает день с ведущим нулем: 08. dddd Отображает день недели: воскресенье (для нерусифицированной версии Windows — Sunday). ddddd Отображает дату в формате ддмм.гг: 08.06.98 dddddd Отображает дату в формате день Месяц год: 8 Июнь 1998 (для неруси- фицированной версии Windows - 8 June 1998). 1 m Отображает число месяца без ведущего нуля: 6. mm Отображает число месяца с ведущим нулем: 06. mmm Отображает сокращенное название месяца: июн. mmmm Отображает полное название месяца: Июнь. у или yy Отображает две последние цифры года: 98. yyy ИЛИ yyyy Отображает все цифры года: 1998. h Отображает час без ведущего нуля: 19. hh Отображает час с ведущим нулем: 19.
106 Глава 5 п Отображает минуты без ведущего нуля: 45. пп Отображает минуты с ведущим нулем: 45. S Отображает секунды без ведущего нуля: 0. SS Отображает секунды с ведущим нулем: 00. t Отображает время в формате чч:мм: 19:45. tt Отображает время в формате чч:мм:сс: 19:45:00. am/pm Отображает время в 12-часовом формате (ат - до полудня, рт - после по- лудня). Для спецификаторов hh:пот am/pm получим 07:45 pm. ampm Отображает время в 12-часовом формате, ио без указания до/после полудня. Для спецификаторов hh: mm ampm получим 07:45. а/p Отображает время в 12-часовом формате (а - до полудня, р - после полуд- ня). Для спецификаторов hh: mm а /р получим 0 7 :45 р. / Отображает используемый в Windows разделитель даты. Для спецификато- ров d/m/у получим 8.6.97. Отображает используемый в Windows разделитель времени. Для специфика- торов h: n: s получим' 19:45:0. Любые другие символы, указанные в строке DisplayValues, а также заключенные в апострофы или кавычки специальные символы-спецификаторы помещаются в выход- ную строку без преобразования, поэтому спецификаторы ' h час л мин' дадут стро- ку19 час 45 мин,а'Ьчас "п" мин' - 19час п мин. 5.9.6. Поля для хранения значений произвольных форматов Поле TBlobField предназначено для хранения больших двоичных объектов (BLOB - Binary Large Object), т.е. ^отформатированных значений произвольной длины. Специфичные свойства: Свойство Назначение property BlobSize: Integer; Содержит текущее значение длины буфера в байтах, необходимого для размещения поля. type TBlobType = (ftBlob, ftMemo, ftGraphic, ftFmtMemo, ftParadoxOle, ftDBaseOle, ftTypedBinary); property BlobType: TBlobType; Определяет содержимое поля: ftBlob - неотфор- матированные данные; ftMemo - многострочный текст, ftGraphic - графическое изображение; ftFmtMemo - многострочный текст в формате RTF; ftParadoxOle - OLE-объект таблиц Paradox; ftDBaseOle - О££-объскт таблиц dBASE; ftTypedBinary - типизированные двоичные дан- ные. property IsNull: Boolean; Содержит True, если поле пусто. property Modified: Boolean; Содержит True, если изменилось значение поля. property Transliterate: Boolean; Указывает, следует ли производить преобразова- ние символов в кодировку ANSI при чтении строк из реальной ТБД и при записи строк об- ратно в ТБД в том случае, если строки в ТБД хранятся не в ЛЛ57-кодировке или содержат расширенные ЛЗСЛ-символы. property Value: String; Используется для чтения/записи данных.
Работа с полями 107 Специфичные методы: Метод Назначение procedure Assign(Source: TPersistent); Загружает в поле значение поля Source друго- го НД. procedure Clear; Очищает поле. procedure LoadFromFile(const FileName: String); Загружает содержимое поля из файла. procedure LoadFromStream (Stream: TStream); Загружает содержимое поля из потока. procedure SaveToFile(const FileName: String); Сохраняет содержимое поля в файле procedure SaveToStream(Stream: TStream); Сохраняет содержимое поля в потоке. Поле TVarBytesField содержит произвольное значение длиной до 65 535 байт. Те- кущая длина хранится в первых двух байтах и может быть получена с помощью свой- ства DataSize. Поле TGraphicField является частным случаем TBlobField и предназначено для хранения изображений.
Глава 6 РАБОТА С НАБОРАМИ ДАННЫХ Под набором данных (НД) понимается группа записей из одной или нескольких ТБД, доступная для использования с помощью компонентов-источников данных TTable, TQuery или TStoredProc. Эти компоненты порождены от общего родительско- го класса TDBDataSet, наиболее важные свойства, методы и события которого рас- сматриваются в этой главе. Работе с TTable и TQuery посвящены две следующие гла- вы, а особенности TStoredProc рассматриваются в гл. 27. 6.1. ОБЗОР СВОЙСТВ, МЕТОДОВ И СОБЫТИЙ 6.1.1. Свойства * Свойство Назначение property Active: Boolean; Открывает/закрывает НД. property ActiveRecord: Integer; Указывает смещение от начала кэша для бу- фера активной записи. property AutoCalcFields: Boolean; Разрешает/запрещает возникновение события OnCalcFields. property BlockReadSize: Integer; Определяет количество записей, которые считываются за один раз из НД. property Bof: Boolean; Содержит True, если курсор НД указывает на первую запись. type TBookmarkStr: String; property Bookmark: TBookmarkStr; Определяет закладку на текущей записи. К помеченной записи можно быстро перейти методом GotoBookmark. property CacheBlobs: Boolean; Разрешает/запрещает кэширование полей BLOB. property CachedUpdates: Boolean; Разрешает/запрещает кэширование вносимых пользователем изменений в НД. property CanModify: Boolean; Указывает, может ли пользователь изменять НД. property Database: TDatabase; Указывает компонент TDatabase, связанный с данным НД. property DatabaseName: String; Содержит псевдоним базы данных. property DataSetField: TDataSetField; Содержит ссылку на собственника вложенно- го НД. property DataSource: TDataSource; Используется в потомках для указания подчи- ненного НД в связй один-ко-многим. type HDBISES: Longlnt; property DBHandle: HDBISES; Указывает дескриптор НД. Используется для непосредственного обращения к BDE API. type TLocale: Pointer; property DBLocale: TLocale; Открывает доступ к языковому драйверу BDE. Используется для непосредственного обраще- ния к BDE API.
Работа с наборами данных 109 property DBSession: TSession; Указывает связанный с НД компонент TSession. property DefaultFields: Boolean; Содержит True, если для НД не создан ни один объект-поле. property EOF: Boolean; Содержит True, если курсор НД сместился за последнюю запись. property Expindex: Boolean; Указывает, используются ли индексные выра- жения в таблицах dBASE. , property FieldCount: Integer; Содержит количество полей НД. property FieldDefList: TFieldDefList; Содержит список описаний полей для вло- женных НД. property FieldDefs: TFieldDefs; Содержит описание полей текущего НД. property FieldList: TFieldList; Содержит список имен всех полей со всеми вложенными полями. property Fields: TFields; Позволяет обратиться к полям по их индексу. Первое поле НД имеет индекс 0. См. примеча- ние 1. property FieldValues[const FieldName: String]: Variant; Позволяет обратиться к значениям полей по имени поля (см. примечание 2). property Filter: String; Задает фильтрующее выражение. property Filtered: Boolean; Разрешает/запрещает фильтрацию записей НД. type TFilterOption = (foCaselnsensi- tive, foNoPartialCompare); TFilterOptions = set of TFilterOption; property Filteroptions: TFilterOptions; Определяет условия фильтрации в строковых полях: foCaselnsensitive - учитывать разницу в высоте букв; foNoPartialCompare - поиск на точное соответствие образцу. property Found: Boolean; Содержит True, если вызов одного из методов FindXXXXбыл успешным. type HDBICur: Longlnt; property Handle: HDBICur; Содержит дескриптор курсора BDE. Исполь- зуется при обращении к BDE API. property KeySize: Word; Указывает размер ключа (в байтах) для теку- щего индекса. type TLocale: Pointer; property Locale: TLocale; Содержит ссылку на языковый драйвер BDE, используемый при работе с НД. Служит пара- метром обращения к некоторым функциям BDEAPI. property Modified: Boolean; Содержит True, если текущая запись НД была изменена. type TComponentName: String; property Name: TComponentName; Определяет имя НД. property Objectview: Boolean; Если содержит True, поля в свойстве Fields хранятся в иерархическом виде, т. е. дочерние поля ссылаются на своих родителей, если False - дочерние поля следуют сразу после родительского поля. property Provider: IProvider; Указывает связанный с НД интерфейс IProvider. Используется в трехзвенных прило- жениях. property RecNo: Longlnt; Определяет номер текущей записи.
по Глава 6 property RecordCount: Longlnt; Содержит количество записей в текущем НД. property Recordsize: Word; Указывает размер в байтах буфера для разме- щения одной записи НД. property SessionName: String; Содержит имя компонента TSession. property SparseArrays: Boolean; Определяет, будут ли создаваться поля- объекты для каждого элемента поля-массива. type TDataSetState = (dslnactive, dsBrowse, dsEdit, dslnsert, dsSetKey, dsCalcFields, dsFilter, dsNewValue, dsOldValue, dsCurValue, dsBlockRead, dsInternalCalc); property State: TDataSetState; Указывает состояние НД: dslnactive - закрыт; dsBrowse - в состоянии просмотра; dsEdit - в состоянии редактирования; dslnsert - в со- стоянии вставки; dsSetKey - в состоянии поис- ка записи; dsCalcFields - в состоянии установ- ки вычисляемых полей; dsFilter - в состоянии фильтрации записей; dsNewValue - в состоя- нии обновления свойства TField.NewValue', dsOldValue - в состоянии обновления свойства TField.OldValue\ dsCurValue - в состоянии обновления свойства TField.CurValue', dsBlockRead-в состоянии чтения блока запи- сей; dsInternalCalc - в состоянии обновления полей TField, для которых FieldKind = fklnternalCalc. type TUpdateMode = (upWhereAll, upWhereChanged, upWhereKeyOnly); property UpdateMode: TUpdateMode; Определяет способ использования полей при поиске обновляемых записей: upWhereAll - все поля; upWhereChanged- только ключевые поля и значения обновленных полей; upWhereKeyOnly - только ключевые поля. property UpdateObject: TDataSetUpdateObj ect; Содержит ссылку на связанный компонент TUpdateSQL, если нужно обновлять кэширо- ванные данные, полученные только для чте- ния. type TUpdateRecordTypes = set of (rtModified, rtlnserted, rtDeleted, rtUnmodified); property UpdateRecordTypes: TUpdateRecordTypes; Определяет, какие записи будут видны поль- зователю: rtModified - измененные; rtlnserted - вставленные; rtDeleted- удаленные; rtUnmodified - не измененные. property UpdatesPending: Boolean; Если содержит True, измененные, вставленные и удаленные записи будут помещаться в бу- фер кэш. Примечание 1. В отличие от предыдущих версий, в Delphi 4 и 5 свойство Fields ссылается на объект класса TFields. Однако свойство Fields типа TField этого объекта является умалчиваемым, и поэтому два следующих оператора эквивалентны: Caption := Table1.Fields[0].Value; Caption := Tablel.Fields.Fields[0].Value; Примечание 2. Свойство FieldValues является умалчиваемым для НД, поэтому два следующих оператора эквивалентны: Caption := Tablel.Fieldvalues['Name']; Caption := Tablel['Name'];
Работа с наборами данных 111 6.1.2. Методы Метод Назначение function ActiveBuffer: PChar; Возвращает указатель на буфер текущей записи. procedure Append; Добавляет пустую запись в конец НД. procedure AppendRecord(const Values: array of const); Добавляет новую запись, заполняет ее поля значениями Values и отсылает ее в ТБД. procedure ApplyUpdates; Записывает кэш обновления в таблицы БД. function BookmarkValld(Bookmark: TBookmark): Boolean; override; Возвращает True, если с закладкой Bookmark связано правильное значение. procedure Cancel; Отменяет все изменения текущей записи, которые не были сохранены в БД. procedure CancelUpdates; Очищает кэш обновления. procedure CheckBrowseMode; Если НД находился в состоянии редактирова- ния или вставки, вызывает Post для записи изменений в БД. type DBIResult: Hord; function CheckOpen(Status: DBIResult): Boolean; Проверяет результат Status, который вернула BDE при попытке открыть НД. procedure ClearFlelds; Очищает все поля текущей записи. procedure Close; Закрывает НД. procedure CloseDatabase(Database: TDatabase); Закрывает базу данных Database. procedure CommltUpdates; Очищает кэш после успешного обновления данных в БД. function CompareBookmarks (Bookmarkl, Bookmark2: TBookmark): Integer; Сравнивает две закладки и возвращает 1, если они отличаются, и 0, если они идентичны или пусты. function ConstraintsDisabled: Boolean; Возвращает True, если вНД временно отклю- чен механизм ограничений ссылочной целост- ности методом DisableConstraints. function ControlsDisabled: Boolean; Возвращает True, если показ новых данных в визуальных компонентах временно запрещен методом DisableControls. type TBlobStreamMode = (bmRead, bmWrite, bmReadWrite); function CreateBlobStream(Field: TField; Mode: TBlobStreamMode): TStream; override; Создает поток для чтения, записи или обнов- ления BLOB-поля Field. Параметр Mode ука- зывает, создается ли поток для чтения (bmRead), записи (bmWrite) или обновления (bmReadWrite) поля. procedure Delete; Уничтожает текущую запись. procedure DisableConstraints; В целях улучшения производительности при навигации по НД временно запрещает исполь- зование ограничений ссылочной целостности. procedure DisableControls; В целях улучшения производительности при навигации по НД временно запрещает смену данных в визуальных компонентах. procedure Edit; Переводит НД в режим редактирования. procedure EnableConstraints; Отменяет действие метода DisableConstraints. procedure EnableControls; Отменяет действие метода DisableControls.
112 Глава 6 procedure FetchAll; Вызывает CheckBrowseMode и затем читает в локальный буфер все записи, начиная с теку- щей позиции курсора и до конца ТБД. function FieldByName(const FieldName: String): TField; Обеспечивает доступ к полю по его имени FieldName. function FindField(const FieldName: String): TField; Ищет поле FieldName в НД и возвращает ссылку на поле или NIL, если поле не найдено. function FindFirst: Boolean; Пытается установить курсор на первую запись НД и возвращает True й случае успеха. function FindLast: Boolean; Пытается установить курсор на последнюю запись НД и возвращает True в случае успеха. function FindNext: Boolean; Пытается установить курсор на следующую запись НД и возвращает True в случае успеха. function FindPrior: Boolean; Пытается установить курсор на предыдущую запись НД и возвращает True в случае успеха. procedure First; Устанавливает курсор на первую запись НД. procedure FlushBuffers; Сохраняет в БД все изменения, хранящиеся в буфере текущей записи и буфере обновлений. procedure FreeBookmark(Bookmark: TBookmark); virtual; Освобождает память, связанную с закладкой Bookmark. function GetBlobFieldData(FieldNo: Integer; var Buffer: TBlobByteData): Integer; override; Читает значение ДДОД-поля FieldNo в буфер Buffer и возвращает размер прочитанных данных. function GetBookmark: TBookmark; virtual; Создает закладку на текущей записи и воз- вращает указатель на нее. function GetCurrentRecord(Buffer: PChar): Boolean; Копирует текущую запись в буфер Buffer и возвращает True в случае успеха. function GetFieldData(FieldNo: Integer; Buffer: Pointer): Boolean; overload; virtual; function GetFieldData(Field: TField; Buffer: Pointer): Boolean; overload; virtual; Читает данные поля, указанного порядковым номером FieldNo или объектом Field в буфер Buffer и возвращает True в случае успеха. procedure GetFieldNames(List: TStrings); Наполняет список List именами всех полей НД. procedure GotoBookmark(Bookmark: TBookmark); Обеспечивает возврат к записи, связанной с закладкой Bookmark. procedure Insert; Переводит НД в режим вставки записей. procedure InsertRecord(const Values: array of const); Создает пустую запись, наполняет ее поля значениями Values и вставляет ее в НД. function IsEmpty: Boolean; Возвращает True, если в НД нет записей. function IsLinkedTo(DataSource: TDataSource): Boolean; Возвращает True, если компонент DataSource связан с НД. function IsSequenced: Boolean; override; Возвращает True, если к записям НД можно обращаться с помощью свойства RecNo. procedure Last; Устанавливает курсор на последнюю запись. function Locate(const KeyFields: String; const KeyValues: Variant; Options:•TLocateOptions): Boolean; Ищет в полях, перечисленных в параметре KeyFields, значения, указанные в KeyValues при условиях, заданных параметром Options. Если запись найдена, делает ее текущей и возвращает True.
Работа с наборами данных 113 function Lookup(const KeyFields: String; const KeyValues: Variant; const ResultFields: String): Variant; Используется в подчиненных НД для поиска в полях KeyFields значений KeyValues. При успе- хе возвращает значения полей ResultFields. function MoveBy(Distance: Integer): Integer; Перемещает курсор на Distance записей вверх или вниз относительно текущей записи. procedure Next; Перемещает курсор к следующей записи. procedure Open; Открывает НД. function OpenDatabase: TDatabase; Открывает связанный с НД компонент TData- base или создает новый компонент с именем, содержащимся в свойстве DatabaseName. procedure Post; virtual; Сохраняет вставленную или отредактирован- ную текущую запись в таблице БД. procedure Prior; Перемещает курсор к предыдущей записи. procedure Refresh; Обновляет НД данными из БД. procedure RevertRecord; Отменяет изменения в текущей записи, если используется кэш обновлений. procedure SetFields(const Values: array of const); Устанавливает значения Values во все поля текущей записи. procedure Translate(Src, Dest: PChar; ToOEM: Boolean); virtual; Преобразует строку Src в кодировку MS-DOS (ТоОЕМ=Тгиё) или Windows (ToOEM=False). 6.1.3. События Все события определены в секции Published класса TDBDataSet и поэтому доступ- ны в Инспекторе Объектов для компонентов-наследников этого класса. Событие С чем связано type TDataSetNotifyEvent = proce- dure (DataSet : TDataSet) of object; property AfterCancel: TDataSetNotifyEvent; Возникает сразу после отмены изменений в текущей записи. property AfterClose: TDataSetNotifyEvent; Возникает сразу после закрытия НД. property AfterDelete: TDataSetNotifyEvent; Возникает сразу после уничтожения текущей записи. property AfterEdit: TDataSetNotifyEvent; Возникает сразу после перехода НД в режим редактирования. property AfterInsert: TDataSetNotifyEvent; Возникает сразу после вставки записи. property AfterOpen: TDataSetNotifyEvent; Возникает сразу после открытия НД. property AfterPost: TDataSetNotifyEvent; Возникает сразу после выполнения метода Post. property AfterScroll: TDataSetNotifyEvent;' Возникает сразу после перехода к другой запи- си. property BeforeCancel: TDataSetNotifyEvent; Возникает непосредственно перед отменой изменений в текущей записи. property BeforeClose: TDataSetNotifyEvent; Возникает непосредственно перед закрытием НД. property BeforeDelete: TDataSetNotifyEvent; Возникает непосредственно перед уничтоже- нием текущей записи.
114 Глава 6 property BeforeEdit: TDataSetNotifyEvent; Возникает непосредственно перед переходом НД в режим редактирования. property Beforeinsert: TDataSetNotifyEvent; Возникает непосредственно перед переходом НД в режим вставки. property BeforeOpen: TDataSetNotifyEvent; Возникает непосредственно перед открытием НД. property BeforePost: TDataSetNotifyEvent; Возникает непосредственно перед выполнени- ем метода Post. property BeforeScroll: TDataSetNotifyEvent; Возникает непосредственно перед переходом к новой записи. property OnCalcFields: TDataSetNotifyEvent; Возникает при необходимости переопределе- ния вычисляемых полей. type TDataAction = (daFail, daAbort, daRetry); TDataSetErrorEvent = procedure (DataSet: TDataSet; E: EDatabase- Error; var Action: TDataAction) of object; property OnDeleteError: TDataSetErrorEvent; Возникает после неудачной попытки удаления текущей записи. Обработчик в параметре Action должен вернуть одно из следующих значений: daFail - прекратить удаление и сообщить об ошибке; daAbort - прекратить удаление, не со- общать об ошибке; daRetry - повторить удаление (в обработчике в этом случае должна быть устра- нена причина возникновения ошибки). property OnEditError: TDataSetErrorEvent; Возникает при неудачной попытке изменения записи (TDataSetErrorEvent - см. выше). type TFilterRecordEvent = procedure(DataSet: TDataSet; var Accept: Boolean) of object; property OnFilterRecord: TFilterRecordEvent; Возникает при фильтрации записей (после установки значения True в свойство Filtered). Обработчик должен поместить True в перемен- ную Accept, если текущая запись удовлетворя- ет критерию фильтрации. property OnNewRecord: TDataSetNotifyEvent; Возникает при вставке или добавлении новой записи. Обработчик может установить началь- ные значения в поля записи и/или произвести каскадные изменения в связанных НД. property OnPostError: TDataSetErrorEvent; Связано с возникновением ошибки при выпол- нении метода Post. type TUpdateAction = (uaFail, uaAbort, uaSkip, uaRetry, uaApplied), TUpdateErrorEvent = procedure (DataSet: TDataSet; E: EDatabaseError; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction) of object; property OnUpdateError: TUpdateErrorEvent; Связано с возникновением ошибки при сохране- нии кэшированных изменений. Обработчик в переменной UpdateAction должен вернуть одно из следующих значений: uaFail—прекратить опера- цию и сообщить об ошибке; uaAbort - прекратить операцию, не сообщать об ошибке; uaSkip—про- пустить ошибочную запись и продолжить опера- цию; uaRetry- повторить операцию после коррек- ции ошибочной записи; uaApplied - не использует- ся в обработчике события. type TUpdateRecordEvent = procedure(DataSet: TDataSet; Update- Kind: TUpdateKind; var UpdateAction: TUpdateAction) of object; property OnUpdateRecord: TUpdateRecordEvent; Возникает при сохранении каждой кэширован- ной записи. Обработчик может произвести необходимые каскадные изменения в связан- ных НД и должен в переменной UpdateAction вернуть одно из следующих значений: uaFail — прекратить операцию и сообщить об ошибке; uaAbort - прекратить операцию, не сообщать
Работа с наборами данных 115 об ошибке; uaSkip - пропустить текущую за- пись и продолжить операцию; uaRetry - не используется в обработчике события; uaApplied - сохранить запись в физической ТБД. 6.2. ОСНОВНЫЕ ПРИЕМЫ РАБОТЫ С НД 6.2.1. Открытие и закрытие НД Начальное состояние любого НД - dslnactive. Чтобы открыть НД, используется ме- тод Open или свойство Active, в которое нужно поместить значение True. После ус- пешного открытия НД переходит в состояние dsBrowse, а его курсор устанавливается на первую запись. Чтобы закрыть НД, вызывается метод Close. Заметим, что, если НД закрывается, находясь в режимах dslnsert или dsEdit, изменения, сделанные в текущей записи, не запоминаются. Перед вызовом Close рекомендуется вызвать CheckBrowseMode, чтобы гарантировать сохранение текущих изменений. Закрыть НД можно также, поместив значение False в его свойство Active. 6.2.2. Программный доступ к записям Для просмотра, вставки, редактирования и удаления записей обычно используются соответствующие визуальные компоненты (TDBGrid, TDBEdit, TDBNavigator и т. д.), которые автоматически вызывают нужные методы НД. В некоторых случаях (например, при обработке статистических данных или при реализации каскадных изменений) необходим программный доступ к информации без использование визуальных компонентов. При программном доступе прежде всего нужно убедиться в том, что НД содержит записи. Для этого нужно вызвать метод IsEmpty, который возвращает False, если в НД есть хотя бы одна запись. После использования или редактирования текущей записи переход к следующей реализуется методами FindNext или Next. Таким образом, для программного доступа ко всем записям НД используются такого рода операторы: Table1.Open; if not Tablel.IsEmpty then repeat {Используем информацию из текущей записи или редактируем ее} until not Tablel.FindNext; // Переходим к следующей записи Tablel.Close; или Tablel.Open; if not Tablel.IsEmpty then while not Tablel.EOF do begin {Используем информацию из текущей записи или редактируем ее} Tablel.Next; // Переходим к следующей записи end; Tablel.Close;
116 Глава 6 Перед редактированием текущей записи набор данных необходимо перевести в со- стояние dsEdit методом Edit, а после редактирования сохранить сделанные изменения в соответствующей ТБД, вызвав метод Post: Tablel.Edit; Tablel['Number'] := 123456; Tablel.Post; По такой же схеме осуществляется вставка или добавление новой записи, при этом вме- сто Edit вызывается Insert (вставка записи) или Append (добавление записи). Если НД не индексирован, вставленная запись помещается на место текущей записи, а текущая запись становится следующей за ней; добавленная запись помещается в конец НД. Если НД индек- сирован, оба метода вставляют запись в позицию, определяемую текущим индексом. Вместо Tablel.Insert; // или Tablel.Append; {Наполнение пустой записи нужными значениями:} Tablel['Number'] := 123456; Tablel['Name'] := 'Иванов И.И.'; Tablel['Children'] := False; Tablel.Post; можно использовать единственный оператор, вызвав метод InsertRecord или AppendRecord: Tablel.InsertRecord([123456, 'Иванов И.И.', False]); Элементы в конструкторе массива должны идти в строгом соответствии с порядком следования полей в НД и содержать допустимые для каждого поля выражения. Заметим, что вставленная запись становится текущей. Если НД индексирован, вставлять запись в цикле просмотра repeat ... until или while not EOF do нельзя, т. к. положение новой записи в НД будет соответствовать значению ее индекс- ного поля и, следовательно, положение курсора НД может измениться. Точно так же в цикле нельзя изменять значения индексных полей. Редактирование, удаление и вставка записей возможны только для НД, свойство которых CanModify содержит True. 6.3. НАВИГАЦИЯ ПО НАБОРУ ДАННЫХ 6.3.1. Общие положения Существует два способа работы с записями в НД. Способ, основанный на использовании операторов SQL, предполагает оперирова- ние группами записей. Именно так работают S^Z-операторы группового обновления НД UPDATE, INSERT, DELETE и выборки групп записей SELECT. Второй способ состоит в оперировании единичными записями. Если необходимо изме- нить, добавить или удалить группу записей, соответствующая операция выполняется для каждой из таких записей. Чтобы отыскать нужные записи в НД, применяются навигацион- ные методы. Они всегда работают с единичной записью и связаны с понятием курсора НД. Под курсором набора данных понимается указатель текущей записи в конкретном НД. Те- кущая запись - это та запись, над которой в данный момент времени можно выполнять какие- либо операции (удаление, изменение, чтение значений, содержащихся в записи полей). С помощью методов First, Last, Next, Prior, MoveBy, FindFirst, FindLast, FindNext, FindPrior можно изменять положение курсора и, следовательно, выбирать нужную запись.
Работа с наборами данных 117 6.3.2. Порядок следования записей Может сложиться впечатление, что первая и последняя записи набора данных всегда фиксированы, что это физически первая и последняя записи в наборе данных. В большинстве случаев это не так. Во-первых, как уже отмечалось выше, набор данных может содержать только часть записей из ТБД. Для ТТаЫе эта часть опреде- ляется условиями фильтрации, для TQuery и TStoredProc - условиями, содержащи- мися в секции WHERE S^Z-запроса. Во-вторых, последовательность расположения записей зависит от их сортировки, которая в ТТаЫе определяется используемым индексом, а в TQuery и TStoredProc - порядком перечисления полей в секции ORDER by. В процессе работы с НД программа может переопределить сортировку, и записи перестроятся в соответствии с новой сортировкой, т. е. логический порядок их сле- дования изменится. Пусть, например, имеется ТБД, состоящая из двух полей: Name (Фамилия сотруд- ника) и Oklad (Оклад). В ТБД имеется 5 записей, которые вводились следующим обра- зом и, следовательно, в таком порядке и хранятся: Name Oklad Иванов 1000 Яковлев 500 Юрьев 1500 Якунин 700 Зюзин 700 ТБД проиндексирована по двум индексам: первый - по возрастанию поля Name, вто- рой - по возрастанию поля Oklad. Если порядок сортировки записей определяется индексом по полю Name, получим НД, показанный на рис. 6.1, а; если индексом по полю Oklad - на рис. 6.1, б. Name Oklad Зюзин 700 Иванов 1000 Юрьев 1500 Яковлев 500 Якунин 700 Name Oklad Яковлев 500 Якунин 700 Зюзин 700 Иванов 1000 Юрьев 1500 а) б) Рис. 6.1. Сортировка таблицы: а) по полю Name; б) по полю Oklad. Как можно заметить, при сортировке по полю Name метод Last установит курсор на запись со значением поля Name, равным Якунин, а при сортировке по полю Oklad — на запись со-эначением Юрьев. Если текущей является третья по счету запись от начала НД, для первого случая это будет Юрьев, для второго - Зюзин. Метод Next переместит курсор для первого случая на запись Яковлев, для второго - на запись Иванов. 6.3.3. Последовательная навигация по записям Для выполнения действий при последовательном переборе записей, начиная от не- которой стартовой записи и до конца набора данных, используют циклы while...do или repeat...until'.
118 Глава 6 with Tablel do или begin First; with Tablel do begin First; while not EOF do begin {Какие-либо действия над очередной записью} Next; repeat {Какие-либо действия над очередной записью} until not FindNext; end; {with} end; {while} end; {with} Точно так же можно перемещаться от конца НД к его началу: with Tablel do или begin Last; with Tablel do begin Last; while not BOF do begin {Какие-либо действий над очередной записью} Prior; repeat {Какие-либо действия над очередной записью} until not FindPrior; end; {with} end; {while} end; {with} Если НД разрешает вызов записи по ее номеру (его свойство IsSequenced в этом случае должно содержать значение True), можно использовать счетный цикл for. На- пример, следующий обработчик события TForm.OnActivate поместит все значения строкового поля FIO НД Tablel в многострочный редактор Меток. procedure TForml.FormActivate(Sender: TObject); var RecNo: Integer; begin if Tablel.IsSequenced then for RecNo := 1 to Tablel.RecordCount do begin Tablel.RecNo := RecNo; Memol.Lines.Add(Tablel['FIO']) end; end; Bo всех случаях следует помнить, что, если в ходе выполнения цикла последова- тельного перебора записей изменится ключевое поле записи или в НД будет вставлена новая (удалена старая) запись, курсор НД может изменить свое положение и правиль- ная работа цикла будет нарушена. Если, например, мы хотим удалить все записи из таблицы, такой фрагмент программы будет неверным: with Tablel do while not EOF do begin Delete; Next; end; После удаления очередной записи курсор НД перейдет на следующую, но вызов Next заставит его сместиться еще раз, и очередная запись будет пропущена. Надо так:
Работа с наборами данных 119 with Tablel do while not EOF do Delete; 6.3.4. Использование закладок Подобно тому как в книге нужную страницу можно заложить закладкой и впослед- ствии быстро найти эту страницу, в НД аналогичные действия можно осуществить для записи. Для этой цели НД обладает следующими методами (см. п. 6.1.2): GetBookmark (создает закладку), GotoBookmark (перемещает курсор к закладке), FreeBookmark (удаляет закладку), BookmarkValid (проверяет закладку), CompareBookmarks (сравнивает две закладки). Использовать закладки предельно просто: var MyBookmark: TBookMark; { заложить закладку } MyBookmark := Tablel.GetBookmark; { перейти на запись, на которой заложена закладка} if Tablel. BookmarkValid(MyBookmark) then Tablel.GotoBookmark(MyBookmark) ; { освободить ресурсы, выделенные для закладки}- if Tablel. BookmarkValid(MyBookmark) then Tablel. FreeBookmark (MyBookiriark) ; 6.4. ПОИСК ЗАПИСЕЙ В НАБОРАХ ДАННЫХ 6.4.1. Метод Locate function Locate(const KeyFields: String; const KeyValues: Variant; Options: TLocateOptions): Boolean; Метод Locate ищет первую запись, удовлетворяющую критерию поиска, и, если такая запись найдена, делает ее текущей. В этом случае в качестве результата возвра- щается True. Если запись не найдена, возвращается False. Список KeyFields указывает поле или несколько полей, по которым ведется поиск. В случае нескольких поисковых полей их названия разделяются точкой с запятой. Критерии поиска задаются в вариантном массиве KeyValues так, что i-e значение в KeyValues ставится в соответствие г-му полю в KeyFields. Options позволяет указать необязательные значения режимов поиска: loCaselnsensitive - поиск ведется без учета высоты букв, т. е. если в KeyValues ука- зано «принтер», а в некоторой записи в данном поле встретилось «Принтер» или «ПРИНТЕР», запись считается удовлетворяющей условию поиска; loPartialKey - запись считается удовлетворяющей условию поиска, если она со- держит часть поискового контекста; например, удовлетворяющими контек- сту «Ма» будут признаны записи со значениями в искомом поле «Машин», «Макаров» и т. д.
120 Гпава 6 Locate производит поиск по любому полю; поле или поля, по которым производит- ся поиск, могут не только не входить в текущий индекс, но и не быть индексными вообще. В случае если поля поиска входят в какой-либо индекс, Locate использует этот ин- декс при поиске. Если искомые поля входят в несколько индексов, трудно сказать, какой из них будет использован. Соответственно, трудно предсказать, какая запись из множества записей, удовлетворяющих критерию поиска, будет сделана текущей - особенно в случае если поиск ведется не по текущему индексу. При поиске по полям, не входящим ни в один индекс, применяются фильтры BDE. Пусть имеется ТБД «Сотрудники кафедры» с целочисленным полем TabNum (Табельный номер) и строковыми полями FIO (ФИО), Doljnost (Должность), UchStepen (Ученая степень). ТБД имеет индексы по полям TabNum, FIO. Осуществим поиск по полям Doljnost и UchStepen (индексное и неиндексное) при различных теку- щих индексах в НД. Поисковый контекст - ' доцент', ' кхн' при режиме частично- го совпадения значений: procedure TFormX.LocateButtonClick(Sender: TObject) ; begin Tablel.Locate('Doljnost;UchStepen', VarArrayOf(['доцент','кхн']),[loPartialKey]); end; Результаты поиска показаны на рис. 6.2 и 6.3. Как видно из рисунков, при различ- ных текущих индексах результаты поиска также могут быть различными. Рис. 6.2. Поиск при текущем индексе по полю TabNum.
Работа с наборами данных 121 Рис. 6.3. Поиск при текущем индексе по полю FIO. 6.4.2. Метод Lookup function Lookup(const KeyFields: String; const KeyValues: Variant; const ResultFields: String): Variant; Метод Lookup находит запись, удовлетворяющую условию, но не делает ее текущей, а возвращает значения некоторых полей этой записи. Тип результата - Variant или вари- антный массив. Независимо от успеха поиска записи указатель текущей записи в НД не изменяется. В отличие от Locate Lookup осуществляет поиск только на точное соответ- ствие критерия поиска и значения полей записи. В KeyFields указывается список полей, по которым необходимо осуществить поиск. При наличии в этом списке более чем одно- го поля соседние поля разделяются точкой с запятой. KeyValues указывает поисковые значения полей, список которых содержится в KeyFields. Если имеется несколько поисковых полей, каждому i-му полю в списке KeyFields ставит- ся в соответствие i-e значение в списке KeyValues. При наличии одного поля его поисковое значение можно указывать в качестве KeyValues непосредственно; в случае нескольких по- лей их необходимо приводить к типу вариантного массива при помощи VarArrayOf. В качестве поисковых полей можно указывать поля как входящие в какой-либо ин- декс, так и не входящие в него; тип текущего индекса не имеет значения. Если поис- ковые поля входят в какие-либо индексы, их использование производится автоматиче- ски; в противном случае используются фильтры BDE. Если в результате поиска запись не найдена, метод Lookup возвращает Null, что можно проверить с помощью оператора if VarType(LookupResults) = varNull then ...
122 Глава 6 В противном случае Lookup возвращает из этой записи значения полей, список ко- торых содержит ResultFields. При этом размерность результата зависит от того, сколь- ко результирующих полей указано в ResultFields: • одно поле - результатом будет значение соответствующего типа или Null, если поле в найденной записи содержит пустое значение; • несколько полей - результатом будет вариантный массив, число элементов в котором меньше или равно числу результирующих полей (некоторые поля най- денной записи могут содержать пустые значения). Пример А. Одно результирующее поле (результат - значение типа Variant). Будем осуществлять поиск в ТБД «Сотрудники» по полю FIO. Поисковое значение, будем вводить в однострочном редакторе Editl. В качестве результата используем значение поля UchStepen (ученая степень) найденной записи: procedure TFormX.LookuplButtonClick(Sender: TObject); var LookupResults: Variant; begin // Осуществляем поиск: LookupResults := Tablel.Lookup('FIO', Editl.Text, 'UchStepen'); // Проверяем, содержит ли результат пустое значение или Nul^: case VarType(LookupResults) of varEmpty : Labell.Caption := 'Пустой результат'; varNull : Labell.Caption := 'Запись не найдена'; else Labell.Caption := LookupResults; end; //case end; Пример Б. Несколько результирующих полей (результат - вариантный массив). Напомним некоторые приемы использования вариантного массива. Если переменная типа Variant является вариантным массивом, функция VarlsArray возвращает True, верхнюю и нижнюю границы массива можно определить при помощи функций VarArrayLowBound и VarArrayHighBound, а тип каждого элемента - с помощью VarType. Будем осуществлять поиск в ТБД «Сотрудники» по полю FIO. Поисковое значение будем вводить в Editl, а в качестве результата используем значения полей TabNum, Doljnost и UchStepen найденной записи (табельный номер, должность, ученая степень), которые будем показывать в трех компонентах TLabel: procedure TFormX.LookupButtonClick(Sender: TObject); var LookupResults: Variant; begin // Ищем запись: LookupResults := Tablel.Lookup('FIO', Editl.Text, 'TabNum;Doljnost;UchStepen'); // Проверяем: результат - вариантный массив? if VarlsArray(LookupResults) then begin Labell.Caption := LookupResults[0]; if LookupResults[1] <> Null then Label2.Caption := LookupResults[1];
Работа с наборами данных 123 if LookupResults[2] О Null then Label3.Caption := LookupResults[2] ; end else // Результат - не вариантный массив, а единичное значение case VarType(LookupResults) of varEmpty: Labell.Caption := 'Пустой результат'; varNull : Labell.Caption := 'Запись не найдена'; end; //case end; Если запись не найдена, VarTypefLookupResults) возвращает значение varNull. Если поиск по какой-либо причине н^ был произведен, VarTypefLookupResults) возвращает значение varEmpty. Если какое-либо из полей, чьи значения возвращаются в результате поиска в вариантном массиве, содержит пустое значение, соответствующий элемент вари- антного массива также ^удет содержать пустое значение (Null). В этом случае обращение к нему возбудит исключительную ситуацию, поэтому нужна предварительная проверка. 6.5. ФИЛЬТРАЦИЯ ЗАПИСЕЙ Помимо описываемых ниже средств для фильтрации данных могут использоваться методы SetRange, ApplyRange (и сопутствующие им методы) в компоненте TTable и секция WHERE SgL-aanpoca в компонентах TQuery и TStoredProc. 6.5.1. Свойство Filter Свойство Filter позволяет задать критерий фильтрации. В этом случае НД будет отфильтрован, как только его свойство Filtered станет равным True. Синтаксис описа- ния критерия похож на синтаксис секции WHERE SQL-запроса с тем исключением, что имена переменных программы указывать нельзя, можно указывать имена полей и литералы (явно заданные значения); можно использовать обычные операции отноше- ния и логические операторы AND, NOT и OR, например: ([Doljnost] = 'доцент') AND ([TabNum] > 300000) Строку критерия фильтрации можно ввести во время прогона программы или на этапе конструирования формы. Например, с помощью такого обработчика события OnChecked компонента ComboBoxl критерий фильтрации считывается из редактора Editl и помещается в свойство Filter компонента Tablel (рис. 6.4 и 6.5): procedure TForml.CheckBoxlClick(Sender: TObject); begin Tablel.Filter := Editl.Text; Tablel.Filtered := ChecjkBoxl .Checked; end; С помощью свойства type TFilterOption = (foCaselnsensitive, foNoPartialCompare); property FilterOptions: TFilterOptions; программист может определить дополнительные условия фильтрации строковых полей: foCaselnsensitive - фильтрация производится без учета разницы в высоте букв; foNoPartialCompare - поиск производится на точное соответствие.
124 Глава 6 Г Сотрудники ► 1 1 1 1 1 I Т abNum FIO Doljnost UchStep 100002 Юлькина В.В. ассистент 100222 Залипалов У.П. доцент кхн 101988 Маслаченко В.Ф. ассистент кхн 111216 Мюратов А.Ф. профессор ДИН 123111 Массалитин В.Ф. ассистент ктн 198765 Пятаков П.Я. профессор дтн 212321 М ануйлова В. А. доцент кхн 221254 МанишкинаА.А. ассистент 222121 МявкинаВ.П. лаборант 545667 Васильев Е.Р. доцент Кфмн 876822 Гаврилов Г. Г. доцент кхн Строка Filler - |[[Doljnost] = 'доцент') AND ([T abNum] > 300000) Г Фильтровать Рис. 6.4. Набор данных до фильтрации. ЦПЛЕЗ «Iе Сотрудники ТabNum FI0 | Doljnost |Uch9tep| А ► 545667 Васильев Е.Р. доцент КфМН _ 876822 Гаврилов Г. Г. доцент КХН Строка Filter -| ([Doljnost] = 'доцент') AND ([ТabNum] > 300000) |7 фильтровать Рис. 6.5. Набор данных после фильтрации.
Работа с наборами данных 125 6.5.2. Событие OnFilterRecord Событие OnFilterRecord возникает при установке значения True в свойство Filtered. Обработчик события имеет два параметра: имя фильтруемого набора данных и переменную Accept, в которую программа должна поместить True, если текущая запись удовлетворяет критерию фильтрации. В отличие от критерия в строке Filtered, ограниченного рамками синтаксиса условно- го выражения, критерий, реализуемый в обработчике события OnFilterRecord, определя- ется синтаксисом Object Pascal и может реализовывать сложные алгоритмы фильтрации. Однако следует помнить о том, что в обработчике OnFilterRecord последовательно пере- бираются все записи ТБД, в то время как методы SetRange, ApplyRange и им сопутст- вующие методы компонента ТТаЫе используют индексно-последовательный метод дос- тупа, т. е. работают с частью записей в физической ТБД. Это делает использование обра- ботчика OnFilterRecord предпочтительным для фильтрации небольших объемов записей и сильно ограничивает его применение при больших объемах. Всякий раз, когда приложение обрабатывает событие OnFilterRecord, НД перево- дится из состояния dsBrowse в состояние dsFilter. Это предотвращает модификацию НД во время фильтрации. После завершения текущего вызова обработчика события OnFilterRecord НД переводится в состояние dsBrowse. Пример. Чтобы создать НД из тех записей ТБД «Сотрудники», в которых поле Doljnost содержит значение «доцент», можно использовать такой обработчик: procedure TForml.TablelFilterRecord(DataSet: TDataSet; var Accept: Boolean); begin Accept := DataSet['Doijnost'] = 'доцент'; end; Еще один пример: отфильтровать ТБД «Сотрудники» по условию «Отобрать всех сотрудников, у которых табельные номера (поле TabNum) больше значения, вводимо- го пользователем в редакторе Editl, и в поле FIO есть подстрока символов, вводимых пользователем в Edit2>>: procedure TForml.TablelFilterRecord(DataSet: TDataSet; var Accept: Boolean); begin Accept := (DataSet['TabNum'] > Editl.Text)) and (Pos(Edit2.Text,DataSet['FIO']) > 0) ; end; Если в строке Filter и в обработчике события OnFilterRecord заданы разные крите- рии фильтрации, выполняются оба. 6.5.3. Навигация в неотфильтрованном НД между записями, удовлетворяющими фильтру Методы FindFirst, FindLast, FindNext, FindPrior позволяют перемещаться в неот- фильтрованном НД (у которого Filtered = False) между записями, удовлетворяющими условию фильтрации. Условие фильтрации задается событием OnFilterRecord и/или свойством Filter. Действие методов таково: они кратковременно переводят НД в от-
126 Глава 6 фильтрованное состояние (Filtered = True) без визуализации этой фильтрации в TDBGrid или другом подобном визуальном компоненте, находят соответствующую запись и переводят НД обратно в неотфильтрованное состояние (Filtered = False). Если искомая запись найдена, методы возвращают True, в противном случае - False. В отличие от этого методы First, Last, Next и Prior учитывают критерий фильтрации, только если НД находится в отфильтрованном состоянии (свойство Filtred содержит True). 6.6. ПОЛУЧЕНИЕ ИНФОРМАЦИИ О ПОЛЯХ 6.6.1. Свойство FieldDefs Свойство FieldDefs набора данных содержит информацию о полях, объявленных в составе ТБД, ассоциированной с НД. Для НД типа ТТаЫе свойство FieldDefs содержит информацию обо всех физически существующих полях в ТБД, ассоциированной с ТТаЫе. Не следует путать это свойст- во со свойством Fields, которое содержит информацию о всех объектах типа TField, объявленных для набора данных с использованием редактора полей. Для НД типа TQuery свойство FieldDefs содержит информацию обо всех полях, объявленных в качестве возвращаемых полей в операторе SELECT. Подробнее см. замечание, помещенное в конце данного подраздела. Свойство FieldDefs содержит ссылку на объект типа TfieldDefs, и, таким образом, с его помощью можно использовать свойства этого объекта для определения количества и характеристик полей. В частности, его свойство Items является набором объектов типа TFieldDef, каждый из которых содержит информацию о конкретном поле. Доступ к характеристикам конкретного поля осуществляется через указание Items [Index], где Index лежит в диапазоне O..Count-l. Свойство Count объекта FieldDefs содержит количество элементов массива Items. Объект типа TFieldDef обладает следующими наиболее важными свойствами: Свойство Назначение type TFieldType - (ftUnknown, ftString, ftSmallint, ftlnteger, ftWord, ftBoolean, ftFloat, ftCurrency, ftBCD; ftDate, ftTime, ftDateTime, ftBytes, ftVarBytes, ftAutoInc, ftBlob, ftMemo, ftGraphic, ftFmtMemo, ftParadoxOle, ftDBaseOle, ftTypedBinary, ftCursor, ftFixedChar, ftWideString, ftLargeint, ftADT, ftArray, ftReference, ftDataSet); property DataType: TFieldType; Определяет тип данных: ftUnknown - неизвест- ный тип; ftString - строковый Tnn;ftSmallint - тип SmalllnT, ftlnteger - тип Integer; ftWord - тип Word', ftBoolean - логический тип; ftFloat - вещественный тип; ftCurrency - денежный тип; ftBCD - двоично-десятичные данные; ftDate - дата; ftTime - времк, ftDateTime - дата-время; ftBytes - строка байтов фиксированной длины; ftVarBytes - строка байтов переменной длины; ftAutoInc - автоинкрементное поле, ftBlob - двоичное поле переменной рдинъг, ftMemo - многострочный текст, ftGraphic - графическое изображение;ftFmtMemo - форматированный TeK.cr,ftParadoxOle - поле ParadoxOle; ftDBaseOle - поле DBaseOle', ftTypedBinary - типизированное двоичное поле;
Работа с наборами данных 127 ftCursor - состояние курсора НД для сохраняе- мой процедуры Oracle', ftFixedChar - строка символов фиксированной длины: ftWideString - строка двухбайтных символов-, ftLargeint - длинное целое значение;/й4/)Т-данные абст- рактного типа-,ftArray - массив;/[Reference - ссылочный тип; ftDataSet - набор данных. property FieldNo: Integer; Содержит физический номер поля, исполь- зующийся BDE для доступа к полю. property Name: String; Возвращает физическое имя поля в ТБД. property Required: Boolean; Содержит True, если поле требует обязатель- ного заполнения каким-либо значением, и False в противном случае. property Size: Integer; Возвращает размер поля. Он важен для полей типов ftString,ftBCD, ftBytes, ft VarBytes, ft Blob, ftMemo,ftGraphic. Для всех остальных типов полей размер поля определяется его типом. Для полей BCD возвращается число знаков после десятичной точки. В следующем примере в ListBoxl помещаются сведения о полях ТБД, ассоцииро- ванной с Tablel (результат работы показан на рис. 6.6): Рис. 6.6. Вывод информации о полях НД. var i: Integer; TipPolja: String; Tmp: String; TmpReq: String[3]; // рабочая переменная // тип поля // результат форматирования
128 Глава 6 begin ListBoxl.Clear; Tablel.FieldDefs.Update ; {В цикле просматриваем объекты FielDef и заносим в ListBoxl сведе- ния об имени поля, его типе, размере и свойстве Required:} for i := 0 to Tablel.FieldDefs.Count - 1 do begin case Tablel.FieldDefs.Items[i].DataType of ftString : TipPolja := 'String'; ftSmallint : TipPolja := 'Smallint'; ftlnteger : TipPolja := 'Integer'; ftWord : TipPolja := 'Word'; ftBoolean : TipPolja := 'Boolean'; ftFloat : TipPolja := 'Float'; ftCurrency : TipPolja := 'Currency'; ftBCD : TipPolja := 'BCD'; ftDate : TipPolja := 'Date' ; ftTime : TipPolja := 'Time'; ftDateTime : TipPolja := 'DateTime'; ftBytes : TipPolja := 'Bytes'; ftVarBytes : TipPolja := 'VarBytes'; ftAutoInc : TipPolja := 'AutoInc'; ftBlob : TipPolja := 'Blob'; ftMemo : TipPolja := 'Memo'; ftGraphic : TipPolja := 'Graphic'; ftFmtMemo : TipPolja := 'FmtMemo'; ftParadoxOle : TipPolja := 'ParadoxOle'; ftDBaseOle : TipPolja := 'DBaseOle'; ftTypedBinary : TipPolja := 'TypedBinary'; end; //case if Tablel.FieldDefs.Items[i]-Required then TmpReq := 'Req' else TmpReq := ' - '; Tmp := Format('%-16s %-10s %-5s %4d', Tablel.FieldDefs.Items[i]-Name, TipPolja, TmpReq, Tablel.FieldDefs.Items[i].Size]); ListBoxl.Items-Add(Tmp); end; //for end; Некоторые методы объекта TFieldDefs: Метод Назначение procedure Add(const Name: String; DataType: TFieldType; Size: Word; Required: Boolean); Добавляет в список Items новый элемент-поле: Name - имя нового поля; DataType - тип поля; Size - размер поля или 0 в случае, когда размер поля явно определяется его типом; Required определяет, должно ли поле в обязательном порядке содержать какое-либо значение. procedure Clear; Очищает FieldDefs. Это необходимо, напри- мер, при создании новой ТБД (см. метод TTable. CreateTable).
Работа с наборами данных 129 function Find(const Name: String): TFieldDef; Ищет поле по его имени, определяемому па- раметром Name. В случае успеха возвращает указатель на объект TIndexDefs.Items. function IndexOf(const Name: String): Integer; Ищет поле по его имени Name и возвращает индекс объекта TIndexDef в списке Items. procedure Update; Обновляет содержимое свойства TFieldDef текущей информацией о полях в составе ТБД. Для НД типа TQuery в свойстве FieldDefs будет содержаться информация только о тех полях, которые представлены в списке после слова SELECT: SELECT P.NN, P.DatePrih, P.Tovar FROM prihod P WHERE ... В этом случае свойство FieldDefs будет содержать информацию только о полях NN, DatePrih, Tovar таблицы Prihod. Это правило действует и для случая двух и более таблиц БД, участвующих в запросе: SELECT P.NN, P.DatePrih, Р.Tovar, P.Kolvo, K.KursUSD FROM prihod P, KursUSD К WHERE K.Datel= P.DatePrih В случае использования в качестве полей результирующего запроса агрегатных функций типа SUM, а также результатов вычисления выражений имя вычисляемого или агрегированного поля берется по его псевдониму, следующему после ключевого слова as: SELECT P.NN, P.DatePrih, P.Tovar, P.Kolvo, K.KursUSD, P.Kolvo * K.KursUSD as Zena FROM prihod P, KursUSD К WHERE K.Datel*» P.DatePrih В данном случае вычисляемое поле P.K0IV0 * K.KursUSD имеет в результирующем НД имя Zena (цена). Если вычисляемому или агрегированному полю псевдоним не присвоен, имя поля берется как копия арифметического выражения, по которому значение этого поля вычисляется. Например: SELECT P.NN, P.DatePrih, Р.Tovar, P.Kolvo, K.KursUSD, P.Kolvo * K.KursUSD FROM prihod P, KursUSD К WHERE K.Datel- P.DatePrih Имя вычисляемого поля будет р. kolvo * к. kursusd. 6.6.2. Свойства FieldCount и Fields Свойство FieldCount содержит количество объектов-полей класса TField, опреде- ленных в редакторе полей для текущего НД. Если не был создан ни один объект-поле, для ТТаЫе, свойство FieldCount будет содержать значение, совпадающее с количест- вом физических полей ТБД, а для TQuery - количество полей, объявленных в качестве полей возвращаемого результирующего НД за словом SELECT:
130 Глава 6 SELECT F.polel, F.pole2, S.pole99, ... FROM first F, second S, ... При этом не важно, сколько имеется ТБД-источников для 5£2/,-запроса и к какому из них принадлежат возвращаемые поля, поскольку они считаются принадлежащими набору данных, возвращаемому в результате выполнения оператора SELECT, и ни с чем иным не ассоциируются. Свойство Fields есть набор объектов-полей TField, определенных для НД. К от- дельному полю можно обратиться, указав Fields [Index], где Index лежит в диапа- зоне O..FieldCount-\. Пример. Пусть в редакторе полей для Tablel определена группа объектов-полей. Необходимо записать в ListBox2 имена физических полей и для каждого из них - имя компонента TField, ассоциированного с полем: ListBox2.Clear; for i := 0 to Tablel.Fieldcount - 1 do ListBox2.Items.Add(Format('%-16s %-10s1, [Tablel.Fields[i].FieldName, Tablel.Fields[i].Name])); Как можно видеть из рис. 6.7, использование свойств FieldDefs и Fields имеет раз- ную природу: если первое выдает информацию о физически объявленных .полях в ТБД, второе выдает сведения о логических полях НД, т. е. полях, добавленных в кол- лекцию объектов типа TField для этого НД. Forml Вии г Tovar ] DatePrih |Postvdhik Kolvo| ZenaEd I Сумма _г А ►! 10.10.99 ТОО "Бакалея" 300 -8.5 2 550,00 Печенье 12.10.99 ТОО "Бакалея" 90 7 630,00 Сахар 10.11.99 АО "Пищепром" 100 7,5 750,00 Сок апельсиновый 15.11.99 Трест №2 250 18 4500,00 Поля и их характеристики из Тablel FieldDefs Имена полей из свойства Тablel Field Имя поля в ТБД Тип Required Длина Имя поля в ТБД Имя компонента TField NN AutoInc - 0 Tovar TablelTovar Tovar String Req 20 DatePrih TablelDatePrih DatePrih Date Req 0 Postvdhik TablelPostvdhik Postvdhik String — 20 Kolvo Tab le IKo Ivo Kolvo Integer Req 0 ZenaEd Tab lei ZenaEd ZenaEd Float — 0 Sklad TablelSklad Sklad Integer - 0 Сумма TablelField Рис. 6.7. Информация о компонентах TJield. Заметим, что поле VychislPole является вычисляемым и физически в структуре ТБД отсутствует.
Работа с наборами данных 131 6.6.3. Свойство DefaultFields и метод ClearFields Свойство property DefaultFields Boolean; содержит True, если для НД используются умалчиваемые поля, и False, если для него создан хотя бы один объект-поле. Метод procedure ClearFields; очищает содержимое полей текущей записи набора данных. Если НД не находится в режиме вставки новой записи или редактирования, возбуждается исключение. В слу- чае успешного выполнения вызывается обработчик события OnDataChange для ком- понента TDataSource, связанного с НД. 6.6.4. Способы обращения к полям набора данных Свойство function FieldByName(const FieldName: String): TField; позволяет обращаться к полю по его имени. Например: Tablel. FieldByName('FIO').Value := 'Иванов И.И.’; С помощью свойства property Fieldvalues[const FieldName: String]: Variant; можно обратиться к значению поля с именем FieldName, например: Tablel.Fieldvalues['FIO'] := 'Иванов И.И.'; Для НД свойство FieldValues является умалчиваемым, поэтому имя свойства при об- ращении к полю можно опускать: Tablel [' FIO' ] := '-Иванов И.И.'; Свойство property Fields[Index: Integer]: TField; позволяет обращаться к полю НД по его индексу. Например: Tablel.Fields[2].Value := 'Иванов И.И.'; К объекту TField, созданному с помощью редактора полей, можно обращаться по его имени, без упоминания имени НД. По умолчанию оно формируется из имени на- бора данных и имени поля: TablelFIO.Value := 'Иванов И.И.'; 6.7. БЛОКИРОВКА ТАБЛИЦ В МНОГОПОЛЬЗОВАТЕЛЬСКОМ РЕЖИМЕ При работе с таблицами локальных СУБД в многопользовательском режиме может возникнуть ситуация, когда на момент внесения изменений в какую-либо таблицу БД
132 Глава б одним пользователем следует блокировать внесение изменений в эту же таблицу со стороны других пользователей. Более жестким ограничением может служить запрет на просмотр содержимого таблицы со стороны других пользователей. Запрет внесения изменений или просмотра другими пользователями НД, ассоции- рованного с данной таблицей, достигается с помощью метода procedure LockTable(LockType: TLockType); где параметр LockType определяет вид запрета: ItReadLock - запретить чтение; ItWriteLock - запретить запись. Можно запретить и чтение, и запись данных, но для этого нужно вызвать метод LockTable дважды. Попытка внесения изменений в НД, ассоциированный с таблицей БД, для которой запрещена запись данных, равно как и попытка чтения из запрещенной для чтения таблицы, приводят к возбуждению исключительной ситуации EDBEngineError с со- общением Table Имя_таблицы is locked. После того как необходимость запрета пропадет, он должен быть отменен методом procedure UnlockTable(LockType: TLockType); где параметр LockType указывает тип снимаемого запрета. Если таблица БД блокирована, исключение возбуждается при попытке отредакти- ровать или удалить какую-либо запись той же таблицы другим пользователем. Чтобы сообщить об этом пользователю, можно написать обработчик события OnEditError, например: procedure TForml.TablelEditError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction); begin ShowMessage('Таблица блокирована другим пользователем'); Action := daAbort; end; Замечание. В архитектуре «клиент-сервер» не возникает потребности в реализа- ции программных блокировок. Возможность одного пользователя вносить изме- нения в данные, уже корректируемые другим пользователем, определяется уров- нем изоляции транзакций в приложении (свойство TransIsolation компонента TDatabase) и уровнем изоляции транзакций сервера, а также механизмом блоки- ровок сервера. Обычно блокировка накладывается на запись, измененную в рам- ках незавершенной транзакции. Однако, например, Sgl-cepeep InterBase не бло- кирует чтение из записей, которые изменяются другим пользователем в рамках еще не завершенной транзакции. В этом случае тот пользователь, чье приложение читает записи, видит записи в их последнем подтвержденном состоянии. 6.8. СИНХРОНИЗАЦИЯ СОДЕРЖИМОГО НАБОРОВ ДАННЫХ Записи НД (компоненты ТТаЫе, TQuery) размещаются в локальной копии на компь- ютере, на котором выполняется приложение. Обновление локальной копии данных про- исходит в случае выполнения из данного приложения операции модификации НД (добавления, изменения или удаления записи, т.е. выполнения методов Post или Delete). Однако возникают случаи, когда, содержимое локальной копии данных на конкретном
Работа с наборами данных 133 компьютере должно быть обновлено из физической таблицы БД (или синхронизировано с физической таблицей БД). Это достигается путем выполнения метода procedure Refresh; При этом может встретиться два варианта применения этого метода. Они рассмат- риваются ниже. 6.8.1. Синхронизация содержимого наборов данных в одном приложении В приложении может существовать несколько НД, ассоциированных с одной и той же таблицей БД. Например, это может быть компонент Tablel, расположенный в мо- дуле данных (Data Module) приложения, и компонент Table2, расположенный в форме и выполняющий там специфические функции, отличные от функций компонента Tablel. Тогда, если эти компоненты активны во время выполнения приложения, нужно обновлять содержимое одного набора данных в случае обновления другого. Если, например, изменяется TDataModulel.Tablel, то для синхронизации измене- ния с содержимым TForml.Table2 следует написать такие обработчики событий: procedure TDataModulel.TablelAfterDelete(DataSet: TDataSet); begin TForml.Table2.Refresh; end; procedure TDataModulel.TablelAfterPost(DataSet: TDataSet); begin TForml.Table2.Refresh; end; 6.8.2. Синхронизация содержимого наборов данных в разных приложениях При многопользовательском режиме доступа к БД из нескольких копий одного и того же приложения бывает необходимо обеспечить такую функциональность, чтобы каждый пользователь видел подтвержденные изменения, внесенные другими пользователями. В клиентских приложениях, работающих в рамках архитектуры «клиент-сервер», по- добная функциональность обеспечивается автоматически при уровне изоляции транзак- ций Read Committed и при условии использования компонента ТТаЫе. Компонент TQuery, выполнив запрос на чтение к удаленной БД, показывает записи НД в неизменном виде независимо от того, изменялось ли после запроса содержимое таблицы БД. Для обновле- ния информации в НД, реализуемом при помощи TQuery, приходится закрывать и по- вторно открывать компонент TQuery, т. е. повторно выполнять запрос к удаленной БД. При уровне изоляции транзакций Repeatable read пользователь в рамках транзакции видит данные только в том состоянии, в котором они находились на момент старта транзакции. В приложениях, работающих в архитектуре «файл-сервер» (с использованием ло- кальных таблиц типа Paradox, dBase), не происходит автоматического обновления в наборе данных (находящемся в режиме просмотра dsBrowse) в приложении одного поль- зователя после внесения изменения в эту таблицу БД другим пользователем в своем приложении. Отображение изменений производится лишь после выполнения данным пользователем метода Post или Delete. Если НД находится в состоянии dsBrowse, его обновление можно реализовать периодически, например, с помощью таймера:
134 Глава б procedure TForml.TimerlTimer(Sender: TObject); begin if Tablel.State = dsBrowse then Tablel.Refresh; end; или, если нужно обновлять все НД приложения: procedure TForml.TimerlTimer(Sender: TObject); var DSCnt: Integer; i: Integer; begin with Session.Databases[0] do begin DSCnt := DataSetCount; // Получаем количество открытых НД for i := 0 to DSCnt - 1 do // Обновляем каждый из них if DataSets[i].State = dsBrowse then DataSets[i].Refresh; end; //with end; Если нужно обновлять лишь некоторые из наборов данных, указатели на них мож- но поместить в список TStringsListl. Этот список можно создать и наполнить в момент создания формы (но не в момент ее активизации, т. к. форма может активизироваться много раз - после минимизации или после вызова другого приложения). При разру- шении формы список удаляется: var DSList : TStringList; procedure TForml.FormCreate(Sender: TObject); begin DSList := TStringList.Create; // Создаем список DSList.AddObject (' Tablel); // Помещаем в него ссылку на Tablel DSList.AddObject(' *,Table2); // и Table2 Timerl.Enabled := True; // Включаем таймер end; procedure TForml.TimerlTimer(Sender: TObject); var i: Integer; begin with DSList do begin for i := 0 to Count - 1 do if (Objectsfi] as TDataSet).State = dsBrowse then (Objects[i] as TDataSet).Refresh; end; //with end; procedure TForml.FormDestroy(Sender: TObject); begin DSList.Free; end;
Работа с наборами данных 135 6.9. ОБРАБОТКА ОШИБОК СМЕНЫ СОСТОЯНИЯ НАБОРА ДАННЫХ В случае неудачи при выполнении методов Insert, Edit, Delete и Post обработку ошибки можно реализовать в соответствующих обработчиках событий OnEditError (ошибки при выполнении Insert и Edit), OnDeleteError (ошибки при выполнении Delete) и OnPostError (ошибки при выполнении Post)-. property OnEditError: TDataSetErrorEvent; property OnDeleteError: TDataSetErrorEvent; property OnPostError: TDataSetErrorEvent; где type TDataAction = (daFail, daAbort, daRetry); TDataSetErrorEvent = procedure(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction) of object; Назначение параметров: DataSet - указатель на компонент, в котором произошла ошибка; Е - ссылка на объект-исключение; Action - действие: daFail - выполнение метода, вызвавшего ошибку, отменяется, выводится сообще- ние об ошибке; daAbort — выполнение метода, вызвавшего ошибку, отменяется, сообщение об ошибке не выводится; daRetry — метод, вызвавший ошибку, после выхода из обработчика выполняется за- ново; при этом в теле обработчика должна быть скорректирована причина ошибки, иначе произойдет зацикливание программы. Пусть необходимо выдать программное сообщение пользователю и отменить вы- полнение ошибочного метода, если возникает ошибка при выполнении, метода Insert или Edit (например, таблица заблокирована другим пользователем). Тогда можно ис- пользовать такой обработчик события OnEditError: procedure TForml.TablelEditError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction); begin ShowMessage('Таблица заблокирована другим пользователем1); Action := daAbort; end; 6.10. ОГРАНИЧЕНИЯ НА ЗНАЧЕНИЯ ПОЛЕЙ Набор данных имеет свойство property Constraints: TCheckConstraints; которое представляет собой коллекцию объектов TCheckConstraints. Каждый такой объект определяет ограничение, накладываемое на значение одного или более полей. Количество ограничений, созданных для НД, определяется свойством Count коллек- ции Constraints. Доступ к отдельному ограничению с индексом Index осуществляется при помощи свойства property Items[Index: Integer]: TCheckConstraint;
136 Глава б При этом значение Index должно находиться в диапазоне 0..Count - 1. На рис. 6.8 пока- зан список ограничений, определенных для НД, как он выглядит при обращении к свойству Constraints набора данных в Инспекторе Объектов. Рис. 6.8. Определение условий ограничений на значения полей. Каждое ограничение имеет тип TCheckConstraint. Рассмотрим свойства этого класса. Свойство Назначение property Customconstraint: String; Содержит текст ограничения на значение поля (полей) в 5£)£-подобном синтаксисе. property ErrorMessage String; Содержит текст сообщения об ошибке. property FromDictionary: Boolean; Указывает источник формирования ограниче- ния - словарь данных (значение True) или непосредственно приложение (False). property ImportedConstraint String; Используется для запоминания SgL-текста ограничения, импортированного из SQL- сервера или словаря данных. Заметим, что свойство ErrorMessage содержит текст сообщения об ошибке. Это сообщение выводится, если пользователь предпримет попытку запомнить запись, поля которой не удовлетворяют данному ограничению (рис. 6.9). Рис. 6.9. Пример сообщения, выдаваемого при нарушении ограничения на значение поля.
Глава 7 РАБОТА С КОМПОНЕНТОМ TTABLE 7.1. ОБЗОР СВОЙСТВ И МЕТОДОВ Компонент ТТаЫе является одним из наследников класса TDBDataSet, свойства, методы и события которого рассмотрены в гл. 6. В этом разделе описываются специ- фичные для ТТаЫе свойства и методы (его события унаследованы от TDBDataSet и здесь на рассматриваются). 7.1.1. Свойства Свойство Назначение property CanModify: Boolean; Указывает, можно лн изменять связанный с таблицей НД (редактировать, вставлять или удалять записи) property DataSource: TDataSource; Содержит имя главного НД в отношении главный-подчиненный (только для чтения). property Defaultindex: Boolean; Если содержит True, НД в момент открытия будет отсортирован по умалчиваемому индек- су. property Exclusive: Boolean; Если содержит True, после открытия таблицы запрещает работу с ней другим приложениям. property Exists: Boolean; Указывает, существует ли нужная таблица БД. property IndexDefs: TIndexDefs; Содержит информацию об индексах таблицы. property IndexFieldCount: Integer; Указывает количество полей, которые обра- зуют текущий ключ. property IndexFieldNames: String; Содержит список полей, образующих текущий ключ. property IndexFields: [Index: Integer]: TField; Открывает доступ к полям индекса. property IndexFiles: TStrings; Содержит список имен индексных файлов для таблицы dBASE. property IndexName: String; Содержит имя используемого индекса. property KeyExclusive: Boolean; Если содержит False, граничные точки диапа- зона при фильтрации по ключам входят в поисковый диапазон, в противном случае - не входят. property KeyFieldCount: Integer; Указывает, сколько полей будут участвовать в индексе при поиске по части индекса. property MasterFields: String; Содержит список полей главной таблицы, по которым в данной подчиненной таблице будет установлена связь главный-детальный. property Mastersource: TDataSource; Определяет имя главной таблицы в связи главный-детальный. property Readonly: Boolean; Если содержит True, ТБД используется только для чтения данных.
138 Глава 7 property StoreDefs: Boolean; Если содержит True, определения индексов и полей таблицы сохраняются вместе с модулем данных. property TableName: TFileName; Содержит имя таблицы. Type TTableType = (ttDefault, ttParadox, ttDBase, ttASCII, ttFoxPro); property TableType: TTableType; Определяет тип таблицы: ttDefault - тип опре- деляется расширением табличного файла; ttParadox - таблица Paradox; ttDBase - табли- ца dBASE; ttASCII-текстовая таблица; ttFoxP - таблица FoxPro. 7.1.2. Методы Метод Назначение type TIndexOption = (ixPrimary, ixUnique, ixDescending, ixCaselnsensitive, ixExpression, ixNonMaintained) ; TIndexOptions = set of TIndexOption; procedure Addindex(const Name, Fields: String; Options: TIndexOptions); Создает новый индекс: Name - имя индекса;' Fields - список индексных полей; Options - параметры индекса, в том числе ixPrimary - первичный индекс; ixUnique - уникальный ин- декс; ixDescending - сортирующий по убыванию значений; ixCaselnsensitive - чувствительный к высоте букв в строковых полях; ixExpression - определенный по индексному выражению; ixNonMaintained - индекс не обновляется авто- матически в момент открытия таблицы. procedure ApplyRange; Включает механизм фильтрации записей по ключевым полям. type TBatchMode = (batAppend, batUpdate, batAppendUpdate, batDelete, batCopy); function BatchMove(ASource: TBDEDataSet; AMode: TBatchMode): Longlnt; Копирует записи из НД ASource в текущую таблицу и возвращает количество перенесен- ных записей. AMode определяет режим копи- рования: batAppend - добавить все записи в конец существующей таблицы; batUpdate - обновить записи с одинаковыми ключевыми полями; batAppendUpdate - обновить записи с одинаковыми ключевыми полями, остальные добавить в конец; batDelete - удалить записи с одинаковыми ключевыми полями; batCopy - удалить все записи текущей таблицы и заме- нить их записями из копируемой таблицы. procedure CancelRange; Отменяет фильтрацию записей по ключевым полям. ! procedure CloselndexFile(const IndexFileName: String); Закрывает индексный файл с именем IndexFileName. procedure CreateTable; Создает новую таблицу по информации, хра- нящейся в свойствах FieldDefs, Fields и IndexDefs. procedure Deleteindex(const Name: String); Уничтожает индекс с именем Name. procedure DeleteTable; Уничтожает таблицу. procedure EditKey; Переводит таблицу в режим dsSetKey и позво- ляет модифицировать ключи поиска.
Работа с компонентом ТТаЫе 139 procedure EditRangeEnd; Позволяет изменить конечное значение диапа- зона фильтрации по индексным полям. procedure EditRangeStart; Позволяет изменить начальное значение диа- пазона фильтрации по индексным полям. procedure EmptyTable; Удаляет все записи из таблицы. function FindKey(const KeyValues: array of const): Boolean; Отыскивает запись по указанным значениям KeyValues ключевых полей и возвращает True в случае успеха. procedure FindNearest(const KeyValues: array of const); Перемещает курсор на запись, наиболее полно удовлетворяющую указанным значениям KeyValues ключевых полей. procedure GetDetailLinkFields(MasterFields, DetailFields: TList); override; Создает два списка ключевых полей - для главной таблицы (MasterFields) и для деталь- ной (DetailFields}. procedure GetlndexNames(List: TStrings); Наполняет список List именами ключевых полей. procedure GetProviderAttributes(List: TList); override; Получает нз BDE и помещает в список List языковый драйвер, маршрут доступа и имя таблицы. procedure GotoCurrent(Table: TTable); Устанавливает курсор текущего НД в положе- ние курсора НД Table, основанного на той же физической ТБД. function GotoKey: Boolean; Пытается отыскать запись, ключевые поля которой соответствуют значениям, связанным с предыдущими вызовами StartKey или EditKey. Перемещает курсор на найденную запись н возвращает True в случае успеха. procedure GotoNearest; Перемещает курсор на запись, наиболее полно удовлетворяющую значениям ключей, уста- новленных методами SetKey или EditKey. type TLockType = (ItReadLock, ItWriteLock); procedure LockTable(LockType: TLockType); Блокирует таблицу, ограничивая доступ к ней другим пользователям: ItReadLock - блокиро- вать запись и чтение; ItWriteLock - блокиро- вать только запись. procedure OpenlndexFile(const IndexFileName: String); Открывает индексный файл с именем IndexFileName. procedure RenameTable(const NewTableName: String); Переименовывает файл локальной таблицы. NewTableName - новое имя файла. procedure SetKey; Переводит таблицу в режим dsSetKey и очи- щает буфер ключей. С помощью FieldByName программа должна установить новый набор ключей и с помощью ApplayRange отфильтро- вать НД по индексным ключам. procedure SetRange(const Startvalues, Endvalues: array of const); Переводит таблицу в режим dsSetKey, уста- навливает новый набор начальных StartValues и конечных EndValues значений буфера клю- чей и фильтрует НД по индексным ключам. procedure SetRangeEnd; Переводит таблицу в режим dsSetKey и очи- щает буфер конечных значений ключей. С помощью FieldByName программа должна
140 Глава 7 установить новый набор ключей и с помощью ApplayRange отфильтровать НД по индексным ключам. procedure SetRangeStart; Переводит таблицу в режим dsSetKey и очи- щает буфер начальных значений ключей. С помощью FieldByName программа должна установить новый набор ключей и с помощью ApplayRange отфильтровать НД по индексным ключам. procedure UnlockTable(LockType: TLockType); Отменяет блокировку таблицы (см. ^ocltfable}^— 7.2. РАБОТА С ИНДЕКСАМИ 7.2.1. Получение информации об индексах Свойство IndexDefs компонента ТТаЫе содержит ссылку на объект класса TIndexDefs. С помощью свойств и методов этого класса можно получить информацию об индексах таблицы. В частности, свойство Count возвращает количество индексов, а property Items[Index: Integer]: TIndexDef; открывает доступ к набору объектов типа TIndexDef, каждый из которых содержит ин- формацию о конкретном индексе. Index должен принадлежать диапазону 0...Count -1. Некоторые свойства TIndexDef. Свойство Назначение property CaselnsFields: String; Содержит перечень полей, которые будут учитывать разницу в высоте букв. property DescFields: String; Содержит перечень полей, в которых значения сортируются в нисходящем порядке. property Expression: String; Содержит индексное выражение. property Fields: String; Содержит список полей, по которым построен индекс. Поля в строке разделены точкой с запятой. property Name: String; Содержит имя индекса. type TIndexOptions = set of (ixPrimary, ixUnique, ixDescending, ixNonMaintained, ixCaselnsensitive); property Options: TIndexOptions Определяет характеристики индекса: ixPrimary - первичный индекс; ixUnique - уникальный индекс; ixDescending - сортиров- ка по нисходящему значению; ixNonMaintained - индекс не обновляется автоматически в момент открытия таблицы; ixCaselnsensitive - учитывается высота букв в строковых полях. Методы' объекта IndexDef не представляют интереса для программиста. В отличие от этого методы объекта-контейнера IndexDefs используются для управления индекс- ными полями.
Работа с компонентом ТТаЫе 141 Методы TIndexDefs'. Метод Назначение procedure Add(const Name, Fields: String; Options: TIndexOptions); Создает новый объект TIndexDef и помещает его в коллекцию TIndexDefs.Items. Параметры Name, Fields и Options используются для уста- новки значений аналогичных свойств объекта TIndexDef, procedure Clear; Очищает элементы коллекции Items. function IndexOf(const Name: String): Integer; Возвращает индекс элемента в коллекции Items, у которого свойство Name совпадает с параметром Name данного метода. function FindlndexForFields(const Fields: String): TIndexDef; Отыскивает индекс по списку его полей, кото- рые содержатся в строке Fields. procedure GetlndexNames(List: TStrings); Возвращает в параметре List список имен индексов. procedure Update; Обновляет элементы коллекции Items текущей информацией из НД, причем обновление мо- жет производиться и без открытия набора данных. Рассмотрим несколько примеров. Пример 1. Записать в ListBoxl имена всех индексов ТБД, ассоциированной с Tablel'. ListBoxl.Clear; Tab1е1•IndexDe f s.Upda te; for i ;» 0 to Tablel.IndexDefs.Count - 1 do ListBoxl.Items.Add(Tablel.IndexDefs[i].Name); Перед считыванием значения свойства Name необходимо выполнить метод Index- Defs. Update для обновления информации обо всех имеющихся индексах. Отметим, что для таблиц Paradox первичный индекс не имеет имени и для него в свойстве Name возвращается пустая строка (рис. 7.1). Рис. 7.1. Первичный индекс таблиц Paradox не имеет имени.
142 Глава 7 Пример 2. Показать в Editl список полей индекса, чье имя является текущим в ListBoxl (результат на рис. 7.2): procedure TForml.ListBoxlClick(Sender: TObject); // Обработчик события выбора элемента в ListBoxl var Teklndex: Integer; begin // Получаем индекс текущего элемента в ListBoxl: Teklndex := ListBoxl.Itemindex; // Записываем в Editl список индексных полей: Editl.Text := Tablel.IndexDefs[Teklndex].Fields; Label2.Caption := 'Поля индекса ' + ListBoxl.Items[Teklndex] + end; Puc. 7.2. Список полей текущего индекса. Пример 3. Поместить в ListBox2 список индексов ТБД, ассоциированной с Tablel (рис. 7.3): • ListBox2.Clear; Tablel.GetlndexNames(ListBox2.Items); Рис. 7.3. Списки индексов, полученные при помощи IndexDefs и GetlndexNames.
Работа с компонентом ТТаЫе 143 Свойство IndexFields компонента ТТаЫе содержит набор полей текущего индекса; обращение IndexFields [Index] возвращает информацию о поле, определенном в текущем индексе под номером Index (нумерация полей начинается с 0). Например, чтобы записать в ListBoxl имена всех полей текущего индекса, можно использовать такой фрагмент программы: var i: Integer; ListBoxl.Clear;, for i:= 0 to Tablel.IndexFieldCount - 1 do ListBoxl.Items.Add(Tablel.IndexFields[I].FieldName); 7.2.2. Установка текущего индекса To, какой индекс является текущим для НД (компонент ТТаЫе), в ряде случаев имеет важное значение. Во-первых, текущий индекс определяет поля, по которым будет отсортирован НД. Это важно как с точки зрения доступа к данным, так и с точки зрения их визуализа- ции: представление данных должно быть максимально информативным. Плюс к этому пользователь должен быстро находить нужную ему информацию и видеть группиров- ки записей внутри НД. Во-вторых, многие методы и свойства ТТаЫе работают напрямую с текущим ин- дексом. Это, например, метод SetRange и связанные с ним методы для фильтрации записей; методы FindKey, FindNearest и другие для поиска записи, удовлетворяющей некоторому условию. Для указания индекса, по которому будет производиться сортировка в НД, связан- ном с данным компонентом ТТаЫе, имеются два взаимоисключающих способа: уста- новка имени индекса в свойство IndexName и установка списка индексных полей в свойство IndexFieldNames'. Tablel.IndexName := 'INDEX_BY_FIO'; или Tablel.IndexFieldNames := 'FIO;Doljnost'; Индекс с указанными полями должен физически существовать. Попытка присвоить свойству IndexFieldNames список полей, не являющихся полями какого-либо индекса, вызовет исключительную ситуацию. Данное свойство особенно ценно для таблиц Paradox, т. к. позволяет сделать текущим главный (первичный) индекс путем перечис- ления входящих в него полей. 7.2.3. Добавление нового индекса Добавление нового индекса происходит в режиме исключительного доступа к ТБД (свойство Exclusive = True) и осуществляется методом procedure Addindex(const Name, Fields: String; Options: TIndexOptions); где параметр Name определяет имя индекса, а параметр Fields - список индексных полей. В случае нескольких полей соседние имена в списке разделяет точка с запятой.
144 Глава 7 Должны указываться только поля, объявленные в структуре ТБД, в противном случае будет возбуждена исключительная ситуация. Параметр Options является множеством, которое содержит значения, определяющие свойства индекса (см. п. 7.1.2). Например, чтобы определить новый индекс с именем WWW, построенный по полям NN и DatePrih и нечувствительный к высоте букв, можно использовать такой фраг- мент программы: Tablel.Close; Tablel.Exclusive := True; Tablel.Open; Tablel.Addlndex('WWW, 1 NN;DatePrih', [ixCaselnsensitive]); Tablel.Close; Tablel.Exclusive := False; Tablel.Open; 7.2.4. Удаление существующего индекса Удаление существующего индекса происходит в режиме исключительного доступа к ТБД (свойство Exclusive = True) и осуществляется методом procedure Deletelndex(const Name: String); где параметр Name определяет имя удаляемого индекса. При попытке удаления несу- ществующего индекса возбуждается исключительная ситуация. Пример. Удалить индекс с именем WWW: Tablel.Close; Tablel.Exclusive := True; Tablel.Open; Tablel.Deletelndex('WWW) ; Tablel.Close; Tablel.Exclusive := False; Tablel.Open; 7.2.5.0 составных индексах Составными называются индексы, построенные по значениям двух и более полей одновременно. Такие индексы широко используются на практике для ускорения поис- ка нужной записи или группы записей методами FindKey и FindNearest. Если в таблице построен индекс только по полю Fieldl, а требуется отыскать запи- си, содержащие нужные значения в полях Fieldl и Field2, то методы FindKey/FindNearest смогут установить курсор лишь в начало целой группы записей, поля Fieldl в которых имеют нужные значения, но отличающихся значениями осталь- ных полей. В этом случае для поиска записей с нужными значениями поля Field2 при- дется использовать последовательный просмотр таблицы, начиная с найденной запи- си. Если построен составной индекс по полям Fieldl и Field2, методы FindKeylFindNearest сразу позиционируют курсор на нужную запись и для таблиц большого размера могут существенно сократить время поиска. Например, в таблице, насчитывающей более 800 000 записей, нужно было отыскать группу записей с задан- ными значениями трех полей. И хотя по каждому полю в отдельности был построен индекс, поиск проходил с заметными задержками (5..6 с.), которые вызывали у поль- зователей вполне оправданное раздражение. После построения составного индекса
Работа с компонентом ТТаЫе 145 время поиска сократилось до долей секунды, и пользователи просто перестали заме- чать задержки. Для создания составного индекса на серверных таблицах и таблицах типа Paradox с помощью SQL Explorer используется 50£-запрос типа CREATE INDEX IndexName ON TableName(Fieldl, Field2, ...) На заметку. В широко распространенных таблицах типа dBASE нельзя соз- дать составной индекс и, следовательно, реализовать быстрый индексный поиск при участии нескольких полей. В базирующихся на них средствах разработки (dBASE III+, dBASE IV, FoxPro, Clipper) для этих целей обычно используется индекс, построенный по индексному выражению, с приведени- ем нужных полей к строковому типу. Например: STR(FINCODE,5)+LEFT(FINTYPE,1)+DTOS(FINDATE) (это индексное выражение формирует строку из пяти символов преобразо- вания целого значения поля fincode, первого символа значения строкового поля FINTYPE и преобразованного к строке значения даты из поля findate). Увы! Delphi не имеет средств поиска по индексным выражениям. Таким об- разом, использование таблиц dBASE в приложениях Delphi менее удобно, чем использование таблиц Paradox: если вы собираетесь разработать прило- жение для пока еще не созданной БД и выбрали традиционную архитектуру файл-серверных таблиц, обратите на это внимание; если вы создаете прило- жение для уже существующей и наполненной БД, в которой используются таблицы dBASE, имеет смысл подумать о перекачке содержащихся в них данных в таблицы Paradox с помощью компонентов TBatchMove. 7.3. ИСКЛЮЧИТЕЛЬНЫЙ ДОСТУП К НД 7.3.1. Установка приоритетного доступа в многопользовательском режиме Свойство Exclusive дает пользователю исключительный доступ к НД (значение True). Это означает, что никто иной не только не может вносить изменения в НД, но и вообще не имеет к нему доступа. Установить исключительный доступ можно, лишь когда ни один пользователь не имеет доступа к НД и тот не открыт. Для серверных таблиц исключительный доступ может означать запрет изменения НД другими пользователями. Однако последние могут просматривать содержимое НД. Замечание. Диалоговая среда Delphi тоже считается пользователем, если на этапе конструирования формы компонент ТТаЫе активизирован (его свойст- во Active содержит True). Поэтому, если в программном коде 'делается по- пытка получения прав исключительного доступа к ТБД и программа запу- щена из Delphi, эта попытка будет блокирована, поскольку на вашей машине имеется два пользователя, осуществляющих доступ к этой ТБД: выполняю- щееся приложение и Delphi. Если ваша программа должна обращаться к ме- тодам, требующим исключительных прав доступа, нужно закрыть таблицу на этапе конструирования и открывать ее программно, например, в обработ- чике события OnCreate главной формы.
146 Глава 7 7.3.2. Удаление записей и таблиц Метод EmptyTable уничтожает все записи в ТБД, связанной с данным НД. После этой операции ТБД будет пустой. Метод применим только к закрытым НД и только в режиме исключительного доступа (см. выше). В противном случае возбуждается ис- ключение и очистка ТБД блокируется. Пример. Очистить от записей ТБД, ассоциированную с НД Tablel-. Tablel.Close ; Tablel.Exclusive := True; Tablel.EmptyTable; Tablel.Exclusive := False; Tablel.Open; Еще раз напомним, что таблица не будет очищена, если этот фрагмент выполняет- ся из среды Delphi при открытой перед запуском программы таблице Tablel. Если в момент запуска таблица закрыта, она может быть очищена с помощью EmptyTable. С помощью процедуры DeleteTable таблица уничтожается физически. Метод при- меним только к закрытым НД, но для его выполнения не нужно блокировать доступ к таблице с помощью свойства Exclusive. 7.3.3. Создание новой таблицы Метод CreateTable создает новую пустую таблицу в момент прогона программы. Этот метод используется редко, т. к. обычно для локальной БД таблицы создаются с помощью утилиты Database Desktop, а для серверной - с помощью предложения create table языка SQL. Перед созданием таблицы для данного компонента ТТаЫе нужно указать: • имя БД - в свойстве DatabaseName-, • имя таблицы - в свойстве TableName-, • тип таблицы - в свойстве ТаЫеТуре-, • описания полей — в свойстве FieldDefs-, • описания индексов - в свойстве IndexDefs. Для добавления описаний полей в свойство FieldDefs оно сначала очищается мето- дом Clear, а затем для каждого поля информация в FieldDefs заносится методом procedure Add(const Name: String; DataType: TFieldType; Size: Word; Required: Boolean); где Name — имя поля; DataType - тип поля; Size - размер поля, если его указание необ- ходимо, или 0, если тип поля подразумевает его размер, например, ftlnteger. Required - определяет, должно ли поле в обязательном порядке содержать значение. Для добавления описаний индексов в свойство IndexDefs оно сначала очищается, а затем для каждого поля информация в IndexDefs заносится методом procedure Add(const Name, Fields: String; Options: TIndexOptions); где Name - название индекса; Fields - список индексных полей; в случае нескольких полей их имена разделяются точкой с запятой; могут быть указаны только те поля, описания которых перед этим добавлены в свойство FieldDefs-, Options — определяет свойства индекса (см. описание метода Addindex).
Работа с компонентом ТТаЫе 147 Пример. Создать новую ТБД с именем NewT в БД bookl, состоящую из двух полей - символьного поля FIO и целочисленного Oklad, с одним первичным индексом по полю FIO: with Tablel do begin // Указываем имя БД, имя новой ТБД, тип ТБД: Active := False; DatabaseName := 'book'; TableName := Editl.Text; TableType := ttParadox; // Описываем поля создаваемой ТБД: with FieldDefs do begin Clear; Add('FIO', ftString, 30, False); Add('Oklad', ftlnteger, 0, False); end; //with FieldDefs // Описываем индексы создаваемой ТБД: with IndexDefs do begin Clear; Add('Index_FIO', 'FIO', [ixPrimary, ixUnique]); end; //with IndexDefs CreateTable; // Создаем ТБД Active := True; // Открываем ее end; //with Tablel Заметим, что ТБД может быть создана также и при помощи 5£>А-оператора CREATE TABLE. Для локальных или файл-серверных таблиц типа Paradox и dBase - это альтернативный методу CreateTable способ, а для серверных таблиц - единствен- ный способ динамического создания ТБД из работающего приложения. 7.4. ПОИСК ЗАПИСЕЙ В НД 7.4.1. Обзор методов Для поиска записей в НД в компоненте ТТаЫе применяются следующие методы: FindKey - ищет запись, точно удовлетворяющую условиям в списке значений; су- ществует также дублирующий его метод GoToKey; FindNearest - ищет запись, наиболее полно удовлетворяющую условиям в списке значений; существует также дублирующий его метод GoToNearest. В отличие от методов Locate и Lookup, которые ТТаЫе унаследовал от TDBDataSet и которые описаны в п. 6.4, эти методы ищут запись по ключевым полям. Если в таблице не построен хотя бы один индекс, использовать методы FindKey и FindNearest нельзя. 7.4.2. Установка значений для поиска Состав полей, используемых для идентификации нужной записи при поиске в НД, определяется текущим индексом. Поэтому в качестве текущего нужно установить индекс, построенный по полям, значения которых и будут отыскиваться. Напомним, что для установки текущего индекса используют свойства IndexFieldNames или IndexName.
148 Глава 7 Для индексов, в состав которых входит более одного поля, должны указываться значения всех полей, входящих в индекс, или значения полей старших уровней вло- женности. Более подробно см. п. 7.4.6. 7.4.3. Точный поиск Для точного поиска (поиска на точное соответствие) применяется метод FindKey. Он пыта- ется отыскать в НД запись, у которой индексные поля соответствуют значениям, указанным в параметре обращения. Если такая запись найдена, метод FindKey возвращает True и указатель текущей записи в НД (курсор НД) устанавливается на эту запись, т. е. она делается текущей. Если найдена группа записей, отвечающая условию, текущей становится логически первая из них. Если запись не найдена, курсор НД не перемещается и метод возвращает False. Пример. Пусть имеется НД с полями GrNum (номер группы), NN (номенклатурный номер товара), Tovar (наименование товара): GrNum NN Tovar 1 10 Махорка 12 1 Веник универсальный 12 4 Кашпо плетеное 12 26 Дискеты BASF 100 1 Книга «Пушкин в изгнании» 100 10 Комплект отверток Рис. 7.4. Записи набора данных. Предположим, что поисковое значение GrNum вводится в Editl, a NN-bEdit2. То- гда обработчик нажатия клавиши поиска FindButton может выглядеть так (для просто- ты в обработчике не контролируется правильность ввода в компонентах Editl, Edit2): procedure TForml.FindButtonClick(Sender: TObject); var GrTmp, NNTmp: Integer; begin GrTmp := StrToInt(Editl.Text) ; NNTmp := StrToInt(Edit2.Text); // Поиск записи: if not Tablel.FindKey([GrTmp,NNTmp]) then ShowMessage('Нет товара с такой группой и номером!'); end; Пусть в Editl введено 12 и в Edit2 - 4. Тогда указатель переместится на запись, у которой значение GrNum равно 12 и значение поля NNравно 4 (рис. 7.5): GrNum NN Tovar 1 10 Махорка 12 1 Веник универсальный >12 4 Кашпо плетеное 12 26 Дискеты BASF 100 1 Книга «Пушкин в изгнании» 100 10 Комплект отверток Рис. 7.5. Перемещение указателя текущей записи. Для точного поиска в Delphi имеется группа методов SetKey, EditKey, GotoKey, ко- торые должны выполняться вместе и которые по функциональности аналогичны
Работа с компонентом ТТаЫе 149 методу FindKey. Использование их менее удобно: сначала НД переводится в состояние dsSetKey (методом SetKey или, если он уже применялся для данного индекса, EditKey), затем присваиваются поисковые значения полям и выполняется метод GotoKey. После этого НД переходит в состояние dsBrowse. Результат выполнения аналогичен резуль- тату, возвращаемому методом FindKey. Например, код , if not Tablel.FindKey([GrTmp,NNTmp]) then ShowMessage('Нет товара с такой группой и номером!'); эквивалентен более громоздкому коду с применением GotoKey. Tablel.SetKey; TablelGrNum.Value := GrTmp; TablelNN.Value := NNTmp; if not Tablel.GotoKey then ShowMessage('Нет товара с такой группой и номером!'); 7.4.4. Неточный поиск Неточный поиск (поиск на неточное, приблизительное соответствие) осуществля- ется методом FindNearest. Он пытается отыскать в НД запись, у которой индексные поля соответствуют указанным значениям. Если такая запись найдена, указатель те- кущей записи в НД перемещается на нее или на следующую за ней запись в зависимо- сти от значения свойства KeyExclusive. Если KeyExclusive = False (по умолчанию), указатель текущей записи перемещается на нее. Если KeyExclusive = True, указатель текущей записи перемещается на следующую запись. Если запись не найдена, указатель текущей записи всегда перемещается на бли- жайшую запись с большим значением индекса. В приводимых ниже примерах подразумевается KeyExclusive = False. Предположим, что поисковое значение GrNum вводится в Editl, a NN- в Edit2. То- гда обработчик нажатия клавиши поиска FindButton может выглядеть так: procedure TForml.FindButtonClick(Sender: TObject); var GrTmp, NNTmp: Integer; begin GrTmp := StrToInt(Editl.Text) ; NNTmp := StrToInt(Edit2.Text); //Поиск записи Tablel.FindNearest([GrTmp,NNTmp]); end; Пусть в Editl введено 12 и в Edit2 введено 4. Тогда указатель переместится на за- пись Кашпо плетеное (рис. 7.6): GrNum NN Tovar 1 10 Махорка 12 1 Веник универсальный 12 4 кашпо плетеное 12 26 Дискеты BASF 100 1 Книга «Пушкин в изгнании» 100 10 Комплект отверток Рис. 7.6. Перемещение указателя текущей записи после поиска.
150 Глава 7 Пусть в Editl введено 12 и в Edit2 - 0. Тогда указатель переместится на запись с 66 льшим значением индекса. Наращивание индекса ведется по внутреннему полю (если оно есть) и только затем - по полю более высокого приоритета. В нашем случае внут- реннее поле в индексе — поле NN и будет выбрана запись Веник универсальный (рис. 7.7): GrNum NN Tovar 1 10 Махорка 12 1 Веник универсальный 12 4 Кашпо плетеное 12 26 Дискеты BASF 100 1 Книга «Пушкин в изгнании» 100 10 Комплект отверток Рис. 7.7. Перемещение указателя после поиска. Однако, если в EditJ введено 50 и в Edit2 - о (или что-либо другое, для данного состояния НД это неважно), записи с той же группой (50) и большим значением но- менклатурного номера нет. Поэтому указатель записи перемещается на запись с 66 льшим номером группы (рис. 7.8): GrNum NN Tovar 1 10 Махорка 12 1 Веник универсальный 12 4 Кашпо плетеное 12 26 Дискеты BASF ->100 1 Книга «Пушкин в изгнании» 100 10 Комплект отверток Рис. 7.8. Перемещение указателя на запись с большим номером группы. Заметим, что теоретически можно опускать в условиях поиска значение внешнего поля индекса (в нашем случае поля GrNum). Например, можно задать в Editl 0 и в Edit2 - 1. Однако вопреки ожиданиям курсор НД не встанет на первую запись с но- менклатурным номером 1 для первой попавшейся группы, а встанет на первую запись с группой, превышающей 0, т. е. на логически первую запись в НД при сортировке по полям GrNum, NN (рис. 7.9): _______________GrNum__________NN_____Tovar_____________________________________ -> 1 10 Махорка 12 1 Веник универсальный 12 4 Кашпо плетеное 12 26 Дискеты BASF 100 1 Книга «Пушкин в изгнании» 100 10 Комплект отверток Рис. 7.9. Перемещение указателя на первую запись с большим номером группы. Существует более громоздкая альтернатива методу FindNearest - выполнение группы методов SetKey, EditKey, GoToNearest и заполнение полей поисковыми значениями (подробнее см. в конце описания метода FindKey). Например, выполнение метода
Работа с компонентом ТТаЫе 151 Tablel.FindNearest([GrTmp,NNTmp]); может быть заменено эквивалентным по последствиям кодом Tablel.SetKey; TablelGrNum.Value : = GrTmp; TablelNN.Value := NNTmp; Tablel.GotoNearest; Замечание. Если нужно осуществить поиск по индексу, отличному от текущего, необходимо: 1. сохранить список текущих индексных полей в строковой переменной; 2. заменить список текущих индексных полей НД на необходимый; 3. осуществить поиск; 4. восстановить список текущих индексных полей НД из строковой переменной. Пример. Пусть текущая сортировка в НД осуществляется по имени товара (т. е. в текущий момент Tablel. indexFieldNames = ' Tovar') и текущей является вторая логическая запись (рис. 7.10): Tovar GrNum NN ______Веник универсальный________________12____________1_________________ Дискеты BASF 2 26 Кашпо плетеное 12 4 Книга «Пушкин в изгнании» 100 1 Комплект отверток 100 10 Махорка 1 10 Рис. 7.10. Набор данных отсортирован по названию товара. Пусть имеется такой обработчик нажатия кнопки FindButton-. procedure TForml.FindButtonClick(Sender: TObject); var GrTmp, NNTmp: Longlnt; OldlndexFieldNames: String; begin OldlndexFieldNames := Tablel.IndexFieldNames; Tablel.IndexFieldNames := 'GrNum;NN'; {...} ’ Tablel.FindNearest([GrTmp,NNTmp]); Tablel.IndexFieldNames := OldlndexFieldNames; end; Тогда, если в Editl введено 100 и в Edit2 — 0, после выполнения обработчика ука- затель встанет на запись Книга «Пушкин в изгнании» (рис. 7.11): Tovar GrNum NN Веник универсальный 12 1 Дискеты BASF 12 26 Кашпо плетеное 12 4 Книга «Пушкин в изгнании» 100 1 Комплект отверток 100 10 Махорка 1 10 Рис. 7.11. Перемещение указателя текущей записи.
152 Глава 7 Перед выполнением последней строки обработчика Tablel.IndexFieldNames := OldlndexFieldNames; НД будет иметь вид, показанный на рис. 7.12. GrNum NN Tovar 1 10 Махорка 12 1 Веник универсальный 12 4 Кашпо плетеное 12 26 Дискеты BASF -> 100 1 Книга «Пушкин в изгнании» 100 10 Комплект отверток Рис. 7.12. Набор данных перед восстановлением исходной сортировки. Восстановление исходного индекса в последней строке приведет к изменению ло- гического следования записей в НД, но текущая запись останется прежней. Причиной этого является правило: простое изменение сортировки в НД, если оно не сопровож- далось изменением условий фильтрации записей, не влечет за собой изменения место- положения курсора НД. 7.4.5. Инкрементальный локатор Под локатором будем понимать механизм поиска (точного или приблизительного) запи- сей в НД с последующим позиционированием на них курсора компонента ТТаЫе. Для реа- лизации локатора обычно применяется один или несколько компонентов TEdit для ввода условий поиска и кнопка TButton, обработчик события нажатия которой и реализует поиск. Описанные выше обработчики события нажатия кнопки FindButton реализуют ло- каторы. Однако вне рассмотрения остался еще один режим: по вводу каждого симво- ла в TEdit курсор БД переходит на запись, ближе всего лежащую к искомой. Чем больше введено символов, тем ближе курсор БД к искомой записи. Такой локатор называется инкрементальным. Пусть необходимо реализовать инкрементальный локатор для уточняющего поиска записи по названию товара. Пусть описанный выше НД отсортирован по индексному полю Tovar и текущая запись в нем - логически первая. Тогда он имеет вид, показан- ный на рис. 7.13. Tovar GrNum NN Веник универсальный 12 1 Дискеты BASF 12 26 Кашпо плетеное 12 4 Книга «Пушкин в изгнании» 100 1 Комплект отверток 100 10 Махорка 1 10 Рис. 7.13. Набор данных отсортирован по названию товара. Ввод значения для поиска осуществляется в компоненте Edit3. Напишем обработ- чик события OnChange, возникающего при любом изменении значения в Edit3\ procedure TForml.Edit3Change(Sender: TObject); begin Tablel.FindNearest([Edit3.Text]); end;
Работа о компонентом ТТаЫе 153 Пусть нам нужно отыскать запись с наименованием товара «Комплект отверток». При использовании описываемого механизма инкрементального локатора не обяза- тельно вводить это название полностью. Курсор НД будет приближаться к искомой записи по мере ввода символов в Edit3. Введем в Edit3 символ «К». Тогда Tablel.FindNearest([Edit3.Text]); есть на самом деле Tablel.FindNearest(['К']); в результате курсор переместится на первую запись, имеющую в поле Tovar значение, большее строки «К» (рис. 7.14): Tovar GrNum NN Веник универсальный 12 1 Дискеты BASF 12 26 -> Кашпо плетеное 12 4 Книга «Пушкин в изгнании» 100 1 Комплект отверток 100 10 Махорка 1 10 Рис. 7.14. Перемещение указателя текущей записи после ввода значения «К» в поле локатора. Введем в Edit3 следующий символ, «о» (Edit3. Text = Ко). В результате курсор пе- реместится на первую запись, имеющую в поле Tovar значение, большее или равное «Ко» (рис. 7.15): Tovar GrNum NN Веник универсальный 12 1 Дискеты BASF 12 26 Кашпо плетеное 12 4 Книга «Пушкин в изгнании» 100 1 -> Комплект отверток 100 10 Махорка 1 10 Рис. 7.15. Перемещение указателя текущей записи после ввода значения «Ко» в поле локатора. Это и есть искомая запись. Заметим, что применение инкрементальных локаторов возможно не только для символьных полей, но и для числовых. 7.4.6. Поиск по части текущего индекса Можно осуществлять поиск по частичному множеству индексных полей. Для этого необходимо с помощью свойства KeyFieldCount указать, сколько начальных полей индекса будет использоваться при поиске. Установка значения свойства KeyFieldCount актуальна только в случае использова- ния методов GotoKey и GotoNearest. Как осуществить поиск по частичному множеству индексных полей для методов Find и FindNearest, см. ниже. Пример. Пусть при текущем индексе по полям Doljnost и FIO необходимо осуще- ствить поиск по Doljnost и по Doljnost + FIO. Условия поиска будем вводить в Editl (должность) и Edit2 (ФИО). Результаты работы приводимого ниже кода показаны на рис. 7.16, а и 7.16, б.
154 Глава 7 •Г Согрцдники Н1=1И • ассистент Маш Манишкреч А А. J ассистент Маслаченко В Ф I ассистент Массалитин В Ф Д ассистент Юлькина В.В. Васильев ЕР доцент Г аврилое ГГ За лиоалое Ы.П. Ман<>1лоеа В А Мявкмна В.П. кхн ктн доцент лаборант профессор Мюратов А Ф. профессор Пятаков П.Я. Кфмн кхн кхн кхн ДИН дтн 221254 101908 123111 100002 545667 876822 100222 212321 222121 ni2ie| 198765 . Условия поиска Должность- !до“'нг ФИО- [г£~ Поиск по должности | Поиск по должности и ФИО | а) б) Рис. 7.16. Поиск: а) по первому полю индекса; б) по всем полям. procedure TForml.OneFieldFindButtonClick(Sender: TObject); begin // Поиск по одному полю индекса with Tablel do begin SetKey; KeyFieldCount := 1; TablelDoljnost.Value := Editl.Text; GoToNearest; end; // with end; procedure TForml.FullKeyFinfButtonClick(Sender: TObject); begin // Поиск по двум полям индекса with Tablel do begin SetKey; KeyFieldCount := 2; TablelDoljnost.Value := Editl.Text; TablelFIO.Value := Edit2.Text; GoToNearest; end; // with end; Подмножество полей индекса должно быть в полном индексе непрерывным, т. е. при индексе из четырех полей можно осуществить поиск по 1-му, 2-му, 3-му полям одновременно и нельзя по 1-му, 4-му, 6-му полям. При использовании методов FindKey и FindNearest необходимости в использова- нии KeyFieldCount нет. Для поиска по частичному соответствию достаточно указать в списке часть полей данного индекса:
Работа с компонентом ТТаЫе 155 // Поиск по одному полю из двух: Tablel.FindNearest([Editl.Text]); // Поиск no двум полям из двух: Tablel.FindNearest([Editl.Text, Edit2.Text]); 7.5. ФИЛЬТРАЦИЯ ЗАПИСЕЙ 7.5.1. Обзор методов Помимо описываемых ниже методов, присущих только ТТаЫе, наборы данных имеют также общие свойства, методы и события для фильтрации записей — Filter, Filtered, OnFilterRecord, FindFirst, FindLast, FindNext, FindPrior. Они описаны в пре- дыдущей главе. Для фильтрации записей ТБД собственно ТТаЫе имеет следующие методы: SetRangeStart — устанавливает нижнюю границу фильтра; EditRangeEnd - устанавливает верхнюю границу фильтра; ApplyRange - осуществляет фильтрацию записей в ТТаЫе', SetRange — имеет тот же эффект, что и последовательное выполнение методов SetRangeStart, SetRangeEnd и ApplyRange. В качестве параметра исполь- зуются массивы констант, каждый из которых содержит значения клю- чевых полей. Заметим, что фильтрация методами ApplyRange/SetRange должна проводиться по ключевым полям. По умолчанию берется текущий индекс, определяемый свойством TTable.IndexName или TTable.IndexFieldNames. Если значения этих свойств не уста- новлены, по умолчанию используется главный индекс ТБД. Поэтому, если нужно использовать индекс, отличный от главного, необходимо явно переустановить значе- ние свойства TTable.IndexName (имя текущего индекса) или TTable.IndexFieldNames (список полей текущего индекса). 7.5.2. Использование SetRange Метод procedure SetRange(const Startvalues, EndValues: array of const); показывает в НД только те записи, индексные поля которых лежат в диапазоне [Startvalues..EndValues]. Пример. Пусть в НД Tablel показываются все записи из ТБД tov . db (Товары). Включим в структуру записи НД Tablel два поля: GrNum (Номер группы) и Tovar (Наименование товара). Пусть текущий индекс построен по полю GrNum. Тогда для фильтрации записей таким образом, чтобы показывались записи только с определенным номером группы, располагаем в форме компоненты Editl (для ввода номера группы) и CheckBoxl. Если CheckBoxl отмечен (CheckBoxl. Checked = True), то производится фильтрация по номеру группы, введенному в Editl, в противном случае показываются все записи из ТБД (рис. 7.17).
156 Глава 7 Рис. 7.17. Неотфильтрованный набор данных. Напишем такой обработчик события CheckBoxl.OnClick'. procedure TForml.CheckBoxlClick(Sender: TObject); var GrNumTmp: Integer; begin if CheckBoxl.Checked then begin GrNumTmp := StrToInt(Editl.Text) ; {-------------фильтрация записей в НД-----------} with Tablel do begin CancelRange; SetRange([GrNumTmp],[GrNumTmp]); end; {with} end else {---------------отмена фильтрации-----------------} Tablel.CancelRange; end; B отфильтрованном НД показываются только те записи, индексное поле текущего индекса у.которых (в нашем случае поле GrNum) имеет значение, лежащее в заданном диапазоне. В данном случае диапазон определяется переменной GrNumTmp. Поэтому для GrNumTmp = 3 будут показаны записи, принадлежащие к одной группе 3 (рис. 7.18): |* Setiange - 1 Н1"1ЕЗ ильтровать Номер группы: [з Рис. 7.18. Набор данных, отфильтрованный по группе 3.
Работа с компонентом ТТаЫе 157 Если бы мы захотели, чтобы в НД фильтровались записи из нескольких групп, то нам следовало бы добавить в форму второй компонент Edit2, в котором вводился бы номер конечной группы, в то время как в Editl вводился бы номер начальной группы: procedure TForml.CheckBoxlClick(Sender: TObject); var GrNumTmpl,GrNumTmp2: Integer; begin if CheckBoxl.Checked then begin GrNumTmpl := StrToInt(Editl.Text); GrNumTmp2 := StrToInt(Edit2.Text) ; {------------------фильтрация записей-----------} with Tablel do begin CancelRange; SetRange([GrNumTmpl],[GrNumTmp2]); end; {with} end else {------------отмена фильтрации------------} Tablel.CancelRange; end; Результаты фильтрации записей с номерами группы от 2 до 4 показаны на рис. 7.19. Рис. 7.19. Набор данных отфильтрован по группам в диапазоне 2...4. 7.5.3. Методы SetRangeStart, SetRangeEnd, ApplyRange Эти методы являются альтернативой методу SetRange, который объединяет в себе функциональность трех указанных методов. В частности, рассмотренная в предыдущем примере фильтрация по начальному и конечному номеру группы может быть реализована таким образом: procedure TForml.CheckBoxlClick(Sender: TObject); ver GrNumTmpl,GrNumTmp2: Integer; begin if CheckBoxl.Checked then
158 Глава 7 begin with Tablel do begin CancelRange; SetRangeStart; Tablel.FieldByName('GrNum').Aslnteger:= GrNumTmpl; SetRangeEnd; Tablel.FieldByName('GrNum').Aslnteger := GrNumTmp2; ApplyRange; end; {with} end else Tablel.CancelRange; end; 7.5.4. Метод CancelRange Метод CancelRange служит для отмены предыдущих условий фильтрации. Если предыдущую фильтрацию не отменить, возможно, следующие фильтрации принесут не такой результат, которого вы ожидаете. Изменим пример, приводившийся выше. Сначала удалим из него вызов метода CancelRange. Пусть сначала НД сортируется по наименованию товара (поле Tovar). Разместим в форме группу зависимых переключателей RadioGroupl, позволяющую переключать текущие индексы Tablel : procedure TForml.RadioGrouplClick(Sender: TObject); begin with RadioGroupl do begin case Itemindex of // Текущий индекс - по полю 'Tovar': 0: Tablel.IndexFieldNames := 'Tovar'; // Текущий индекс - по полю 'GrNum' 1: Tablel.IndexFieldNames := 'GrNum'; end; //case end; //with end; Когда пользователь хочет произвести фильтрацию по номеру группы, он нажимает кнопку «Фильтровать», для которой реализован следующий обработчик нажатия: procedure TForml.ButtonlClick(Sender: TObject); var GrNumTmpl,GrNumTmp2: Integer; begin with Tablel do begin // Отмечаем строку текущего выбора в RadioGroupl: RadioGroupl.Itemindex := 1; // Смена текущего индекса: IndexFieldNames := 'GrNum'; SetRange([GrNumTmpl],[GrNumTmp2]); end; {with} end;
Работа с компонентом ТТаЫе 159 Как видно, для фильтрации по полю GrNum необходимо сменить текущий индекс таким образом, чтобы GrNum было индексным полем. Начинаем работу с неотфильтрованным НД, показанным на рис. 7.20. Рис. 7.20. Неотфилыпрованный набор данных. Фильтруем НД по номеру группы, например, только по 3-й группе (рис. 7.21): Рис. 7.21. Набор данных отфильтрован по группе 3. Через некоторое время возвращаемся к сортировке по товару. Для этого отмечаем соответствующий переключатель в группе RadioGroupl. При этом видим, что показы- ваются записи всех групп (рис. 7.22):
160 Глава 7 Рис. 7.22. При смене текущего индекса фильтрация отключается. Это происходит оттого, что при смене текущего индекса невозможно осуществить фильтрацию по другому индексу, уже не являющемуся текущим. Пусть через некоторое время нам вновь необходимо отфильтровать НД по третьей группе. Напомним, что обработчик нажатия кнопки «Фильтровать» снова сделает текущим индекс, построенный по полю GrNum. И с удивлением отмечаем, что, хотя сортировка и меняется (по GrNum), фильтрации не происходит (рис. 7.23): Рис. 7.23. Против ожидания, фильтрация не включилась. Однако, если попробовать сделать фильтрацию по группе с номером 2, фильтрация будет осуществлена (рис. 7.24). Результат можно объяснить следующим образом. Первоначально имеет место фильтрация по индексу GrNum в диапазоне номеров групп [3...3]. После этого мы
Работа с компонентом ТТаЫе 161 делаем текущим индекс, построенный по полю Tovar. Фильтрация по группам теперь невозможна, поскольку поле GrNum не входит в новый текущий индекс. Когда мы вновь делаем текущим индекс, построенный по полю GrNum, при этом не меняя диа- , пазона групп [3...3], фильтрация не выполняется, поскольку она не отменена и диапа- зоны фильтрации не изменились. Когда же мы задаем новые условия фильтрации в диапазоне групп [2...2], НД фильтруется, т. к. изменился диапазон. Рис. 7.24. При смене условия фильтрации фильтрация включилась. Чтобы исключить указанный результат, следует перед новой фильтрацией (по ка- кому бы то ни было индексу) отменять результаты предыдущей фильтрации методом CancelRange: procedure TForml.ButtonlClick(Sender: TObject); var GrNumTmpl,GrNumTmp2: Integer; begin with Tablel do begin CancelRange; RadioGroupl.Itemindex 1; // Смена текущего индекса: IndexFieldNames 'GrNum'; SetRange([GrNumTmpl],[GrNumTmp2]); end; {with} end; 7.5.5. Методы EditRangeStart, EditRangeEnd Эти методы предназначены для смены условий фильтрации, установленных ранее с использованием соответственно методов SetRangeStart и SetRangeEnd. Напомним, что сама фильтрация в этом случае выполняется методом ApplyRange. Преимущества их использования ясны не всегда. Например, можно было бы предположить, что для рас- смотренной в п.7.5.4 ситуации эти методы способны заменить использование CancelRange:
162 Глава 7 procedure TForml.ButtonlClick(Sender: TObject); var GrNumTmpl,GrNumTmp2: Integer; const Num: Integer =0; begin inc(Num); with Tablel do begin if Num = 1 then begin SetRangeStart; FieldByName('GrNum').Aslnteger:= GrNumTmpl; SetRangeEnd; FieldByName('GrNum').Aslnteger := GrNumTmp2; ApplyRange; end else begin EditRangeStart; FieldByName('GrNum').Aslnteger := GrNumTmpl; Edi tRangeEnd ; FieldByName('GrNum').Aslnteger := GrNumTmp2; ApplyRange; end end; {with} end; Однако результат будет таким же ошибочным. Указанный код будет правильно ра- ботать только в случае, когда индекс по GrNum является принятым по умолчанию и в процессе работы не изменяется (представим, что в показанном выше примере мы уда- лили переключатели RadioGroupl для выбора текущего индекса). Однако в этом слу- чае правильно работает и такой код procedure TForml.ButtonlClick(Sender: TObject); var GrNumTmpl,GrNumTmp2: Integer; begin with Tablel do begin SetRangeStart; FieldByName('GrNum').Aslnteger:= GrNumTmpl; SetRangeEnd; FieldByName('GrNum').Aslnteger := GrNumTmp2; ApplyRange; end; {with} end; 7.5.6. Свойство KeyExclusive Свойство KeyExclusive применяется для фильтрации записей в ТТаЫе с использо- ванием методов SetRangeStart, SetRangeEnd и EditRangeStart, EditRangeEnd. Свойство KeyExclusive влияет на включение в отфильтрованный НД записей, у кото- рых индексные поля содержат граничные значения диапазона фильтрации. KeyExclusive включается и отключается отдельно для начального и конечного условия фильтрации.
Работа с компонентом ТТаЫе 163 Если в свойство KeyExclusive для данной границы диапазона фильтрации (верхней или нижней) установлено значение False, записи, содержащие в индексном поле (полях) значение, указанное в качестве данной границы диапазона, включаются в от- фильтрованный НД, в противном случае не включаются. По умолчанию применяется значение False. Например: with Tablel do begin CancelRange; SetRangeStart; KeyExclusive := True; FieldByName(1GrNum').Aslnteger:= GrNumTmpl; SetRangeEnd; ’ FieldByName('GrNum').Aslnteger := GrNumTmp2; ApplyRange; end; {with} 7.5.7. Фильтрация по составному индексу Если индекс, по которому необходимо осуществить фильтрацию, состоит из более чем одного поля: • при использовании метода SetRange значения полей должны перечисляться че- рез запятую внутри квадратных скобок; • при использовании SetRangeStart, SetRangeEnd и т. д. значение каждого поля должно устанавливаться явно. Пример. Пусть рассмотренный выше НД отсортирован по индексу, состоящему из полей GrNum и Tovar. Реализуем фильтрацию записей по заданному значению поля GrNum и любому значению поля Tovar. Для простоты проверку правильности ввода но- мера группы не производим. Обработчик выбора CheckBoxl «Фильтровать» выглядит так: procedure TForml.CheckBoxlClick(Sender: TObject); var GrNumTmp: Integer; TovarTmp: String; begin if CheckBoxl.Checked then begin GrNumTmp := StrToInt(Editl.Text); TovarTmp := Edit2.Text; {------------фильтрация записей в НД---------—} with Tablel do begin CancelRange; SetRange([GrNumTmp,TovarTmp],[GrNumTmp,'яя']); end; {with} end else {------------отмена фильтрации-----------------} Tablel.CancelRange; end; Реализацию выборки обеспечивает оператор SetRange([GrNumTmp,TovarTmp],[GrNumTmp,'яя']);
164 Глава 7 Отметим, что если требуется показывать в НД все записи группы, начинающиеся со значения в Edit2. Text, то в качестве значения товара в конечном условии фильтра- ции нужно объявить максимально возможное значение, которое только может встре- титься в качестве названия товара. Поскольку строчные буквы имеют бблылие коды, чем заглавные, и название товара не может начинаться с 1 я я', эти символы вполне могут использоваться как верхний ограничитель наименования товара. Пусть введена группа и не введено наименование товара. В этом случае в отфильт- рованный НД попадут все товары данной группы, т. е. записи, у которых определено наименование товара (рис. 7.25). Рис. 7.25. Фильтрация по первой группе. Название товара в условиях фильтрации не указано. Пусть введен номер группы и наименование товара Макароны. В этом случае в отфильтрованный НД попадут товары данной группы, у которых наименование боль- ше или равно Макароны (рис. 7.26). Рис. 7.26. Фильтрация по первой группе. Название товара больше или равно «Макароны».
Работа с компонентом ТТаЫе 165 7.5.8. Фильтрация по частичному соответствию Для случаев сортировки по символьным полям полезно делать фильтрацию по час- тичному соответствию индексного поля (полей) условиям фильтрации. Пример. Пусть рассматривавшаяся выше ТБД отсортирована по наименованию товара Tovar. Обработчик отметки CheckBoxl («Фильтровать»): procedure TForml.CheckBoxlClick(Sender: TObject); begin if CheckBoxl.Checked then begin with Tablel do begin CancelRange; SetRange([Editl.Text], ['яя']); end; {with} end else • Tablel.CancelRange; end; B этом случае в НД будут показаны все записи с названием товара, равным или 66 льшим указанного в Editl (рис. 7.27): Рис. 7.27. После фильтрации показываются все товары с названием, равным ли ббльшим «М». Изменим вызов метода SetRange на следующий: SetRange([Editl.Text],[Editl.Text + 'яя']); Тогда в результате фильтрации в отфильтрованный НД попадут только записи, начи- нающиеся с введенного в Editl фрагмента названия товара, т. е. с буквы «М» (рис. 7.28).
166 Глава 7 Рис. 7.28. После фильтрации показываются все товары с названием, начинающимся на «М». 7.5.9. Фильтрация по части составного индекса В качестве условий фильтрации могут быть заданы не все поля текущего индекса, а только ведущее поле или группа ведущих полей. В частности, для предыдущего при- мера можно указать в качестве текущего индекс Tovar+GrNum. Тогда применение метода SetRange([Editl.Text],[Editl.Text + 'яя']); означает, что, поскольку в квадратных скобках в качестве начального и конечного условия фильтрации указаны не два значения поля, а одно, фильтрацию следует про- водить на предмет соответствия ведущего поля индекса (в нашем случае Tovar) задан- ному поисковому значению (в нашем случае начальное значение — Editl. Text; конеч- ное значение - Editl. Text + ' яя'). 7.5.10. Ограничения возможностей фильтрации Заметим, что рассмотренный механизм фильтрации позволяет отфильтровывать только те записи, у которых значения ключевых полей больше или равны нижней границе и меньше или равны верхней границе фильтрации. Иными словами, затрудни- тельно задать сложное условие типа «все записи, у которых поле А < 100 или поле Z содержит вхождение строки поиск». При возникновении подобных проблем вместо компонента ТТаЫе нужно исполь- зовать компонент TQuery. 7.6. СОВМЕЩЕНИЕ КУРСОРОВ ДВУХ НД Если в программе одновременно используются два и более компонента ТТаЫе, ра- ботающие с одной и той же физической таблицей БД, может возникнуть необходи- мость совмещения курсоров двух НД. Для этой цели применяется метод синхрониза- ции GotoCurrent. Например: Tablel .GotoCurrent (ТаЫе2) ; что означает установку курсора в НД Tablel на ту же запись, на которой располагается в данный момент курсор НД ТаЫе2.
Работа с компонентом ТТаЫе 167 Однако на практике «размножение» идентичных ТТаЫе в двух и более формах встречается крайне редко и обычно свидетельствует о неправильном конструировании программы. Вместо того чтобы совмещать курсоры у двух ТТаЫе, нужно работать с одним компонентом, помещенным в общедоступный модуль - на главную форму программы или в специально для этих целей предназначенный модуль данных TPataModule. В этом случае в любой программной форме, в которой требуется работа с ТТаЫе, нужно лишь сослаться в предложении Uses на модуль данных. После этого форме станут доступны компонент ТТаЫе и связанный с ним компонент TDataSource, который обычно также размещается в модуле данных.
Глава 8 РАБОТА С КОМПОНЕНТОМ TQUERY 8.1. ОБЩИЕ СВЕДЕНИЯ Характерной особенностью компонента TQuery является использование в нем специального языка для работы с реляционными БД - SQL (Structured Query Languague — язык структурированных запросов). С помощью этого языка программа составляет S^Z-запрос, который TQuery передает машине баз данных BDE. Последняя имеет встроенный интерпретатор SQL, позволяющий ей выполнить описанные в запросе действия. Если запрос требует получения из БД нужных сведений (запрос SELECT), сформированные с помощью BDE данные помещаются в локальную таблицу в виде временного файла в каталоге запуска программы и TQuery становится владельцем этой таблицы. Данные из временной таблицы через компонент-посредник TDataSource передаются визуальным компонентам и отображаются в них точно так же, как если бы они были получены компонентом ТТаЫе. Однако в отличие от ТТаЫе пользователь не может их изменять, т. к. они представляют собой лишь копию реальных данных. Для изменения хранящейся в БД информации формируются специальные запросы (INSERT, UPDATE, DELETE), которые TQuery передает BDE. В этом случае BDE не формирует новые и никак не использует ранее созданные временные таблицы, но лишь интерпретирует запрос и уведомляет программу о том, насколько успешно прошло его выполнение. Таким образом, необходимость программного изменения запроса в случае модификации НД является другим характерным отличием TQuery от ТТаЫе. Следует оговориться, что при некоторых ограничениях на 50£-запрос TQuery может создать «живой» НД, т. е. вносимые в него изменения BDE будет отображать в таблице БД точно так же, как это делает компонент ТТаЫе. Подводя итог, нужно сказать, что при работе с локальными или файл-серверными БД скорость доступа к данным у TQuery в общем случае меньше, чем у ТТаЫе, т. к. для своей работы TQuery создает временные таблицы. С другой стороны, мощные возможности SQL позволяют с помощью TQuery получать НД, которые невозможно получить с помощью ТТаЫе (например, объединение в одном НД данных из нескольких таблиц БД). При работе с серверными БД ТТаЫе теряет всяческие преимущества, т. к. в этом случае он создает временную таблицу, являющуюся локальной копией всей серверной ТБД, а уже затем формирует из нее нужный НД. Затраты времени на создание больших локальных таблиц и значительно более скромные возможности ТТаЫе в отношении получения сложных НД практически исключают его использование в клиент-серверных приложениях. И последнее замечание. Несмотря на очевидную направленность TQuery для работы с распределенными СУБД (с серверными БД), мы решили рассмотреть его в этой части книги, т. к. он может с успехом использоваться и в локальных СУБД (т. е. при работе с локальными или файл-серверными БД). Наиболее важные конструкции SQL описываются во второй части книги, посвященной особенностям создания
Работа с компонентом TQuery 169 клиент-серверных приложений. Если вы не любите забегать вперед, можете либо на время пропустить эту главу и вернуться к ней после изучения SQL, либо принимать приводящиеся в ней 52£-запросы «на веру», либо, наконец, прибегнуть к помощи средства SQL Builder для создания собственных запросов (см. следующий раздел). 8.2. ИСПОЛЬЗОВАНИЕ SQL BUILDER SQL Builder (SQLB) представляет собой встроенное в Delphi средство, позволяющее создать 5£)£.-запрос без какого-либо знания языка SQL. Его использование профессиональными программистами может вызвать по меньшей мере удивление, т. к. такие программисты должны знать SQL: время написания сложного 5£)£-запроеа будет значительно меньше, чем время для создания такого же запроса с помощью SQLB, а его качество - значительно выше. Тем не менее на первых порах, когда вы еще не владеете в достаточной степени SQL, это средство поможет вам не только быстро создать запрос, но и изучить язык. 8.2.1. Окно SQLB Для вызова SQLB поместите на форму компонент TQuery и щелкните по нему правой кнопкой мыши. В появившемся локальном меню выберите SQL Builder (рис. 8.1). Рис. 8.1. Окно SQL Builder. В окне SQLB с помощью списков Database и Table нужно выбрать псевдоним БД и одну или несколько ее таблиц, к которым адресуется запрос. Пусть, например, в БД с псевдонимом PROBA существует таблица PERSONAL. После выбора псевдонима и таблицы окно SQLB примет вид, показанный на рис. 8.2. В его верхней части отображается небольшое окно с названиями и типами полей выбранной таблицы, а нижняя часть занята многолистным окном с закладками. Повторный выбор в списке Table добавит к формируемому вами запросу новую таблицу, но не удалит из него ранее выбранную: если вы захотите избавиться от выбранной таблицы, щелкните по заголовку ее окна и нажмите клавишу Delete или щелкните по нему правой кнопкой и выберите Remove Table.
170 Глава 8 Рис. 8.2. Окно SQLB с выбранной таблицей. Закладки в нижней части окна открывают доступ к панелям, предназначенным для уточнения запроса. С помощью Criteria формируется критерий отбора записей. В колонке Field or Value можно выбрать поле или его значение, а в колонке Compare - нужную операцию отношения. Панель Selection предназначена, в основном, для назначения пользовательских имен полям результирующего НД. Если выбран переключатель Remove Duplicates, в набор не будут включаться поля связи из дочернего НД, значения в которых повторяют значения соответствующих полей родительского НД. С помощью Grouping можно группировать НД по отдельным полям, а с помощью Sorting - сортировать записи. Панель Joins используется для присоединения данных из другого НД. 8.2.2. Работа с SQLB Работу с SQLB следует начинать с выбора тех полей, которые будут возвращаться из ТБД в результате запроса. Для выбора поля нужно щелкнуть по переключателю
Работа с компонентом TQuery 171 слева от имени поля в окне таблицы или по переключателю в заголовке окна - чтобы выбрать все поля. Выберите любую доступную вам таблицу, щелкните по переключателю в левой части заголовка окна и нажмите F9 - вы увидите результат выполнения простейшего запроса, текст которого вы сможете увидеть после нажатия F7 (рис. 8.3). Вместо нажатия клавиши F7 можно щелкнуть по кнопке на инструментальной панели, а вместо F9 - по кнопке а) б) Рис. 8.3. Простейший запрос: а) текст запроса (клавиша F7); б) результат запроса (F9). Чтобы отфильтровать запрос, используйте таблицу панели Criteria. Если, например, в НД нужно включить только те записи, у которых значение поля KodDol не превышает 3, выберите в левой колонке поле KodDol, в средней - операцию <=, а в правой введите 3 (рис. 8.4). а) б) Рис. 8.4. Фильтрация НД: а) сформированный запрос; б)результат запроса.
172 Гпава 8 Попробуйте самостоятельно отсортировать поля с помощью панели Sorting. Например, для сортировки в нисходящем порядке по полю FIO таблицы personal . db, вид панели Sorting показан на рис. 8.5. Если нужно добавить поле, по которому осуществляется сортировка, нужно выбрать его в левой части панели и щелкнуть по кнопке Add. Чтобы выбрать порядок сортировки, щелкните по полю в правой части панели и выберите одну из кнопок A..Z (нисходящий порядок) или Z..A (восходящий порядок) в средней части. Рис. 8.5. Сортировка НД. Для создания запроса главный-детальный выбираются две таблицы - сначала главная, затем детальная. Удобно сначала выбрать все поля каждой таблицы, чтобы свободно оперировать ими. Затем с помощью Criteria устанавливается связь по ключевому полю (рис. 8.6). Если какие-то поля в результирующем НД не нужны, удалите их выбор в соответствующих окнах таблиц (на рис. 8.6 поля связи KodDol не включены в результат запроса). На рис. 8.7 показан запрос и его результат для связи главный-детальный таблиц perosonal . db (детальная) и oklady . db (главная).
Работа с компонентом TQuery 173 Рис. 8.6. Создание связи главный-подчиненный. fa SQI Query Гек! Lnhy HRE3 I fa Query Re ulli file £di Query help QatabaM |PROBa"И SELECT PersonaLTebNurn. Personal FIO. jJ Oklady.DoIjnost Oklady.Oklad FROM "personal.DB" Personal. “oklady.DB" Oklady WHERE Personal.KodDol - Oklady.KodDol < I I I I _____________ iTabNum dFIO |Dolinptt" |Oklad J HHKSSS Ванина E.C. Доцент 2 БООрГ 123457 СивыЙХМ Профессор 3000р. 123458 Харин П.Б. Ассистент 1 700р. 234567 Базаренкова Н С. Ассистент 1700р. 345676 Суровое В.Д. Профессор 3 000р. 456789 Найрымоа В.С. Инженер 1 700р. 567890 Лавочкина С.3 Доцент 2 500р. *| а) б) Рис. 8.7. Связь главный-подчиненный: а) текст запроса; б) результат запроса. После того как 50£-запрос создан и опробован, его можно поместить в свойство SQL компонента TQuery. Для этого нужно закрыть окно и в ответ на запрос Save changes to query? Сохранить изменения в запросе? щелкнуть по кнопке Yes.
174 Глава 8 8.3. ОБЗОР СВОЙСТВ И МЕТОДОВ 8.3.1. Свойства Свойство Назначение property Constrained: Boolean; Если содержит True, в «живом» НД на записи, вводимые или изменяемые пользователем, накладываются ограничения секции WHERE оператора SELECT. property DataSource: TDataSource; Содержит ссылку на компонент TDataSource, используемый для формирования параметрического запроса. property Local: Boolean; Содержит True, если TQuery работает с локальной или файл-серверной БД. property ParamCheck: Boolean; Если содержит True, список параметров будет автоматически обновляться при изменении запроса на этапе прогона программы. property Params[Index: Word]: TParams; Содержит массив объектов-параметров класса TParams (см. ниже примечание). property Prepared: Boolean; Содержит True, если запрос был подготовлен к выполнению методом Prepare. property RequestLive: Boolean; Содержит True, если TQuery должен возвращать изменяемый («живой») НД. property RowsAffected: Integer; Содержит количество записей, которые были изменены или удалены в результате | выполнения запроса. property SQL: TStrings; Содержит текст ^/.-запроса. property Text: PChar; Содержит текст ^/.-запроса, который был в действительности передан BDE. property UniDirecticnal: Boolean; Если содержит True, курсор НД может перемещаться только вперед. Такие НД требуют меньше памяти и быстрее обрабатываются. Примечание. Свойство Params содержит набор ссылок на объекты класса TParams. Ниже перечисляются наиболее важные свойства и методы этого класса. Свойства класса TParams property Items[Index: Word]: TParam; Содержит массив объектов-параметров класса TParam (см. ниже). Это свойство для TParams является умалчиваемым. Index должен быть в диапазоне 0...Count-1. property ParamValues[const ParamName: String]: Variant; Открывает доступ к значению параметра по его имени ParamName. property Count: Integer; Содержит количество параметров в массиве Items. Методы класса TParams procedure AddParam(Value: TParam); Добавляет объект-параметр к массиву Items. type TParamType = (ptUnknown, ptinput, ptOutput, ptInputoutput, ptResult) ; Создает объект-параметр и добавляет его к массиву Items. Тип параметра РагатТуре'. ptUnknown - не определен; перед передачей в
Работа с компонентом TQuery , Т75 function CreateParam(FldType: TFieldType; const ParamName: String; ParamType: TParamType): TParam; сохраняемую процедуру этот тип должен быть заменен на другой; ptinput - предназначен для передачи значения в запрос; ptOutput - предназначен для получения значения из запроса; ptResult - определяет результат запроса; в Items может быть только один параметр этого типа. function FindParam(const Value: String) : TParam; Ищет объект-параметр по его имени Value. procedure RemoveParam(Value: TParam); Удаляет объект-параметр Value из списка Items. Наиболее важные свойства класса ТРагат: Свойства класса TParam property AsXXXX: ХХХХ; Эти свойства (AsString, Aslnteger и т. д.) служат для преобразования значения параметра к нужному типу. property IsNull: Boolean; Содержит True, если с параметром не связано значение. property Name: String; Содержит имя параметра. property Value: Variant; Содержит значение параметра. 8.3.2. Методы Наиболее важные методы TQuery: Метод Назначение procedure ExecSQL; Выполняет запросы INSERT, UPDATE, DELETE и CREATE TABLE. Для выполнения запроса SELECT вместо него используется метод Open или свойство Active. procedure GetDetailLinkFields (MasterFields, DetailFields: TList); override; Заполняет список MasterFields полями главной таблицы и список DetailFields полями подчиненной таблицы. function ParamByName(const Value: String): TParam; Открывает доступ к параметру по его имени Value. procedure Prepare; Передает BDE запрос для того, чтобы сама BDE и удаленный сервер БД распределили свои ресурсы и дополнительно оптимизировали запрос. procedure (JnPrepare; Отменяет последствия вызова Prepare. 8.4. ВЫПОЛНЕНИЕ СТАТИЧЕСКИХ ЗАПРОСОВ Статические запросы создаются (а запросы SELECT могут и выполняться) на этапе конструирования программы. Преимущества статических запросов связаны с возможностью создания и использования компонентов-полей с помощью редактора полей, а также с тем, что такой запрос может быть автоматически создан с помощью средства SQL Builder.
176 Гпава 8 Для формирования статического запроса вручную используется редактор свойства SQL, который вызывается после щелчка по кнопке в строке SQL инспектора объектов. На рис. 8.8 показано окно редактора. Рис. 8.8.Текст SQL-запроса в редакторе свойства SQL. 8.5. ОТКРЫТИЕ И ЗАКРЫТИЕ TQUERY Компонент TQuery может возвращать НД* (если компонент использует оператор SELECT, то есть осуществляет выборку из одной или более таблиц БД) и выполнять действие над одной или несколькими таблицами БД (S^-операторы INSERT, UPDATE, DELETE). В случае запроса SELECT компонент открывается на этапе прогона программы методом Open, а на этапе конструирования - установкой значения True в свойство Active. Если в ^/.-запросе используется один из операторов INSERT, UPDATE или DELETE, набор данных не возвращается. Такой запрос выполняется методом ExecSQL. Закрытие НД, созданного оператором SELECT, осуществляется методом Close или установкой в свойство Active значения False. Заметим, что любое изменение свойства SQL как на этапе конструирования, так и при прогоне программы автоматически закрывает открытый НД. Для компонента TQuery, не возвращающего набора данных, выполнение метода Close не имеет последствий, поскольку с ним не связан открытый НД. 8.6. ИЗМЕНЯЕМЫЕ НАБОРЫ ДАННЫХ Записи НД, возвращаемые компонентом TQuery, могут изменяться подобно тому, как это происходит в компоненте ТТаЫе. Возможность изменения НД, возвращаемого после выполнения оператора SELECT, определяется свойством CanModify, значение
Работа с компонентом TQuery 177 которого устанавливается автоматически в зависимости от выполнения следующих условий: • свойство RequestLive компонента должно содержать значение True-, • НД формируется обращением только к одной физической ТБД; • НД не сортируется (т. е. в запросе не должно быть секции ORDER BY); • в НД не создаются значения с помощью агрегатных функций SUM, COUNT, AVG,MIN,MAX; • НД не кэшируется (свойство CashedUpdates должно содержать значение False); • при обращении к серверу Sybase SQL Server ТБД должна иметь уникальный индекс. Если изменения, внесенные в НД методами Post, Delete, не отображаются в НД, его содержимое можно обновить методом Refresh. Однако в этом случае оператор SELECT должен быть выполнен для таблицы локальной БД и эта таблица должна иметь уникальный индекс. Для НД, возвращенных в результате выполнения запроса к удаленной БД, выполнение метода Refresh не влечет за собой никаких последствий. Свойство RequestLive компонента TQuery игнорируется, если нарушено хотя бы одно из перечисленных выше условий. Такие НД могут лишь отображать данные, а попытка вызвать любой из их методов Insert, Edit и Delete создаст исключительную ситуацию. Для редактирования, вставки или удаления записей в этом случае нужно использовать либо специальные компоненты TUpdateSQL, либо соответствующие запросы Update, Insert, Delete, передаваемые Х^'СерверУ с помощью метода TQuery.ExecSQL. И в том, и в другом случае имеется одна существенная проблема, связанная с соответствующим обновлением данных на экране пользователя. Дело в том, что в этом случае обновление записей реализуется независимо от их отображения. Сетка DBGrid, например, будет содержать данные из НД, который лишь отображает их из нескольких таблиц и ничего «не подозревает» о возможном изменении одной или нескольких из них. Чтобы обновить демонстрируемые записи, в локальных БД часто используется метод Refresh компонентов ТТаЫе и TTgwery, однако использование этого метода в серверных БД запрещено: если пользователь желает обновить демонстрируемые на экране данные, он должен закрыть НД и затем заново его открыть. При отображении сложного запроса, содержащего тысячи записей, это может привести к существенным задержкам времени, а если обновление данных осуществляется часто (например, при вводе нескольких записей подряд), обновление вообще становится невозможным. Разумеется, в этом случае можно не обновлять отображаемые записи, а лишь тем или иным способом известить пользователя о том, что демонстрируемый ему экран нуждается в обновлении. Наиболее часто для этих целей используется дополнительная кнопка, первоначально скрытая от пользователя и появляющаяся только после внесения изменений. Для привлечения внимания пользователя к новому интерфейсному объекту, кнопку можно сделать «мигающей» и подать соответствующий звуковой сигнал. В некоторых случаях на экран «вешается» окно- поплавок, частично перекрывающее демонстрируемые данные. В следующем фрагменте после обращения к методу ExecSQL компонента quUpdate класса TQuery в центре экрана появляется окно-поплавок класса TAlarm, содержащее сообщение о необходимости освежить данные, и одновременно становится видимой кнопка bbRefresh, с помощью которой он сможет это сделать:
178 Глава 8 procedure TForml.bbUpdateClick(Sender: TObject); begin try quUpdate.ExecSQL; //Изменяем данные Application.CreateForm(TAlarm, Alarm); with Alarm do begin //Вешаем окно-поплавок Show; SetWindowPos(Handle, hwnd_TopMost,(Screen.Width-Width) div 2, (Screen.Height-Height) div 2, Width, Height, swp_NoActivate); end; bbRefresh.Show; //Показываем кнопку для обновления MessageBeep(mb_IconExclemation); //Издаем звук except ShowMessage('Ошибка изменения данных!') end end; Однако все это ничуть не уменьшает затраты времени, связанные с закрытием и повторным открытием сложного НД. Обновление «живых» НД реализуется значительно быстрее, однако в этом случае работу по созданию сложного НД приходится выполнять на клиентском месте, что обычно медленнее и приводит к заметным задержкам при «листании» данных. Пусть, например, осуществляется выборка сразу из трех таблиц: главной таблицы MOVEDOC, содержащий документацию об отгрузке книг покупателям, и двух справочных таблиц PARTNERLIST и skladlist, содержащих информацию о покупателях и складах, с которых происходила отгрузка. Таблицы связаны по парам полей MOVEDOC.PARTNERCODE - PARTNERLIST.CODE И MOVEDOC.SKLADCODE - skladlist.CODE. Тогда такой S£^-3anP0C создаст необновляемый НД, который, однако, будет достаточно быстро листаться в сетке DBGrid, так как необходимое соединение полей реализуется сервером: SELECT ..... /* Перечень полей, выбираемых из таблицы MOVEDOC */, P.NAME /* Поле с именем покупателя */, S.NAME /* Поле с наименованием склада */ FROM MOVEDOC М, PARTNERLIST Р, SKLADLIST S WHERE P.CODE=M.PARTNERCODE AND S.CODE=M.SKLADCODE Вместо этого можно создать обновляемый НД SELECT ....... /* Перечень полей, выбираемых из таблицы MOVEDOC */ FROM MOVEDOC добавив к нему объекты-поля типа fkLookup со ссылкой на два вспомогательных НД с данными из partnerlist и skladlist. Для конечного пользователя результат
Работа с компонентом TQuery 179 будет одинаков, но второй НД можно обновлять методами EDIT...POST, в то время как для первого придется использовать S£M^3anP0C UPDATE {INSERT, DELETE) с последующими CLOSE и OPEN. Поскольку для второго НД соединение полей реализуется клиентом, он будет листаться с заметными паузами. В практике одного из авторов, например, одна из главных таблиц имела размер в несколько сотен тысяч записей, в то время как справочные таблицы состояли из нескольких тысяч. В первом случае (при объединении полей сервером) листание полного экрана (компонент TDBGrid) проходило в доли секунды и было практически незаметным, в то время как во втором - с паузами 3...4 с. Однако обновление записи главной таблицы в первом случае реализовывалось за 1...2 с, в то время как во втором - более чем за минуту, т.е. в 30...60 раз медленнее. Таким образом, при реализации сложных запросов следует искать компромисс между скоростью навигации и временем обновления: для каких-то групп пользователей может быть важным одно, а для других - другое (в описываемом примере, как нетрудно догадаться, начальству понравился первый вариант, в то время как на рабочих местах операторов, занимающихся непосредственным вводом данных, пришлось использовать обновляемый НД). Оператор SELECT обычно фильтрует записи с помощью условий, описанных в секции WHERE. В изменяемом запросе пользователь может ввести или изменить записи так, что они перестанут удовлетворять условиям секции WHERE. Как должен вести себя в этом случае НД, определяется значением свойства Constrained: если оно содержит True, введенная или модифицированная запись фильтруется и исчезает из НД (но сохраняется в реальной ТБД), в противном случае присутствует в НД наряду с «правильными» записями. Так сказано в документации и встроенной справочной службе Delphi. На практике это не так: свойство Constrained никак не влияет на поведение НД. Чтобы убедиться в этом, проделайте такой эксперимент. 1. Создайте новое приложение и поместите на форму TQuery, ТТаЫе два компонента TDataSource, две таблицы TDBGrid, две кнопки TButton и переключатель TCheckBox так, как показано на рис. 8.9. Над НД, связанным с TQuery, мы будем экспериментировать, модифицируя его записи так, чтобы нарушить условие фильтрации WHERE. Компонент ТТаЫе будет связан с той же физической ТБД, что и TQuery, и показывать реальное состояние таблицы. 2. Свяжите Queryl с БД DBDEMOS1, установите значение True в его свойство RequestLive, чтобы создать «живой» НД, раскройте редактор свойства SQL и поместите в него такой запрос (высота букв в 5£Д-’запРосах не имеет значения; мы выделяем заглавными буквами ключевые слова просто потому, что так же выделяет их SQL Builder): SELECT * FROM Animals WHERE Weight<=20 что расшифровывается так: «выбрать все поля (*) из таблицы Animals при условии, что значение поля Weight не превысит 20». 1 Демонстрационная БД DBDEMOS входит в комплект поставки Delphi и по умолчанию находится в каталоге C:\Program Files\Common Files\Borland Shared\Data. При инсталляции Delphi автоматически создается псевдоним DBDEMOS. Если на вашем ПК такой псевдоним не создан, создайте его с помощью BDE Administrator.
180 Глава 8 Рис. 8.9. Вид формы на этапе конструирования. 3. Закройте редактор кнопкой ОК и поместите значение True в свойство Active компонента Query 1. Если появится сообщение об ошибке, еще раз проверьте правильность запроса, правильность псевдонима и существование в его каталоге файла animals . db. 4. Свяжите DataSourcel с Queryl, a DBGridl с DataSourcel. Таблица должна наполниться записями «живого» НД. 5. Выберите в списке свойства DatbaseName компонента Tablel псевдоним DBDEMOS, а в списке TableName - таблицу ANIMALS.BD и откройте НД: поместите True в свойство Active. 6. Свяжите DataSource2 с Tablet, a DBGrid2 с DataSource2. 7. Напишите такие обработчики событий Buttonl.OnClick, Button2.OnClick и CheckBoxl.OnClick'. procedure TForml.ButtonlClick(Sender: TObject); begin Queryl.Close; Queryl.Open; end; procedure TForml.Button2Cl£ck(Sender: TObject); begin Tablel.Refresh end; procedure TForml.CheckBoxlClick(Sender: TObject); - begin Queryl.Constrained := CheckBoxl.Checked end;
Работа с компонентом TQuery 181 Запустите программу, измените значение поля WEIGHT (вес) первой записи с 2 на 30 (используйте таблицу, связанную с Query 1) и сместите курсор на строчку вниз. Как только курсор НД покинет первую запись, она исчезнет. Щелкнув по кнопке Button2, вы увидите, что в реальной ТБД поле WEIGHT изменилось, но теперь запись фильтруется условием WEIGHT<=20 секции WHERE 5£)£-запроса и поэтому она исчезла из НД Query 1. Восстановите с помощью НД Tablel прежнее значение 2 в поле WEIGHT первой записи (напомним, что запись реально обновляется только после перехода курсора НД к новой записи или после обновления таблицы), обновите НД Queryl с помощью кнопки Buttonl и повторите эксперимент, предварительно щелкнув по переключателю CheckBoxl. Вы увидите, что поведение НД Queryl не изменилось, т. е. на него никак не влияет значение свойства Constrained. 8.7. ВЫПОЛНЕНИЕ ПАРАМЕТРИЧЕСКИХ ЗАПРОСОВ 8.7.1. Понятие параметрического запроса Параметрическим является запрос, в 5£)£-операторе которого в процессе выполнения приложения могут изменяться отдельные его составляющие. В этом случае изменяемая часть оператора оформляется как параметры. Например, в процессе выполнения приложения может быть выдан запрос «показать все записи из таблицы RASHOD, относящиеся к расходу товара Сахар со склада 10 января 1997 г.»: SELECT * FROM RASHOD WHERE TOVAR = "Сахар” AND DAT_RASH = "10.01.97" и запрос «показать все записи из таблицы RASHOD, относящиеся к расходу товара Кока-кола со склада 20 января 1997 г.»: SELECT * FROM RASHOD WHERE TOVAR = "Кока-кола" AND DAT_RASH = "20.01.97" а также аналогичные запросы по расходу других товаров за другие даты. Нетрудно заметить, что такого рода запросы отличаются только названием конкретного товара и датой его отгрузки со склада. Поэтому можно сформировать параметрический запрос, в котором изменяющиеся части заменены на параметры: SELECT * FROM RASHOD WHERE TOVAR = :TOVAR AND DAT_RASH = :DAT_RASH Параметрами в этом запросе являются имена, предваренные двоеточием «:». В параметрических запросах параметры заменяют значения, которые могут изменяться в процессе выполнения. Имена параметров произвольны и могут не совпадать со значениями полей таблицы, которым они ставятся в соответствие: SELECT * FROM RASHOD WHERE TOVAR = :PARAMETR1 AND DAT RASH = :ZNACHENIE
182 Глава 8 8.7.2. Формирование параметрического запроса Для формирования параметрического запроса необходимо: 1. выбрать для существующего компонента TQuery в Инспекторе Объектов свойство SQL и нажать кнопку текстового редактора; 2. в появившемся окне текстового редактора набрать текст SQL -запроса с параметрами; 3. выбрать в Инспекторе Объектов свойство Params и нажать кнопку в строке этого свойства; в появившемся окне (рис. 8.10) будут показаны имена всех параметров, введенных в текст параметрического .^/.-запроса на шаге 2; список параметров отслеживается автоматически всякий раз при изменении содержимого свойства SQL', 4. каждому параметру из списка необходимо поставить в соответствие определенный тип и стартовое значение в окне Инспектора Объектов. Рис. 8.10. Установка типов параметров. Стартовые значения на стадии конструирования программы присваивать не обязательно (в этом случае они получат значения NULL), однако к моменту открытия компонента TQuery должны быть определены типы всех параметров. 8.7.3. Программная установка значений параметров Всякий раз, когда необходимо изменить значения параметров запроса, программа должна закрыть TQuery, присвоить значения параметрам и повторно открыть его. Параметры компонента TQuery доступны через его свойство Params, представляющее собой массив объектов класса TParams. Индекс этого массива должен быть в диапазоне 0...ParamCount-1, где ParamCount есть количество параметров, которое можно получить с помощью свойства ParamCount компонента TQuery. Обратиться к конкретному параметру можно: 1) указав индекс параметра в свойстве Params, например, Params [0]; порядок следования параметров аналогичен порядку их отображения в окне редактора параметров; 2) с помощью метода ParamByName. Для установки значения конкретного параметра используется одно из свойств AsXXXX {AsString, Aslnteger и т.д.) объекта TParam или более общее свойство Value типа Variant: RashodQuery.Params[0].AsDate := StrToDate(Editl.Text) ; RashodQuery.ParamByName('DAT_RASH').Value := TmpDat_Rash;
Работа с компонентом TQuery 183 Пример. Для следующего параметрического запроса SELECT * FROM RASHOD WHERE TOVAR = :TOVAR AND DAT_RASH = :DAT_RASH будем вводить текущие значения параметров в компоненты типа TEdit с именами Dat Rash^Edit и Tovar Edit. Открытие НД (компонент TQuery с именем RashodQuery) будем производить после нажатия кнопки GoButton (рис. 8.11). Рис. 8.11. Значения параметров динамического запроса вводятся пользователем в форме при помощи компонентов TEdit. Обработчик события OnClick кнопки GoButton'. procedure TForml.GoButtonClick(Sender: TObject); var TmpDat_Rash: TDateTime; begin try TmpDat_Rash := StrToDate(Dat_Rash_Edit.Text); with RashodQuery do begin Close; ParamByName('DAT_RASH').Value := TmpDat_Rash; ParamByName('TOVAR').Value := Tovar_Edit.Text; Open ; end; //with except ShowMessage('Неверная дата'); Dat_Rash_Edit.SetFocus; Exit; end; //try end; Вначале производится проверка введенного пользователем значения даты на соответствие формату даты, а затем присвоение значений параметрам и открытие НД.
184 Глава 8 Замечание 1. По разным причинам попытка открыть НД может оказаться неуспешной. Поэтому рекомендуется помещать программный код, реализующий открытие НД, в защищенный блок try...except, как это сделано в предыдущем примере. Замечание 2. Параметры более чувствительны к значениям даты и времени, чем поля (компонент TField). Если при вводе значений в поля типа ftDateTime одна из составляющих значения - время или дата - отсутствует, она считается равной 0. Для параметра, предназначенного только для даты или только для времени, автоматическое преобразование введенного значения даты-времени в его свойстве Variant может стать источником ошибки. Чтобы этого не случилось, назначьте параметру тип ftDate или ftTime и используйте явное преобразование AsXXXX-. SomeQuery.ParamByName('DateParam').AsDate := ... SomeQuery.ParamByName('TimeParam').AsTime := ... 8.7.4. Методы Prepare и Unprepare Синтаксис 5£)£-операторов проверяется только при их выполнении методами Open или ExecSQL. Проверка синтаксиса требует некоторого времени, которое можно сэкономить, заранее подготовив запрос к многократному использованию методом Prepare компонента TQuery. После его выполнения свойство Prepared компонента TQuery получит значение True и при очередном выполнении параметрического запроса его синтаксис уже не будет проверяться. При выполнении запрос компилируется и запоминается в буфере BDE, которая предварительно его оптимизирует, чтобы выполнить запрос в кратчайшее время. Для освобождения выделенных запросу ресурсов используется метод UnPrepare. При выполнении неподготовленного запроса всегда сначала вызывается Prepare, а после выполнения - UnPrepare. Поскольку синтаксис параметрического запроса не меняется, для него следует вызвать метод Prepare явно. 8.7.5. Указание значения NULL для параметров Значение NULL свидетельствует о том, что полю не присвоено никакого значения («пустое» поле). Значение 0 для числовых полей или пустая строка для строковых полей - это вполне определенные, отличные от NULL значения. Чтобы присвоить параметру значение NULL при прогоне программы, следует выполнить метод Clear компонента TParam (на этапе конструирования нужно очистить свойство Value параметра в окне Инспектора Объектов). Пусть, например, для корректировки записи в таблице RASHOD используется компонент TQuery с именем UpdateQuery, в свойстве SQL которого указан такой оператор: UPDATE RASHOD SET DAT_RASH = :new_DAT_RASH, KOLVO = :new_KOLVO, TOVAR = :new_TOVAR, POKUP = :new_POKUP WHERE N_RASH = :old_N_RASH
Работа с компонентом TQuery 185 Значение параметра : new_pokup (название покупателя) вводится пользователем в редакторе TEdit с именем PokupEdit. На значение поля POKUP в таблице RASHOD наложено ограничение, согласно которому оно должно либо быть равным NULL, либо совпадать с одним из значений поля POKUP в таблице POKUPATELI. Если пользователь ввел пустую строку (' '), присваивание UpdateQuery.ParamByName('new_POKUP').Value := Pokup_Edit.Text; и последующее выполнение метода UpdateQuery.ExecSQL приведет к ошибке, поскольку в таблице POKUPATELI нет записи со значением ' ' в поле POKUP. Поэтому в случае, если в Pokup_Edit введена пустая строка, параметру нужно присвоить значение NULL: if Pokup_Edit.Text = '' then ParamByName('new_POKUP').Clear else ParamByName('new_POKUP').Value := Pokup_Edit.Text; 8.7.6. Передача параметров через свойство DataSource Для передачи параметров может служить свойство DataSource компонента TQuery. Если это свойство определено, явные присваивания значений параметрам динамического запроса не производятся, то есть не используются операторы присваивания типа Queryl.ParamByName('ИмяПараметра').Value := Значение; В этом случае в наборе данных, связанном с DataSource, отыскиваются поля, имена которых совпадают с именами параметров запроса. Если такие поля есть, их текущие значения берутся в качестве значений параметров, в противном случае возбуждается исключительная ситуация. Пример. Пусть используется такой параметрический запрос: SELECT * FROM RASHOD WHERE TOVAR = :TOVAR ORDER BY DAT_RASH, KOLVO Компонент RashodQuery открыт при старте приложения, и значение параметру TOVAR перед этим не присваивалось. Свойство RashodQuery.DataSet содержит ссылку на компонент DS_TovarQuery типа TDataSource. Он связан с набором данных TovaryQuery, у которого есть поле Tovar. Поэтому в качестве значения параметра TOVAR берется текущее значение поля Tovar из НД TovaryQuery, как если бы это было сделано при помощи оператора RashodQuery.ParamByName('Tovar').Value := TovaryQuery.FieldByName('Tovar').Value; Отметим, что при смене текущей записи в НД TovaryQuery набор данных Rashod- Query автоматически переоткрывается и в нем показываются записи, соответствующие текущему значению поля Tovar НД TovaryQuery. Вид окна программы, содержащей оба названных компонента, показан на рис. 8.12.
186 Глава 8 Рис. 8.12. Значения параметра :Tovar нижнего НДавтоматически берутся из текущего значения поля Tovar верхнего НД. 8.8. ДИНАМИЧЕСКИЕ ЗАПРОСЫ Часто один компонент TQuery используется для выполнения различных отстоящих друг от друга по времени запросов. Такой подход уменьшает количество используемых компонентов, но может привести к возрастанию программного кода. Свойство SQL компонента TQuery имеет тип Tstrings, и поэтому содержимое свойства SQL может формироваться программно методами Add (добавить строку), Delete (удалить строку), Clear (очистить список) и прочими. Пример. Пусть компонент RashodQuery используется для выполнения параметрического запроса SELECT * FROM RASHOD WHERE POKUP = ':POKUP Значение параметра POKUP берется из поля POKUP текущей на данный момент записи в таблице POKUPATELI (компонент PokupQuery). Такой запрос можно сформировать динамически с помощью следующего фрагмента программы; with RashodQuery do begin SQL.Add('SELECT * FROM RASHOD'); SQL.Add('WHERE POKUP = ' + PokupQuery.FieldByName('POKUP').AsString); Open; end;
Работа с компонентом TQuery 187 Другой пример. Пусть для оператора UPDATE, выполняющего корректировку записи в таблице RASHOD, значения полей DAT RASH, KOLVO, TOVAR, POKUP могут вводиться компонентами TEdit с именами DatRashEdit, Kolvo_Edit, Tovar_Edit, Pokup_Edit. Требуется сформировать такой S^Z-оператор, чтобы в нем указывались в качестве обновляемых только те поля, для которых введено значение в соответствующих компонентах. Вид формы показан на рис. 8.13. Для отображения динамически формируемого запроса используется компонент Memol в нижней части окна. Рис. 8.13. В окне «Текст запроса» показывается текст формируемого SQL-onepamopa. Для формирования оператора UPDATE используется такой обработчик нажатия экранной кнопки Изменить: procedure TForml.UpdateButtonClick(Sender: TObject); var Cnt: Integer; // Счетчик непустых TEdit Separator: Char; // Запятая или пробел begin { Проверку соответствия значения в Dat_Rash_Edit.Text формату даты и значения в Kolvo_Edit.Text формату целого числа для простоты не производим } Cnt := 0; if Dat_Rash_Edit.Text о '’ then inc(Cnt); if Kolvo_Edit.Text <> '' then inc (Cnt); if Tovar_Edit.Text <> '' then inc(Cnt); if Pokup_Edit.Text <> 11 then inc(Cnt); if Cnt = 0 then
188 Глава 8 begin ShowMessage('Не введено ни одно новое значение'); Dat_Rash_Edit.SetFocus; Exit; end; // Формируем текст оператора UPDATE with UpdateQuery, SQL do begin Clear; add('UPDATE RASHOD'); add('SET'); if Dat_Rash_Edit.Text <> 11 then begin dec(Cnt); if Cnt > 0 then Separator := ',' else Separator := ' '; add(' DAT_RASH = "' + Dat_Rash_Edit.Text + "" tSeparator); end; //if if Kolvo_Edit.Text <> '' then begin dec (Cnt); if Cnt > 0 then Separator := else Separator := ' '; add(' KOLVO = ' + Kolvo_Edit.Text + Separator); end; //if if Tovar_Edit.Text <> '' then begin dec(Cnt); if Cnt > 0 then Separator := ',' else Separator := ' '; add(' TOVAR = + Tovar_Edit.Text + "" + Separator); end; //if if Pokup_Edit.Text <> '' then add(' POKUP = '" + Pokup_Edit.Text + "" ) ; add('WHERE N_RASH = ' + RashodQuery.FieldByName('N_RASH').AsString); Memol.Lines.Assign(SQL); // Выполнение сформированного SQL-оператора try ExecSQL; except on EDBEngineError do ShowMessage('Ошибка БД. ' + 'Проверьте уникальность номера расхода!'); else SHowMessage('Неклассифицированная ошибка при ExecSQL'); Exit; end; //try end; //with
Работа с компонентом TQuery 189 8.9. ПОЛУЧЕНИЕ АГРЕГИРОВАННЫХ ЗНАЧЕНИЙ Часто нужно подсчитать некоторые агрегированные значения данных (минимум, максимум, среднее, счетчик повторений), т. е. значения, полученные на основе анализа всех значений некоторого поля. В дальнейшем полученные значения могут входить в какие-либо условные операторы или операторы выбора. Агрегированные значения получаются с помощью агрегатных функций Avg (воз- вращает арифметическое среднее значений поля), Count (возвращает количество записей в поле), Мах (возвращает максимальное значение поля), Min (возвращает минимальное значение поля), Sum (возвращает сумму всех значений). Пусть, например, необходимо помещать уникальное значение в поле N_RASH (номер записи расхода со склада) в таблицу RASHOD. SgL-операторы INSERT и UPDATE не работают с автоинкрементными полями в таблицах локальных БД. Поэтому при занесении новой записи в таблицу RASHOD уникальное значение поля N_RASH можно определить, найдя максимальное значение этого поля и добавив к нему единицу. Сделаем такой запрос в компоненте MaxQuery: with MaxQuery do begin SQL.Clear; SQL.Add('SELECT COUNT(*), MAX(N_RASH) AS MAX FROM RASHOD'); Open; end; //with Этот запрос вернет максимальное значение поля N_RASH таблицы RASHOD в виде поля Мах текущей записи НД MaxQuery. Пусть добавление новой записи реализуется в компоненте InsertQuery таким параметрическим запросом: INSERT INTO RASHOD (N_RASH, DAT_RASH, KOLVO, TOVAR,'POKUP) VALUES(:N_RASH, :DAT_RASH, :KOLVO, :TOVAR, :POKUP) Тогда в параметр N_RASH следует поместить увеличенное на единицу значение поля Мах НД WorkQuery. with InsertQuery do begin ParamByName('N_RASH').Aslnteger := WorkQuery.FieldByName('Max').Aslnteger + 1; ExecSQL; end; Заметим, что НД WorkQuery всегда содержит одну строку, даже если таблица RASHOD пуста. Это достигается включением в запрос агрегатной функции Count. Если бы мы не вставили обращение к этой функции: SELECT MAX(N_RASH) AS MAX FROM RASHOD НД WorkQuery оказался бы пустым для пустой таблицы RASHOD и мы не смогли бы воспользоваться его полем Мах. Используемый вариант SELECT COUNT(*), MAX(N RASH) AS M
190 Глава 8 гарантирует возврат хотя бы значения NULL в этом поле, которое Aslnteger преобразует в значение 0. 8.10. СОРТИРОВКА В ОБРАТНОМ ПОРЯДКЕ Сортировка в обратном порядке легко реализуется в ТТаЫе, если с таблицей связан нисходящий (DESCENDING) индекс. Однако при работе с TQuery явно использовать тот или иной индекс нельзя - выбор индекса реализуется сервером БД. В части ORDER BY предложения SELECT также нельзя указать порядок сортировки или какое- либо выражение вместо списка полей, поэтому в общем случае нисходящая сортировка в 5^"запРосе невозможна. Однако при сортировке по числовым полям и работе с файл-серверными БД этого можно добиться, введя вычисляемое поле, содержащее «обратные» значения, и сортируя по нему. Если, например, в столбце Summa указана сумма и требуется нисходящий порядок сортировки по этому столбцу, нужно ввести в запрос вычисляемый столбец, в котором положительные значения Summa заменяются на отрицательные, и отсортировать по этому новому столбцу: SELECT ..., -Summa AS SortSumma FROM ... ORDER BY SortSumma К сожалению, этот прием не подходит для нисходящей сортировки по строковым столбцам. Следует заметить, что, хотя 561-интерпретатор BDE допускает сортировку по вычисляемым столбцам, стандарт SQL92 этого не требует, поэтому при работе с серверной БД нужно проверять возможность такой сортировки. В InterBase, например, невозможно сортировать по вычисляемым столбцам и для нисходящей сортировки нужно использовать реальный «обратный» столбец.
Глава 9 ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDBGRID 9.1. ОБЗОР СВОЙСТВ, МЕТОДОВ И СОБЫТИЙ 9.1.1. Свойства Свойство Назначение type TFormBorderStyle = (bsNone, bsSingle, bsSizeable, bsDialog, bsToolWindow, bsSizeToolWin); TBorderStyle = bsNone..bsSingle; property Borderstyle: TBorderStyle; 1 Определяет стиль рамки компонента: bsNone - нет рамки; bsSingle - рамка толщиной 1 пиксель; bsSizeable - обычная рамка изменяемых размеров; bsDialog - диалоговая рамка (без возможности изменения размеров); bsToolWindow - рамка bsSingle с уменьшенным заголовком; bsSizeToolWin - рамка bsSizeable с уменьшенным заголовком. property Columns: TDBGridColumns; Содержит индексированный набор объектов- столбцов типа TDBGridColumns (см. ниже примечание). property DataSource: TDataSource; Содержит ссылку на компонент типа TData- Source, служащий источником данных. property DefaultDrawing: Boolean; Если содержит True, содержимое ячеек сетки прорисовывается автоматически, в противном случае - в обработчиках событий OnDrawColumnCell или OnDrawDataCell. property EditorMode: Boolean; Если содержит True, пользователь может редактировать ячейку после нажатия F2 или Enter. Игнорируется, если Options включает goEditing или goAlwaysShowEditor. property FieldCount: Integer; Содержит количество столбцов. property Fields[Index: Integer]: TField; Открывает индексированный доступ к полям связанного с компонентом НД. property FixedColor: TColor; Определяет цвет фиксированных полей. type TDBGridOption = (dgEditing, dgAlwaysShowEditor, dgTitles, dglndicator, dgColumnResize, dgColLines, dgRowLines, dgTabs, dgRowSelect,dgAlwaysShowSelection, dgConfirmDelete, dgCancelOnExit, dgMultiSelect); TDBGridOptions = set of TDBGridOption; property Options: TDBGridOptions; Определяет вид и поведение компонента: dgEditing - разрешает изменение НД; dgAlwaysShowEditor - автоматически переводит поле в режим редактирования при его выборе; dgTitles - показывает заголовки столбцов; dglndicator - показывает индикатор текущей записи в самом левом фиксированном столбце; dgColumnResize - разрешает пользователю вручную изменять ширину столбцов; dgColLines - показывает разделяющие вертикальные линии; dgRowLines - показывает разделяющие горизонтальные линии; dgTabs - разрешает переход от столбца к столбцу с помощью
192 Глава 9 клавиши Tab', dgRowSelect - разрешает выделение цветом всей выбранной записи; dgAlwaysShowSelection - выделение текущей записи сохраняется, если компонент теряет фокус ввода; dgConflrmDelete - уничтожение записи должно подтверждаться; dgCancelOnExit - если пользователь вставил пустую запись и покинул ее, пустая запись не помещается в НД; dgMultiSelect - разрешает множественный выбор записей. property Readonly: Boolean; При значении True запрещает модификацию данных. property SelectedField: TField; Открывает доступ к полю НД, связанному с текущим столбцом. property Selectedlndex: Integer; Определяет индекс выбранного столбца в свойстве Columns. property TitleFont: TFont; Определяет шрифт для заголовков столбцов. Примечание. Объекты класса TDBGridColumns имеют умалчиваемое свойство Items, открывающее индексированный доступ к объектам-столбцам класса TColumn, и свойство Count, возвращающее количество элементов в Items. Ниже перечисляются наиболее важные методы TDBGridColumns и свойства класса TColumn. Методы класса TDBGridColumns function Add: TColumn; Создает и возвращает объект-столбец. procedure RebuildColumns; Уничтожает старые определения объектов- столбцов и создает новые с умалчиваемыми свойствами. procedure RestoreDefaults; Восстанавливает умалчиваемые определения всех объектов-столбцов. Свойства класса TColumn type TColumnButtonStyls - (cbsAuto, cbsElllpsls, cbsNone); property Buttonstyle: TColumnButtonStyle; Определяет тип назначенной столбцу кнопки: cbsAuto - столбец содержит кнопку раскрывающегося списка; cbsElllpsls - столбец содержит кнопку, щелчок по которой вызывает событие OnEditButtonClick; cbsNone - столбец не содержит кнопки. property Color: TColor; Определяет цвет столбца. property DisplayName: String; Содержит заголовок столбца. property Field: TField; Содержит ссылку на связанное со столбцом поле НД. property FieldName: String; Содержит имя связанного со столбцом поля НД. property Font: TFont; Определяет шрифт столбца. property PickList: TStrings; Определяет элементы выпадающего списка, если в ButtonStyle установлено значение cbsAuto. property Readonly: Boolean; Если содержит True, данные в столбце нельзя изменять. property Title: TColumnTitle; Содержит ссылку на объект-заголовок, имеющий стандартные для видимых
Использование компонента TDBGrid 193 компонентов свойства Alignment, Caption, Color и Font, позволяющие управлять текстом в заголовке столбца. property Visible: Boolean; Если содержит False, столбец не показывается в сетке. property Width: Integer; Содержит ширину столбца в пикселях. 9.1.2. Методы Метод Назначение procedure DefaultDrawColumnCell (const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); Осуществляет умалчиваемую прорисовку ячейки таблицы: Rect - координаты ячейки; DataCol - индекс столбца; Column - столбец ячейки; State - состояние прорисовки. function ValidFieldlndex (Fieldindex: Integer): Boolean; Возвращает True, если с колонкой с индексом Fieldindex связано поле НД. Если поле - вычисляемое или если с колонкой не связано поле, возвращает False. 9.1.3. События Событие С чем связано type TDBGridClickEvent = procedure (Column: TColumn) of object; property OnCellClick: TDBGridClickEvent; Возникает после щелчка мыши на ячейке. property OnColEnter: TNotifyEvent; Возникает в момент получения ячейкой фокуса ввода. property OnColExit: TNotifyEvent; Возникает перед тем, как ячейка потеряет фокус ввода. type TMovedEvent = procedure (Sender: TObject; Fromlndex, Tolndex: Longlnt) of object; property OnColumnMoved: TMovedEvent; Возникает при перемещении столбца: Fromlndex - позиция столбца до перемещения; Tolndex - новая позиция столбца. type DrawColumnCellEvent = procedure (Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState) of object- property OnDrawColumnCell: TDrawColumnCellEvent; Возникает при необходимости прорисовки ячейки: Rect - прямоугольник прорисовки; DataCol - индекс столбца в свойстве Columns; Column - столбец; State - состояние прорисовки. property OnEditButtonClick: TNotifyEvent; Возникает при нажатии на кнопку в столбце. type TDBGridClickEvent = procedure (Column: TColumn) of object; property OnTitleClick: TDBGridClickEvent; Возникает при щелчке по заголовку. 9.2. ПОНЯТИЕ СТОЛБЦОВ TDBGRID Компонент TDBGrid используется для показа содержимого НД в табличном формате, когда строки соответствуют записям НД, а столбцы - полям записи. Свойство DataSource компонента TDBGrid содержит имя компонента-источника
194 Глава 9 TDataSource, который ссылается на соответствующий НД (компоненты ТТаЫе или TQuery). Изменяя значение свойства DataSource во время выполнения, можно использовать один компонент TDBGrid для показа содержимого различных наборов данных. При продуманном сценарии диалога с пользователем это может существенно минимизировать объем и конструктивную сложность создаваемых приложений. Для определения состава столбцов в TDBGrid можно использовать редактор столбцов {Columns Editor). Порядок следования столбцов в сетке TDBGrid определяется порядком следования определений столбцов в редакторе столбцов. Если редактор столбцов не использовался, берутся поля (компоненты TField), объявленные при помощи редактора полей НД. При этом вад столбцов определяется соответствующими характеристиками компонентов TField, а порядок следования столбцов - порядком их определений. В случае когда для НД компоненты TField не создавались, порядок следования полей и их характеристики соответствуют тем, что были заданы при определении структуры записи ТБД в момент создания таблицы. Заметим, что умалчиваемый порядок следования столбцов в TDBGrid можно изменить лишь при помощи редактора полей или редактора столбцов. Пример. В ТБД teachers.db находятся сведения о сотрудниках кафедры. С этой таблицей в приложении ассоциирован набор данных Tablel. определения полей в этом НД редактор полей не использовался; следовательно, по умолчанию для НД Tablel используются все поля, объявленные в структуре записи таблицы БД teachers.db. Пусть порядок следования столбцов в компоненте TDBGrid, связанном с НД Tablel, взят по умолчанию и совпадает с порядком следования полей в записи таблицы teachers . DB (рис. 9.1). Рис. 9.1. Порядок следования и состав столбцов TDBGrid непосредственно после соединения с компонентом TDataSource. Чтобы изменить местоположение столбца Doljnost с 3-го на 2-е, необходимо: 1. вызвать редактора полей для набора данных Tablel', 2. добавить все объекты-поля; 3. в Инспекторе Объектов выбрать поле Doljnost и изменить значение свойства Index с 2 на 1 или «перетащить» поле мышью на нужное место в окне редактора полей. Чтобы изменить заголовки столбцов в TDBGrid, следует в Инспекторе Объектов нужным образом изменить значения свойств DisplayLabel для каждого поля (рис. 9.2).
Использование компонента TDBGrid 195 ¥ Рис. 9.2. Результат изменения заголовков столбцов TDBGrid. На заметку. Если в сетке TDBGrid нужно оставить пустым заголовок какой-либо колонки, недостаточно просто удалить содержимое опции Caption свойства Title соответствующего объекта-колонки: нужно поместить в заголовок хотя бы один пробел. 9.3. ДИНАМИЧЕСКИЕ СВОЙСТВА СТОЛБЦОВ Столбцы показываются в сетке TDBGrid в том состоянии, которое определяется их текущими свойствами. Если то или иное свойство столбца менять в процессе выполнения приложения, эти изменения немедленно отобразятся в TDBGrid. Например, в ходе выполнения можно менять ширину столбца (свойство DisplayWidth связанного со столбцом объекта TField), его видимость (Visible), возможность редактирования значения столбца (Readonly), порядковый номер (Index), заголовок столбца (DisplayLabel). Пример. Воспользуемся формой из предыдущего примера и добавим в нее компонент CheckBoxl. Установим свойство Visible объекта TablelUchStepen (поле «Ученая степень»), равным свойству CheckBoxl.Checked. Тогда отметка CheckBoxl приведет к визуализации столбца (рис. 9.3, а), а снятие отметки - к его исчезновению (рис. 9.3, б). а) б) Рис. 9.3. Изменение свойства Visible столбца «Ученая степень»: а) столбец виден; б) столбец не виден.
196 Глава 9 procedure TForml.CheckBoxlClick(Sender: TObject); begin TablelUchStepen.Visible := CheckBoxl.Checked; end; Изменение свойств объектов-полей НД автоматически отражается на свойствах соответствующих столбцов компонента TDBGrid. Но программа может обращаться к столбцам сетки и напрямую: procedure TForml.CheckBoxlClick(Sender: TObject); begin DBGridl[2].Visible : = CheckBoxl.Checked; end; При обращении к свойствам объекта-поля программист использует семантически значащее имя объекта (см. TablelUchStepen в предыдущем примере), что делает текст программы более осмысленным, чем обращение к столбцу DBGridl по его индексу (2). 9.4. СОЗДАНИЕ И ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ-СТОЛБЦОВ Объекты-столбцы создаются на этапе конструирования программы с помощью редактора столбцов, а на этапе прогона - с помощью метода Add объекта TDBGrid.Co- lumns. 9.4.1. Работа с редактором столбцов Редактор столбцов вызывается или двойным щелчком на компоненте, или из его вспомогательного меню (щелкните по компоненту правой кнопкой мыши и выберите опцию Columns Editor), или, наконец, после щелчка по кнопке в строке свойства Columns в окне Инспектора Объектов. В диалоговом окне редактора устанавливаются свойства объектов-столбцов. Вначале список объектов пуст. Чтобы добавить столбец, не связанный ни с каким полем набора данных, нужно нажать кнопку Е-3 (рис. 9.4, а). а) б) Рис. 9.4. Создание объекта-столбца: а) столбец не связан с конкретным полем НД; б) столбец связан с полем Doljnost. Чтобы поставить такому столбцу в соответствие какое-либо поле НД, следует в инспекторе объектов раскрыть список в свойстве FieldName и выбрать нужное поле. В этом случае столбец будет назван так же, как поле (рис. 9.4, б).
Использование компонента TDBGrid 197 Когда нужно определить объекты-столбцы, соответствующие всем или большинству полей набора данных, лучше нажать кнопку И (она станет доступна, если открыть соответствующий НД - ТТаЫе или TQuery). После нажатия на кнопку в список будут включены объекты-столбцы, соответствующие всем полям НД. При помощи кнопки & можно удалить определения столбцов, которые не должны показываться в TDBGrid. Свойства столбца устанавливаются в окне Инспектора Объектов. Они определяют особенности отображения столбца в TDBGrid. Пример. Используя НД таблицы teachers.db, создадим объекты-столбцы и установим различные свойства отображения этих столбцов (рис. 9.5). В частности, шрифт и цвет заголовка поля FIO изменен в окне Инспектора Объектов после двойного щелчка по свойству Title и выбора свойств Font и Color в появившемся списке, а цвет всего столбца Doljnost - с помощью свойства Color этого объекта- столбца (текст заголовков всех полей изменен с помощью свойств Title.Caption для каждого объекта-столбца). Рис. 9.5. Столбцы TDBGrid имеют различные свойства отображения. 9.4.2. Изменение свойств столбцов цо время выполнения программы Обратиться к конкретному столбцу компонента TDBGrid можно как к элементу индексированного массива: TDBGridl.Columns.Items[i] или (поскольку свойство Items - умалчиваемое) TDBGridl.Columns[i], где i принадлежит диапазону 0.. DBGridl.Columns.Count-\. Каждый элемент массива Items, в свою очередь, имеет свои собственные свойства и методы, перечисленные в п. 9.1.1. Пример. Введем в программу, отображающую таблицу teachers . DB, возможность динамического изменения свойств столбцов TDBGrid. Для этого разместим на форме компоненты ТТаЫе - TDataSource - TDBGrid, а также стандартные диалоговые окна TFontDialog, TColorDialog для выбора шрифта и цвета и кнопки TButton (рис. 9.6).
198 Глава 9 Рис. 9.6. Форма с кнопками, нажатие которых реализует изменение шрифта и цвета фона у столбца или заголовка столбца. Напишем следующие обработчики событий нажатия кнопок: procedure TForml.AssigningColumnsFontButtonClick(Sender: TObject); {Обработчик нажатия кнопки "Шрифт столбцов" вызывает диалоговое окно выбора шрифта и устанавливает выбранный шрифт для всех столбцов} var i: Integer; begin // В диалоговом окне выбора шрифта текущим будет шрифт 0-го столбца FontDialogl.Font := DBGridl.Columns.Items[0].Font; if FontDialogl.Execute then for i := 0 TO DBGridl.Columns.Count - 1 do DBGridl.Columns.Items[i].Font := FontDialogl.Font; end; procedure TForml.AssignFontToColumnsCaptionsButtonClick(Sender: TObject); {Обработчик нажатия кнопки "Шрифт заголовков" устанавливает выбранный шрифт для заголовков всех столбцов) var i: Integer; begin FontDialogl.Font := DBGridl.Columns.Items[0].Title.Font; if FontDialogl.Execute then for i := 0 TO DBGridl.Columns.Count - 1 do DBGridl.Columns.Items[i].Title.Font := FontDialogl.Font; end; procedure TForml.AssignFontToSelectedColumnButtonClick(Sender: TObject); {Обработчик нажатия кнопки "Шрифт столбца" устанавливает выбранный шрифт для текущего столбца, индекс которого определяется как DBGridl.Selectedlndex) begin {В диалоговом окне выбора шрифта текущим будет шрифт текущего столбца}
Использование компонента TDBGrid 199 FontDialogl.Font := DBGridl.Columns.Items[DBGridl.Selectedlndex].Font; if FontDialogl.Execute then DBGridl.Columns.Items[DBGridl.Selectedlndex].Font := FontDialogl.Font; end; procedure TForml.AssignColorToSelectedColumnButtonClick(Sender: TObject); {Обработчик нажатия кнопки "Фон столбца" устанавливает выбранный цвет для фона текущего столбца, индекс которого определяется как DBGridl.Selectedlndex} < begin ColorDialogl.Color : = DBGridl.Columns.Items[DBGridl.Selectedlndex].Color; if ColorDialogl.Execute then DBGridl.Columns.Items[DBGridl.Selectedlndex].Color := ColorDialogl.Color; end; procedure TForml.AssignColorToSelectedColumnsTitleButtonClick( Sender: TObject); {Обработчик нажатия кнопки "Фон заголовка столбца" устанавливает выбранный цвет для фона заголовка текущего столбца, индекс которого определяется как DBGridl.Selectedlndex} begin ColorDialogl.Color := DBGridl.Columns.Items[DBGridl.Selectedlndex].Title.Color; if ColorDialogl.Execute then DBGridl.Columns.Items[DBGridl.Selectedlndex].Title.Color := ColorDialogl.Color; end; Пример динамического изменения характеристик столбцов показан на рис. 9.7. Рис. 9.7. Пример изменения характеристик столбцов при прогоне программы.
200 Глава 9 9.4.3. Пустые столбцы Объект-столбец в TDBGrid не обязательно должен быть ассоциирован с каким-либо полем набора данных. В этом случае свойство столбца FiledName остается незаполненным. Такие столбцы называются пустыми. В них можно выводить какую- либо информацию, основываясь на информации из действительно существующих полей набора данных. Например, из трех полей набора данных, содержащих соответственно фамилию, имя и отчество, можно в пустом столбце показать фамилию, имя и отчество вместе. Другим назначением пустого столбца является вывод информации по требованию. Например, графические поля, если их выводить для каждой записи в отдельном столбце сетки или даже для текущей записи с помощью отдельного компонента TDBImage, требуют существенных временных затрат для вывода. Поэтому поступают так: устанавливают в свойство пустого столбца ButtonStyle значение cbsEllips и кодируют обработчик события OnEditButtonClick, которое наступает, когда нажата кнопка (...) в конкретной строке пустого столбца. Заметим, что кнопка становится видна только в режиме редактирования столбца. В обработчике события OnEditButtonClick предусматривают вызов формы, показывающей содержимое графического поля текущей записи, или активизацию компонента TDBImage, расположенного на той же форме, что и TDBGrid. Аналогичным образом можно поступать с мемо-полями набора данных. Используя кнопку (...), можно также выводить форму, показывающую записи дочерних НД или форму для расшифровки того или иного значения. Например, при показе значения «на руки» для программы расчета заработной платы с помощью кнопки можно вызвать форму, в которой расшифровывается, какая сумма начислена данному работнику, какова сумма премий, как производились вычеты, налогообложение и пр. Для рассмотренного выше примера приложения введем пустой столбец с именем «Пустой столбец», назначим ему кнопку и напишем обработчик, констатирующий, что данный сотрудник работает в конкретной должности (рис. 9.8). Рис. 9.8. Пример использования пустого столбца.
Использование компонента TDBGrid 201 procedure TForml.DBGridlEditButtonClick(Sender: TObject); begin with Tablel do ShowMessage(FieldByName('FIO').Value + ' - ' + FieldByName('Doljnost').Value); end; 9.4.4. Формирование списка возможных значений столбца Если значения столбца должны выбираться из некоторого фиксированного множества и вы хотите получать список значений при редактировании столбца непосредственно в TDBGrid, этот список должен содержаться в свойстве PickList данного постоянного столбца. Можно заполнить список PickList на этапе конструирования программы с помощью текстового редактора этого свойства. В свойство ButtonStyle необходимо установить значение cbsAuto. При прогоне программы в момент, когда пользователь начнет редактирование значения в этом поле, в нем автоматически будет помещаться диалоговый элемент ComboBox. Например, пусть сотрудники кафедры могут обладать учеными степенями доктора физико-математических наук (д.ф.-м.н.), доктора технических наук (д.т.н.), кандидата физико-математических наук (к.ф.-м.н.) и кандидата технических наук (к.т.н.). Предположим, что на работу не принимаются обладатели иных ученых степеней (например, доктора и кандидаты химических или социологических наук). Таким образом, список возможных значений столбца «Ученая степень» состоит из 4 значений (рис. 9.9). Рис. 9.9. Текстовый редактор позволяет определить список PickList.
202 Глава 9 НЕГО *Г Списочный состав кафедры ТИ 1 Таб. № ФИО Уч. степень Должность Z 123456 Ванина Е.С. К.Т.Н. доцент 234324 Сивый Х.М. Д.Ф.-М.Н. профессор 345213 Харин П.Б. ^ассистент 345678 Базаренкова Н.С. К.Т.Н. ассистент , профессор 354234 Суровев В.Д. Д.З.Н. I 432123 Найрымов М.С. ’ 1 инженер 765423 Лавочкина С.З. К.Т.Н. доцент 876534 Айга Ю.З. К.Т.Н. зав. лаб. । 823433 Ванилинов К.Г. К.Т.Н. ассистент Рис. 9.10. Если для столбца определен список PickList, выбор одного из значений происходит после нажатия стрелки. Нажатие кнопки приведет к появлению выпадающего списка (рис. 9.11). |* Списочный состав кафедры ТИ 1 Т аб № ФИО Уч. степень Должность I 123456 Ванина Е.С. K.T.H. доцент 234324 Сивый Х.М. Д.Ф.-М.Н ^профессор 345213 Харин П.Б. ассистент 345678 Базаренкова Н.С. K.T.H. 1 ассистент 354234 Суровев В.Д. Д.З.Н. профессор I 432123 Найрымов М.С. Г инженер 765423 Лавочкина С.З. Д.Ф.-М.Н. доцент 876534 Айга Ю.З. Д-Т.н. к.Ф.-м.н. зав. лаб. 823433 Ванилинов К.Г 136ШВМ1 ассистент Рис. 9.11. Выпадающий список возможных значений столбца. При этом выбор значения в списке приведет к копированию его в текущую ячейку столбца и, если данный столбец связан с полем ТБД, к последующему запоминанию в качестве значения поля в текущей записи. Заметим, что наличие выпадающего списка не препятствует пользователю занести в столбец и иную информацию (рис. 9.12).
Использование компонента TDBGrid 203 |* Списочный состав кафедры ТИ-1 Таб. № ФИО | Уч степень! Должность 123456 Ванина Е.С. К.Т.Н. доцент 234324 Сивый Х.М. Д.Ф.-М.Н. профессор 345213 Харин П.Б. ассистент 345678 Базаренкова Н.С. К.Т.Н. ассистент 354234 Суровов В.Д. Д.Э.Н. профессор I 432123 Найрымов М.С K.X.HJ ▼ - инженер 765423 Лавочкина С.З. К.Т.Н. доцент 876534 Айга Ю.З. К.Т.Н. зав. лаб. 823433 Ванилинов К.Г. к.т.н. ассистент Рис. 9.12. Пользователь может ввести в ячейку столбца значение, не содержащееся в списке PickList. Список возможных значений столбца может формироваться программно. Почти всегда в таком случае источником значений служит другая, справочная ТБД. В процессе работы справочная ТБД может меняться, записи в ней могут корректироваться, добавляться или удаляться. Все внесенные изменения должны отображаться в списке PickList. Пусть для нашего примера список возможных должностей находится в ТБД UCHSTEP.DB (в поле UchStepen). Тогда при создании формы, на которой расположен TDBGrid, можно следующим образом сформировать список PickList-. procedure TForml.FormActivate(Sender: TObject); begin with Table2 do begin First; while not EOF do begin DBGridl.Columns.Items[3].PickList.Add( FieldByName('UchStepen').Value); Next; end; {while} end; {with} end; 9.4.5. Изменение порядка следования столбцов На этапе конструирования программы имеются две возможности изменения порядка следования столбцов в сетке TDBGrid. Первый заключается в том, что объекты-столбцы создаются с помощью редактора столбцов в нужном порядке. В отличие от предыдущих версий Delphi в редакторе столбцов версий 4 и 5 нет возможности изменить порядок следования уже созданных
204 Глава 9 объектов-столбцов, поэтому установить нужный порядок можно только путем соответствующего порядка их создания. Не используйте кнопку И для создания всех объектов-столбцов: они будут созданы в порядке объявления соответствующих полей вНД, и этот порядок уже нельзя будет изменить. Вместо нее используйте кнопку и связывайте созданный ею пустой столбец с нужным полем НД. Заметим, что, если создан хотя бы один объект-столбец, в сетке TDBGrid будут отображаться только те поля НД, для которых создан соответствующий объект-столбец. Второй способ связан с тем, что, если с помощью редактора полей созданы объекты-поля НД и не создан ни один объект-столбец, порядок следования столбцов в сетке будет определяться порядком следования объектов-полей. В редакторе полей можно перетаскивать мышью ранее созданные объекты-поля и менять их последовательность нужным образом или создавать эти объекты в нужном порядке. Если с помощью редактора столбцов создан хотя бы один объект-столбец, порядок создания объектов-полей игнорируется. На этапе прогона программы пользователь может изменять положение столбцов, перетаскивая их мышью на новое место. Программное изменение порядка следования столбцов на практике обычно не используется. Если такая необходимость возникнет, то нужно очистить список TDBGrid.Columns и создать его заново с нужным порядком следования столбцов: with DBGridl do begin Columns .Clear;. Columns.Add; Columns[0].FieldName := 'FIO'; Columns[0].Title.Caption := 'ФИО'; Columns.Add; Columns[1].FieldName := 'TabNum'; Columns[1].Title.Caption := 'Ta6.№'; end; Если потребуется восстановить умалчиваемый порядок следования, достаточно свойству TDBGrid.State присвоить значение csDefault. 9.5. УПРАВЛЕНИЕ ОТОБРАЖЕНИЕМ ДАННЫХ На характеристики представления значений в ячейках TDBGrid оказывают влияние такие свойства компонентов TField, как DisplayFormat и EditFormat, которые позволяют задавать формат поля соответственно при его показе и при редактировании, причем эти свойства актуальны для всех компонентов, визуализирующих значение поля (полей), а не только для столбцов TDBGrid. Перечисленные средства носят безусловный характер, то есть позволяют изменять характеристики показа всех значений в конкретном столбце. Однако может потребоваться выделять тем или иным способом не целый столбец, а определенные ячейки столбца, отвечающие какому-либо условию. Например, для бухгалтерских и складских программ обычно принято выделять красным цветом отрицательные остатки или остатки, находящиеся ниже установленного лимита. То, как осуществляется прорисовка данных в TDBGrid - стандартным способом или по особому сценарию, - определяется свойством DefaultDrawing. Если это свойство
Использование компонента TDBGrid 205 имеет значение False, алгоритм прорисовки должен содержаться в обработчиках событий OnDrawColumnCell или OnDrawDataCell. Заметим, что обработчик события OnDrawDataCell введен для совместимости с ранними версиями Delphi. property OnDrawColumnCell: TDrawColumnCellEvent; TDrawColumnCellEvent = procedure (Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState) of object; Здесь: Rect - координаты области прорисовки; DataCol - содержит порядковый номер текущего столбца, начиная с 0-го; Column - определяет текущий столбец; State - состояние ячейки: gdSelected — ячейка выбрана; gdFocused - ячейка имеет фокус ввода; gdFixed - ячейка относится к фиксированному ряду (заголовку) или столбцу. Если State содержит значение gdSelected (т. е. ячейка является выбранной), в обработчике события OnDrawColumnCell необходима прорисовка инверсной полосы, а если gdFocused - необходима прорисовка пунктирного прямоугольника выбора. Для вывода ячеек стандартным образом используется метод DefaultDrawColumnCell. Следует заметить, что если для сетки свойство DefaultDrawing содержит умалчиваемое значение True, обработчики будут вызваны после того, как завершится стандартная процедура прорисовки, т. е. программный вывод в этом случае будет накладываться на стандартный. Это может стать причиной неправильного отображения данных, например, так, как показано на следующем рисунке. Рис. 9.13. Пример двойной прорисовки для строк, в которых «Цена» равна нулю. В этом примере использовался такой обработчик события OnDrawColumnCell: procedure TForml.DBGridlDrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); begin if Tablel[1 Price']=0 then with DBGridl.Canvas do begin Font.Style :- [fsltalic]; TextOut(Rect.Left, Rect.Top, Column.Field.Text) end end;
206 Глава 9 Для большей наглядности нестандартный вывод сделан намеренно отличающимся от стандартного (курсив, текст не выравнивается). Чтобы исключить нежелательное влияние стандартного вывода, нужно вначале закрасить прямоугольник прорисовки: procedure TForml.DBGridlDrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); begin if Tablel['Price']=0 then with DBGridl.Canvas do begin FillRect(Rect); //Удаляем стандартный вывод Font.Style := [fsltalic]; TextOut(Rect.Left, Rect.Top, Column.Field.Text) end end; При двойной прорисовке может замедлиться скорость листания данных в сетке, особенно для компьютеров с невысокой производительностью, В этом случае нужно в DefaultDrawing поместить False и позаботиться о стандартной прорисовке: procedure TForml.DBGridlDrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); begin if Tablel['Price']=0 then with DBGridl.Canvas do begin Font-Style := [fsltalic]; TextOut(Rect.Left, Rect.Top, Column.Field.Text) end else //Без следующего оператора стандартные строки будут пустыми! DBGridl.DefaultDrawColumnCell(Rect, DataCol, Column, State) end; Используя информацию, содержащуюся в передаваемых в обработчик OnDraw- ColumnCell параметрах, можно изменять отображение как целых строк, так и отдельных ячеек. Пусть, например, необходимо выводить белым шрифтом на красном фоне те строки НД, у которых поле Doljnost содержит значение профессор (рис. 9.14). Рис. 9.14. Цветовое выделение строк, для которых столбец «Должность» содержит значение «профессор».
Использование компонента TDBGrid 207 procedure TForml.DBGridlDrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); begin with DBGridl.Canvas do begin // Поле "Должность" содержит значение «профессор»? if (TablelDoljnost.AsString = 'профессор') and not (gdSelected in State) then // Да. Выводим все ячейки строки белым шрифтом на красном фоне begin Brush.Color := clRed; FillRect(Rect); Font.Color := clWhite; if Field.DataType=ftString then //Текст прижимается влево TextOut(Rect.Left+2,Rect.Top+2,Field.Text) else //Остальные данные - вправо TextOut(Rect.Right-TextWidth(Field.Text)- 2,Rect.Top+2,Field.Text) end else { Нет, текущее значение "Должности" - не "профессор". Выводим все ячейки строки стандартным образом: } DBGridl.DefaultDrawColumnCell(Rect, DataCol, Column, State); end; {with} end; В этом обработчике проверяется активность ячейки и, если ячейка активна (gdSelected in state), для вывода используется умалчиваемый обработчик DefaultDrawDataCell. Сдвиг на 2 пикселя вправо и вниз относительно левого верхнего угла прямоугольника Rect ячейки используется в умалчиваемом обработчике DefaultDrawDataCell для стандартного шрифта MS Sans Serif высотой 8 пунктов, поэтому, если вы хотите, чтобы выводимый вами текст не отличался от стандартного, используйте эти смещения (для другого шрифта эти величины нуждаются в корректировке). В обработчике проверяется тип данных: строковые данные прижимаются к левому краю колонки, остальные - к правому (именно так поступает DefaultDrawDataCell). В отличие от предыдущего примера в следующем выделяются не строки, а ячейки (рис. 9.15): procedure TForml.DBGridlDrawColumnCell(Sender: TObject; const Rect: TRect;. DataCol: Integer; Column: TColumn; State: TGridDrawState); begin with DBGridl.Canvas do begin if (Column.Field.FieldName = 'Doljnost') and not (gdSelected in State) then if Column.Field.Text = 'профессор' then begin Brush.Color := clRed; Font.Color := clWhite; FillRect(Rect) ; TextOut(Rect.Left+2, Rect.Top+2, Column.Field.Text); Exit; end;
208 Гпава 9 // Сюда попадаем, если текущий столбец не "Должность" // или "Должность", но значение ячейки столбца не "профессор" DBGridl.DefaultDrawColumnCell(Rect, DataCol, Column, State); end; {with} end; ИЕЕ • 1* Списочный состав кафедры ТИ-1 Таб № |ФИ0 |Уч. степень Должность 31 123456 Вагина Е.С. К.Т.Н. доцент 234324 Сивый Х.М. Д.Ф.-М.Н. профессор 345213 Харин П.Б. 1“ ассистент 345676 Базаренкова Н.С. К.Т.Н. ассистент 354234 Суровое В.Д. д.з.н. профессор 432123 Найрымов М.С. К.Т.Н. инженер 765423 Лавочкина С.3. К.Т.Н. доцент ► Айга Ю.Э. К.Т.Н. зав. лаб. J 923433 Ванилинов К.Г К.Т.Н. ассистент Рис. 9.15. Выделение цветом в столбце «Должность» ячеек со значением «профессор». 9.6. ПЕРЕМЕЩЕНИЕ МЕЖДУ ЯЧЕЙКАМИ Для перехода от текущего столбца к другому можно воспользоваться: • клавишей Tab - для перехода к столбцу справа от текущего; • комбинацией клавиш Shift+Tab - для перехода к столбцу слева от текущего; • мышью - независимо от того, к какому столбцу нужно перейти. При смене столбца возникают два события в такой очередности: • OnColExit - наступает в момент, когда фокус ввода покидает текущий столбец; • OnColEnter - наступает в момент, когда новый столбец становится текущим. В следующем примере на экран выводятся имена полей, ассоциированных с покидаемым и новым столбцами: procedure TForml.DBGridlColEnter(Sender: TObject); begin ShowMessage('Текущим стал столбец '+ DBGridl.SelectedField.FieldName ); end; procedure TForml.DBGridlColExit(Sender: TObject); begin ShowMessage('Текущим был столбец '+ DBGridl.SelectedField.FieldName ); end; 9.7. РАБОТА С КОМПОНЕНТОМ TDBCTRLGRID Как известно, существуют два способа отображения данных НД: таблицы и формы. В таблицах пользователь может видеть одновременно несколько записей, что облегчает ему контроль за данными и выбор необходимой записи. При использовании формы пользователь в каждый момент видит на экране лишь одну запись, но зато имеет простой доступ к любому ее
Использование компонента TDBGrid 209 полю, в том числе многострочному или графическому. Компонент TDBCtrlGrid в известной мере сочетает в себе удобства обоих способов: он представляет собой таблицу, каждый ряд или каждая ячейка которой отображается в виде формы (рис. 9.16). Рис. 9.16. Пример использования компонента TDBCtrlGrid. Для работы с компонентом необходимо поместить его на форму, связать с компонентом TDataSource, который, в свою очередь, связать с каким-либо набором данных {ТТаЫе или TQuery) и затем разместить на нем необходимые компоненты для работы с полями базы данных - TDBText, TDBEdit, TDBCheckBox и т. п. - в верхней строке или, если количество столбцов ColCount компонента больше 1, в левой верхней ячейке компонента (рис. 9.17). Рис. 9.17. Пример работы с TDBCtrlGrid на этапе конструирования программы. Во время выполнения приложения расположение компонентов в верхней строке (ячейке) TDBCtrlGrid и их состав будет реплицирован на все оставшиеся строки (ячейки), как это видно из рис. 9.16 и 9.18.
210 Глава 9 Рис. 9.17. Пример сетки TDBCtrlGrid с двумя колонками. Приемы работы с TDBCtrlGrid аналогичны приемам работы с TDBGrid. Текущая строка (ячейка) выделяется пунктирным прямоугольником. Для навигации по сетке компонента используются те же клавиши, что и для TDBGrid. Для вставки новой записи необходимо нажать на клавиатуре клавишу Insert или попытаться перейти с последней записи в НД вниз на одну строку (для сетки с одной колонкой). Для изменения записи достаточно ввести новое значение в какое-либо поле. Для удаления записи необходимо нажать комбинацию клавиш Ctrl+Delete. При этом на возможности редактирования, добавления и удаления записей в TDBCtrlGrid влияет ряд свойств как других компонентов, размещенных на панелях TDBCtrlGrid, так и самого компонента TDBCtrlGrid. Ниже приводится обзор некоторых свойств, методов и событий TDBCtrlGrid. Свойство Назначение property AllowDelete: Boolean; Разрешает/запрещает удаление записей. property Allowlnsert: Boolean; Разрешает/запрещает вставку записей. property ColCount: Integer; Определяет количество колонок панелей на сетке компонента. type TDBCtrlGridOrientation = (goVertical, goHorizontal); property Orientation: TDBCtrlGridOrientation; Определяет ориентацию сетки: goVertical - вертикальная (по умолчанию); goHorizontal - горизонтальная (с горизонтальной полосой прокрутки). type TDBCtrlGridBorder = (gbNone, gbRaised); property PanelBorder: TDBCtrlGridBorder; Тип рамки вокруг каждой панели: gbNone - нет рамки; gbRaised - выпуклая рамка.
Использование компонента TDBGrid 211 property PanelHeight: Integer; Высота панели в пикселях. property Panelwidth: Integer; Ширина панели в пикселях. property RowCount: Integer; Определяет количество одновременно показываемых строк сетки. property SelectedColor: TColor; Определяет цвет выбранной строки (ячейки). property ShowFocus: Boolean; Разрешает/запрещает обводить пунктирным прямоугольником ячейку с фокусом ввода. Некоторые методы компонента TDBCtrlGrid'. Метод Назначение type TDBCtrlGridKey = (gkNull, gkEditMode, gkPriorTab, gkNextTab, gkLeft, gkRight, gkUp, gkDown, gkScrollUp, gkScrollDown, gkPageUp, gkPageDown, gkHome, gkEnd, gklnsert, gkAppend, gkDelete, gkCancel); procedure DoKey(Key: TDBCtrlGridKey); Выполняет действие, определяемое параметром Key: gkNull - ничего не делать; gkEditMode - перевести компонент в режим редактирования; gkPriorTab — передать фокус ввода предыдущей ячейке; gkNextTab - передать фокус ввода следующей ячейке; gkLeft - передать фокус ввода ячейке слева; gkRight - передать фокус ввода ячейке справа; gkUp - передать фокус ввода ячейке сверху; gkDown - передать фокус ввода ячейке снизу; gkScrollUp - прокрутить сетку на строку вверх; gkScrollDown - прокрутить сетку на строку вниз; gkPageUp - прокрутить сетку на экран вверх; gkPageDown - прокрутить сетку на экран вниз; gkHome - передать фокус ввода первой ячейке; gkEnd - передать фокус ввода последней ячейке; gklnsert - перевести компонент в режим редактирования; gkAppend - перевести компонент в режим добавления записи; gkDelete - перевести компонент в режим уничтожения записи; gkCancel - восстановить режим просмотра. Для TDBCtrlGrid определены следующие события, аналогичные одноименным событиям TDBGrid'. OnClick, OnDblClick, OnDragDrop, OnDragOver, OnEndDrag, OnEnter, OnExit, OnKeyDown, OnKeyPress, OnKeyUp, OnStartDrag. Дополнительно введено событие property OnPaintPanel: TPaintPanelEvent; TPaintPanelEvent = procedure(DBCtrIGrid: TDBCtrlGrid; Index: Integer) of object; которое наступает для каждой панели TDBCtrlGrid перед ее показом. Обработчик этого события может управлять прорисовкой панели. Параметр TDBCtrlGrid показывает, какой именно компонент TDBCtrlGrid нуждается в прорисовке; параметр Index определяет индекс отображаемой панели.
Глава 10 ВИЗУАЛЬНЫЕ КОМПОНЕНТЫ ДЛЯ РАБОТЫ С ТЕКУЩЕЙ ЗАПИСЬЮ НАБОРА ДАННЫХ 10.1. КОМПОНЕНТ TDBTEXT Компонент TDBText применяется для показа значения текстового поля текущей записи НД. Изменять значение, показываемое при помощи TDBText, нельзя. Компонент является аналогом компонента TLabel за исключением того, что значение для отображения берется из текущей записи НД. Для использования компонента TDBText нужно связать его с соответствующим полем НД с помощью свойств DataSource и DataField. Пример. Пусть имеем ТБД TOV.db, в состав полей которой входят поля GrNum (номер группы товаров) и Tovar (наименование товара). Требуется в компоненте TDBGrid показывать только наименования товаров, а под TDBGrid — номер группы для текущей записи в НД. Расположим на форме компоненты Tablel, DataSourcel и DBGridl, связанные между собой и с НД tov.db стандартным образом. Расположим также на форме компоненты Labell (свойство Caption = 'Номер группы текущей записи - ') и DBTextl. Назначим в Инспекторе Объектов свойства DBTextl.DataSource = DataSourcel и DBTextl.DataField = GrNum. Теперь во время выполнения программы DBTextl будет показывать содержимое поля GrNum текущей записи (рис. 10.1). Рис. 10.1. Использование компонента TDBText для вывода номера группы текущей записи набора данных. 10.2. КОМПОНЕНТ TDBEDIT Компонент TDBEdit позволяет редактировать значение строкового поля текущей записи НД. Он повторяет функциональность компонента TEdit (позволяющего корректировать значение переменной), но источником данных и их приемником в этом случае служит поле НД. Пример. Пусть необходимо корректировать, изменять, удалять текущую запись в НД, связанном с ТБД tov.db (см. пример выше). Поместим на форму компоненты
Визуальные компоненты для работы с текущей записью 213 Tablel (связанный с ТБД TOV.db) и связанный с ним компонент DataSource 1. Пусть группа кнопок реализует перевод НД в режимы dslnsert, dsEdit, а также обращение к методам Delete, Post, Cancel. Поместим в форму компоненты DBEditl (указывает на поле GrNum) и DBEdit2 (указывает на поле GrNazv). При добавлении новой записи или при изменении существующей пользователю будет необходимо вводить и корректировать значения полей в этих компонентах (рис. 10.2). Рис. 10.2. Использование компонентов TEdit для ввода значений в поля текущей записи. При вводе значения в TDBEdit программа автоматически следит за тем, чтобы введенное значение было совместимо по формату с полем НД, с которым связан данный компонент TDBEdit. Ввод неверных значений блокируется. .Например, если в компонент, связанный с полем типа дата-время, попытаться поместить произвольный текст, будет возбуждена исключительная ситуация. Свойства, методы и события компонента аналогичны свойствам, методам и событиям стандартного компонента TEdit. 10.3. КОМПОНЕНТ TDBCHECKBOX Он обладает функциональностью стандартного компонента TCheckBox, но источником данных и их приемником для него служит поле НД. Пример. Расширим пример, приведенный выше для компонента TDBEdit. Пусть в ТБД TOV. db имеется поле Uzenka (тип Boolean). Значение True в этом поле указывает на то, что товары данной группы подлежат уценке, значение False - не подлежат. Добавим в форму компонент DBCheckBoxl (свойство DataSource = DataSource 1, свойство DataField = Uzenka). Тогда в процессе работы можно устанавливать в данное поле значения True или False в зависимости от состояния компонента DBCheckBoxl (рис. 10.3). Замечание. При показе содержимого НД Tablel в TDBGrid в столбце Uzenka для тех записей, у которых оно содержит True, выводится символ «+», а для тех записей, у которых поле содержит False, - ничего не выводится. Такая функциональность реализуется в следующем обработчике события OnGetText для поля Uzenka (объект-поле Tablel Uzenka).
2Т4 Глава 10 Рис. 10.3. Использование компонента TDBCheckBox для указания значения поля «Уценка». procedure TForml.TablelUzenkaGetText(Sender: TField; var Text: String; DisplayText: Boolean); begin if DisplayText and TablelUzenka.Value then Text := '+'; end; Напомним, что событие OnGetText наступает перед показом содержимого поля каждой записи в визуальных компонентах. Компонент TDBChechBox можно связывать не только с логическим, но и с символьным полем. В этом случае необходимо соответствующим образом установить значения строковых свойств ValueChecked и ValueUnchecked компонента TDBChechBox. Например: DBCheckBoxl.ValueChecked := 'Тгие;Уез;Оп;Да;Д'; DBCheckBoxl .ValueUnchecked := 'False;No;Off;HeT;H'; 10.4. КОМПОНЕНТ TDBRADIOGROUP Компонент TDBRadioGroup служит для представления фиксированного набора возможных значений поля при помощи группы зависимых переключателей. Этот компонент обладает функциональностью компонента TRadioGroup, но источником данных и их приемником для него служит поле НД. Количество и названия вариантов возможных значений поля определяются в свойстве property Items: TStrings; Пример. Расширим пример, приведенный выше для компонента TDBCheckBox. Пусть в ТБД tov.db имеется строковое поле Hranenie, которое может принимать только 2 значения: до 5 дней и свыше 5 дней. Для выбора одного из этих значений добавим в форму компонент DBRadioGroupl, связав его с DataSourcel и указав в свойстве DataField поле Hranenie (рис. 10.4).
Визуальные компоненты для работы с текущей записью 215 Рис. 10.4. Использование компонента TDBRadioGroup для выбора значения поля «Хранение». 10.5. СПИСОЧНЫЕ КОМПОНЕНТЫ Управляющие компоненты БД типа TDBListBox, TDBComboBox, TDBLookupListBox и TDBLookupComboBox имеют связанные с ними списки TStrings и поэтому дальше объединяются общим названием списочные компоненты БД. Списочные компоненты предназначены для отображения состояния конкретного поля текущей записи НД, а также для ввода в это поле нового значения (но никак не для навигации по НД!). Первые два компонента имеют первоначально пустые списки, которые программист должен наполнить перед тем, как они станут доступны пользователю. Справочные компоненты TDBLookupXXXX формируют свои списки значениями, взятыми из поля другого НД, и не нуждаются в их наполнении - вот, собственно, и вся разница между ними и первыми двумя компонентами. Компоненты TDBListBox и TDBLookupListBox могут отображать текущее значение связанного с ними поля только в том случае, если это значение совпадает с одним из списочных значений компонента, два других отображают любое значение поля, даже если этого значения нет в связанных с компонентами списках. Точно так же вводить новое значение компоненты XXXXListBox могут только из своих списков, в то время как компоненты ХХХХСотЬоВох в модификациях csSimple и csComboBox содержат текстовый редактор, с помощью которого пользователь может задать значение, которого нет в списке. В ряде случаев бывает необходимо предоставить пользователю интерфейсные свойства этих компонентов, не связывая их с редактированием поля НД. Например, в диалоговом окне требуется создать выпадающий список, чтобы пользователь мог выбрать интересующего его партнера, перечень которых содержит ТБД PARTNERLIST. 1\ля этого можно использовать списочные компоненты, не связывая их с какой-то таблицей. Поскольку в этом случае TDBListBox и TDBComboBox ничем не отличаются от соответственно TListBox и TComboBox, имеет смысл использовать компоненты TDBLookupXXXX для автоматического формирования списка возможных значений. 10.5.1. Компонент TDBListBox Компонент TDBListBox применяется, когда нужно выбрать значение поля из списка возможных значений. Возможные значения содержатся в качестве строк свойства Items компонента TDBListBox.
216 Глава 10 Пример. Пусть в ТБД TOV.DB имеется поле Hranenie типа String, которое может принимать только 2 значения: до 5 дней и свыше 5 дней. Для выбора одного из этих значений добавим в форму компонент DBListBoxl, связав его с DataSource! и полем Hranenie (рис. 10.5). Работа с TDBListBox № грчппы Хранение Выйти | Запомнить I Отменить Рис. 10.5. Использование компонента TDBListBox для выбора значения поля «Хранение». 10.5.2. Компонент TDBComboBox Этот компонент аналогичен компоненту TDBListBox, за исключением того, что в режиме dsBrowse текущее значение поля показывается аналогично TDBEdit, а в режимах добавления (dslnsert) и редактирования (dsEdit) при занесении значения в поле появляется выпадающий список (рис. 10.6.). Длина списка в строках определяется свойством DropDownCount. Рис. 10.6. Использование компонента TDBComboBox для выбора значения поля «Хранение».
Визуальные компоненты для работы с текущей записью 217 10.5.3. Компонент TDBLookupComboBox Компонент TDBLookupComboBox применяется для выбора значений в поле одного набора данных (назовем его НД-1) из списка значений, источником которого служат значения какого-либо поля из другого набора данных (НД-2). Наборы НД-1 и НД-2 связываются по полю связи, которое должно иметь один и тот же тип в обоих НД. При этом реляционная связь может устанавливаться без помощи первичного (НД-1) и внешнего (НД- 2) индексов, т. е. поля связи могут не входить ни в текущий, ни в какой-либо иной индекс. Свойства компонента: Свойство Назначение pb property DataField: String; Содержит поле НД-1, в которое будет помещаться значение из НД-2. pb property DataSource: TDataSource; Указывает на компонент TDataSource, связанный с НД-1. type TDropDownAlign = (daLeft, daRight, daCenter); pb property DropDownAlign: TDropDownAlign; Определяет положение раскрывающегося списка относительно окна редактора: daLeft - слева; daRight - справа; daCenter - по центру. pb property DropDownRows: Integer; Определяет количество элементов списка. pb property DropDownWidth: Integer; Определяет ширину списка. pb property Field: TField; Открывает доступ к полю НД-2, из которого берутся значения. pb property KeyField: String; Устанавливает поле связи между НД-1 и НД-2. property KeyValue: Variant; Содержит текущее значение поля связи. Если во время выполнения программы его значение изменится, будет осуществлена попытка отыскать в НД-2 запись со значением, содержащимся в этом свойстве. pb property ListField: String; Определяет список полей НД-2, значения которых будут показываться в списке выбора. Соседние поля в списке разделяются точкой с запятой. Если значение не заполнено, берется значение свойства KeyField. pb property ListFieldlndex: Integer; Определяет номер поля в списке ListField, значения которого будут использоваться при инкрементном поиске. pb property ListSource: TDataSource; Определяет НД-2. ro property ListVisible: Boolean; Содержит True, если список раскрыт. pb property Readonly: Boolean; Если содержит True, компонент не может изменять значение поля DataField. ro property Text: String; Содержит текст в окне редактора компонента. Методы компонента: Метод Назначение procedure CloseUp(Accept: Boolean); Закрывает раскрытый список. Параметр Accept определяет, будет ли использоваться текущее значение списка для изменения поля DataField. procedure DropDown; Раскрывает список.
218 Глава 10 Пример. Пусть в ТБД «Сотрудники» (поле Doljnost} нужно вводить значения из списка, определяемого полем Doljnost ТБД «Должности, оклады». Для иллюстрации будем в списке показывать также значения поля Oklad ТБД «Должности, оклады». С ТБД «Сотрудники» связан НД TeachersTable, с ТБД «Должности, оклады» - НД OkladyTable. С ними соответственно связаны компоненты TDataSource DS_Teachers и DS_Oklady. Для ввода в поле Doljnost (НД TeachersTable) разместим на форме компонент DBLookupComboBoxl и установим такие значения его свойств: DataSource DataField ListSource ListField KeyField DS_Teachers Doljnost DS_Oklady OklaD;Doljnost Doljnost Тогда при занесении значения в поле Doljnost НД TeachersTable получим комбинированный список значений оклада и должности из НД OkladyTable (рис. 10.7). Рис. 10.7. Выбор должности в таблице «Сотрудники» из таблицы «Должности, оклады». 10.5.4. Компонент TDBLookupListBox Компонент TDBLookupListBox повторяет функциональность компонента TDBLookupComboBox за исключением того, что выбор производится из постоянно присутствующего на экране, а не раскрывающегося списка. В связи с этим отсутствуют следующие свойства и методы компонента TDBLookupComboBox'. DropDownAlign, DropDownRows, DropDownWidth, DropDown и CloseUp. 10.6. КОМПОНЕНТ TDBMEMO Компонент TDBMemo предназначен для показа мемо-полей (полей комментариев). Поля комментариев могут хранить многострочные тексты. Компонент TDBMemo
Визуальные компоненты для работы с текущей записью 219 является аналогом компонента ТМето с той лишь разницей, что источником данных в этом случае служит поле НД. При корректировке текста в компоненте TDBMemo набор данных, к которому принадлежит поле, автоматически переводится в состояние dsEdit. Пример. Пусть в ТБД tov.db имеется поле комментария Komment. Расположим в форме компонент DBMemol и укажем в его свойстве DataSource имя компонента TDataSource, связанного с НД, к которому принадлежит поле комментария. После этого в свойстве DataField из выпадающего списка выберем имя поля Komment. Если нужно представлять текст комментария только для просмотра, установим в свойство ReadOnly компонента DBMemol значение True, а если текст может изменяться - значение False (рис. 10.8). Рис. 10.8. Использование компонента TDBMemo для показа содержимого поля комментария из текущей записи НД. Свойства компонента: Свойство Назначение type TAlignment = (taLeftJustify, taRightJustify, taCenter); pb property Alignment: TAlignment; Определяет выравнивание текста: taLeftJustify - влево; taRightJustify - вправо; taCenter - по центру. pb property AutoDisplay: Boolean; В состоянии True указывает на необходимость того, чтобы любые изменения в поле НД автоматически отображались и в TDBMemo. В состоянии False подобные действия не производятся и обновление информации в TDBMemo необходимо производить программно.
220 Глава 10 ro property CaretPos: TPoint; Содержит текущие координаты текстового курсора. pb property DataField: String; Содержит имя мемо-поля НД. pb property DataSource: TDataSource; Ссылается на связанный с компонентом источник данных. ro property Field: TField; Открывает доступ к мемо-полю. property Lines: TStrings; Содержит набор строк мемо-поля. С помощью его методов LoadFromFile и SaveToFile программа может читать текст из файла или записывать в него текст. pb property Readonly: Boolean; Если содержит True, мемо-поле нельзя изменять. type TScrollStyle = (ssNone, ssHorizontal, ssVertical, ssBoth); pb property ScrollBars: TScrollStyle; Определяет наличие полос прокрутки: ssNone - нет полос; ssHorizontal - горизонтальная полоса; ss Vertical - вертикальная полоса; ssBoth - обе полосы. property Text: String; Интерпретирует текст компонента как набор символов с символами-разделителями конца строк. property WantTabs: Boolean; Если содержит True, нажатие Tab вызывает ввод в текст символа табуляции, в противном случае - обрабатывается системой. Для ввода символа табуляции в этом случае следует нажать Ctrl+Tab. pb property Wordwrap: Boolean; Разрешает/запрещает разрывать строки на границе слов. Пример. Требуется строчные буквы всех строк в DBMemol заменить на заглавные. Для преобразования используем стандартную функцию AnsiUpperCase-. procedure TForml.ButtonlClick(Sender: TObject); var i: Integer; begin Tablel.Edit; with DBMemol do for i := 0 to Lines.Count - 1 do Lines [i] := AnsiUpperCase(Lines[i]); Tablel.Post; end; Нужно отметить, что того же эффекта можно добиться, обращаясь к содержимому поля комментария как к длинному текстовому полю с использованием его свойства Text. Например: procedure TForml.Button2Click(Sender: TObject); begin Tablel.Edit; DBMemol.Text := AnsiUpperCase(DBMemol.Text); Tablel.Post; end;
Визуальные компоненты для работы с текущей записью 221 Заметим, что изменение значения свойств Text или Lines не переводит НД в режим редактирования и эти изменения не переносятся в поле НД, с которым связан компонент. Поэтому перед внесением изменений в значения свойств Lines или Text следует переводить набор данных в режим dsEdit, а затем запоминать изменения методом Post. Методы компонента: Метод Назначение procedure Clear; virtual; Очищает поле. procedure ClearSelection; Удаляет выделенную часть текста. procedure ClearUndo; Очищает буфер отката. procedure CopyToClipboard; Копирует выделенную часть текста в Clipboard. procedure CutToClipboard; Удаляет выделенную часть текста и помещает ее в Clipboard. function GetSelTextBuf(Buffer: PChar; BufSize: Integer): Integer; virtual; Копирует не более BufSize символов выделенной части текста в буфер Buffer и возвращает количество скопированных символов. procedure PasteFromClipboard; Вставляет в текущую позицию текстового курсора содержимое Clipboard. procedure SelectAll; Выделяет весь текст. procedure SetSelTextBuf(Buffer: PChar); Заменяет выделенную часть текста на содержимое Buffer. procedure Undo; Отменяет последнее изменение текста. Иногда требуется отыскать в тексте комментария слово или фрагмент текста. В этом случае с содержимым компонента работают не построчно (свойство Lines), а как с единым целым (текстовой переменной) с помощью свойства Text. Для поиска используется символьная функция Pos, которая возвращает значение, отличное от О, если поиск был удачен. С помощью свойств SelStart и SelLength найденный фрагмент текста выделяется. Например, в обработчике события OnFind стандартного компонента FindDialog поиск можно реализовать следующим образом: procedure TForml.FindDialoglFind(Sender: TObject); var Found: Integer; // Результат поиска begin Found := Pos(FindDialogl.FindText, DBMemol.Text); if Found > 0 then begin {Отметим найденный фрагмент} DBMemol.SelStart:= Found -1; DBMemol.SelLength := Length(FindDialogl.FindText); end else MessageDlg(Фрагмент +FindDialogl.FindText'" не найден', mtlnformation, [mbOK], 0); end; Единственное специфичное для компонента событие property OnChange: TNotifyEvent; возникает при всяком изменении текста.
222 Глава 10 10.7. КОМПОНЕНТ TDBRICHEDIT 10.7.1. Назначение компонента Компонент TDBRichEdit позволяет просматривать и корректировать информацию в поле форматированного комментария (рис. 10.9). Рис. 10.9. Форматированный комментарий Текст форматированного комментария может содержать фрагменты, набранные различным шрифтом, размером, стилем, цветом и т. д. В отличие от компонента TDBMemo, который позволяет работать только с однородным (неформатированным) текстом, компонент TDBRichEdit умеет интерпретировать специальные символы разметки текста в формате RTF (Rich Text Format - расширенный текстовый формат). 10.7.2. Свойства, методы и события Многие свойства, методы и события компонента аналогичны по назначению одноименным свойствам, методам и событиям компонента TDBMemo и в этом разделе не описываются. Специфичные свойства компонента: Свойство Назначение property DefAttributes: TTextAttributes; Содержит ссылку на объект, описывающий умалчиваемые атрибуты текста (см. ниже). TConversionClass = class of TConversion; property Defaultconverter: TConversionClass; Свойство указывает класс конвертора, использующегося для преобразования текстовых форматов при чтении или записи текста. По умолчанию иет преобразований. property HideScrollBars: Boolean; Определяет, будет ли редактор автоматически вставлять полосы прокрутки, если текст отсекается границами компонента. Игнориру- ется, если ScrollBars содержит ssNone.
Визуальные компоненты для работы с текущей записью 223 property Hideselection: Boolean; Указывает, будет ли убираться выделение текста, если компонент-потеряет фокус ввода. property PageRect: TRect; Указывает размеры страницы при печати на принтере. property Paragraph: TParaAttributes; Содержит атрибуты текущего абзаца, т. е. абзаца с выделением или с текстовым курсором. Программа не может изменить это свойство, но может изменить свойства связанного с ним абзаца. property PlainText: Boolean; Запрещает/разрешает записывать в файл или читать из него служебную информацию формата RTF (True - запрещает). Специфичные методы: Метод Назначение TSearchType = (stWholeWord, stMatchCase); TSearchTypes = set of TSearchType; function FindText(const SearchStr: String; StartPos, Length: Integer; Options: TSearchTypes): Integer; Ищет в тексте строку SearchStr и возвращает индекс первого ее символа при удачном поиске: StartPos - начало поиска; Length - длина строки; Options - указывает, будет ли поиск идти по целым словам и надо ли учитывать высоту букв. procedure Print(const Caption: String); Форматирует текст по границам листа бумаги и печатает его иа умалчиваемом принтере. Caption определяет заголовок печати. TConversionClass = class of TConversion; class procedure RegisterConversionFormat(const AExtension: String; AConversionClass: TConversionClass); Устанавливает соответствие между расширением файла AExtension и конвертором текста. Специфичные события: Событие С чем связано TRichEditProtectChange = procedure(Sender: TObject; StartPos, EndPos: Integer; var AllowChange: Boolean ) of object; property OnProtectChange: TRichEditProtectChange; Возникает при попытке изменить текст, имеющий атрибут Protected'. StartPos, EndPos - соответственно начальная и конечная позиции изменяемого текста. В параметре AllowChange обработчик возвращает True, если можно изменять текст. TRichEditResizeEvent = procedure(Sender: TObject; Rect: TRect) of object; property OnResizeRequest: TRichEdit- ResizeEvent; Событие связано с перерисовкой текста из-за изменения размеров шрифта. Rect содержит прямоугольник, который будетперерисован. TRichEditSaveClipboard = procedure(Sender: TObject; NumObjects, NumChars: Integer; var SaveClipboard: Boolean) of object; property OnSaveClipboard: TRichEditSaveClipboard; Возникает в момент уничтожения компонента и извещает его о том, что в Clipboard осталась информация, которая помещена им. Если обработчик вернет False в параметре SaveClip- board, буфер обмена будет очищен. property OnSelectionChange: TNot i f yEvent; Возникает при изменении выделенного текста.
224 Глава 10 10.7.3. Работа с абзацем Компонент использует вспомогательные объекты класса TTextAttributes для хранения атрибутов шрифта. Эти атрибуты распространяются на весь текст через свойство редактора DefAttributes или на выделенную часть текста - через его свойство SelAttributes. Помимо обычных шрифтовых свойств CharSet, Color, Height, Пате, Pitch, Size и Style, объект TTextAttributes содержит также свойства ConsistentAttributes и Protected. Первое доступно только для чтения и содержит набор текстовых характеристик, общих как для всего текста, так и для его выделенной части. Свойство Protected защищает весь текст или его части от редактирования. Попытка изменить текст, имеющий атрибут Protected, вызывает обработчик события OnProtectChange, который может разрешить или запретить изменения. По умолчанию изменения запрещены. Для каждого текстового абзаца создается объект класса TParaAttributes, в котором сохраняются атрибуты абзаца. Эти атрибуты доступны через следующие свойства класса TParaAttributes: property Alignment: TAlignment; Определяет горизонтальное выравнивание текста абзаца относительно границ компонента. property Firstlndent: Longlnt; Указывает отступ текста абзаца в пикселях от предыдущего абзаца. property Leftindent: Longlnt; Указывает отступ текста абзаца в пикселях от левого края компонента. TNumberingStyle = (nsNone, nsBullet); property Numbering: TNumberingStyle; Указывает, надо ли вставлять слева от абзаца символы списка. Если содержит nsBullet, символы списка вставляются. property Rightindent: Longlnt; Указывает отступ текста абзаца в пикселях от правого края компонента. property Tab[Index: Byte): Longlnt; Для табулостопа с индексом Index содержит его позицию в пикселях от левого края компонента. property Tabcount: Integer; Определяет количество табулостопов в строке абзаца. Пример. Определим кнопку, реализующую выравнивание текущего абзаца, и напишем обработчик события нажатия этой кнопки: procedure TForml.SpeedButtonlClick(Sender: TObject); begin DBRichEditl.Paragraph.Alignment := taCenter; end; Фрагмент текста форматированного комментария выделяется так же, как и в популярном текстовом процессоре WinWord-, для выделения необходимо установить указатель мыши на начало выделяемого фрагмента, нажать левую кнопку мыши и, не отпуская кнопки, установить указатель мыши на конец фрагмента, после чего кнопку отпустить. Есть и .другой способ (при помощи клавиатуры): следует переместить курсор на начало выделяемого фрагмента, нажать кнопку с изображением стрелки влево (вправо) и одновременно - кнопку Shift-, движение курсора следует остановить на конце выделяемого фрагмента. Пример. Установить шрифт, цвет, размер и стиль выделенного текста аналогичными параметрам шрифта, выбранного посредством компонента TFontDialog:
Визуальные компоненты для работы с текущей записью 225 if FontDialogl.Execute then begin with DBRichEditl.SelAttributes do begin Color := FontDialogl.Font.Color; Size := FontDialogl.Font.Size; Name := FontDialogl.Font.Name; Style := FontDialogl.Font.Style; end; //with end; //if Еще один пример. В случае если пользователь пытается изменить символ, ранее помеченный как защищенный, запрашивается подтверждение; если пользователь подтверждает изменение защищенного символа, тот изменяется; если не подтверждает, изменение символа отвергается. procedure TForml.DBRichEditlProtectChange(Sender: TObject; StartPos, EndPos: Integer; var AllowChange: Boolean); begin if MessageDlg('Изменяемый текст является защищенным. ' + 'Желаете его изменить?',mtConfirmation, [mbYes,mbNo],0) = mrYes then AllowChange := True; end; Выделенный фрагмент текста может быть помещен в системный буфер Clipboard и затем вставлен в другой компонент. Работу с буфером обмена поясняет следующий пример. Определим кнопки «Копировать в буфер обмена», «Вырезать в буфер обмена », «Вставить из буфера обмена» и напишем обработчики события нажатия этих кнопок: procedure TForml.SpeedButtonlClick(Sender: TObject); // Кнопка "Копировать в буфер обмена" begin DBRichEditl.CopyToClipboard; end; procedure TForml.SpeedButton2Click(Sender: TObject); // Кнопка "Вырезать в буфер обмена" begin DBRichEditl.CutToClipboard; end; procedure TForml.SpeedButton3Click(Sender: TObject); // кнопка "Вставить из буфера обмена" begin DBRichEditl.PasteFromClipboard; end; Метод FindText ищет в тексте компонента строку SearchStr. Поиск производится во фрагменте текста, начиная с позиции StartPos и заканчивая StartPos + Length - 1. Первый символ текста имеет номер 0. Если поиск успешен, возвращается номер символа, начиная с которого SearchStr входит в текст комментария. В противном случае возвращается (-1). Параметр Options представляет собой множество, в которое
226 Глава 10 могут входить такие элементы: stWholeWord — поиск ведется целыми словами; stMatchCase - игнорируется разница в высоте букв. Пример. Произвести поиск во всем тексте DBRichEditl. Поисковая строка находится в Editl. Text. Если фрагмент найден, выделить его: procedure TForml.FindButtonClick(Sender: TObject); begin with DBRichEditl do begin SelStart := FindText(Editl.Text, 0,GetTextLen, []); SelLength := Length(Editl.Text); end; //with end;
Глава 11 КОМПОНЕНТ TSESSION 11.1. НАЗНАЧЕНИЕ КОМПОНЕНТА Для каждого приложения в Delphi автоматически создается объект типа TSession с именем Session. Он управляет всеми соединениями с базами данных. Его методы и свойства носят глобальный характер для всего приложения. Обычно в приложении имеется один поток команд {Thread) для работы с БД. В некоторых случаях можно, создать два и более потока. Если в каждом из этих потоков реализовать запросы к БД, они будут выполняться независимо друг от друга. В случае нескольких потоков компонент типа TSession создается для каждого из них. 11.2. РАБОТА С УСТАНОВКАМИ BDE Метод procedure GetAliasNames(List: TStrings); очищает список List и затем заносит в него список псевдонимов БД, определенных в' BDE. Пример: Session.GetAliasNames(ListBoxl.Items); Метод procedure GetAliasParams(const AliasName: String; List: TStrings); очищает список List и затем помещает в него параметры псевдонима AliasName. Пример: var TekAllias: String; Session.GetAliasParams(TekAllias, ListBox2.Items); Примеры возвращаемых результатов: SERVER NAME=C:\Program Files\Borland\IntrBase\EXAMPLES\EMPLOYEE.GDB USER NAME=SYSDBA OPEN MODE=READ/WRITE SCHEMA CACHE SIZE=8 LANGDRIVER= SQLQRYMODE= SQLPASSTHRU MODE=SHARED AUTOCOMMIT SCHEMA CACHE TIME=-1 PATH=d:\book\data DEFAULT DRIVER=PARADOX ENABLE BCD=FALSE
228 Глава 11 11.3. ЧТЕНИЕ ИМЕН ТАБЛИЦ БАЗЫ ДАННЫХ procedure GetTableNames(const DatabaseName, Pattern: String; Extensions, SystemTables: Boolean; List: TStrings); Метод GetTableNames очищает список List и добавляет в него имена всех таблиц, определяемых параметром DatabaseName. Если необходимо добавить имена только некоторых таблиц, шаблон их имени указывается параметром Pattern. Значение параметра SystemTables, равное True, включит в состав таблиц, чьи имена занесены в List, имена системных таблиц (для серверных баз данных). Установка значения True в параметр Extensions приведет к включению в имена таблиц расширений имен файлов. Пример: var TekAllias: String; Session.GetTableNames(TekAllias, False, False, ListBox5.Items); Метод procedure GetDriverNames(List: TStrings); очищает список List и затем заносит в него информацию об установленных на текущий момент драйверах BDE. Заметим, что драйверов PARADOX и DBASE не существует, поскольку эти БД управляются драйвером STANDARD. Пример: Session.GetDriverNames(ListBox3.Items); Метод procedure GetDriverParams(const DriverName: String; List: TStrings); очищает список List и затем заносит в него умалчиваемые параметры для драйвера, названного в параметре DriverName. В случае использования таблиц Paradox или dBASE (DriverName = 'standard’) возвращается только один параметр PATH. Драйверы SgZ-cepBepoB могут иметь переменное число параметров. Пример обращения: var TekDriver: String; Session.GetDriverParams(TekDriver,ListBox4.Items); Пример результата для драйвера INTRBASE: SERVER NAME=IB_SERVER:/PATH/DATABASE.GDB USER NAME=MYNAME OPEN MODE=READ/WRITE SCHEMA CACHE SIZE=8 LANGDRIVER= SQLQRYMODE= SQLPASSTHRU MODE=SHARED AUTOCOMMIT SCHEMA CACHE TIME=-1 MAX ROWS=-1 BATCH count=2oo
Компонент TSession 229 ENABLE SCHEMA CACHE=FALSE SCHEMA CACHE DIR= ENABLE BCD=FALSE Пример результата для драйвера STANDARD: РАТН= DEFAULT DRIVER=PARADOX 11.4. РАБОТА С БАЗАМИ ДАННЫХ 11.4.1. Список объявленных БД Метод procedure GetAliasNames(List: TStrings); возвращает список псевдонимов БД, объявленных для BDE на данном компьютере. Количество активных в данном приложении БД возвращает свойство property Databasecount: Integer; При этом отсчет активных БД ведется с 0. Свойство property Databases[Index: Integer]: TDatabase; обеспечивает доступ к активной БД с индексом Index', при этом каждой БД из списка Databases соответствует объект типа TDatabase. Таким образом, при работе с каждой активной БД из списка Databases можно пользоваться свойствами, методами и событиями, присущими компоненту TDatabase. Например, поместить в ListBoxl список активных БД: var i : Integer; ListBoxl.Clear; with Session do for i := 0 to DatabaseCount - 1 do ListBoxl.Items.Add(Databases[i].DatabaseName); 11.4.2. Хранение соединения с неактивной БД Пусть некоторая БД имела наборы данных, открытые в определенный период времени, но затем эти НД были закрыты. Свойство property KeepConnections: Boolean; определяет, следует ли приложению хранить соединение с неактивными БД. False означает не хранить; при значении True соединение с неактивными БД будет ликвидировано либо при окончании работы приложения, либо после выполнения метода procedure Dropconnections; К преимуществам постоянного соединения с неактивной БД можно отнести отсутствие необходимости выполнять повторное соединение с БД при открытии
230 Глава 11 набора данных, использующего таблицы этой БД. К недостаткам - расход ресурсов компьютера на поддержание постоянного соединения. 11.4.3. Работа с активными БД function OpenDatabase(const DatabaseName: String): TDatabase; Метод OpenDatabase пытается отыскать объект TDatabase, у которого свойство DatabaseName совпадает с параметром DatabaseName метода FindDatabase. В случае успеха возвращается указатель на найденный и открытый объект TDatabase-, если такой объект не существует, он создается, открывается и указатель на него возвращается в качестве результата работы метода. Метод OpenDatabase следует использовать в паре с методом CloseDatabase в блоке try...finally для обработки исключений, которые могут возникнуть при выполнении данных методов. procedure CloseDatabase(Database: TDatabase); Метод CloseDatabase закрывает открытую БД, определяемую параметром Database, который содержит ссылку на объект типа TDatabase. Принудительное закрытие БД практикуется достаточно редко, поскольку закрытие всех открытых БД производится автоматически в момент окончания работы приложения. function FindDatabase(const DatabaseName: String): TDatabase; Метод FindDatabase пытается отыскать БД (объект TDatabase) в коллекции TSession.Databases (коллекция открытых БД). Имя искомой БД определяется строковым параметром DatabaseName. Если в коллекции открытых БД сессии имеется такая БД, у которой значение DatabaseName совпадает с параметром DatabaseName метода FindDatabase, в качестве результата возвращается указатель на найденный объект TDatabase. В противном случае возвращается nil. 11.5. УПРАВЛЕНИЕ ПАРОЛЬНОЙ ЗАЩИТОЙ Ниже описывается работа с паролями Paradox. При работе с иными БД обеспечение парольной защиты может отличаться. 11.5.1. Работа с паролями для таблиц Paradox Базой данных в Paradox считается набор ТБД, хранящихся в одном каталоге. Каждая из таких таблиц физически занимает один файл и может иметь один или несколько паролей. При этом каждый из паролей может обеспечивать свои права доступа к таблице - доступ для чтения и записи, только для чтения и т. д. Чтобы установить или снять парольную защиту с таблицы Paradox, нужно открыть ее в утилите Database Desktop, вызвать окно определения таблицы (Table/Restructure) и выбрать Password Security в списке Table Properties (рис. 11.1, а). Окно Auxiliary Passwords (дополнительные пароли) позволяет определить для ТБД Paradox несколько паролей с разными правами доступа (рис. 11.1,6).
Компонент TSession 231 Г Auxiliary Parewords... a) 6) Рис. 11.1. Парольная защита таблиц Paradox: а) окно Password Security, 6) установка нескольких паролей в окне Auxiliary Passwords. Можно установить следующие права доступа: All Над ТБД можно осуществлять любые действия. Insert & Delete Можно добавлять, удалять, очищать данные, но нельзя реструктурировать таблицу и удалять ее из БД. Data Entry Можно добавлять и изменять записи, но нельзя их удалять. Update Можно просматривать записи ТБД и изменять в записях значения неиндексных полей. Read Only Можно только просматривать записи. None Данные нельзя ни просматривать, ни изменять. 11.5.2. Средства TSession для работы с паролями Три метода TSession позволяют добавить пароль к списку паролей, удалить текущий пароль или удалить все пароли: procedure AddPassword(const Password: String); procedure RemoveAllPasswords; procedure RemovePassword(const Password: String); Можно не прибегать к помощи стандартного окна ввода паролей, а вводить их, например, с помощью компонента TEdit и затем добавлять в список паролей: Sessionl.AddPassword(Editl.Text) ; Если в сессии установлен нужный пароль, программа может беспрепятственно открыть защищенную паролем таблицу Paradox, в противном случае будет вызвано стандартное окно запроса паролей. Можно создать собственное окно ввода паролей, если вас не устраивает стандартное окно. Принудительный вызов стандартного диалога ввода паролей реализуется методом function GetPassword: Boolean;
232 Глава 11 Он вызывает обработчик события OnPassword (если определен). В противном случае выдает стандартный диалог ввода пароля. Если пользователь нажал кнопку ОК, метод возвращает True', в случае нажатия кнопки Cancel - возвращает False. 11.6. ОПРЕДЕЛЕНИЕ СЛУЖЕБНЫХ КАТАЛОГОВ СЕССИИ property NetFileDir: String; Свойство NetFileDir указывает каталог, в котором содержится управляющий сетевой файл BDE PDOXUSRS.NET. Это свойство обеспечивает одновременную работу нескольких пользователей в сети. Значение свойства NetFileDir замещает значение сетевого каталога, определяемого в параметрах драйвера Paradox в утилите настройки параметров BDE (BDE Administrator). property PrivateDir: String; Значение свойства PrivateDir определяет каталог, который используется для хранения временных файлов сессии БД. 11.7. СОБЫТИЕ АКТИВИЗАЦИИ СЕССИИ Событие property OnStartup: TNotifyEvent; наступает при активизации сессии в тот момент, когда сессия еще не стала активной. Используя обработчик данного события, во время выполнения приложения можно устанавливать значения свойств NetFileDir и PrivateDir.
Глава 12 КОМПОНЕНТ TDBCHART Для построения графиков используется компонент TDBChart, расположенный на странице Data Controls палитры компонентов Delphi. 12.1. СОЗДАНИЕ ГРАФИКА Чтобы создать график, поместите на форму компонент TDBChart. В форме будет создана заготовка (рис. 12.1). Рис. 12.1. Заготовка графика. Чтобы вызвать окно редактора графика, щелкните мышью по этой заготовке два раза или щелкните по ней правой кнопкой и выберите Edit Chart в локальном меню. В среде редактора можно установить свойства графика и его серий. Содержимое окна редактора представляет собой табулированный блокнот. Для нового графика первой всегда показывается закладка Chart и для страницы Chart - закладка Series (рис. 12.2).
234 Глава 12 Рис. 12.2. Редактор графика. Каждая из закладок на странице Chart предназначена для установки параметров того или иного компонента графика. Series - содержит серии графика. Серией называется набор точек графика. На графике серии соответствует отдельная линия или ряд столбцов. Если в графике несколько серий, будет показываться несколько линий или рядов столбцов. Например, на рис. 12.3 показан график, состоящий из двух серий. Первая показывает общую сумму продаж за 1-й, 2-й, 3-й кварталы; вторая - сумму продаж по конкретному покупателю. General — служит для установки общих параметров графика, таких как его объемность, отступы от краев, возможность увеличения (Zoom) и др. Axis - используется для определения осей графика (рис. 12.4).
Компонент TDBChart 235 260 000; 240000. 220000 200 000 160000; 160000 S 140 000 0120 000 100 000 60000 60000 40 000 20000 Сопоставление продаж по покуп телю с суммой продаж | 231 750 Квартал |И Сумма всех продаж М База №28 Рис. 12.3. График поквартального отпуска товаров конкретному покупателю и суммарного отпуска товаров по кварталам. С помощью переключателей Show Axis можно выбрать нужную ось - левую, правую, верхнюю или нижнюю. На странице, определяемой закладкой Scales, устанавливаются свойства масштаба значений по выбранной оси. Automatic устанавливает автоматическое масштабирование данных - минимум и максимум вычисляются динамически, исходя из текущих значений серии. При отмене автоматического масштабирования можно установить автоматическое масштабирование минимального (Minimum) или максимального (Maximum) значения (отметка Auto). Для установки значения максимума и (или) минимума вручную следует нажать соответствующую кнопку Change. Шаг масштаба по оси выбирается автоматически, если в Desired Increment установлено значение 0. Установить фиксированное значение шага можно, нажав кнопку Change. Закладка Title используется для определения текста заголовка по оси, угла его расположения и шрифта, которым выводится заголовок. Закладка Labels служит для определения параметров меток, а с помощью закладки Tiks устанавлйваются параметры линии оси.
236 Глава 12 Рис. 12.4. Редактор графика - окно установки свойств осей. Titles - содержит инструменты для определения заголовка графика - текста заголовка, его шрифта, выравнивания и др. С помощью закладки Legend определяются параметры легенды - области графика, в которой приводится поясняющая информация. Например, на следующем рисунке показана легенда, которая на рис. 12.3 расположена под графиком. IИ Сумма всех продаж И База № 28 Рис. 12.5. Легенда графика. Panel предназначена для установки параметров панели, на которой располагается график, Paging — параметров многостраничного графика, а с помощью Walls определяется «стенка» графика (на рис. 12.3 «стенка» расположена по левой вертикальной оси).
Компонент TDBChart ТУТ 12.2. ДОБАВЛЕНИЕ СЕРИИ В ГРАФИК И УСТАНОВКА ЕЕ СВОЙСТВ 12.2.1. Добавление серии На графике одновременно может располагаться несколько серий. В большинстве случаев их значения строятся по одинаковому закону и две и более серий одновременно показываются в графике для сравнения. Чтобы добавить в график серию, следует на странице Chart (закладка Series') нажать кнопку Add. После этого появится окно выбора типа серии (рис. 12.6). Рис. 12.6. Редактор графика - окно выбора типа серии. После выбора типа серии в окне страницы Chart (закладка Series) будет показана серия (рис. 12.7).
238 Глава 12 Рис. 12.7. Редактор графика - список серий графика. Кнопка Add используется для добавления других серий, кнопка Delete - для удаления текущей серии. После нажатия кнопки Title можно определить заголовок серии, кнопки Clone - создать новый экземпляр такой же серии в этом же графике, кнопки Change - изменить тип текущей серии. 12.2.2. Выбор источника данных На странице DataSource определяется источник данных для серии. Выпадающий список ниже закладки позволяет определить тип источника данных для серии. No Data - серии не назначается источник данных на этапе проектирования, он будет определен в ходе выполнения программы. Random Values - набор случайных чисел. Бывает полезен при формировании заготовки серии, настоящий источник данных которой будет устанавливаться программно. Function - источник данных - стандартная функция {Copy, Average, Low, High, Divide, Multiply, Subtract, Add), с помощью которой строится график на основании данных в двух или более сериях. DataSet - позволяет указать НД, значения полей которого будут использоваться для формирования точек серии. В качестве НД могут указываться компоненты ТТаЫе, TQuery, TStoredProc. Для примера на рис. 12.8 показан выбор полей Kapital (по оси X) и ProcKPred (по оси У) таблицы Tablel. К моменту выбора полей компонент Tablel должен быть помещен на форму и связан с нужной таблицей. Замечание. Не все типы серии требуют значений по осям Y или X. Jlflx серий типа Pie, Ваг можно указывать значения только по одной из осей и значения меток Labels. В качестве меток могут использоваться символьные поля и поля типа даты и времени. На рис. 12.9 показаны графики отпуска товара конкретному покупателю, где в качестве данных по оси У или X берется сумма
Компонент TDBChart 239 отпуска конкретного товара, а в качестве меток — названия товаров. Показаны три графика - серии типа Pie, Vertical Ваг (или просто Ваг) и Horizontal Ваг. Рис. 12.8. Определение источников для формирования координат графика. Рис. 12.9. Графики серии типа Pie, Vertical Ваг и Horizontal Bar. Если после указания источника данных нажать кнопку Close и выйти из редактора графика, на ранее пустой панели будет построен график (рис. 12.10).
240 Глава 12 Кроме закладки DataSource, на странице Series имеются закладки Format, General, Marks. С помощью Format определяются свойства палитры, линий графика и т. д., с помощью General задаются форматы данных, а закладка Marks предназначена для установки марок - значений над точками серии. На рис. 12.10 марки показывают значения узловых точек по оси Y (175, 160, 140 и 120). Марки отображаются на графике, если отмечен переключатель Visible (рис. 12.11). Переключатели Style определяют вид марок. Рис. 12.11. Редактор графика - окно определения вида марок серии.
Компонент TDBChart 241 12.2.3. Определение функций Как уже говорилось, в качестве источника данных можно определить функцию. Функция обычно используется для показа отношений между другими сериями. Пусть для графика определены две серии, показывающие общую сумму продаж товаров по месяцам и сумму продаж для конкретного покупателя. Источниками данных этих серий служат два компонента TQuery. Определим третью серию, которая показывала бы разницу между первыми двумя. Для этого добавим в график новую серию и на странице Series | Data Source выберем Function. В диалоговом окне определения функций выберем тип функции - Subtract (вычитание). В области Source Series | Available показаны ранее определенные в графике серии «Сумма всех продаж» и «По покупателю». Используя кнопку », переместим эти серии в область Selected (рис. 12.12). Рис. 12.12. Редактор графика - окно определения функций. Тогда на графике получим три серии, причем белым цветом показана серия, источником которой служит функция разности значений первых двух серий (рис. 12.13).
ж Глава 12 Рис. 12.13. Поквартальный график продаж конкретному покупателю, суммарных продаж, разницы между суммой квартальных продаж и суммой квартальных продаж покупателю. 12.3. ДОБАВЛЕНИЕ СЕРИИ ВО ВРЕМЯ ПРОГОНА ПРОГРАММЫ Количество серий, присутствующих в текущий момент в графике, можно определить при помощи метода function SeriesCount : Longlnt ; компонента TDBChart. Список серий графика содержится в его свойстве property Series[Index: Longlnt]: TChartSeries; где Index лежит в диапазоне Q..SeriesCount-\. Метод procedure RefreshData; обновляет данные в серии из наборов данных, которые служат их источником. Например, пусть график показывает отпуск товара конкретному покупателю. Источником данных для серии графика служит Query], которому в качестве параметра передается имя покупателя. При всякой смене имени покупателя происходит обновление Queryl, сопровождающееся изменением данных. Эти изменения отражаются в графике так: Queryl.Open; DBChartl. RefreshData; Чтобы вставить новую серию в график в процессе прогона программы, необходимо создать объект класса TXXXSeries (TLineSeries, TAreaSeries и т.п.), поместить в его свойство ParentChart имя компонента TDBChart и нужным образом определить остальные свойства этого объекта.
Компонент TDBChart 243 Например: var MySeries : TBarSeries; MySeries := TBarSeries.Create(Self); MySeries.Parentchart := DBChartl; MySeries.SeriesColor := clGreen; MySeries.DataSource := Queryl; MySeries.XLabelsSource := 1MES'; MySeries.YValues.ValueSource := 'S'; MySeries.Active := True; MySeries.Title := TablelPOKUP.Value; В приведенном примере создается серия типа Ваг (вертикальная столбчатая диаграмма), в качестве родительского графика ей назначается компонент DBChartl, а в качестве источника данных - Queryl. Для формирования значений серии используются поля S (по оси У) и MES (в качестве Labels) компонента Queryl. Затем серия активизируется (после этого она становится видна в графике) и ее заголовку присваивается наименование покупателя. 12.4. СВОЙСТВА, МЕТОДЫ И СОБЫТИЯ КЛАССА TCHARTSERIES Класс TChartSeries является родительским классом для серий графика (для объектов классов TLineSeries, TAreaSeries, TPointSeries, TBarSeries, THorizBarSeries, TPieSeries, TChartShape, TFastLineSeries, TArrowSeries, TGanttSeries, TBubbleSeries ) и определяет их общие свойства, методы и события. 12.4.1.Свойства Ниже указаны наиболее важные свойство компонента. Полный перечень свойств вы найдете в справочной службе Delphi. Свойство Назначение property Active: Boolean; Разрешает/запрещает показ серии. property ColorEachPoint: Boolean; Если содержит False, все точки серии рисуются одним цветом, определенным в свойстве SeriesColor, в противном случае точки отображаются набором цветов свойства ValueColor. property Colorsource: String; Содержит имя поля НД, значения которого используются как цвета для разных точек графика. property DataSource: TComponent; Содержит имя НД, который служит источником данных для серии. type THorizAxis = (aTopAxis, aBottomAxis); property HorizAxis : THorizAxis; Определяет положение горизонтальной оси: aTopAxis - вверху графика; aBottomAxis - внизу графика. property Marks: TSeriesMarks; Определяет свойства марок (см. ниже примечание 1).
244 Глава 12 property Parentchart: TCustomChart; Содержит ссылку на компонент-график, в котором отображается серия. property PercentFormat: String; Описывает формат представления процентных значений (см. «Спецификаторы форматирования чисел» в п. 5.8.2). property SeriesColor: TColor; Определяет цвет точек серии. property ShowInLegend: Boolean; Разрешает/запрещает показ легенды. property Title: String; Содержит текст заголовка серии. property ValueFormat: String; Определяет формат для представления чисел в метках и марках (см. п. 5.8.2). type TVertAxis = (aLeftAxis, aRightAxis); property VertAxis: TVertAxis; Определяет положение вертикальной оси графика: aLeftAxis - слева; aRightAxis - справа. property XLabelsSource: String; Содержит имя поля НД, из которого берутся значения меток. Поле может быть любого типа, для которого определено свойство AsString. property XValues: TChartValueList; Содержит ссылку на объект, в котором хранятся значения серин по оси X (см. ниже примечание 2), property YValues: TChartValueList; Содержит ссылку на объект, в котором хранятся значения серии по оси У (см. ниже примечание 2). Примечание 1. Объекты класса TSeriesMarks содержат такие свойства: property Arrow: TChartPen; Определяет свойства пера, которым рисуется марка. property BackColor: TColor; Определяет цвет фона марки. По умолчанию $ 8 0 FFFF (желтый). property Clip: Boolean; Если содержит True, марки не могут накладываться на другие элементы графика ( на легенду, метки осей и т. д.). property Font: TFont; Определяет шрифт, которым выводится информация внутри марки. property Parentseries : TChartSeries; Содержит указатель на серию, к которой принадлежат марки. type TSeriesMarksStyle = (smsValue, smsPercent, smsLabel, smsLabelPercent, smsLabelValue, smsLegend, smsPercentTotal, smsLabelPercentTotal, smsXValue); property Style: TSeriesMarksStyle; Определяет содержимое метки (в скобках показаны примеры): smsValue - значения по оси У (1234); smsPercent - процент (12%) smsLabel - метка (Cars); smsLabelPercent - метка и процент (Cars 12 %); smsLabelValue - метка и значение (Cars 1234); smsLegend - легенда; smsPercentTotal - процент от полного значения (12 % of 1234); smsLabelPercentTotal - метка и процент от полного значения (Cars 12 % of 1234); smsXValue - значение по оси А"(01.02.1997). property Transparent: Boolean; Значение True определяет, что цвет фона марки не используется («прозрачная» марка). property Visible: Boolean; Разрешает/запрещает показ меток.
Компонент TDBChart 245 Примечание 2. Объекты класса TChartValueList содержат такие свойства: property Value [Index: Longlnt] : Double; Обеспечивает доступ к элементу серии с индексом Index (значение в диапазоне 0.. Count -1). property Valuesource: String; Указывает источник данных для формирования значений по оси (имя поля или имя другой серин). 12.4.2. Методы Метод Назначение function AddXY(Const AXValue, AYValue: Double; Const AXLabel: String; AColor: TColor) : Longlnt; Добавляет новую точку в серию. Параметры AXValue и AYValue содержат соответственно значения по осям X и Y, AXLabel определяет метку для добавляемой точки серии, a AColor - цвет. Функция возвращает позицию новой точки в серии. function AddY(Const AYValue: Double; Const AXLabel: String; AColor: TColor) : Longlnt; Добавляет в серию новое значение по оси Y. Применяется для тех серий, в которых график строится по У и меткам значений по X (например, Pie, Ваг). Назначение параметров такое же, как у метода AddXY. procedure Assignvalues(Source : TChartSeries); Копирует все точки из серии Source в текущую серию. procedure CheckDataSource; Обновляет точки в серии независимо от того, какой компонент является источником данных - набор данных или другая серия. Обновление производится по текущим Данным источника. Метод рекомендуется вызывать в случае изменений данных в источнике. procedure Clear; Удаляет все значения из серии. procedure ColorRange(AValueList: TChartValueList; Const FromValue, ToValue: Double; AColor: TColor); Изменяет цвет указанного диапазона точек серии. A ValueList - либо XValues, либо YValues. FromValue указывает начальное, a ToValue - конечное значение в списке A ValueList. AColor - новый цвет. function Count : Longlnt; Возвращает количество точек в серии. procedure Delete(Valuelndex : Longlnt); Удаляет из серии точку с номером Valuelndex. График, к которому принадлежит серия, автоматически перерисовывается. function GetCursorValuelndex : Longlnt; Возвращает индекс точки серии в TChartValueList, ближе всего к которой расположен курсор мыши. Если такую точку определить не удается, возвращает (-1). procedure GetCursorValues(Var X, Y: Double); Возвращает значения по Хи У точки графика (а не только серии), ближе всего к которой расположен курсор мыши. function GetHorizAxis: TChartAxis; Возвращает ссылку на объект, определяющий горизонтальную ось.
246 Глава 12 function GetVertAxis: TChartAxis; Возвращает ссылку на объект, определяющий вертикальную ось. function MaxXValue: Double; virtual; Возвращает максимальное значение по оси X. function MaxYValue: Double; virtual; Возвращает максимальное значение по оси У. function MinXValue: Double; virtual; Возвращает минимальное значение по X. function MinYValue: Double; virtual; Возвращает минимальное значение по У. procedure RefreshSerles; Обновляет значения серии из источника данных, указанного в свойстве DataSource. procedure Repaint; Заново рисует весь график. function ValuesListCount: Longlnt; Возвращает количество списков значений точки, используемых в серии. Обычно это 2 (XValues и YValues), но некоторые серии используют 3 (BubbleSeries - XValues, YValues, Radius', GanttSeries - Y, Start, End). function VisibleCount: Longlnt; Возвращает количество точек серии, видимых на графике. 12.4.3. События Событие С чем связано type TSeriesOnBeforeAdd = Function (Sender: TChartSeries): Boolean of object; property OnBeforeAdd: TSeriesOnBeforeAdd; Наступает перед добавлением точки в серию или при связывании серии с источником данных. type TSeriesOnAfterAdd = procedure (Sender:TChartSeries; Valuelndex: Longlnt) of object; property OnAfterAdd: TSeriesOnAfterAdd; Возникает после добавления точки с индексом Valueindex в серию. type TSeriesOnClear = procedure (Sender: TChartSeries) of object; property OnClearValues: TSeriesOnClear; Возникает при очистке серии от точек. type TSeriesClick = procedure (Sender:TChartSeries; Valuelndex: Longlnt; Button: TMouseButton; Shift: TShiftState; X,Y: Integer); property OnClick : TSeriesClick; Возникает при щелчке мышью на серии: Valueindex - индекс ближайшей к курсору точки серии; Button - состояние кнопок мыши; Shift - состояние клавиш Shift, Alt, Ctrl; X.Y- координаты указателя мыши. type TSeriesOnGetMarkText = procedure(Sender: TChartSeries; Valuelndex: Longlnt; Var MarkText: String); property OnGetMarkText: TSeriesOnGetMarkText; Возникает при формировании марки для точки в серии. Обработчик может использоваться для изменения содержимого марки.
Глава 13 ВВЕДЕНИЕ В ПОСТРОЕНИЕ ОТЧЕТОВ 13.1. КОМПОНЕНТЫ ДЛЯ ПОСТРОЕНИЯ ОТЧЕТОВ На странице палитры компонентов QReport расположено более двух десятков компонентов, применяемых для построения отчетов. Центральным компонентом является TQuickRep, определяющий поведение отчета в целом. С помощью других компонентов создаются составные части отчета. TQRBand — заготовка для расположения данных, заголовков, титула отчета и др. Отчет, в основном, строится из компонентов TQRBand, которые реализуют: • область заголовка отчета; • область заголовка страницы; • область заголовка группы; • область названий столбцов отчета; • область детальных данных, предназначенную для отображения данных самого нижнего уровня детализации; • область подвала группы; • область подвала страницы; • область подвала отчета. TQRStringsBand - имеет то же назначение, что и TQRBand. Отличается встроенным списком строк Items, содержимое которого становится видным в режиме печати и предварительного просмотра, если на компонент TQRStringsBand положен компонент TQRExpr. Для каждой строки в Items выводится своя полоса TQRStringsBand. TQRSubDetail - определяет область, в которой располагаются данные детальной таблицы при реализации в отчете связи главный-детальный на основе существующей реляционной связи между ТБД. TQRChildBand - дочерняя полоса. Привязывается к родительской полосе и служит для ее расширения. Любая полоса может стать родительской с помощью установки значения True в ее свойство HasChild. TQRGroup - применяется для группировок данных в отчете. TQRLabel - позволяет разместить в отчете произвольную текстовую строку. TQRDBText - служит для вывода в отчет содержимого текстового поля набора данных. TQRExpr - применяется для вывода значений, являющихся результатом вычисления выражений. Алгоритм вычисления выражений строится при помощи редактора формул данного компонента. TQRSysData - служит для вывода в отчете системной величины: даты, времени, номера страницы и т. п.
248 Глава 13 TQRMemo — вставляет в отчет многострочный текст. TQRExprMemo - используется для создания многострочных вычисляемых полей. TQRRichText - вставляет в отчет многострочный текст в формате RTF. TQRDBRichText - служит для вывода в отчете полей НД, содержащих многострочный текст в формате RTF. TQRShape - служит для вывода в отчете графических фигур, например, прямоугольников. TQRJmage - служит для вывода в отчете графической информации, источником которой является поле набора данных. TQRPreview - базовый компонент для создания нестандартных окон предварительного просмотра. Стандартное окно реализуется с помощью метода Preview компонента TQuickRep. TQRXXXFilter - фильтрующие компоненты для преобразования отчета в текст, страницу HTML и т. п. при печати отчета. TQRChart - служит для встраивания в отчет графиков. 13.2. КОМПОНЕНТ TQUICKREP При размещении этого компонента на форме в ней появляется сетка отчета (рис. 13.1). В дальнейшем в этой сетке располагаются составные части отчета, например, полосы TQRBand (рис. 13.2). Рис. 13.1. Пустая сетка отчета. Образуется после размещения на форме компонента TQuickRep.
Введение в построение отчетов 249 Рис. 13.2. Сетка отчета с размещенными в ней компонентами отчета. Перечислим важнейшие свойства, методы и события компонента TQuickRep. 13.2.1. Свойства Свойство Назначение property Bands: TQuickRepBands; Объект Bands содержит логические свойства, которые после установки в них значений True включают в отчет: HasColumnHeader - заголовки столбцов; HasDetail - детальную информацию; HasPageFooter - подвал страницы; HasPageHeader - заголовок страницы; HasSummary - подвал отчета; HasTitle - заголовок отчета. property DataSet : TDataSet; Указывает набор данных, на основе которого создается отчет. Если нужно вывести связанную информацию из нескольких таблиц БД, ее объединяют в одном НД при помощи компонента TQuery. Информацию из нескольких связанных НД можно включать в отчет, если эти НД связаны в приложении отношением главный-подчиненный. В этом случае в качестве НД отчета указывается главный набор, а ссылка на соответствующие подчиненные наборы осуществляется в компонентах TQRSubDetail. Если в отчет нужно включить информацию из не связанных НД применяют композитный отчет, то есть отчет, составленный из группы других отчетов.
250 Глава 13 property Frame : TQRFrame; Определяет параметры рамки отчета: Color - цвет линий; DrawBottom - наличие линии снизу; DrawLeft- наличие линии слева; DrawRight - наличие линии справа; DrawTop - наличие линии сверху; Style - стиль линии (сплошная, пунктирная и т. п.); Width — толщина линии в пикселях. property Options: TQuickReportOptions; Содержит множество из следующих логических значений: HasFirstHeader - разрешает печатать заголовок первой страницы; HasLastFoother - разрешает печатать подвал последней страницы; Compression - разрешает сжимать отчет при выводе его в метафайл. property Page: TQRPage; Определяет параметры страницы отчета. Все подсвойства этого сложного свойства доступны в окне Report Settings (см. ниже группы Page size и Margin окна редактора свойств). property PrintlfEmpty: Boolean; Разрешает/запрещает печатать отчет в том случае, если он ие содержит данных. property ReportTltle: String; Имя отчета (не его заголовок!). Используется для идентификации отчета в задании иа сетевую печать, возвращается компонентом QRSysData при Data=ReportTitle и может использоваться для выбора одного из нескольких доступных отчетов. property ShowProgress: Boolean; Разрешает/запрещает показывать индикатор процесса печати отчета. property SnapToGrid: Boolean; Если содержит True, размещаемые в отчете компоненты привязываются к сетке отчета. type TQRUnits = (Inches, MM, Pixels, Native, Characters); property Units: TQRUnits; Определяет единицы измерения расстояний в отчете: Inches - дюймы; ММ- миллиметры; Pixels - пиксели; Native - внутренние единицы TQuickRep (равны 0,1 мм); Characters - символы текста. property Zoom: Integer; Определяет масштаб отображения отчета (в процентах от его размеров на листе бумаги) на этапе разработки. Может иметь значение в диапазоне 1..300. Значение свойства не учитывается при печати отчета или в режиме его предварительного показа. Многие свойства отчета можно установить на этапе конструирования с помощью редактора свойств - вызовите локальное меню компонента TQuickRep и выберите опцию Report Settings (рис. 13.3).
Введение в построение отчетов 251 Рис. 13.3. Окно установки параметров отчета. Группа Paper size задает характеристики страницы: ее формат (А4 210 х 270 тт), ширину (Width), длину (Length) и направление печати - вдоль короткой стороны листа (Portrait) или вдоль длинной (Landscape). Группа элементов Margin указывает поля отчета: сверху (Тор), снизу (Bottom), слева (Left), справа (Right), а также количество колонок (Number of columns) и расстояние между ними (Column Space). С помощью элементов группы Other можно задать шрифт (Font), его высоту (Size) и используемые единицы измерения длины (Units). Группа Page frame определяет свойства рамки: наличие линии сверху (Тор), снизу (Bottom), слева (Left), справа (Right), цвет линий (Color) и их толщину (Width). Группа Bands определяет наличие полос заголовков и подвалов (Page header — заголовок страницы; Title — заголовок отчета; Column header — заголовок колонок; Detail band — полоса для детальной информации; Page footer — подвал страницы; Summary — подвал отчета), а также высоту соответствующей полосы (строка Length справа от переключателя выбора). После выбора типа и высоты полосы она появляется в отчете, если окно закрыто кнопкой ОК или была нажата кнопка Applay. Элементы Print first page header и Print last page footer управляют соответственно печатью заголовка на первой странице отчета и подвала на его последней странице.
252 Глава 13 13.2.2. Методы Метод Назначение procedure NewColumn; Реализует вывод информации в следующей колонке отчета, а если определена единственная колонка - в его следующей странице. procedure NewPage; Реализует вывод информации в следующей странице отчета. procedure Prepare; Готовит отчет для вывода в файл (см. ниже примечание 1). procedure Preview; Выводит стандартное окно предварительного просмотра (см. ниже примечание 2). procedure Print; Печатает отчет на принтере. procedure PrintBackground; Инициирует печать отчета в фоновом режиме (в от- дельном потоке команд). После завершения печати вызывается обработчик события OnAfierPrint. procedure Printersetup; Вызывает стандартное окно установки параметров принтера. Примечание 1. Для вывода отчета в файл нужно сначала подготовить его с помощью обращения к методу Prepare, затем сохранить в файле методом Save объекта TQuickPer.QRPrinter, после чего уничтожить этот объект и поместить NIL в свойство TQuickPer. QRPrinter. MyReport.Prepare; MyReport.QRPrinter.Save('REPORT.QRP'); MyReport.QRPrinter.Free; MyReport.QRPrinter := NIL; Примечание 2. Стандартное окно предварительного просмотра показано на рис. 13.4. Рис. 13.4. Окно предварительного просмотра отчета.
Введение в построение отчетов 253 Чтобы на этапе конструирования просмотреть в окне предварительного просмотра содержимое отчета в том виде, как он будет выводиться на печать, нужно выбрать опцию Preview во вспомогательном меню компонента QuickRep. Следует заметить, что при этом не будут видны некоторые данные, например, значения вычисляемых полей. Они будут выводиться только во время выполнения. Назначение инструментальных кнопок окна: И н S3 ии о && Масштабирует отчет так, чтобы его страница полностью показывалась в окне. Отображает отчет в масштабе 1:1. Масштабирует отчет так, чтобы ширина страницы отчета соответствовала ширине окна. Показывает первую (последнюю) страницу отчета. Показывает предыдущую (следующую) страницу. Вызывает стандартное окно настройки принтера (печатает отчет). Сохраняет отчет в файле (загружает отчет из файла). 13.2.3. События Событие С чем связано property AfterPreview: TQRAfterPreviewEvent; Возникает в момент закрытия окна предварительного просмотра отчета. property AfterPrint: TQRAfterPrintEvent; Наступает после печати отчета или его подготовки к печати. property BeforePrint: TQRBe forePrintEvent; Наступает в момент начала генерации отчета (до выдачи окна предварительного просмотра отчета или до его печати). property OnEndPage: procedure (Sender : TObject); Возникает в момент подготовки к генерации последней страницы отчета. property OnNeedData: procedure (Sender: Tobject; var MoreData: boolean) Используется при создании отчета по данным, которые берутся не из НД, а из текстового файла, списка строк, массива и т. п. В параметре MoreData обработчик должен вернуть True, если источник данных еще не исчерпан. property OnPreview: procedure (Sender : TObject) Используется для связывания с отчетом нестандартного окна просмотра (см. ниже). property OnStartPage: procedure (Sender : TObject) ; Наступает в момент подготовки к генерации первой страницы отчета. Стандартное окно не всегда удобно. Во-первых, его невозможно изменить и, следовательно, русифицировать оперативные ярлычки-подсказки. Кроме того, оно не закрывается нажатием Esc и никак не реагирует на Enter, т. е. ведет себя не так, как большинство модальных диалоговых окон. В то же время в Delphi 4 и 5 включен компонент TQRPreview, предназначенный для создания нестандартных диалоговых окон, однако этот компонент не документирован и его использование вызывает определенные трудности. Чтобы отобразить отчет в нестандартном окне просмотра, нужно прежде всего поместить этот компонент на любой подходящий контейнер (на отдельную форму или нужных размеров панель). Поскольку в проекте, использующем компоненты
254 Глава 13 QuickReport, обычно имеется отдельная форма для размещения компонента-отчета TQuickRep, нестандартное окно можно установить на эту форму и сделать ее видимой в нужный момент (обычно окно-контейнер для TQuickRep не визуализируется). Далее в обработчике события TQuickRep. OnPreview следует написать такой код: procedure QRForm.QuickReplPreview(Sender: TObject); begin PrevForm.QRPreviewl.QRPrinter := TQRPrinter(Sender); if not PrevForm.Visible then PrevForm.Show end; Здесь PrevForm — форма с размещенным на ней компонентом QRPreview 1, a QRForm - форма-контейнер для QuickRepl. Проверку видимости формы PrevForm можно убрать, если заведомо известно, что соответствующее окно имеется на экране. Чтобы явное приведение типа TQRPrinter (Sender) стало возможным, необходима ссылка на модуль QRPmtr в предложении uses соответствующего модуля. На следующем рисунке показан один из возможных вариантов реализации нестандартного окна. Проем л р дикдменгл Enlei - печень. Esc вкход ^вп»яги5юсваена₽ fWTFTAR "Хнхголяб" Иффмацог осрысфммгы ло «м. (<Ж) ЭЫ-ЗОН СЧЕТ-ФАКТУРА Ы от 01.03.1999 Поставщик: ООО "ЦСК Ридас" Адрес : 117607, г.Москва, ул. Раменки, л.8, корп.1 Телефон : (095) 954-3044 (две линии), 919-6210, 919-6211, 952-2839 р/с 40702810600000000053 в КБ "Объединенный Транспортный Банк" к/с 30101810800000000365 БИК 044579365 ИНН 7729359619 Код по ОКОНХ 87100 ОКПО 47428886 Грузоотправитель и его адрес: Главный склад Грузополучатель и его адрес: Магазин. Дом технич.книги К платежно-расчетному документу 1»от Наименование товара 17 ! I I ЭЛЕКТРОНмВГоФЙС Эе-Т-й-той КАРЛТЫГИН~с7а./ нТзёкек/ Питер 2. !!! ЭЛЕКТРОННЫЙ ОФИС 98 2-й ток/ Н.Зехек/ Питер Итого к оплате: б 300,00 руб. Сумма прописью: шесть тысяч триста рублей 00 копеек Рис. 13.5. Нестандартное окно просмотра 13.3. КОМПОНЕНТ TQRBAND Компоненты TQRBand являются основными составными частями отчета и используются для размещения на них отображающих компонентов, таких как TQRLabel, TQRDBText, TQRImage, TQRDBImage и т. п.
Введение в построение отчетов 255 Свойства компонента: Свойство Назначение property AlignToBottom: Boolean; Если имеет значение True, полоса печатается непосредственно над подвалом страницы вместо обычного расположения справа/снизу от предыдущей полосы. type TQRBandType = (rbTitle, rbPageHeader, rbDetail, rbPageFooter, rbSummare, rbGroupHeader, rbGroopFooter, rbSubDetail, rbColumnHeader); property BandType : TQRBandType; Указывает назначение полосы: rbTitle - содержит заголовок отчета; rbPageHeader - содержит заголовок страницы (на первой странице печатается под rbTitle)', rbDetail - содержит информацию из НД; выводится всякий раз при переходе на новую запись в НД; эта полоса повторяется для всех записей DataSet, начиная с первой записи и заканчивав последней; позиционирование на первую запись и последовательный их перебор осуществляется компонентом TQuickRep автоматически; rbPageFooter - содержит подвал страницы; выводится в конце каждой страницы отчета после всех других полос; rbSummare - подвал отчета; выводится на последней странице отчета после всей иной информации, но перед подвалом последней страницы; rbGroupHeader - содержит заголовок группы; применяется при группировках информации в отчете и выводится всякий раз при выводе новой группы; rbGroopFooter - содержит подвал группы; выводится всякий раз при окончании вывода группы, после всех данных группы; rbSubDetail - содержит детальную информацию из подчиненного НД при выводе в отчете информации из двух или более наборов данных, связанных в приложении как главный-подчиненный; этот тип назначается полосе автоматически при размещении на форме компонента TQRSubDetail", rbColumnHeader - содержит заголовки столбцов; размещается на каждой странице отчета после заголовка страницы. property Enabled: Boolean; Разрешает/запрещает печать полосы. property ForceNewColumn: Boolean Если содержит True, полоса печатается в следующей колонке. property ForceNewPage: Boolean; Если содержит True, полоса печатается на новой странице. property HasChild: Boolean; Если содержит True, полоса имеет дочернюю полосу TChildBand. Установка True в это свойство автоматически создает в отчете дочернюю полосу.
256 Глава 13 События property AfterPrint: TQRAfterPrintEvent; и property BeforePrint: TQRBeforePrintEvent; наступают соответственно до и после печати полосы. Метод function AddPrintable(PrintableClass: TQRNewComponentClass): TQRPrintable; используется для вставки в полосу отображающего компонента в процессе прогона программы. Он автоматически устанавливает между полосами отношение собственности. Два следующих фрагмента выполняют одинаковую работу: with DetailBandl.AddPrintable(TQRLabel) do begin Size.Left := 20; Size.Top := 5; Caption := 'Новая полоса'; end; var aLabel : TQRLabel; begin aLabel := TQRLabel.Create(ReportForm); aLabel.Parent := DetailBandl; with aLabel do begin Size.Left := 20; Size.Top := 5; Size.Caption := 'Новая полоса'; end; end; 13.4. СОЗДАНИЕ ПРОСТЕЙШЕГО ОТЧЕТА Компоненты TQuickRep и TQRBand являются минимально достаточными для создания простого отчета, не содержащего внутри себя группировок информации. Пусть имеется таблица БД Rashod.DB, содержащая сведения об отпуске материалов со склада. В состав ТБД входят поля • N_RASH — уникальный номер события отпуска товара; • DEN - номер дня; • MES — номер месяца; • GOD - номер года; • TOVAR - наименование отпущенного товара; • POKUP — наименование покупателя; • KOL VO - количество единиц отпущенного товара. Заметим, что дата отпуска товара хранится в разбивке на день, год и месяц. Сделано так специально, с целью показать, как в отчетах используются выражения и вычисляемые поля. Создадим простейший отчет, состоящий из заголовка и сведений об отпуске товара. В отчет включаются все факты отпуска товара. Сортировка производится по номеру
Введение в построение отчетов 257 события отпуска товара. Для этого разместим на форме компонент ТТаЫе, свяжем его с таблицей Rashod.DB и откроем (Active — True). Разместим на форме компонент TQuickRep. Поместим в его свойство DataSet значение Tablel, назначив таким образом отчету НД, записи которого будут выводиться в отчете. Добавим в отчет компонент TQRBand. В его свойство BandType компонента QRBandl по умолчанию будет установлено значение rbTitle, то есть компонент QRBandl определяет заголовок отчета. Разместим на QRBandl компонент TQRLabel. Установим в свойство Caption этого компонента значение Отпуск товаров со склада и выберем в свойстве Font жирный наклонный шрифт высотой 16 пунктов. Вид формы отчета к этому моменту показан на рис. 13.5. Рис. 13.6. В отчете определен только его заголовок. Теперь разместим в отчете данные, соответствующие текущей записи таблицы Rashod. Для этого поместим в отчет новый компонент TQRBand (имя QRBand2) и установим в его свойство BandType значение rbDetail. Затем разместим на полосе QRBand2 шесть компонентов TQRDBText. Свяжем эти компоненты с полями НД - N RASH, TOVAR, KOLVO, DEN, MES, GOD. Для этого в свойство DataSet каждого компонента QRDBText установим значение Tablel, а в свойство DataField - имя соответствующего поля. Вид отчета к этому моменту показан на рис. 13.7. Рис. 13.7. Отчет с заголовком и группой детальной информации.
258 Глава 13 Для просмотра получившегося отчета щелкнем по нему правой кнопкой мыши и из всплывающего меню выберем элемент Preview. Окно предварительного просмотра отчета показано на рис. 13.8. Рис. 13.8. Содержимое отчета в окне предварительного просмотра. Чтобы окно предварительного просмотра открывалось при активизации формы, создадим такой обработчик события OnActivate формы: procedure TForml.FormActivate(Sender: TObject); begin QuickRepl.Preview; end; а чтобы после выхода из окна предварительного просмотра закрывалась бы и форма, на которой расположен отчет, используем такой обработчик события AfterPreview. procedure TForml.QuickReplAfterPreview(Sender: TObject); begin Forml.Close; end; 13.5. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TQREXPR Из рис. 13.7 видно, что в простейшем отчете выводится дата, составленная из трех полей - DEN, MES, GOD. Объединим значения из этих полей в одно значение, являющееся результатом вычисления выражения. Выражение в отчетах формируется при помощи компонента TQRExpr. Удалим из компонента QRBandl компоненты QRDBText4, QRDBText5 и QRDBText6, связанные с полями DEN, MES, GOD. Вместо них разместим в отчете компонент TQRExpr (имя QRExprl). Выражения в TQRExpr формируются с помощью специального редактора, который вызывается в окне инспектора объектов кнопкой в поле данных свойства Expression этого компонента (рис. 13.9).
Введение в построение отчетов 259 Рис. 13.9. Окно редактора формул компонента TQRExpr. В поле Enter expession можно ввести или отредактировать выражение, которое обычно состоит из имен полей НД, преобразующих функций и переменных, связанных операциями отношения. Имена полей НД добавляются в текущее положение курсора (поле Enter expession) с помощью вспомогательного окна, связанного с кнопкой Database field, функции - с помощью окна, связанного с кнопкой Function, а переменные — с кнопкой Variable. Нажмите кнопку Function, в левом окне выберите категорию Other (другие) и функцию STR в правом окне - эта функция преобразует числовое значение в строковое. Нажмите Continue, чтобы перейти к вводу параметров (рис. 13.10). Надпись над строкой ввода окна Expression Wizard напоминает о том, что выбранная нами функция имеет один числовой параметр. Рис. 13.10. Формирование части выражения.
260 Глава 13 Для его ввода нажмите кнопку справа от строки ввода - на экране вновь появится начальное окно редактора формул. Поскольку мы хотим преобразовывать в строку номер дня, нажмите кнопку Database field и выберите поле DEN в списке полей таблицы Tablel. Нажмите ОК, чтобы вставить поле, затем ОК, чтобы закрыть окно редактора формул, и еще раз ОК, чтобы завершить ввод параметра. В поле Enter expression будет сформирована часть формулы - STR(Tablel.DEN). На панели Insert at cursor position нажмем кнопку «+» и вручную введем разделитель ' . ' (рис. 13.11). Рис. 13.11. Создание части формулы выражения. Продолжите формировать выражение так, чтобы в конце концов оно приобрело такой вид: STR(Tablel.DEN) + '.' + STR(Tablel.MES) + + STR(Tablel.GOD) (возможно, проще ввести его вручную). Затем нажмите кнопку ОК, чтобы закрыть окно редактора форму. С помощью Инспектора Объектов установите в свойство AutoSize компонента QRExprl значение False, измените размеры компонента так, чтобы он мог отображать примерно 10 символов, и установите выравнивание вправо (свойство Alignment = taRightJustify). Запустите режим предварительного просмотра содержимого отчета (рис. 13.12). Как видим, дата отпуска товара приобрела более привычный вид.
Введение в построение отчетов 261 Рис. 13.12. Результат вычисления выражения появился в отчете. Замечание. Другим способом составления значения даты из трех полей могло бы быть создание вычисляемого поля (например, SumData) и определение алгоритма вычисления его значения в таком обработчике события OnCalcFields: procedure TForml.TablelCalcFields(DataSet: TDataSet); begin TablelSumData.Value := TablelDEN.AsString + + TablelMES.AsString + + TablelGOD.AsString; end; 13.6. ИСПОЛЬЗОВАНИЕ TQRBAND ДЛЯ ПРЕДСТАВЛЕНИЯ ЗАГОЛОВКОВ СТОЛБЦОВ Компонент TQRBand, у которого в свойство BandType установлено значение rbColumnHeader, используется для размещения заголовков столбцов. Собственно заголовки столбцов формируются при помощи компонентов TQRLabel. В рассмотренном в предыдущих разделах отчете разместим компонент TQRBand (имя QRBandT) и установим в его свойство BandType значение rbColumnHeader. На полосе QRBand3 разместим четыре компонента TQRLabel (имена QRLabel2 ... QRLabel5') и установим в свойства Caption этих компонентов соответственно значения №№, Товар, Количество, Дата. В свойствах Font компонентов выберем наклонный и подчеркнутый шрифт. Вызовем окно предварительного просмотра отчета — для каждой страницы отчета теперь будут выводиться названия столбцов (рис. 13.13).
262 Глава 13 Рис. 13.13. В отчете появились заголовки столбцов. 13.7. ИСПОЛЬЗОВАНИЕ TQRBAND ДЛЯ ПОКАЗА ЗАГОЛОВКА И ПОДВАЛА СТРАНИЦЫ Компонент TQRBand, у которого в свойство BandType установлено значение rbPageHeader, используется для показа заголовка страницы, а если в это свойство установлено значение rbPageFooter — для показа подвала страницы. Заголовок выводится в начале каждой страницы, а подвал - в ее конце. Информация в заголовке и подвале страницы может формироваться на основе статического текста (компоненты TQRLabeT), значений полей (компоненты TQRDBText) и результатов вычисления выражений (компоненты TQRExpr). Вернувшись к предыдущему примеру, разместим в отчете компонент TQRBand (имя QRBand4) и установим в его свойство BandType значение rbPageHeader. Не будем размещать в заголовке страницы никакого текста, просто отчеркнем линию вверху страницы. Для этого установим в свойство компонента заголовка страницы Frame.DrawTop значение True, что обеспечивает вывод линии по верхнему краю области, занимаемой компонентом. Аналогичным образом определим в отчете компонент подвала страницы (имя QRBandS) и установим в его свойство Frame.DrawBottom значение True, что обеспечивает вывод линии по нижнему краю области, занимаемой компонентом. Войдя в режим предварительного просмотра, увцдим, что вверху и внизу каждой страницы отчета выводятся линии. 13.8. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TQRSYSDATA Компонент TQRSysData используется для показа вспомогательной и системной информации. Вид показываемой информации определяется свойством property Data: TQRSysDataType;
Введение в построение отчетов 263 Ниже указаны возможные значения этого свойства. Значение Что выводится qrsColumnNo Номер текущей колонки отчета (для одноколончатого отчета всегда 1). qrsDate Текущая дата. qrsDateTime Текущие дата и время. qrsDetailCount Количество записей в НД, а при использовании нескольких НД - количество записей в главном наборе. Для случая, когда НД представлен компонентом TQuery, эта возможность может быть недоступной, что связано с характером работы компонента TQuery, который возвращает столько записей, сколько необходимо для использования в текущий момент, а остальные предоставляет по мере надобности. qrsDetailNo Номер текущей записи в НД. qrsPageNumber Номер текущей страницы отчета. qrsPageCount Общее количество страниц отчета. qrsReportTitle Заголовок отчета. qrsTime Т екущее время. Разместим в компоненте QRBand5 подвала отчета два компонента TQRSysData. В свойство Data первого из них установим значение qrsDate (текущая дата), второго - qrsPageNumber (номер текущей страницы отчета). В режиме предварительного просмотра увидим, что теперь в подвале каждой страницы выводятся номер страницы и текущая дата (рис. 13.14). НПЕ31 ш| к| « | ► | н| g|ff| И|с?||Г 16 Сахар 700 26 1 1997 17 Соус томатный 100 27 1 1997 16 Крупа манная 300 28.1.1997 19 Крупа манная 200 29 1 1997 20 Сахар 1000 291 1997 22 Крупа манная 200 1.21997 23 Крупа манная 400 2.21997 24 Шпроты 300 3.2 1997 Стр. 1 26/09/97 Рис. 13,14. Показ номера страницы и текущей даты в подвале страницы. 13.9. ГРУППИРОВКИ ДАННЫХ Для группировки информации используется компонент TQRGroup. Его свойство Expression указывает некоторое выражение, которое и используется для группировки,
264 Глава 13 иными словами, в группу входят записи НД, удовлетворяющие условию этого выражения. При смене выражения происходит смена группы. Для каждой группы выводятся ее заголовок и подвал. В качестве заголовка группы используется компонент TQRBand со значением свойства BandType, равным rbColumnHeader, а в качестве подвала - со значением rbGroupFooter. Свойство FooterBand компонента TQRGroup должно содержать ссылку на компонент подвала группы. В заголовке группы, как правило, выводится группирующее выражение, а в подвале группы - агрегированная информация: суммарные, средние и т. п. значения по группе в целом. Пример. Построим отчет о расходе товара со склада, в котором информация группируется по наименованию товара. Для этого определим набор данных отчета (компонент ТТаЫе, имя Tablel). Установим у НД текущим индекс по полю Tovar (в свойстве FieldlndexNames или IndexName). Разместим в отчете: • заголовок отчета - компонент TQRBand с именем QRBandl, свойство BandType — rbTitle', • заголовок столбцов - компонент TQRBand с именем QRBand2, свойство BandType = rbColumnHeader, • группу — компонент TQRGroup с именем QRGroupF, • область детальной информации - компонент TQRBand с именем QRBand3, свойство BandType = rbDetaib, • подвал группы - компонент TQRBand с именем QRBand4, свойство BandType = rbGroupFooter. В компоненте QRGroupl установим: • в свойство FooterBand значение QRBand4-, • в свойство Expression значение Tablel.tovar, которое является формулой и строится в редакторе формул. Поскольку свойство Expression не визуализирует значения выражения, необходимо разместить в группе компонент TQRExr (имя QRExrl) и определить значение его свойства Expression так, чтобы оно содержало Tablel. TOVAR. В компоненте подвала группы QRBand4 будем подсчитывать сумму по полю Kolvo (сумму отпущенного конкретного товара). Для этого разместим в подвале группы компонент TQRExr (имя QRExr2) и определим значение его свойства Expression так, чтобы оно содержало формулу SUM (Tablel. KOLVO). В группе детальной информации разместим компоненты TQRDBText, связанные с полями Рокир и Kolvo. Заполним другие области отчета статическим текстом, как это показано на рис. 13.15.
Введение в построение отчетов 265 Рис. 13.15. Макет отчета с группировкой по товару. На рис. 13.16 показан отчет в режиме предварительного просмотра. Рис. 13.16. Отчет с группировкой по товару в окне предварительного просмотра. На рис. 13.17. показан подвал одной из групп, в котором там выводится информация о суммарном расходе товара.
266 Глава 13 ofai ml н | < | ► | м [ э|бд| h|g»| !гдд?П -----------------------------:------------------------------------------------ Покупатель Количество Гермес 300 Гермес 300 Иванов и К ИЗО Роспродукг 1000 Гермес 200 Гермес 100 Роспродукг 100 Гермес 100 Итого по товару 2900 «1________________________________________________________I г/ РэдеЗ^З Рис. 13.17. Подвал группы с итоговым значением. 13.10. МНОЖЕСТВЕННАЯ ГРУППИРОВКА ДАННЫХ Часто внутри группы должны содержаться другие группы, например, по названию товара и внутри каждой группы - по покупателям. В этом случае внутри одной группы определяют другую группу посредством дополнительных компонентов TQRGroup. Пусть требуется представить в отчете сведения о расходе товаров со склада, группируя данные по товарам, а внутри группы - по покупателям. Установим текущий индекс в НД Tablel по полям Tovar, Рокир. Общий вид отчета на этапе конструирования приводится на рис. 13.18, а в окне предварительного просмотра - на рис. 13.19. Рас. 13.18. Макет отчета с вложенными группами.
Введение в построение отчетов 267 Рис. 13.19. Отчет с вложенными группами 13.11. ПОСТРОЕНИЕ ОТЧЕТА ГЛАВНЫЙ-ДЕТАЛЬНЫЙ Если необходимо построить отчет на основе более чем одной ТБД, можно поступить двумя способами: 1. с помощью компонента TQuery произвести соединение данных из нескольких таблиц БД в один НД, после чего определить в отчете нужные группировки; 2. создать в приложении по одному НД на каждую таблицу, соединить эти наборы между собой связью главный-детальный (используя свойства MasterSource, MasterFields набора данных) и применить в отчете компонент (или несколько компонентов) TQRSubDetail для вывода информации из детального НД (или группы детальных НД); для вывода информации из главного НД, как и в обычных отчетах, применяется компонент TQRBand, у которого в свойстве BandType установлено значение rbDetail. Построение отчета для первого случая осуществляется аналогично тому, как это описано выше. Построение отчета для второго случая имеет некоторые отличительные особенности. Рассмотрим второй способ. Компонент TQRSubDetail предназначен для показа в отчете информации из детального НД. Его свойство property DataSet: TDataSet; указывает имя детального НД, информация из которого будет выводиться в пространстве компонента TQRSubDetail. В остальном использование этого компонента аналогично использованию компонента TQRBand, у которого в свойство BandType установлено значение rbDetail. Пусть имеется таблица БД tovary.db, содержащая помимо прочих поле Tovar (название товара). Пусть также имеется таблица БД rashod . db, содержащая сведения об отпуске материалов со склада. В ее состав входят поля N_RASH (уникальный номер события отпуска товара), DEN (номер дня), MES (номер месяца), GOD (номер года),
268 Глава 13 TOVAR (наименование отпущенного товара), POKUP (наименование покупателя) и KOLVO (количество единиц отпущенного товара). Таблицы tovary . db и rashod . db находятся в отношении один-ко-многим, то есть одному товару может соответствовать более одного факта отпуска товара со склада. Разместим на форме компонент ТТаЫе (имя TovaryTable), ассоциированный с ТБД TOVARY.DB, и связанный с ним компонент TDataSource (имя DSTovaryTable). Разместим также еще один компонент ТТаЫе (имя RashodTable), ассоциированный с ТБД rashod.db, и установим между НД связь главный-детальный. Для этого установим в свойство RashodTable.MasterSource значение DS_TovaryTable, а в свойство RashodTable.MasterFields значение TOVAR (рис. 13.20). Рис. 13.20. Установка связи главный-детальный. Заметим, что после установления связей между НД в НД RashodTable текущим индексом должен быть индекс по полю Tovar (свойство RashodTable.IndexFieldNames). Приступим к разработке отчета. Определим заголовок отчета - компонент TQRBand с именем QRBandl, в свойство BandType которого установлено значение rbTitle. Установим в качестве основного НД отчета TovaryTable, указав QuickRepl.DataSet = TovaryTable. Разместим в отчете компонент типа TQRBand с именем QRBand2 и установим в его свойство BandType значение rbDetail. Этот компонент будет использоваться для отображения детальной информации из НД TovaryTable. Разместим в области компонента QRBand2 компонент TQRDBText (имя QRDBTextl) и свяжем его с полем Tovar НД TovaryTable. Разместим в отчете компонент TQRSubDetail (имя QRSubDetaill). Установим в его свойство DataSet значение RashodTable, связав таким образом данный компонент с подчиненным НД. Разместим в области компонента QRSubDetaill три компонента TQRDBText и свяжем их соответственно с полями Pokup, Kolvo и D НД RashodTable. (поле D определено в НД RashodTable как вычисляемое по значениям полей DEN, MES, GOD). Разместим в области компонента QRBand2 заголовки столбцов.
Введение в построение отчетов 269 Вид формы отчета показан на рис. 13.21. Рис. 13.21. Макет отчета, в котором показываются записи из связанных наборов данных. В результирующем отчете (рис. 13.22) для каждой записи НД TovaryTable выводятся подчиненные ей записи из НД RashodTable. Рис. 13.22. Отчет, в котором показываются записи из связанных наборов данных. Замечание. Если необходимо определить заголовок и подвал для информации, группируемой в компоненте TQRSubDetail, следует воспользоваться свойством property Bands: TQRSubDetailGroupBands; этого компонента, которое имеет два логических подсвойства (HasHeader и HasFooter), указывающих на наличие или отсутствие соответственно заголовка и подвала.
270 Глава 13 13.12. ПОСТРОЕНИЕ КОМПОЗИТНОГО ОТЧЕТА Композитный (составной, сложный) отчет объединяет в себе несколько простых отчетов. При печати композитного отчета входящие в его состав простые отчеты печатаются друг за другом. Композитный отчет реализуется при помощи компонента TQRCompositeReport. В его обработчике события OnAddReport ранее определенные простые отчеты добавляются в списковое свойство Report. Например, так: procedure TCompozitnyjOtchet.QRCompositeReportlAddReports(Sender: TObject); begin with QRCompositeReportl do begin Reports.Add(ManyGroup.QuickRepl); Reports.Add(Prostoj.QuickRepl); end; //with end; B этом примере композитный отчет составляется из двух отчетов: QuickRepl (определенный в форме ManyGroup') и QuickRepl (определенный в форме Prostoj). Печать композитного отчета или его предварительный просмотр осуществляется так же, как для простых отчетов, например, QRCompositeReportl.Preview; На рис. 13.23 показан композитный отчет, построенный из двух ранее разработанных нами отчетов - отчета без группировок и отчета с множественными группировками по товару и покупателю. а|иУю н| < | ► | н| а|а| н|е»|[Г Page5of 7 Рис. 13.23. Композитный отчет, составленный из двух простых отчетов.
Введение в построение отчетов 271 13.13. ДРУГИЕ ВОЗМОЖНОСТИ 13.13.1. Использование дочерних полос Иногда пространство основной полосы TQRBand требуется разделить на ряд следующих друг за другом более узких областей, так как основная полоса не может делиться на части и, если она не умещается целиком на печатной странице, ее печать переносится на следующую страницу. Для размещения в отчете всей необходимой информации (например, при печати крупных изображений) в этом случае используется одна или несколько дочерних полос TQRChildBand. Дочерняя полоса следует непосредственно за основной, но при необходимости может переноситься на следующую страницу. Заметим, что для создания дочерней полосы не следует размещать на форме компонент TQRChildBand (страница QReport палитры компонентов Delphi). Вместо этого используется свойство HasChild основной полосы: после установки в это свойство значения True автоматически создается дочерняя полоса и между ней и основной полосой устанавливаются необходимые связи. Таким же образом вновь созданная дочерняя полоса может быть связана со своей дочерней полосой, а та - со своей и т. д. - количество следующих друг за другом дочерних полос не ограничено. Использование дочерней полосы рассмотрим на примере отчета, созданного по данным демонстрационной таблицы animals.db, в которую входит поле BMP с изображениями животных. Эта таблица включается в комплект поставки Delphi и автоматически связывается с псевдонимом DBDEMOS (каталог размещения по умолчанию c:\Program Files\Common Files\Borland SharedXData). Положите на пустую форму компонент ТТаЫе и свяжите его с таблицей ANIMALS. DB (DatabaseName = DBDEMOS, TableName = Animals.db). Разместите на форме также отчет TQuickRep с полосами: заголовка QRBandl (BandType = rbTitle), заголовков столбцов QRBand2 (BandType = rbColumnHeader) и детальной информации QRBand3 (BandType = rbDetail). На полосе заголовка поместите компонент TQRLabel, привязав его к полосе (AlignToBane = True) и разместив его в центре (Alignment = taCenter). Выберите для него крупный (24 пункта) жирный шрифт и напишите в Caption заголовок отчета — Животные. В полосе заголовков колонок поместите три заголовка - Название, Область обитания, Размер, а в полосе детальной информации - три компонента TQRDBText, связав их с полями Name, Area и Size соответственно. Для полосы детальной информации QRBand3 в свойство HasChild поместите значение True — ниже полосы будет автоматически вставлена новая полоса ChildBandl. Установите для нее высоту (Size.Height) 120 и ширину (Size. Width) 190 мм. Положите на нее компонент TQRDBImage и свяжите его с полем BMP таблицы Tablel. Его размеры выберите совпадающими с размерами дочерней полосы и установите в свойство Stretch значение True, чтобы изображение заняло все размеры компонента. Вид отчета в окне предварительного просмотра показан на рис. 13.24.
272 Глава 13 Если вы полистаете отчет, то заметите, что на границах некоторых страниц изображение отделяется от текста. Если изменить отчет так, чтобы компонент TQRDBImage располагался на основной полосе QRBand3 непосредственно ниже компонентов RQRDBText, такого разрыва не будет. 13.13.2. Компонент TQRStringsBand Компонент TQRStringsBand во всем подобен основному строительному компоненту отчета TQRBand и отличается от него лишь наличием дополнительного свойства Items типа TStrings. В списке Items программист может разместить произвольные текстовые строки (как на этапе конструирования, так и в ходе прогона программы), которые станут видны в отчете, если на полосу положен компонент TQRExpr, свойство которого Expression ссылается на имя строковой полосы. Таким образом, полоса TQRStringsBand в общем случае может выводить информацию не только из НД, но и из произвольных текстовых файлов. Заметим, что для каждой строки списка Items формируется отдельная полоса TQRStringsBand. Применение полосы проиллюстрируем следующим примером, в котором отчет содержит исходный текст проекта программы (рис. 13.25).
Введение в построение отчетов 273 Рис. 13.25. Полосы TQRStringsBand содержат строки текстового файла. Разместите на пустой форме компонент TQuickRep и поместите в отчет компонент TQRStringsBand, а на строковую полосу - компонент TQRExpr. В свойство QRExprl.Expression поместите имя строковой полосы QRStringsBandl (это имя придется вводить вручную либо в окне Инспектора Объектов, либо в поле Enter expression построителя формул). Напишите такой обработчик события OnCreate для формы (в этом обработчике в список Items строковой полосы загружается файл проекта программы): procedure TForml.FormCreate(Sender: TObject); begin with QRStringsBandl do Items.LoadFromFile(copy( Application.ExeName,1,pos('.’,Application.ExeName))+'dpr') end; Чтобы увидеть динамически загруженные строки, необходимо в работающей программе обратиться к методу TQuickRep.Preview. Разместите на форме кнопку или элемент меню, чтобы с его помощью вызвать этот метод. Например: procedure TForml.ButtonlClick(Sender: TObject); begin QuickRepl.Preview end;
Глава 14 ВВЕДЕНИЕ В РАЗРАБОТКУ СИСТЕМ ПРИНЯТИЯ РЕШЕНИЙ 14.1. ПОНЯТИЕ МНОГОМЕРНЫХ ДАННЫХ • В процессе принятия решений приходится анализировать многофакторную информацию о предметной области. Пусть, к примеру, организация занимается продажами товаров. При закупках товаров сотрудники организации должны знать, насколько устойчив спрос на товары, каковы темпы роста спроса - по товару, по региону, по конкретным организациям, каковы расходы, прибыль и многое другое. Ситуация, когда аналитику приходится иметь дело с многофакторными данными, осложняется тем, что человек, как правило, может одновременно оперировать не более чем тремя-четырьмя сущностями, учитывая при этом связи между ними. С увеличением числа сущностей (факторов) эффективность обработки информаций человеком резко падает. Математики давно оперируют многомерными пространствами, однако в области практических применений наша способность оперировать многомерными данными, к сожалению, является ограниченной. При возникновении необходимости обработки многофакторной информации применяют или многомерные базы данных (они остаются за рамками нашего рассмотрения), или в реляционных СУБД реализуют такие механизмы организации данных и доступа к ним, которые резко повышают эффективность анализа многофакторных данных. Такие механизмы есть и в Delphi, их обзору посвящена эта глава. 14.2. МЕТАКУБ (МНОГОМЕРНЫЙ КУБ) Сущность подхода, при помощи которого Delphi позволяет оперировать многомерными данными, состоит в следующем. Данные представляются в виде так называемого метакуба (или многомерного куба), где каждому фактору соответствует двое измерение. Для примера на рис. 14.1 приведен такой куб, в котором представлена информация по продажам. Одно измерение соответствует городам, в которых осуществлялись продажи; второе - фирмам, которым продавался товар, третье - временным периодам. В конкретной ячейке, как правило, представляются агрегированные данные - сумма, среднее, максимальное значение - или новые многомерные данные (кубы). Пример такого рода куба, содержащего информацию об отпусках товара по конкретной фирме из некоторого города для заданного временного отрезка, представлен на рис. 14.2.
Введение в разработку систем принятия решений 275 Январь Февраль . .. Декабрь Рис. 14.1. Данные в трехмерном кубе. Январь Февраль . . . Декабрь Рис. 14.2. Ячейка метакуба, в свою очередь, может быть метакубом.
276 Глава 14 14.3. ДИНАМИЧЕСКАЯ ФИКСАЦИЯ КОЛИЧЕСТВА ИЗМЕРЕНИЙ При помощи обсуждайлых ниже компонентов Delphi в источнике данных фиксируются поля-измерения метакуба и поля, по которым должно производиться агрегирование данных (суммирование, подсчет среднего и т. д.). Затем определяется, какие измерения показываются как столбцы, какие - как строки. После этого пользователю предоставляется таблица многомерных данных. В форме приложения может быть расположен план метакуба, то есть список измерений, в котором каждому измерению соответствует своя кнопка. Нажимая нужную кнопку, пользователь активизирует показ данных по тому или иному измерению куба. На рис. 14.3. показана информация из пятимерного куба. Пользователь зафиксировал текущие измерения - «Покупатель», «Месяц», «Тип товара». В ячейках показывается суммарный отпуск товара. Рис. 14.3. Сетка многомерной информации ~ компонент TDecisionGrid. На рис. 14.4. показан тот же куб, в котором зафиксированы три другие измерения - «Город», «Товар», «Месяц». В ячейках показывается суммарный отпуск товара.
Введение в разработку систем принятия решений 277 Рис. 14.4. Визуализированы 3 измерения. На рис. 14.5 показан тот же куб, в котором выбраны четыре измерения - «Город», « Покупатель», «Товар», «Месяц». В ячейках показывается суммарный отпуск товара. Рис. 14.5. Визуализированы четыре измерения.
278 Глава 14 I 14.4. ОБЗОР КОМПОНЕНТОВ DELPHI ДЛЯ РАЗРАБОТКИ СИСТЕМ ПРИНЯТИЯ РЕШЕНИЙ Компоненты для разработки систем принятия решений расположены в Delphi на странице Decision Cube. Компонент TDecisionQuery служит для определения НД, на основании которого затем будет создаваться многомерный куб. Компонент разработан специально для указанных целей, и поэтому его использование при разработке систем принятия решений является более предпочтительным, чем использование ТТаЫе и TQuery. Однако и эти компоненты могут применяться для хранения данных, на которых строится метакуб. Компонент TDecisionCube, собственно; и реализует многомерный куб данных. Он соединяется с НД при помощи свойства DataSet. Компонент TDecisionSource является аналогом компонента TDataSource, но адаптирован для работы с многомерными данными. Многомерный источник данных соединяется с компонентом TDecisionCube при помощи свойства DecisionCube. Компонент TDecisionPivot позволяет открывать и закрывать измерения метакуба. Для этого пользователю следует нажать (или отжать нажатую) кнопку, соответствующую конкретному измерению. Компонент связывается с TDecisionSource при помощи свойства DecisionSource. Применение этого компонента необязательно, поскольку компонент TDecisionGrid содержит собственные средства для открытия и закрытия данных по измерениям куба. Компонент TDecisionGrid в системах многомерных данных имеет то же функциональное предназначение, что и компонент TDBGrid при работе с обычными НД. В TDecisionGrid показываются данные из многомерного куба. Компонент связывается с TDecisionSource с помощью свойства DecisionSource. Компонент TDecisionGraph предназначен для показа графиков, источником которых служат многомерные данные. Этот источник указывается в свойстве DecisionSource компонента. 14.5. ПРИМЕР МНОГОМЕРНЫХ ДАННЫХ Пусть имеем три таблицы БД. Таблица tovary . db содержит сведения о товарах. В состав таблицы входят поля Tovar (наименование товара), TypTovara (тип товара), Ed_Izm (единица измерения товара) и Zena (цена за единицу измерения товара). Первичный ключ построен по полю Tovar. Таблица POKUP.db содержит сведения о покупателях товара. В ее состав входят поля Рокир (наименование покупателя) и Gorod (город, в котором расположена организация-покупатель). Первичный ключ таблицы построен по полю Рокир. Наконец, в таблице rashod. db содержатся сведения о расходе товара со склада. Она содержит поля N_Rash (уникальный номер расхода), Den (день), Mes (месяц), God (год даты расхода), Tovar (наименование товара), Рокир (наименование покупателя) и Kolvo (количество единиц отпущенного товара). Первичный ключ построен по полю N_Rash.
Введение в разработку систем принятия решений Т19 Таблица находится в отношении многие-к-одному с таблицами tovary . db и pokup . db. Для реализации целостности построены внешние индексы по полям Tovary и Pokup. Необходимо построить многомерный куб для представления общей и средней суммы расхода по измерениям товар, город, покупатель, тип товара и месяц из даты отпуска. 14.6. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONQUERY Разместим на форме компонент TDecisionQuery с именем DecisionQueryl. Установим в его свойство Database псевдоним базы данных (в нашем случае это locskld), а в свойстве SQL определим оператор SELECT, в котором произведем внутреннее соединение таблиц Rashod, Tovary и Pokup-. SELECT P.GOROD, R.POKUP, T.TYP_TOVARA, R.TOVAR, R.MES, SUM(R.KOLVO * T.ZENA), AVG(R.KOLVO * T.ZENA) FROM "Rashod.DB" R, "Pokup.DB" P, "Tovary.DB" T WHERE R.POKUP= P.POKUP AND T.TOVAR = R.TOVAR GROUP BY P.GOROD, R.POKUP, TYP_TOVARA, R.TOVAR, R.MES Существуют следующие правила объявления измерений в многомерном кубе: 1. поля, по которым должны строиться измерения многомерного куба, перечисляются после ключевого слова SELECT, 2. поля, по которым измерений строить не нужно, в операторе SELECT в качестве возвращаемых полей результирующего набора данных не перечисляются; 3. только после полей-источников для измерений куба перечисляются агрегатные выражения - сумма, среднее и число повторений; агрегированные данные затем будут выводиться по указанным ранее измерениям; 4. обязательно использование раздела GROUP BY, в котором необходимо перечислить все поля, по которым строятся измерения куба, причем в том же порядке, в котором они следуют после ключевого слова SELECT. Поясним приведенный выше оператор SELECT. Измерения будут построены по полям GOROD, POKUP, TYP TOVARA, TOVAR и MES. В качестве данных по измерениям будет также выводиться (по выбору) результат одной из следующих агрегаций: суммарная стоимость отпущенного товара (SUM) и его средняя стоимость (AVG). Данные берутся из таблиц RASHOD.DB (ее краткое имя - R), POKUP.db (Р) и TOVARY. DB (Т). Таблицы R и Р связаны по полю Pokup, а Т и R - по полю Tovar. Компонент TDecisionQuery похож на TQuery и не имеет отличных от него свойств, методов и событий. Установим значение True в его свойство Active. Теперь набор многомерных данных активен в нашем приложении. 14.7. РЕДАКТОР МНОГОМЕРНОГО ЗАПРОСА Отвлечемся на некоторое время от нашего примера и рассмотрим, как использовать еще одно средство компонента TDecisionQuery — редактор многомерного запроса - для построения 52^'опеРатоРа выборки многомерных данных. Щелкните по
280 Глава 14 компоненту TDecisionQuery правой кнопкой мыши и в локальном меню выберите опцию Decision Query Editor. Появится окно редактора, показанное на рис. 14.6. Рис. 14.6. Окно редактора многомерного запроса. В списке List of Available Fields перечислены поля таблицы БД, имя которой выбрано с помощью списка Table. Используя кнопки с изображением стрелок, поля, по которым должны строиться измерения в многомерном кубе, перемещаются в список Dimensions. Тем же способом в список Summaries перемещаются поля, по которым необходимо производить агрегацию. Тип агрегации запрашивается тут же. Это SUM (сумма), COUNT (счетчик повторений) и A VERAGE (среднее значение). На странице SQL Query можно просмотреть и, если необходимо, отредактировать текст ^/.-оператора, построенного на основе наших действий. Расположенная на этой странице кнопка Query Builder позволяет перейти в режим запроса по образцу {Query By Example) для построения оператора SELECT. 14.8. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONCUBE Вернемся к примеру. Разместим на форме компонент TDecisionCube с именем DecisionCube 1. В его свойство DataSet поместим имя набора данных DecisionQueryl. Определим свойства куба. Для этого отыщем в Инспекторе Объектов свойство DimensionMap и в поле данных этого свойства нажмем кнопку (...). Появится окно редактора многомерного куба (Decision Cube Editor). Оно показано на рис. 14.7.
Введение в разработку систем принятия решений 281 Рис. 14.7. Окно редактора многомерного куба. В списке Available Fields перечислены поля, по которым строятся измерения куба, и формулы для расчета агрегированных значений. Для текущего поля или формулы слева показываются параметры: • Display Name — метка, которая будет показываться для измерения в компонентах TDecisionGrid, TDecisionPivot, TDecisionGraph-, • Type - тип поля (Dimension, Sum, Average, Count)-, • Active Type — определяет показываемую информацию: As Needed (показывается, когда необходимо); Active (показывается всегда); Inactive (не показывается никогда); • Format — формат представления значений; • Grouping - группировка по году, кварталу, месяцу и отдельному значению; • Initial Value - начальное значение. Закладка Memory Control позволяет определить максимальное число измерений, сумм, ячеек (группа Cube Maximums) и показывать ли только заголовки данных во время разработки приложения для экономии времени и ресурсов компьютера (группа Designer Data Options). Рассмотрим основные свойства, методы и события компонента TDecisionCube. Свойство Назначение property Capacity: integer; Определяет максимальный размер буфера в байтах для хранения многомерного массива данных куба. В случае нехватки памяти возбуждается исключение ELowCapacityError.
282 Глава 14 property DataSet: TDataSet; Содержит имя НД, который указывает данные для представления в кубе. Рекомендуется в качестве НД использовать созданный специально для указанных целей компонент TDecisionQuery. property DimensionCount: Integer; Содержит текущее число измерений в кубе. property DimensionMap: TCubeDims; Определяет параметры компонента TDecision- Cube, в том числе количество и состав измерений куба; формат представления значений по конкретному измерению; метки, которые будут соответствовать каждому измерению в компонентах TDecisionGrid и TDecisionPivof, максимальное число измерений; состав показываемых значений. Установка указанных параметров осуществляется в редакторе куба (Decision Cube Editor). property DimensionMapCount: Integer; Содержит количество полей НД, которые участвуют в формировании многомерного куба. property MaxDimensions: Integer; Определяет максимальное количество измерений куба. property MaxSummaries: Integer; Содержит максимальное количество сумм для куба. property ShowProgressDialog: Boolean; Разрешает/запрещает показывать прогресс- индикатор во время формирования куба. property SununaryCount: Integer; Возвращает количество полей, использованных для формирования сумм. Метод procedure ShowCubeDialog; компонента в ходе прогона программы осуществляет вызов редактора куба, в котором могут быть изменены или переопределены различные свойства куба (см. свойство DimensionMap). Событие TCapacityErrorEvent = procedure(var Action: TErrorAction) of object; TErrorAction = (eaFail, eaContinue); property OnLowCapacity: TCapacityErrorEvent; наступает в случае, когда не хватает памяти буфера для хранения многомерных данных. Максимальный размер буфера устанавливается в свойстве Capacity. Событие TCubeRefreshEvent = procedure(DataCube: TCustomDataStore; DimMap: TCubeDims) of object; property OnRefresh: TCubeRefreshEvent; возникает перед изменением плана куба и связано с изменением параметров куба во время выполнения (см. метод ShowCubeDialog).
Введение в разработку систем принятия решений 283 14.9. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONSOURCE Компонент TDecisionSource выполняет в системах представления многомерных данных те же функции, что и компонент TDataSource в системах представления обычных данных, то есть служит источником данных для визуальных компонентов TDecisionGrid и TDecisionGraph. Свойства и события компонента описаны ниже. Некоторые из них связаны с компонентами TDecisionPivot и TDecisionGrid, которые будут рассмотрены в следующих разделах. Свойства компонента: Свойство Назначение property DecisionCube: TDecisionCube; Содержит имя компонента TDecisionCube. property SparseCols: Boolean; Разрешает/запрещает показывать столбцы с пустыми значениями. property SparseRows: Boolean; Разрешает/запрешает показывать строки с пустыми значениями. События: Событие С чем связано property OnBeginPivot: TNotifyEvent; Наступает перед изменением данных в кубе при нажатии (отжатии) кнопки, соответствующей измерению куба в компоненте TDecisionPivot, или при раскрытии/свертке данных по измерению в TDecisionGrid. property OnEndPivot: TNotifyEvent; Вызывается той же причиной, что и событие OnBeginPivot, однако наступает после изменения данных в кубе, но до отображения этих изменений в связанных с TDecisionSource компонентах. property OnLayoutChange: TNotifyEvent; Наступает при изменении схемы данных в кубе после события OnBeginPivot, но перед событием OnEndPivot и только в том случае, когда данные в кубе остаются теми же. При изменении состава измерений, которое сопровождается изменением данных в кубе, наступает событие OnNewDimensions. property OnNewDimensions: TNotifyEvent; Наступает после изменения данных в кубе. property OnStateChange: TNotifyEvent; Наступает после изменения свойств куба (компонент TDecisionCube). 14.10. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONGRID 14.10.1. Создание сетки многомерных данных Компонент TDecisionGrid служит для представления многомерных данных в виде таблицы, в которой измерениям соответствуют строки и столбцы. Ячейки компонента заполняются данными, полученными на основе расчета агрегатных функций.
284 Глава 14 Разместим в форме компонент TDecisionGrid с именем DecisionGridl. Установим в его свойство DecisionSource имя компонента DecisionSource 1. После этого произойдет визуализация многомерных данных (рис. 14.8). Рис. 14.8. Визуализация многомерных данных в сетке компонента TDecisionGrid. Вспомним описанный в п. 14.6 оператор SELECT, в котором определены измерения и агрегация данных в многомерном кубе: SELECT P.GOROD, R.POKUP, T.TYP_TOVARA, R.TOVAR, R.MES, SUM(R.KOLVO*T.ZENA), AVG(R.KOLVO*T.ZENA) FROM "Rashod.DB" R, "Pokup.DB" P, "Tovary.DB" T WHERE R.POKUP= P.POKUP AND T.TOVAR = R.TOVAR GROUP BY P.GOROD, R.POKUP, TYP_TOVARA, R.TOVAR, R.MES В кубе имеются измерения по полям GOROD, POKUP, TYP TOVARA, TOVAR, MES, а в качестве данных по измерениям используются результаты агрегатных функций SUM и A VG. Как видно из рис. 14.8, одни измерения показываются по горизонтали, построчно ( «Покупатель», «Тип товара», «Товар»), другие - вертикально, как столбцы («Город»), По умолчанию в ячейках показываются данные той агрегатной функции, которая в списке агрегатных функций в операторе SELECT указана первой. В нашем случае это суммарная стоимость отпущенного товара sum(r.kolvo*t. zena) . 14.10.2. Раскрытие и закрытие данных по измерениям средствами компонента TDecisionGrid Каждому измерению в сетке данных соответствует знак «+» или «-». Он указывается в ячейке, в которой содержится название предыдущего измерения.
Введение в разработку систем принятия решений 285 Например, знак для измерения «Тип товара» находится в ячейке, в которой показывается название предыдущего измерения - «Покупатель». Знак «+» свидетельствует о том, что информация по соответствующему измерению в сетке данных не показывается. Знак «-», наоборот, свидетельствует о том, что данные по измерению показываются. Если щелкнуть на значке мышью, значок переходит в противоположное состояние, при этом данные по измерению прячутся, если они показывались в сетке, и наоборот. Важно помнить, что сворачивание данных по какому-либо измерению ведет к одновременному сворачиванию данных по всем измерениям, расположенным правее от него. На рис. 14.9 показан результат сворачивания данных по измерению «Тип товара». Как видно из рисунка, скрыты не только данные по измерению «Тип товара», но и по расположенному справа от него измерению «Товар». Рис. 14.9. Результат сворачивания данных по измерению «Тип товара». 14.10.3. Показ промежуточных сумм Как видно из рис. 14.8, для каждого значения данных по измерению показываются промежуточные суммы. Их показ можно устранить. Для этого нужно щелкнуть по компоненту TDecisionGrid правой кнопкой мыши и во вспомогательном меню выбрать Subtotals on/off. Повторный выбор данного элемента меню приведет к включению промежуточных сумм в сетку. Подобного же эффекта, но для конкретного измерения можно добиться, если выбрать мышью ячейку заголовка конкретного измерения и нажать правую кнопку. Выбор элемента Display Data and Subtotals приводит к показу промежуточных сумм. Выбор элемента Display Data Only устраняет показ промежуточных сумм для измерения. Те же действия осуществимы и для конкретного значения по измерению. Если щелкнуть правой кнопкой мыши по ячейке данных, соответствующих нужному
286 Глава 14 измерению, то появится меню, в котором к элементам Display Data and Subtotals и Display Data Only добавится элемент Display Subtotals Only. Выбор этого элемента приводит к показу только промежуточных сумм. Установка одного из названных режимов показа относится не к измерению в целом, а к данным, касающимся того значения измерения, которое показывается в ячейке. Все возможности, о которых идет речь в данном подразделе, доступны как во время разработки приложения, так и во время его выполнения. 14.10.4. Исключение и включение измерения в состав показываемых в сетке данных То или иное измерение может быть исключено из сетки данных или вновь добавлено в нее как во время разработки, так и во время выполнения приложения. Выше рассмотрено использование значков «+» и «-». Однако сворачивание и раскрытие данных в случае использования этих значков затрагивает и соседние измерения, расположенные справа. Механизм, о котором пойдет речь в этом подразделе, позволяет исключать или вновь включать в сетку данные, соответствующие конкретному измерению, не затрагивая при этом данных по другим измерениям. Чтобы скрыть данные по измерению, можно воспользоваться двумя способами. Первый — щелкнуть правой кнопкой мыши по ячейке данных измерения, которое показывается построчно, и во вспомогательном меню выбрать элемент Drill in to this value. Второй способ — нажать правую кнопку мыши: • над заголовком измерения; • над пустой ячейкой в ряду ячеек сетки, расположенным над рядом, в котором показываются названия измерений; • над крайним левым (пустым) столбцом сетки и в списке измерений снять отметку с необходимого измерения. Чтобы включить измерение в состав показываемых, нужно воспользоваться вторым способом и установить отметку у необходимого измерения. 14.10.5. Установка свойств показа данных по отдельному измерению Свойство property Dimensions: TDisplayDims; представляет собой коллекцию объектов TDisplayDim, каждый из которых позволяет управлять представлением данных, соответствующих конкретному измерению. В свойстве property Count: Integer; класса TDisplayDims содержится количество измерений в TDecisionGrid, то есть количество объектов класса TDisplayDim в коллекции TDisplayDims, в то время как свойство property Items[Index: Integer]: TDisplayDim;
Введение в разработку систем принятия решений 287 открывает индексированный доступ к объектам TDisplayDim, каждый из которых соответствует серии данных по одному измерению. Рассмотрим свойства объекта TDisplayDim, то есть серии данных по конкретному измерению: Свойство Назначение property Alignment: TAlignment; Определяет выравнивание текста. property Color: TColor; Определяет цвет фона в ячейках. property DisplayName: String; Содержит текст заголовка. property FieldName: String; Указывает имя поля, по которому строится измерение. property Format: String; Определяет формат представления значений в ячейках. property Subtotals: Boolean; Разрешает/запрещает показывать значения промежуточных сумм по измерению. На этапе конструирования программы значения приведенных выше свойств можно установить, выбрав в Инспекторе Объектов свойство Dimensions. После этого на экране появится список измерений и агрегированных значений (рис. 14.10). Сделав текущим нужное измерение или агрегированное значение, с помощью окна инспектора объектов можно переопределить принятые по умолчанию значения его свойств. Рис. 14.10. Список измерений и агрегированных значений метакуба. 14.10.6. Свойства и события, управляющие поведением TDecisionGrid в целом Свойства: Свойство Назначение property CaptionColor: TColor; Содержит цвет фона для заголовков столбцов и строк.
288 Глава 14 property CaptionFont: TFont; Содержит шрифт для заголовков столбцов и строк. property Cells[ACol, ARow: Integer]: String; Возвращает в строковом виде содержимое ячейки на пересечении столбца ACol и строки ARow. property ColCount: Integer; Возвращает текущее количество столбцов. property DataColor: TColor; Определяет цвет фона во всех ячейках. property DataFont: TFont; Определяет шрифт во всех ячейках. property DataSumColor: TColor; Определяет цвет фона в ячейках, в которых показываются суммы. property Decisionsource: TDecisionSource; Содержит имя ассоциированного компонента TDecisionSource. property DefaultColWidth: Integer; Определяет умалчиваемую ширину (в пикселях) всех ячеек TDecisionGrid. Установка нового значения этого свойства приводит к изменению ширины сразу всех ячеек компонента. property DefaultRowHeight: Integer; Определяет умалчиваемую высоту (в пикселях) всех ячеек. property FixedCols:Integer; Возвращает количество столбцов, используемых для показа заголовков измерений и меток данных, то есть для показа служебной информации. property FixedRows: Integer; Возвращает количество строк, используемых для показа служебной информации. property GridLineColor: TColor; Определяет цвет линий, отделяющих ячейки друг от друга. property GridLineWidth: Integer; Определяет толщину линий (в пикселях). property LabelColor: TColor; Содержит цвет фона для заголовков столбцов и строк. property LabelFont: TFont; Определяет шрифт для заголовков столбцов и строк. property LabelSumColor: TColor; Задает цвет фона ячеек, в которых показываются заголовки сумм. type TDecisionGridOption = (cgGridLines, cgOutliner, cgPivotable); TDecisionGridOptions = set of TDecisionGridOption;. property Options: TDecisionGridOptions; Определяет параметры показа: cgGridLines - выводятся вертикальные и горизонтальные линии, отделяющие ячейки друг от друга; cgOutliner - заголовки измерений куба содержат кнопки «+» и «-», которые позволяют открывать или закрывать измерения без помощи компонента TDecisionPivot, cgPivotable - разрешается перетаскивать измерения способом Drag&Drop. property RowCount: Integer; Возвращает количество строк в TDecisionGrid. property Totals: Boolean; Разрешает/запрещает показывать значения подсумм по измерениям.
Введение в разработку систем принятия решений 289 События: Событие С чем связано type TGridDrawState = set of (gdSelected, gdFocused, gdFixed); TDecisionDrawState = set of (dsGroupStart, dsRowCaption, dsColCaption, dsSum, dsRowValue, dsColValue, dsData, dsOpenAfter, dsCloseAfter, dsRowIndicator, dsColIndicator, dsRowPlus, dsColPlus, dsNone); TDecisionDrawCellEvent = procedure (Sender: TObject; Col, Row: Longlnt; var Value: String; var AFont: TFont; var AColor: TColor; AState: TGridDrawState; ADrawState: TDecisionDrawState) of Object; property OnDecisionDrawCell: TDecisionDrawCellEvent; Наступает при прорисовке ячейки: Со/ - номер столбца; Row - номер строки; Value - символьное представление значения, показываемого в ячейке; AFont - шрифт, которым показывается значение в ячейке; AColor - цвет фона в ячейке; AState - состояние ячейки (gdSelected выбранная ячейка; gdFocused - ячейка с фокусом ввода; gdFixed - фиксированная ячейка); ADrawState - множество, определяющее вид показываемых в ячейке данных; dsGroupStart - ячейка - первая строка или первый столбец для измерения; в этом случае в множество входят также значения dsRowValue или dsCol- Value; dsRowCaption - в ячейке показывается заголовок строки; dsColCaption - в ячейке показывается заголовок столбца; dsSum - в ячейке показывается метка или значение промежуточной суммы; в этом случае в множество входят также значения dsRowValue, dsColValue и dsData; dsRowValue - в ячейке показывается одно из значений измерения (метка), а не собственно данные; название измерения в этом случае расположено в ячейке слева; dsColValue - в ячейке показывается одно из значений измерения (метка), а не собственно данные; название измерения в этом случае расположено в ячейке сверху; dsData - в ячейке показываются данные; dsOpenAfter — ячейка имеет кнопку «+», использующуюся для раскрытия данных по следующему измерению; в этом случае в множество входят также значения dsRowCaption, dsColCaption, dsRowPlus и dsColPlus; dsCloseAfter - ячейка имеет кнопку «-»; в этом случае в множество входят также значения dsRowCaption, dsColCaption, dsRowPlus и dsColPlus; dsRowIndicator - ячейка используется только для показа кнопок «+» или «-» и располагается в самом левом столбце TDecisionGrid; dsColIndicator - ячейка используется только для показа кнопок «+» или «-» и располагается в самой верхней строке; dsRowPlus - ячейка, определяемая dsRowIndicator, содержит кнопку «+»; появляется только совместно с dsRowIndicator, dsColPlus — ячейка,
290 Глава 14 определяемая dsColIndicator, содержит кнопку «+»; появляется только совместно с dsColIndicator, dsNone - у ячейки нет специального предназначения. type TDecisionExamineCellEvent = procedure (Sender: TObject; ICol, IRow: Longlnt; ISum: Integer; const ValueArray: TSmalllntArray) of Object; property OnDecisionExamineCell: TDecisionExamineCellEvent; Наступает, когда пользователь, находясь в ячейке данных, нажимает правую кнопку мыши: ICol - номер столбца ячейки; IRow - номер строки ячейки; ISum - номер текущей суммы; ValueArray - массив координат в - координатной сетке TDecisionSource (а не TDecisionGrid). Для получения имени измерения, соответствующего конкретному элементу массива, следует использовать метод GetDimensionName. property OnTopLeftChanged: TNotifyEvent; Наступает при прокрутке данных в сетке TDecisionGrid. Пример. В следующем обработчике события OnDecisionDrawCell красным фоном выделяются все ячейки, кроме текущей (в ней используется стандартный синий фон выбора), значения в которых больше 2000. Для тех полей области данных, которые содержат пустые значения, выводится 0. procedure TForml.DecisionGridlDecisionDrawCell(Sender: TObject; Col, Row: Integer; var Value: String; var aFont: TFont; var aColor: TColor; AState: TGridDrawState; aDrawState: TDecisionDrawState); begin if dsData in aDrawState then begin if Value = '1 then Value := 'O'; if not (gdFocused in AState) and (StrToInt(Value) > 2000) then AColor := clRed; end; //i f end; 14.11. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TDECISIONPIVOT 14.11.1. Создание переключателя активных измерений Компонент TDecisionPivot позволяет динамически определять текущие измерения куба, а также выбирать содержимое ячеек данных. Каждому измерению куба в компоненте соответствует своя кнопка. Когда кнопка нажата, измерение в составе куба является открытым, то есть данные, соответствующие измерению, показываются в кубе. Когда кнопка, соответствующая измерению, не нажата (отжата), данные, соответствующие измерению, в кубе не показываются. Агрегатным функциям соответствует отдельная кнопка.
Введение в разработку систем принятия решений 291 Разместим на форме компонент TDecisionPivot (имя DecisionPivotl). Установим в его свойство DecisionSource имя компонента DecisionSourcel. Общий вид окна программы на этапе конструирования показан на рис. 14.11. Рис. 14.11. Форма с компонентом TDecisionPivot. 14.11.2. Изменение способа показа данных по измерению Из рисунка 14.11 видно, что кнопки в компоненте TDecisionPivot размещены в трех областях. В левой области размещена кнопка «Стоимость (сумма)». Это кнопка предназначена для выбора агрегатных функций. По умолчанию в качестве текущей агрегатной функции берется первая из функций, определенных в операторе SELECT (компонент TDecisionQuery). Чтобы сменить агрегатную функцию, нужно нажать кнопку и из появившегося списка выбрать нужную функцию. Средняя область компонента TDecisionPivot содержит кнопки измерений, данные по которым показываются построчно. В нашем случае это кнопки, соответствующие измерениям «Тип товара», «Покупатель», «Товар», «Месяц». Правая часть компонента содержит кнопки измерений, данные по которым показываются в виде столбцов. В нашем случае это кнопка, соответствующая измерению «Город». Чтобы перевести кнопку из одной области в другую, следует щелкнуть по ней правой кнопкой мыши и во вспомогательном меню выбрать опцию Move to Row Area (при переводе измерения в построчный показ) или Move to Column Area (при переводе измерения в показ по столбцу).
292 Глава 14 14.11.3. Фиксация значения в измерении Обычно по измерению показываются все его данные. Например, по измерению « Покупатель» показываются все покупатели. Однако часто бывает необходимо показывать данные не по всем, а по конкретному покупателю. Для этого следует переместить указатель мыши на кнопку соответствующего измерения, нажать правую кнопку мыши и отметить опцию Drilled in во вспомогательном меню. Кнопка «Покупатель» изменит свой заголовок на «Покупатель. All Values». Теперь, чтобы зафиксировать конкретного покупателя, следует нажать правую кнопку мыши и в появившемся списке выбрать название конкретного покупателя. При этом его имя будет показываться в кнопке, соответствующей измерению «Покупатель», а данные в кубе будут сечением по координате измерения, соответствующей конкретному покупателю (рис. 14.12). Рис. 14.12. Сечение по конкретному покупателю ("База № 28"). Чтобы вновь показывать все значения по измерению (например, всех покупателей по измерению «Покупатель»), нужно во вспомогательном меню снять отметку с опции Drilled in. 14.11.4. Свойства, управляющие отображением компонента TDecisionPivot Свойство Назначение property ButtonAutoSize:' Boolean; Разрешает/запрещает автоматически согласовывать размеры кнопок, соответствующих измерениям куба в компоненте TDecisionPivot, с размерами самого компонента.
Введение в разработку систем принятия решений 293 property ButtonHeight: Integer; Определяет высоту (в пикселях) кнопок для случая, когда свойство ButtonAutoSize = False. property Buttonspacing: Integer; Определяет расстояние (в пикселях) между кнопками. property Buttonwidth: Integer; Определяет ширину (в пикселях) кнопок для случая, когда свойство ButtonAutoSize = False. property DecisionSource: TDecisionSource; Содержит имя компонента TDecisionSource. type TDecisionButtonPosition = (xtHo- rizontal, xtVertical, xtLeftTop); property GroupLayout: TDecisionButtonPosition; Определяет способ расположения кнопок измерений: xtHorizontal - горизонтальное; xtVertical ~ вертикальное; xtLeftTop - кнопки, соответствующие измерениям, которые выводятся в виде строк, помещаются слева внизу; кнопки, соответствующие измерениям- столбцам, - вверху и кнопки, соответствующие суммам, - слева вверху. type TDecisionPivotOption = (xtRows, xtColumns, xtSummaries); TDecisionPivotOptions = set of TDecisionPivotOption; property Groups: TDecisionPivotOptions; Определяет, какие кнопки входят в TDecision- Pivof. xtRows - кнопки измерений куба, которые выводятся как строки; xtColumns - кнопки измерений куба, которые выводятся как столбцы; xtSummaries - кнопки, соответствующие суммам. property GroupSpacing: Integer; Определяет расстояние (в пикселях) между группами кнопок, соответствующих измерениям по строкам, по столбцам и суммам.
Часть 2 СОЗДАНИЕ КЛИЕНТ/СЕРВЕРНЫХ ПРИЛОЖЕНИЙ
Глава 15 ВВЕДЕНИЕ В ТЕХНОЛОГИЮ КЛИЕНТ-СЕРВЕР 15.1. АРХИТЕКТУРЫ ФАЙЛ-СЕРВЕР И КЛИЕНТ-СЕРВЕР Эффективная работа современного офиса невозможна без использования сотрудниками общей базы данных. Общность БД достигается ее установкой в локальной сети предприятия на специально для этого выделенный компьютер (сервер) при одновременном доступе к серверу компьютеров пользователей (клиентов). До недавнего времени для организации коллективных БД использовалась, как правило, архитектура файл-сервер, в которой организация и управление БД целиком ложилась на клиентов, а сама БД представляла собой набор файлов в одном или нескольких каталогах на сетевом сервере. В ходе эксплуатации таких систем были выявлены общие недостатки архитектуры файл-сервер, которые состоят в следующем: • вся тяжесть вычислительной работы ложится на компьютер клиента; например, если в результате запроса клиент должен получить 2 записи из таблицы объемом 100 000 записей, все 100 000 записей будут скопированы с файл- сервера на клиентский компьютер; в результате возрастает загрузка сети (сетевой трафик) и увеличиваются требования к аппаратным мощностям пользовательского компьютера; заметим, что потребности в постоянном увеличении вычислительных мощностей клиентского компьютера обусловливаются постоянно возрастающим объемом накапливаемой и обрабатываемой информации; • поскольку БД представляет собой набор файлов на сетевом сервере, доступ к таблицам регулируется только сетевой операционной системой, что делает такую БД по сути беззащитной от случайного или намеренного искажения хранящейся в ней информации, уничтожения или хищения; • бизнес-правила в системах файл-сервер реализуются в программе клиента, что в принципе не исключает проектирование противоречащих бизнес-правил в различных программах; смысловая целостность информации при этом может нарушаться; • недостаточно развитый аппарат транзакций локальных СУБД служит потенциальным источником ошибок как при одновременном внесении изменений в одну и ту же запись, так и при реализации отката результатов серии объединенных по смыслу в единое целое операций над БД, когда некоторые из них завершились успешно, а некоторые - нет; это может нарушать ссылочную и смысловую целостность БД. Перечисленных недостатков во много лишены СУБД, построенные по архитектуре клиент-сервер, характерной особенностью которой является перенос вычислительной нагрузки на сервер БД (5£Д'сеРвеР) и максимальная разгрузка клиента от вычислительной работы, а также существенное укрепление безопасности данных - как от злонамеренных, так и просто ошибочных изменений. Как и в архитектуре файл-сервер, БД в этом случае помещается на сетевом сервере, однако программа клиента лишена возможности прямого доступа к БД. Доступ к БД
298 Глава 15 регулируется специальной программой - сервером БД (5б^’сеРвеРом)- Взаимодействие сервера БД и клиента реализуется с помощью З^’3311?0008» которые формирует и отсылает серверу клиент. Сервер, приняв Запрос, выполняет его и возвращает результат клиенту. В клиентском приложении в основном осуществляется интерпретация полученных от сервера данных, реализация пользовательского интерфейса, а также реализация части бизнес-правил. Преимущества архитектуры клиент-сервер: • большинство вычислительных процессов происходит на сервере, что снижает требования к вычислительным мощностям компьютера клиента; увеличение вычислительной мощности одного сервера эквивалентно одновременному увеличению мощности всех клиентских мест; • снижается сетевой трафик за счет посылки сервером клиенту только тех данных, которые он запрашивал; например, если необходимо сделать из таблицы объемом 100 000 записей выборку, результатом которой будут всего 2 записи, сервер выполнит запрос и перешлет клиенту только нужные ему 2 записи; • БД на сервере представляет собой, как правило, единый файл, в котором содержатся таблицы, ограничения целостности и другие компоненты БД; взломать, похитить или испортить такую БД значительно труднее; существенно увеличивается защищенность БД от ввода неправильных значений, поскольку сервер БД проводит автоматическую проверку соответствия вводимых значений наложенным ограничениям и автоматически выполняет необходимые бизнес-правила; кроме того, сервер отслеживает уровни доступа для каждого пользователя и блокирует попытки выполнения не разрешенных для пользователя действий; все это позволяет говорить о значительно более высоком уровне обеспечения безопасности БД, ссылочной и смысловой целостности информации; • сервер реализует управление транзакциями и предотвращает попытки одновременного изменения одних и тех же данных; различные уровни изоляции транзакций позволяют определить поведение сервера в этом случае; • безопасность системы возрастает за счет переноса большей части бизнес- правил на сервер; падает удельный вес противоречащих друг другу бизнес- правил в клиентских приложениях, выполняющих разные действия над БД; определить противоречивые бизнес-правила все еще можно, однако намного труднее их выполнить ввиду автоматического отслеживания сервером БД правильности данных. Для реализации архитектуры применяют так называемые промышленные серверы баз данных, такие как InterBase, Oracle, Informix, Sybase SQL Server, DB2, MS SQL Server. В дальнейшем примеры реализации архитектуры клиент-сервер будут приводиться для сервера InterBase. Объяснить такой выбор нетрудно. Во-первых, InterBase - «родной» сервер для Delphi и для доступа к нему не нужно устанавливать дополнительных драйверов. Во-вторых, в вариант поставки Delphi Enterprise входит InterBase 5.1 для Windows 32 на 5 пользователей, что делает его доступным для большинства читателей этой книги.
Введение в технологию клиент/сервер 299 15.2. SQL-СЕРВЕР INTERBASE И ЕГО ОСНОВНЫЕ КОМПОНЕНТЫ З^Т-сервер InterBase предназначен для хранения и обработки больших объемов информации в условиях одновременной работы с БД множества клиентских приложений. Масштаб информационной системы при этом произволен — от системы уровня рабочей группы (под управлением Novell Netware или Windows 32 на базе IBM- совместимых ПК) до системы уровня большого предприятия (на базе серверов IBM, Hewlett-Packard, SUN). Ниже рассматривается ряд технологий InterBase, использование которых обеспечивает максимальную вычислительную разгрузку клиентского приложения и гарантирует высокую безопасность и целостность информации. Для задания ссылочной и смысловой целостности в БД определяются: • отношения подчиненности между таблицами БД путем определения первичных (PRIMARY) ключей у родительских и внешних (FOREIGN) ключей у дочерних таблиц; • ограничения на значения отдельных столбцов (CONSTRAINT)', условия ограничений могут быть разнообразны — от требования удовлетворения вводимых значений определенному диапазону или соответствия некоторой маске до требуемого отношения с одной или несколькими записями из другой таблицы (или многих таблиц) БД; • триггеры (TRIGGER) - подпрограммы, автоматически выполняемые сервером до или (и) после события изменения записи в таблице БД; • генераторы (GENERATOR) для создания и использования уникальных значений нужных полей. Для ускорения работы клиентских приложений с удаленной БД могут быть определены хранимые процедуры (STORED PROCEDURE), которые представляют собой подпрограммы, принимающие и возвращающие параметры и способные выполнять запросы к БД, условные ветвления и циклическую обработку. Хранимые процедуры пишутся на специальном алгоритмическом языке. В них программируются часто повторяемые последовательности запросов к БД. Текст процедур хранится на сервере в откомпилированном виде. Преимущества в использовании хранимых процедур очевидны: • отпадает необходимость синтаксической проверки каждого запроса и его компиляции перед выполнением, что убыстряет выполнение запросов; • отпадает необходимость реализации в клиентской программе запросов, определенных в теле хранимых процедур; • увеличивается скорость обработки транзакций, т. к. вместо подчас длинного З^Т-запроса по сети передается относительно короткое обращение к хранимой процедуре. В составе записи БД могут определяться BLOB-поля (Binary Large Object — большой двоичный объект), предназначенные для хранения больших объемов данных в виде последовательности байтов. Таким образом могут храниться текстовые и графические документы, файлы мультимедиа, звуковые файлы и т. д. Интерпретация BLOB-поля выполняется в приложении, однако разработчик может определить так
300 Глава 15 называемые ДЮД-фильтры для автоматического преобразования содержимого BLOB- поля к другому виду. InterBase дает возможность использовать определяемые пользователем функции {User Defined Function, UDF), в которых могут реализовываться функциональности, отсутствующие в стандартных встроенных функциях InterBase (вычисление максимума, минимума, среднего значения, преобразование типов и приведение букв к заглавным). Например, в UDF можно реализовать извлечение из значения даты номера дня, года; определение длины символьного значения; усечение пробелов; разные математические алгоритмы и т. п. Функция пишется на любом алгоритмическом языке, позволяющем разрабатывать DLL (библиотеки динамического вызова), например, на Object Pascal. InterBase может посылать уведомления клиентским приложениям о наступлении какого-либо события {EVENT). Одновременно работающие приложения могут обмениваться сообщениями через сервер БД, вызывая хранимые процедуры, в которых реализована инициация нужного события. Для обеспечения быстроты выполнения запросов и снятия с клиентского приложения необходимости такие запросы выдавать в БД можно определить виртуальные таблицы (или просмотры, VIEW), в которых объединяются записи из одной или более таблиц, соответствующих некоторому условию. Работа с просмотром из клиентского приложения ничем не отличается от работы с обычной таблицей. Поддерживает просмотр сервер, реагируя на изменение данных в БД. Просмотры могут быть изменяемыми или не допускающими внесения в них изменений. Для доступа к БД используется утилита Windows Interactive SQL (WISQL). Она работает с БД напрямую через InterBase API, минуя BDE. С помощью WISQL можно писать любые запросы к серверу, будь то создание БД, таблиц, изменение структуры данных, извлечение данных из БД или их изменение, а также назначение прав доступа к информации для отдельных пользователей. Для управления ОДЛ-сервером в целом и отдельными БД в частности используется утилита InterBase Server Manager. С ее помощью можно определять параметры SQL- сервера, производить сохранение, восстановление БД, сборку «мусора», определять новых пользователей, их пароли и т. д. Для просмотра БД, работы с таблицами, индексами, доменами, ограничениями и др. могут использоваться утилиты Database Desktop (весьма ограниченно) и SQL Explorer. Для просмотра и анализа реальных процессов, происходящих на сервере при реализации пользовательского запроса, используется утилита SQL Monitor. 15.3. INTERBASE: НЕКОТОРЫЕ ТЕХНИЧЕСКИЕ ХАРАКТЕРИСТИКИ InterBase был разработан в начале 80-х годов группой разработчиков из американской корпорации DEC. В дальнейшем разработка данного продукта велась независимыми компаниями InterBase Software и впоследствии слившейся с ней Ashton- Tate. Borland приобрела права на InterBase у Ashton-Tate после слияния с нею. InterBase активно используется в государственном и военном секторах США, что, видимо, и стало преградой для его продвижения в Россию. Интерес к этому серверу возрос только в последнее время в связи с включением его локальной (а
Введение & технологию хлиент/сервер 301 начиная с Delphi 3, и 4-пользовательской) версии в состав Delphi Client/Server Suite и Delphi Enterprise. Внимание разработчиков БД и приложений InterBase привлек, во-первых, потому, что это «родной» продукт Borland (а средства разработки приложений этой компании давно зарекомендовали себя с положительной стороны), во-вторых, потому что InterBase весьма прост в установке, настройке и в администрировании по сравнению с другими 5^'сеРвеРами, и в-третьих, потому что он обладает прекрасными функциональными возможностями. Ниже приводятся некоторые технические характеристики сервера (К - 1024 байта). Характеристика Значение Максимальный размер одной БД Рекомендуется не выше 10 Гбайт. Однако известны случаи объема одной БД в 10-20 Гбайт. Максимальное количество таблиц в одной БД 65 536 Максимальное количество полей (столбцов) в одной таблице 1 000 Максимальное количество записей в одной таблице Не ограничено Максимальная длина записи 64 К (не считая полей BLOB) Максимальная длина поля 32 К (кроме полей BLOB) Максимальная длина поля BLOB Не ограничена Максимальное количество индексов в БД 65 536 Максимальное количество полей в индексе 16 Максимальное количество вложенностей SQL-запроса 16 Максимальный размер хранимой процедуры или триггера 48 К Максимальное количество UDF в базе данных Длина имени UDF - не более 31 символа, каждая UDF должна иметь уникальное имя, поэтому максимальное количество UDF ограничивается только требованием уникальности имени 15.4. ФИЗИЧЕСКАЯ ОРГАНИЗАЦИЯ БАЗЫ ДАННЫХ INTERBASE База данных InterBase состоит из последовательно, начиная с 0, пронумерованных страниц. Нулевая страница является служебной и содержит информацию, необходимую для соединения с БД. Размер страницы - 1 (по умолчанию), 2, 4 или 8 Кбайт. Он устанавливается при создании БД, но может быть изменен при сохранении и восстановлении базы. Одна страница читается сервером за один логический доступ к БД. Объем буфера ввода-вывода для операций чтения-записи определяется в количестве страниц (по умолчанию - 75). Если БД будет чаще читаться, объем буфера .следует увеличить. Если в нее будет чаще осуществляться запись, размер буфера можно уменьшить. В InterBase поддерживается многоверсионная структура записей. При изменении записи какой-либо транзакцией создается новая версия записи, куда помимо данных записывается номер транзакции и указатель на предыдущую версию записи. Старая версия помечается как измененная; ее указатель на следующую версию записи содержит ссылку на вновь созданную версию. Каждая стартующая транзакция
302 Глава 15 работает с последней версией записи, изменения для которой подтверждены. Таким образом, параллельно работающие с БД транзакции всегда используют разные версии записей, что позволяет снимать блокировки для клиентских приложений, одновременно работающих с одними и теми же данными в БД. Более подробно об этом рассказано в р^вделе, посвященном управлению транзакциями. При удалении записи она также физически не удаляется с диска, а помечается как удаленная до тех пор, пока не завершена хотя бы одна активная транзакция, использующая эту запись. InterBase располагает на одной странице БД версии одной записи таблицы БД. После удаления записей на странице образуются «дырки». При добавлении новой записи анализируется размер максимальной «дырки» и, если он меньше длины добавляемой записи, происходит компрессия страницы, в процессе которой «дырки» объединяются. Если освободившегося пространства не хватает для размещения новой записи, та записывается с новой страницы. Загрузка страницы считается нормальной в случае, если «дырки» занимают не более 20% объема страницы. Выделение страниц никак не оптимизировано. На отдельной служебной странице БД хранятся номера всех свободных страниц. При выделении страниц не предпринимается никаких действий по выделению непрерывных страниц для хранения записей одной таблицы БД, а выделяется первая страница в списке свободных. Если свободной страницы нет, добавляется новая в конец БД. Только в этом случае размер БД возрастает. Многоверсионная структура записей и неоптимальное выделение страниц ведет к высокой фрагментации БД и как следствие - к замедлению работы с ней. Поэтому необходимо периодически производить дефрагментацию БД. Дефрагментированная БД характеризуется расположением записей таблиц БД на непрерывных страницах и отсутствием «мусора». Под мусором понимаются версии записей, с которыми не работает никакая активная транзакция: многоверсионный механизм гарантирует, что вновь стартующая транзакция не будет работать с ранней версией записи, если имеются ее более поздние версии. Существует несколько способов проведения дефрагментации. Первый состоит в сохранении БД на дисковом носителе и последующем ее восстановлении из сделанной резервной копии с помощью утилиты InterBase Server Manager. Этот способ является предпочтительным, поскольку гарантирует сбор всего мусора (в момент сохранения и восстановления БД не должно быть активных подключений к БД со стороны иных пользователей и потому не может быть активных транзакций). Второй способ состоит в автоматическом сборе мусора. Интервал, через который происходит сборка мусора, измеряется в транзакциях и по умолчанию составляет 20 000 транзакций. Это значение может быть изменено с помощью InterBase Server Manager. Данный способ дефрагментации БД менее предпочтителен, поскольку удаляются только те версии записей, для которых нет активных транзакций. В результате могут быть удалены не все старые версии. При большом числе активных транзакций процесс сборки мусора может существенно замедлить их выполнение. Если на вашей машине установлен InterBase (локальный или многопользовательский), его старт происходит автоматически при загрузке
Введение в технологию клиент/сервер 303 операционной системы. Об этом сигнализирует значок I справа на нижней панели Windows 32. Щелкнув на этом значке правой кнопкой мыши, можно вызвать вспомогательное меню. Опция Startup Configuration этого меню позволяет просмотреть и переопределить стартовые установки InterBase. Опция Shutdown завершает работу SQZ-сервера. Опция Properties позволяет просматривать свойства InterBase и текущей сессии, например, число активных подключений и число используемых БД. На заметку. За работой сервера внимательно следит утилита InterBase Guardian - это именно ее пиктограмма видна на нижней панели Windows. Эта утилита осуществляет начальный запуск сервера а также его перезапуск, если по каким-либо причинам сервер «рухнул». Фактически восстановление работоспособности сервера происходит в доли секунды, так что многие пользователи даже, возможно, не заметят сбоя в его работе. 15.5. DELPHI И INTERBASE Delphi предназначена для создания программ, работающих как с локальными, так и с удаленными данными. В последнем случае с помощью Delphi создаются клиентские программы (забегая вперед, заметим, что в многозвенной архитектуре с помощью Delphi создаются также и серверы приложений - промежуточное звено между клиентом и SQZ-сервером). Создание программ в архитектуре клиент-сервер имеет следующую специфику: • не рекомендуется использовать компонент ТТаЫе, поскольку он требует передачи всех данных результата выполнения запроса к серверу; в отличие от этого компонент TQuery получает от сервера только ту их часть, которая должна быть визуализирована; • изменение записей БД следует производить не методами Insert, Edit, Delete, Post, Cancel, которые оперируют с одной (текущей) записью, а при помощи SQZ-операторов INSERT, UPDATE, DELETE, которые оперируют сразу множеством записей; • необходимо особое внимание уделять управлению транзакциями и в первую очередь - выбору адекватного потребностям программы уровня изоляции транзакций; • бизнес-правила, где это возможно, нужно переносить на сервер, разгружая от них клиентское приложение; • следует как можно чаще использовать хранимые процедуры, выполняющиеся быстрее обычных SQZ-запросов и уменьшающие загрузку сети; • следует везде, где это необходимо, явно вызывать методы TDatabase.StartTran- saction и TDatabase.Commit для старта и подтверждения транзакций; подтверждение единичных изменений БД неявно стартуемыми и завершаемыми (в режиме SQLPASSTHRU = SHARED AUTOCOMMIT) транзакциями ведет к возрастанию загрузки сети и, как следствие, к замедлению работы, часто весьма существенному;
304 Глава 15 • необходимо уделять существенное внимание оптимизации запросов к БД, особенно при чтении данных {SELECT), поскольку оптимально построенный запрос может выполняться в несколько раз быстрее и требовать меньшего количества ресурсов. Эти особенности разработки клиентского приложения, так же как и особенности организации серверных БД и доступа к ним, будут рассмотрены в следующих разделах. 15.6. ВОПРОСЫ СОЕДИНЕНИЯ С УДАЛЕННЫМ СЕРВЕРОМ При работе с локальным сервером достаточно установить псевдоним БД (утилита BDE Administrator) и затем использовать этот псевдоним в компонентах TDatabase и, может быть, ТТаЫе и TQuery (когда соединение с БД производится, минуя компонент TDatabase). При доступе к локальному серверу с помощью утилиты WISQL указывается путь к БД {InterBase работает с БД, используя собственный API, минуя BDE). При работе с удаленным (сетевым) сервером необходимо «прописать» его на компьютере, с которого происходит обращение к серверу. При использовании протокола TCP/IP'. 1) /P-адрес сервера и имя должны быть описаны в файле HOSTS, например: 10.12.0.41 spv 2) протокол доступа к InterBase должен быть описан в файле SERVICE: gds_db 3050/tcp Оба указанных файла находятся в каталоге WINDOWS.
Глава 16 ПРИМЕР СОЗДАНИЯ УДАЛЕННОЙ БД И ПРОГРАММЫ ДЛЯ РАБОТЫ С НЕЙ 16.1. ОПИСАНИЕ БД «УЧЕТ ТОВАРОВ НА СКЛАДЕ» На рис. 16.1 приведена модель БД, которая будет далее использоваться в примерах второй части книги. ТОВАР - - Название товара Ед_измерения |Цена_ед_измерения I РАСХОД-ТОВАРА Номер расхода___________ Дата_расхода Количество_расхода Название_товара (FK) Название_покупателя (FK) ПОКУПАТЕЛИ Название покупателя Город_покупателя Адрес_покупателя Рис. 16.1. IDEFlX-диаграмма базы данных примера.
306 Глава 16 Структура процессов, подлежащих учету в нашей БД, проста. Каждый день на склад поступают товары. Допустим, процесс прихода товаров и проблема их учета нас не интересуют. Например, сведения о приходе и количестве товара аккумулируются в другой БД. Нам необходимо лишь вести учет отпуска товаров со склада. При этом подразумевается, что, если товар отпускается, он на складе есть и как минимум в том количестве, которое нужно отпустить. Пусть контроль за соответствием остатков товара запросам на его продажу ведет кто-нибудь другой - не важно, программа или человек. Каждый товар имеет уникальное название (по нему построен первичный ключ) и обладает двумя атрибутами - единицей измерения и стоимостью за единицу измерения. Если один и тот же товар присутствует на складе в разных единицах измерения (например, сахар в мешках, фасованный в пакетах, фасованный в коробках и т. д.; огурцы в банках и огурцы развесные и пр.), для каждого такого случая один и тот же товар должен иметь семантически одинаковое, но синтаксически разное название, например, «Огурцы баночные», «Огурцы развесные». Другим вариантом является создание составного первичного ключа по столбцам НАЗВАНИЕ ТОВАРА и ЕД. измерения, чтобы иметь возможность указывать синтаксически одно название товара для разных случаев единиц измерения. Чтобы не усложнять задачу, условимся считать, что цена товара не меняется во времени. Договоримся также, что в течение одного и того же дня один и тот же покупатель может многократно получать со склада один и тот же товар. Таким образом, в таблице «Расход товара» нельзя создать уникальный первичный ключ по любому полю или комбинации полей, т. к. некоторые записи в ней могут дублироваться. Поэтому введем столбец номер расхода, который должен содержать некоторый уникальный номер, и построим на нем первичный ключа. Информация о покупателе не требует никаких оговорок за исключением того, что столбец город выделен нами из адреса для дальнейших упражнений по составлению запросов и использованию оператора SELECT. Первичным ключом в данном случае служит столбец название покупателя. Как видим, таблица «Расход товара» является дочерней таблицей для таблиц «Товары» и «Покупатели» и находится с ними в связи многие-к-одному. Поэтому между дочерней таблицей и каждой из ее родительских таблиц должна быть установлена ссылочная целостность. Для этого в таблице «Товары» определим внешний ключ по столбцу НАЗВАНИЕ товара, ссылающийся на первичный ключ таблицы «Товары», и внешний ключ по столбцу НАЗВАНИЕ покупателя, ссылающийся на первичный ключ таблицы «Покупатели». 16.2. СОЗДАНИЕ БД И ЕЕ ПСЕВДОНИМА Пусть БД будет находиться в каталоге D:\BOOK\IB_SKLAD (этот каталог следует предварительно создать) и иметь имя IBSKLAD.GDB. Фактически это имя файла, который будет создан сервером и в котором будут храниться таблицы, индексы и вся другая информация, относящаяся к БД. Файлы БД в InterBase имеют стандартное расширение GDB. Чтобы создать файл БД, воспользуемся утилитой InterBase Windows ISQL (WISQL). Эта утилита доступна с помощью выбора Пуск | Программы | InerBase Client 5.1 |
Пример создания удаленной БД и программы для работы с ней 307 InterBase Windows ISQL в главном меню Windows. Запустим WISQL и щелкнем по кнопке 3S на инструментальной панели. Рис. 16.2. Утилита WISQL - окно создания базы данных. В появившемся диалоговом окне (рис. 16.2) укажем полное имя БД (напомним, что к этому моменту уже должен существовать каталог размещения БД), имя пользователя SYSDBA, пароль masterkey (имя и пароль системного администратора для локального сервера InterBase), а также в окне DataBase Options - параметр DEFAULT CHARACTER SET WIN1251 (установка умалчиваемого набора символов WIN1251). С помощью BDE Administrator назначим вновь созданной БД псевдоним ib_skl (рис. 16.3). Обратите внимание: для правильной обработки русскоязычной текстовой информации в строковых столбцах с использованием набора символов WIN1251 и порядка сортировки PXW_CYRL следует выбрать драйвер Pdox ANSI Cyrillic (строка LANGDRIVER). Чтобы не ошибиться при вводе имени файла размещения БД, используйте диалоговое окно поиска, которое раскрывается кнопкой в строке SERVER NAME. Замените стандартное имя пользователя MYNAME (строка USER NAME) на имя системного администратора SYSDBA, все остальные параметры оставьте без изменения.
308 Глава 16 Рис. 16.3. Создание псевдонима базы данных ib_skl. 16.3. СОЗДАНИЕ ТАБЛИЦ БД Для создания таблиц БД вновь воспользуемся WISQL. Перед соединением с БД необходимо установить набор символов WIN1251 с помощью диалогового окна опции Session | Advanced Settings главного меню утилиты (рис. 16.4). Рис. 16.4. Утилита WISQL - выбор набора символов. Соединение с БД производится с помощью диалогового окна опции File | Connect to DataBase (это же окно появляется после щелчка по кнопке ®1). Для соединения необходимо указать имя БД, имя пользователя и пароль. Поэтапно введем в окне SQL Statement (рис. 16.5) и с помощью кнопки выполним следующие операторы:
Пример создания удаленной БД и программы для работы с ней 309 CREATE TABLE POKUPATELI( POKUP VARCHAR(20) NOT NULL COLLATE PXW_CYRL, GOROD VARCHAR(12) COLLATE PXW_CYRL, ADRES VARCHAR(20) COLLATE PXW_CYRL, PRIMARY KEY(POKUP) CREATE TABLE TOVARY( TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, ED_IZM VARCHAR(IO) NOT NULL COLLATE PXW_CYRL, ZENA INTEGER NOT NULL, PRIMARY KEY(TOVAR) TABLE RASHOD( N_RASH INTEGER NOT NULL, DAT_RASH DATE NOT NULL, KOLVO INTEGER NOT NULL, TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, POKUP VARCHAR(20) COLLATE PXW_CYRL, PRIMARY KEY(N_RASH), FOREIGN KEY(POKUP) REFERENCES POKUPATELI, FOREIGN KEY(TOVAR) REFERENCES TOVARY Puc. 16.5. Утилита WISQL - создание таблиц базы данных. Заметим, что синтаксис 52Л-запросов не чувствителен к расположению текста по строкам и к высоте букв. То, что всюду в книге для запросов используются заглавные
310 Глава 16 буквы, - дело вкуса авторов, а разбиение их на строки, на наш взгляд, повышает наглядность текста. 16.4. СОЗДАНИЕ ПРОГРАММЫ ДЛЯ ВВОДА ДАННЫХ Для ввода данных в таблицы БД, как и вообще для любых действий с нею, можно было бы использовать WISQL. Однако для этого пользователю нужно хорошо изучить как саму WISQL, так и язык структурированных запросов SQL. Чтобы избавить пользователя от лишних проблем, обычно создаются клиентские программы. Создадим такого рода программу и мы. 16.4.1. Соединение с БД из программы Соединение с серверной БД обычно осуществляется при помощи компонента TDatabase. Разместите на форме такой компонент с именем Databasel и дважды щелкните по нему, чтобы вызвать редактор свойств компонента (рис. 16.6). Раскройте список Alias пате и выберите в нем псевдоним ib skl нашей БД. В строке Name напишите in sklad, а в окне Parameter overrides - имя пользователя и пароль USER_NAME=SYSDBA1 PASSWORD=masterkey после чего снимите отметку с флажка Login Prompt. Заметим, что высота букв существенна только в пароле, поэтому mas ter key следует набирать строчными буквами. Кроме того, слева и справа от знаков равенства не может быть пробелов. Рис. 16.6. Установка свойств соединения с БД. 1 Формат строки USER_NAME используется только в версии 5.5, которая поставляется вместе с Delphi 5 в комплектации Enterprise или Professional. В предыдущих версиях InterBase эта строка имела такой вид: USER NAME (без знака подчеркивания).
311 Пример создания удаленной БД и программы для работы с ней , - - - - — J ’ • = = =а» = = •= : - - 3 . as Нажмите кнопку Ок. Теперь, поскольку в параметрах Databasel указаны имя пользователя и пароль, все компоненты-источники, ссылающиеся на имя in_sklad, будут соединяться с БД автоматически, без участия пользователя. Наоборот, в момент активизации компонентов, ссылающихся непосредственно на псевдоним ibskl, сервер потребует от пользователя ввести имя и пароль. Для соединения с БД установим значение True в свойство Connected. 16.4.2. Создание формы для ввода данных Для каждой из таблиц создадим НД. Для этого разместим на форме три компонента ТТаЫе с именами TovaryTable, PokupTable и RashodTable. В свойство DatabaseName каждого из них установим значение in_sklad, выбрав его из выпадающего списка псевдонимов, свяжем компоненты с нужными таблицами и откроем их. После этого разместим на форме по три компонента TDataSource и TDBGrid и соединим каждый компонент TDataSource с одним из компонентов ТТаЫе (свойство DataSet), а каждый из компонентов TDBGrid - с соответствующим компонентом TDataSource (свойство DataSource). Запустив программу (рис. 16.7), вы получите возможность вводить информацию в таблицы, ничего не зная об SQL и даже не подозревая о существовании сервера InterBase. Введите в таблицы данные, как это показано на рис. 16.7. Эти данные подобраны таким образом, чтобы в дальнейшем рассмотреть широкий спектр возможностей оператора SELECT. Самая объемная глава второй части книги, посвященная часто используемому SQZ-оператору SELECT, будет построена на использовании запросов к созданной нами учебной БД. Рис. 16.7. Форма ввода значений в таблицы БД «Расход со склада».
312 Глава 16 16.4.3. Создание триггеров для поддержания каскадных воздействий В рассматриваемом варианте БД соединения дочерних и родительских таблиц являются «жесткими». Напомним, что, если не указаны параметры ON DELETE, ON UPDATE, при реализации ссылочной целостности на уровне PRIMARY KEY - FOREIGN KEY нельзя изменять значение поля связи как в дочерней, так и в родительской таблице, если для данного значения первичного ключа существуют дочерние записи. При создании таблицы RASHOD мы ввели ограничения по внешнему ключу: TABLE RASHOD( FOREIGN KEY(POKUP) REFERENCES POKUPATELI, FOREIGN KEY(TOVAR) REFERENCES TOVARY); и не указали параметры ссылочной целостности ON DELETE и ON UPDATE, которые определяют способ изменения в дочерней таблице при изменении или удалении записи в родительской таблице. Таким образом, если в таблице TOVARY для какого- либо товара существует хотя бы одна запись в таблице RASHOD, изменение его названия будет блокировано. При попытке изменить в таблице TOVARY значение наименования товара, например, с «Сахар» на «Сахар кусковой» будет возбуждено исключение из-за нарушения жестких ограничений целостности (рис. 16.8). Рис. 16.8. Сообщение о нарушении ограничений ссылочной целостности. Нетрудно заметить, что такого рода жесткие ограничения ссылочной целостности делают работу с БД менее удобной. Более гибкий вариант связи - реализация каскадных воздействий, в ходе которых при изменении значения столбца TOVAR в таблице TOVARY или столбца POKUP таблицы POKUPATELI эти изменения должны быть отражены в дочерних записях таблицы RASHOD, а при удалении записи в таблицах TOVARY или POKUPATELI должны быть удалены соответствующие дочерние записи в таблице RASHOD. Для достижения этой цели нужно сначала удалить не устраивающие нас ограничения целостности и затем определить в БД триггеры, реализующие каскадные воздействия.
Пример создания удаленной БД и программы для работы с ней 313 Для удаления внешнего ключа FOREIGN KEY(TOVAR) REFERENCES TOVARY необходимо удалить определяемое этим ключом ограничение ссылочной целостности. Однако в явном виде, например, CONSTRAINTS TOV_RASH FOREIGN KEY(TOVAR) REFERENCES TOVARY ограничение не было определено. Поэтому необходимо установить системное имя, присвоенное InterBase данному ограничению внешнего ключа. Это системное имя INTEG32 можно увидеть в сообщении, выдаваемом при возбуждении исключения по нарушению жесткой ссылочной целостности (см. рис. 16.8). Следует убедиться, что INTEG32 действительно есть ограничение целостности, которое требуется удалить. Для этой цели необходимо воспользоваться утилитой SQL Explorer, найдя для таблицы RASHOD узел Refrential Constraints (рис. 16.9). Искомое нами ограничение действительно имеет имя INTEG_32. Удалим его. Для этого вызовем WISQL, свяжемся с БД и выполним оператор ALTER TABLE RASHOD DROP CONSTRAINT INTEG_32 Puc. 16.9. Узел Refrential Constraints-ограничения целостности для таблицы RASHOD. Для создания триггера, реализующего каскадное изменение в таблице RASHOD при изменении названия товара в таблице TOVARY, выполним оператор:
314 Глава 16 CREATE TRIGGER BU_TOVARY FOR TOVARY ACTIVE BEFORE UPDATE AS BEGIN IF (OLD.TOVAR <> NEW.TOVAR) THEN UPDATE RASHOD SET TOVAR = NEW.TOVAR WHERE TOVAR = OLD.TOVAR; END С помощью следующего оператора создается триггер, реализующий каскадное удаление дочерних записей в таблице RASHOD при удалении родительской записи в таблице TOVARY: CREATE TRIGGER AD_TOVARY FOR TOVARY ACTIVE AFTER DELETE AS BEGIN DELETE FROM RASHOD WHERE RASHOD.TOVAR = TOVARY.TOVAR; END Теперь при изменении наименования товара в таблице TOVARY произойдут каскадные изменения наименования товара в дочерних записях таблицы RASHOD (рис. 16.10 и 16.11). Рис. 16.10. До каскадного изменения.
Пример создания удаленной БД и программы для работы с ней 315 Рис. 16.11. После каскадного изменения. 16.6.4. Использование генератора и хранимой процедуры для присвоения уникального значения столбцу первичного индекса В таблице RASHOD по столбцу N_RASH (номер события расхода товара со склада) построен первичный ключ, поскольку никакие другие столбцы или их комбинации не могут уникально идентифицировать запись, т. к. один и тот же покупатель может в течение одной даты не только произвести несколько закупок одного и того же товара, но в двух или более таких закупках приобрести одинаковое количество одного и того же товара. Чтобы по столбцу N_RASH создать первичный ключ, он должен содержать уникальные значения. Для генерации уникальных числовых значений в InterBase используются генераторы. Определим генератор и установим 1 в его стартовое значение, выполнив в WISQL операторы CREATE GENERATOR RASHOD_N_RASH; И SET GENERATOR RASHOD_N_RASH TO 1; Затем определим для БД хранимую процедуру GET_N_RASH, которая помещает текущее значение генератора в возвращаемый целочисленный параметр NR-. CREATE PROCEDURE GET_N_RASH RETURNS (NR INTEGER) AS BEGIN NR = GEN_ID(RASHOD_N_RASH,1); END
316 Глава 16 В программе поместим на форму компонент StoredProcl для вызова хранимой процедуры GET_N_RASH. Свойству DatabaseName этого компонента присвоим значение in ski (значение свойства DatabaseName компонента Database 1), а свойству StoredProcName - значение GET N RASH (имя хранимой процедуры). Для компонента RashodTable, ассоциированного с таблицей RASHOD, определим обработчик события AfterInsert, наступающего немедленно после перевода Rashod- Table в состояние dslnsert. procedure TForml.RashodTableAfterlnsert(DataSet: TDataSet); begin StoredProcl.ExecProc; RashodTable.FieldByName('N_RASH').Value := StoredProcl.ParamByName('NR').Value; end; Замечание. Тот же код вместо обработчика события AfterInsert можно поместить в обработчик события OnNewRecord, наступающего также после перевода RashodTable в состояние dslnsert, обработчик этого события обычно используется для установки умалчиваемых значений в поля добавляемой записи до того, как значения полей будут визуализированы и станут доступны для изменения пользователем.
Глава 17 СОЗДАНИЕ БАЗЫ ДАННЫХ 17.1. ОПЕРАТОР CREATE DATABASE Для создания БД используется оператор SQL, имеющий следующий формат1: CREATE {DATABASE | SCHEMA} "<имя_файла>" [USER "имя_пользователя" [PASSWORD "пароль"]] [PAGE_SIZE [=] целое] [LENGTH [=] целое [PAGE[S]]] [DEFAULT CHARACTER SET набор_символов] [<вторичный_файл>]; <вторичный_файл> = FILE "<имя_файла>" [<файлов_информ>] [<вторичный_файл>] <файлов_информ> = LENGTH [=] целое [PAGE[S]] | STARTING [AT [PAGE]] целое [<файлов_информ>] Аргумент Описание "<имя_файла>" Указывает спецификации файла, в котором будет храниться создаваемая БД. USER "имя_пользователя" Имя пользователя, которое вместе с паролем проверяется при соединении пользователя с сервером.. PASSWORD "пароль" Пароль, который вместе с именем пользователя проверяется при соединении пользователя с сервером. PAGE_SIZE [=] целое Размер страницы БД в байтах. Допустимые размеры: 1024 (по умолчанию), 2048,4096 или 8192. DEFAULT CHARACTER SET набор_символов Определяет набор символов, применимый в БД. Если не указан, по умолчанию берется NONE. FILE "<имя_файла>" Имя одного или нескольких файлов, в которых будет располагаться БД. STARTING [AT [PAGE]] Если БД занимает несколько файлов, это предложение позволяет определить, с какой страницы располагается БД в указанном файле. LENGTH [=] целое [PAGE[S]] Длина файла в страницах. По умолчанию 75 страниц. Минимум 50 страниц. Максимум ограничен фактически имеющимся дисковым пространством. 1 Здесь и далее при описании форматов операторов SQL в квадратных скобках указываются необязательные элементы, а в фигурных перечисляются возможные элементы, которые отделяются друг от друга символом |.
318 Глава 17 17.2. МНОГОФАЙЛОВАЯ БД При добавлении записей БД увеличивается в объеме. Если нет никаких ограничений на размер БД, с теоретической точки зрения размер файла, в котором БД хранится, ограничен лишь объемом диска. Однако бывает необходимо, чтобы физический размер файла БД был фиксирован. Подобная необходимость может возникать в условиях ограничений на доступное дисковое пространство. В этом случае БД хранится не в одном файле, а в нескольких, а сами файлы могут располагаться на физически разных носителях. Самый первый файл БД называется первичным, остальные - вторичными. Предложение STARTING AT [PAGE] указывает, с какой страницы начинается тот или иной вторичный файл. Предложение LENGTH = целое указывает длину того или иного файла. Например: CREATE DATABASE "D:\BD\SKLAD.GDB" FILE "D:\BD\SKLAD.GD1" STARTING AT PAGE 1001 LENGTH 500 FILE "D:\BD\SKLAD.GD2" Здесь определяется БД D:\BD\SKLAD.GDB, состоящая из 3 файлов: первичного D:\BD\SKLAD.GDB (длиной 1000 страниц), D:\BD\SKLAD.GD1 длиной 500 страниц и D:\BD\SKLAD.GD2 неопределенной длины. Если для вторичного файла не объявлена длина, следует указывать, с какой страницы он должен начинаться. 17.3. ОПРЕДЕЛЕНИЕ ПАРОЛЯ Пароль при создании БД указывается для того, чтобы сервер смог идентифицировать владельца БД по паре значений имя пользователя — пароль'. CREATE DATABASE "D:\BD\SKLAD.GDB" USER "XXX" PASSWORD "YYY" Эти имя и пароль принадлежат пользователю, создающему БД, и служат для его идентификации: после создания БД ни один другой пользователь, кроме системного администратора, не имеет прав доступа к базе. Впоследствии системный администратор может предоставить другим пользователям те или иные права доступа к БД. 17.4. УКАЗАНИЕ РАЗМЕРА СТРАНИЦЫ БД Размер страницы указывается в байтах и может быть 1024, 2048, 4096 или 8192 байт, например: CREATE DATABASE "BAZA.GDB" PAGE_SIZE 4096; Увеличение размера страницы может привести к ускорению работы с БД, поскольку: • уменьшается глубина индексов (число шагов, за которое при помощи индекса будут найдены требуемые записи); • уменьшается количество операций чтения при считывании длинных записей, поскольку за одну операцию чтения всегда считывается одна страница, запись, расположенная на одной странице, будет считана за один раз; при малом
Создание базы данных 319 объеме страницы запись располагается на нескольких страницах и соответственно считывается за несколько операций чтения. Увеличение размера страницы неоправданно в том случае, если запросы на чтение к БД в основном возвращают небольшое количество записей, не занимающих всю страницу. Поскольку за операцию чтения считывается одна страница БД, будет считываться много лишних записей. 17.5. УКАЗАНИЕ НАЦИОНАЛЬНОЙ КОДИРОВКИ СИМВОЛОВ Для символьных столбцов в составе таблиц БД используются различные национальные кодировки. При создании БД национальный набор символов устанавливается предложением DEFAULT CHARACTER SET набор_символов Например: CREATE DATABASE "BAZA.GDB" ... DEFAULT CHARACTER SET WIN1251; В дальнейшем всем создаваемым символьным столбцам таблиц БД (типы CHAR, VARCHAR) ставится в соответствие указанный набор символов, например, объявление в одной из таблиц базы данных BAZA.GDB столбца FAMILIA VARCHAR(25); интерпретируется как объявление FAMILIA VARCHAR(25) CHARACTER SET WIN1251; Изменить кодировку, принятую для БД по умолчанию, можно при определении конкретных доменов и столбцов. Для БД, в которых столбцы будут хранить русские слова, рекомендуется кодировка WIN1251. К сожалению, на уровне создания БД невозможно установить по умолчанию порядок сортировки символов (collation order). Его определяют при объявлении доменов и отдельных столбцов (см. соответствующие разделы). Для получения списка доступных кодировок обратитесь к встроенной системе помощи InterBase.
Глава 18 ТИПЫ СТОЛБЦОВ INTERBASE 18.1. ОБЗОР ТИПОВ ДАННЫХ INTERBASE В ТБД InterBase могут использоваться столбцы1 следующих типов: Тип столбца Размер, байт Описание SMALLINT 2 Целочисленные значения от-32768 до+32767. INTEGER 4 Целочисленные значения от -2 147 483 648 до +2 147 483 647. FLOAT 4 Вещественные числа до 7 значащих цифр в диапазоне от 3,4*10*38 ДО 3,4*10‘38. DOUBLE PRECISION 8 Вещественные числа до 15 значащих цифр в диапазоне от 1,7*1О"308 ДО 1,7*10+зое. CHAR(n) или CHARACTER 0-32767 Символьный столбец длиной в и символов. VARCHAR(n) или CHARACTER] VARYING 0-32767 Символьный столбец переменной длины, содержащий до п символов. DATE 8 Дата в пределах от 01.01.0100 до 11.12.5941. Также может хранить сведения о времени. BLOB переменный Любой тип двоичных данных. Столбцы могут определяться в SgZ-onepaTopax CREATE TABLE — создать таблицу БД, CREATE DOMAIN - создать домен и ALTER TABLE - изменить структуру таблицы. Синтаксис определения столбцов: <тип_данных> = { {SMALLINT | INTEGER | FLOAT | DOUBLE PRECISION} [<размерность_массива>] | {DECIMAL | NUMERIC} [(точность [, масштаб])] [<размерность_массива>] | DATE [<размерность_массива>] | {CHAR | CHARACTER | CHARACTER VARYING | VARCHAR} [(целое)] [<размерность_массива>] [CHARACTER SET набор_символов] | {NCHAR | NATIONAL CHARACTER | NATIONAL CHAR} [VARYING] [ (целое)] [<размерность_массива>] I BLOB [SUB_TYPE {целое I имя_подтипа}] [SEGMENT SIZE целое] [CHARACTER SET набор_символов] | BLOB [(длина_сегмента [, подтип])] } Заметим, что указываемый в описании синтаксиса необязательный элемент <раз- мерность_массива> определяет так называемый столбец-массив (см. ниже п. 18.8). 1 Для локальных БД типа Paradox и dBase колонки таблицы принято называть полями записи {fields), в то время как для серверных БД их обычно называют столбцами (columns). Далее будем следовать этой устоявшейся традиции.
Типы столбцов InterBase 321 18.2. ЦЕЛОЧИСЛЕННЫЕ ЗНАЧЕНИЯ Столбец типа SMALLINT хранит целочисленные значения в диапазоне от -32768 до +32767. При определении структуры таблицы БД в Database Desktop этот тип называется SHORT. Столбец типа INTEGER хранит целочисленные значения в диапазоне от +2 147 483 647 до -2 147 483 648. При определении структуры таблицы БД в Database Desktop этот тип называется LONG. Формат определения целочисленных столбцов: <имя_столбца> [=] {SMALLINT I INTEGER} [<размерность_массива>] Например: ' CREATE TABLE MYTABLE (INT_COLUMN INTEGER); 18.3.ВЕЩЕСТВЕННЫЕ ЗНАЧЕНИЯ Управлять количеством знаков в дробной части при объявлении вещественного числа нельзя. В один и тот же столбец типа FLOAT или DOUBLE PRECISION можно поместить значения и 222.3333, и 22.33. Однако в программе на значения таких полей можно наложить маску с точным количеством знаков после запятой. Например, наложение маски #,###. 00 приведет к тому, что будут показываться только два знака после запятой. Если значение столбца имеет в дробной части больше знаков, чем указано в маске, они будут округляться. Значение 100.678 в этом случае будет округлено как 100.68. Если в дробной части значения столбца меньше знаков, чем указано в маске, недостающие разряды будут дополняться нулями. Так, значение 100.2 будет показано как 100.20. Значения указанных типов следует применять в столбцах, где число знаков в дробной части при хранении значений несущественно и где может понадобиться большая точность представляемых вещественных значений, например, до 7 {FLOAT) или до 15 {DOUBLE PRICISION) знаков после запятой. При попытке представления числа с большим количеством разрядов, чем это разрешено типом данных, хранимые данные округляются до последней значащей цифры в разрешенном разряде. Например, попытка записать в столбец типа FLOAT значение 123.456789 приведет к запоминанию значения 123.4568. Формат определения целочисленных значений: <имя_столбца> [=] {FLOAT | DOUBLE PRECISION} [<размерность_массива >] Например: CREATE TABLE MYTABLE (FLOAT_COLUMN FLOAT); 18.4. ФИКСИРОВАННО-ДЕСЯТИЧНЫЕ ЗНАЧЕНИЯ Типы DECIMAL и NUMERIC задают вещественные значения и определяют в них фиксированное количество знаков после запятой. Формат определения этих типов <имя_столбца> [=] {DECIMAL | NUMERIC} [(точность [, масштаб])} [<размерность_массива >] где точность определяет общее количество знаков в хранимом числе (максимум 15), а масштаб - количество знаков в дробной части и может быть равен нулю. Во всех случаях масштаб должен быть меньше точности.
322 Глава 18 Например: CREATE TABLE MYTABLE (DECIMAL_COLUMN DECIMAL(9,2)); Заметим, что специальных типов DECIMAL и NUMERIC не существует, а вместо них используются типы INTEGER или DOUBLE PRECISION: если точность (коли- чество знаков в числе) меньше 10, то реальный тип столбца INTEGER, если больше или равно 10, реальный тип столбца DOUBLE PRECISION. Поскольку в столбцах типа INTEGER хранить дробные значения нельзя, то независимо от того, указано количество знаков после десятичной точки или нет, объявление DECIMAL и NUMERIC с общим числом разрядов, меньшим 10, приведет к тому, что фактически в столбце будут храниться только целочисленные значения. Например, если в столбец DECIMAL-CO- LUMN таблицы MYTABLE (см. выше) поместить значение 123.456, фактически в таблице будет храниться число 123, а все цифры дробной части будут безвозвратно потеряны. 18.5. ЗНАЧЕНИЯ ТИПА ДАТЫ Столбцы типа DATE позволяют хранить значения даты в пределах от 01.01.0100 до 11.12.5941, а также вместе с датой - и значения времени. Формат объявления: <имя_столбца> [=] DATE [<размерность_массива >] Например: CREATE TABLE MYTABLE (DATE_COLUMN DATE); Тип DATE InterBase полностью совместим с типом TDateTime в Object Pascal. Этот тип позволяет одновременно хранить в одной переменной дату и время. Если ввод данных в столбец типа DATE производится из утилиты WISQL, значения даты должны указываться в формате InterBase. Согласно этому формату значения даты состоят из номера дня (01-31), месяца (JAN-DEC) и года. Эти значения отделяются разделителями. Стандартным разделителем является символ «-», но принимаются и пробел, правый слеш «/» и точка «.». Значения дат в InterBase должны лежать в диапазоне 1-JAN-100..11-DEC-5941. Интерпретация формата представления значений типа даты зависит от настроек - программных или операционной системы компьютера. Полезно всякий раз при старте приложения программно переустанавливать формат даты и времени к привычному нам российскому формату: procedure TForml.FormCreate(Sender: TObject); begin DateSeparator := '.'; ShortDateFormat := 'dd.mm.yyyy'; ShortTimeFormat := 'hh:mm:ss'; end; Таким образом можно игнорировать неопределенность текущих установок операционной системы на конкретном компьютере. Замечание. В InterBase значения типа даты совместимы с рассматриваемыми ниже строковыми типами. Поэтому, если в 5£)1,-операторах InterBase необходимо интерпретировать значения типа даты как строку, нет необходимости в приведении типов. Например, можно так записать в символьный столбец S1 значение типа даты (символы | | означают операцию сцепления строк, функция NOW возвращает текущие дату и время):
Типы столбцов InterBase 323 UPDATE SOMETABLE (SET SI = "Дата отгрузки " || NOW); Кроме того, присвоение значения полям типа дата также производится в символьном формате: INSERT INTO SOMETABLE (DATA_PRIHODA, KOLVO) VALUES("01-FEB-1997",100); 18.6. СИМВОЛЬНЫЕ ТИПЫ ДАННЫХ Столбцы типа CHAR(n) и VARCHAR(n) позволяют хранить строковые значения длиной до п символов. В любом случае п не может превышать 32 Кб. Как сказано в документации, CHAR(n) определяет строковые столбцы фиксированной длины. Определение «фиксированная длина» означает, что даже если в столбец записано меньше п символов, незаполненное пространство заполняется пробелами. При хранении хвостовые пробелы усекаются, что позволяет хранить действительно значащее содержимое столбцов типа CHAR(n) и сближает их со строками переменной длины. При чтении значения столбца хвостовые пробелы вставляются в столбец снова (см. замечание ниже). Такой механизм придуман для экономии дискового пространства, когда действительная длина значений данного столбца в различных записях варьируется достаточно широко. VARCHARfn) определяет строковые столбцы переменной длины. Определение «переменная длина» означает, что в записи хранится ровно столько символов, сколько их имеется в значении столбца. Например, если для varchar (100) значения столбцов для одной записи - «Петров», а для другой - «Барабанов», в первом случае хранится 6 символов, а во втором — 9. Столбцы VARCHARfn) позволяют экономить дисковое пространство, давая возможность серверу располагать больше записей на странице БД. К недостаткам относится то, что VARCHARfn) читаются медленнее, чем CHAR(n). Попытка записать в столбец более чем п символов приведет к усечению лишних символов. Замечание. Опытным путем нетрудно убедиться в том, что Delphi при работе с InterBase интерпретирует столбцы типа CHAR(n) и VARCHAR(n) как TStringField. При этом: • столбцы типа CHAR(n) всегда читаются без хвостовых пробелов; • при занесении хвостовых пробелов в столбец (а часто это бывает необходимо) CHAR(n) всегда усекает их, в то время как VARCHARfn), наоборот, всегда их хранит. Формат определения символьных столбцов: <имя_столбца> [=] {CHAR | CHARACTER | CHARACTER VARYING I VARCHAR) [(n)] [<размерность_массива >] [CHARACTER SET набор_символов] Здесь n определяет длину символьного столбца. Если п опущено, по умолчанию подразумевается 1. CHARACTER SET позволяет установить кодировку путем указания имени используемого набора символов. Если CHARACTER SET опущен, используется кодировка, принятая по умолчанию для БД (устанавливается в предложение DEFAULT CHARACTER SET оператора CREATE DATABASE).
324 Глава 18 Если определен набор символов, то в столбец невозможно ввести символы, не входящие в данную таблицу кодировки. Для представления столбцов, которые будут содержать русскоязычные тексты, рекомендуем использовать набор символов WIN 1251, например: CREATE TABLE MYTABLE (SYMPOLE CHARACTER(20) CHARACTER SET WIN1251); Если при создании БД и описании столбца набор символов не указан, принимается по умолчанию набор NONE, который позволяет вводить в символьные столбцы символы любых кодировок. Данные хранятся так, как они вводятся. Однако для столбцов с набором символов NONE могут возникнуть проблемы при записи их значения в столбцы, которым набор символов явно назначен. Порядок сортировки символов определяет принцип, по которому символьные значения будут сравниваться и сортироваться в операторах SELECT (если в нем присутствуют разделы WHERE, ORDER BY), при обновлении индексов и т. д. Он может явно указываться предложением COLLATE <collation order> при объявлении символьного поля. Для кириллического набора символов WIN1251 рекомендуется использовать порядок сортировки PXWCYRL. Например: CREATE TABLE MYTABLE (SYM_COL CHARACTER(20) CHARACTER SET WIN1251 COLLATE PXW_CYRL); Следует учесть, что значения в строковых столбцах с набором WIN1251 и сортировкой PXW CYRL упорядочиваются в стиле Windows 32: буквы идут по парам, строчные буквы предшествуют заглавным: аАбБвВгГ ... яЯ С другой стороны, если набор символов и сортировка не указаны,- столбцы упорядочиваются в стиле MS-DOS: АБВГ ... Яабвг ... я 18.7. ЗНАЧЕНИЯ ТИПА BLOB В столбцах типа BLOB хранят данные, которые невозможно хранить в столбцах других типов. BLOB имеет переменную длину и интерпретируется как последовательность байтов. В В£ОВ-столбцах можно хранить графические образы, тексты большой длины, звуковые файлы, видеоизображения, мультимедиа-информацию. В них можно также хранить указатели на неструктурированные файлы информации, которые расположены в ином, фиксированном месте. При этом интерпретация содержимого ВЛОВ-столбца может ложиться не только на клиентскую часть, но также и на сервер. В последнем случае в серверной БД могут быть определены так называемые В£О5-фильтры, определяющие способы преобразования В£О5-значений от одного вида к другому. Например, преобразование изображений из одного графического формата в другой. InterBase хранит значения В£ОВ-столбцов в самой БД в виде сегментов. Одна операция ввода-вывода при доступе к ВАОВ-информации оперирует 'с одним сегментом. В таблице БД, если в ней объявлен столбец типа BLOB, хранится указатель на начальный сегмент столбца в области хранения ВАОВ-информации этой БД. Длина сегмента BLOB не влияет на производительность InterBase. Однако исходя из некоторых соображений ее можно определять явно при создании ВАОВ-столбца:
Типы столбцов InterBase 325 BLOBPOLE BLOB SEGMENT SIZE 1024 По умолчанию длина сегмента составляет 80 байт, максимальная длина сегмента 32 Кбайт (32 768 байт). Для BLOB может быть указан подтип, определяющий, какой тип данных будет записан в этот столбец. Подтип есть число. Положительные значения зарезервированы InterBase- 0 (по умолчанию) для указания двоичных данных и 1 для указания ASCII- текстов. Отрицательные значения могут использоваться программистами для определения собственных подтипов. В общем случае указание подтипа производится через опцию SUB TYPE-. <имя_столбца> BLOB SUB_TYPE -1 CHARACTER SET WIN1251 Однако InterBase автоматически не отслеживает правильность занесения информации в столбцы типа BLOB. Это необходимо делать в приложениях. 18.8. СТОЛБЦЫ-МАССИВЫ В InterBase могут определяться столбцы-массивы. Такие столбцы вместо единичного значения содержат массив значений одинакового типа, доступ к каждому из которых возможен с помощью индекса. Базовыми типами для элементов массива могут быть любые из рассмотренных типов InterBase, кроме BLOB. Для создания столбца-массива в конце его определения указывается в квадратных скобках целое число, определяющее количество элементов массива. Например: CREATE TABLE MYTABLE (ARR_COL INTEGER [30]) В этом примере столбец ARR COL будет содержать по 30 целочисленных значений в каждой записи таблицы MYTABLE. По умолчанию нумерация элементов начинается с 1, однако программист может явно указать границы индексов: CREATE TABLE MYTABLE (ARR_COL INTEGER [0:29]') Массив может содержать до 16 измерений, которые перечисляются через запятые: CREATE TABLE MYTABLE (ARR_COL INTEGER [0:29, 15, 10]) что эквивалентно такому объявлению Object Pascal'. var Arr_Col: array [0..29, 1..15, 1..10] of Integer; 18.9. СОВМЕСТИМОСТЬ ТИПОВ При выполнении операций над столбцами разного типа InterBase пытается автоматически привести типы таким образом, чтобы значения, участвующие в операции, принадлежали совместимым типам. Кроме того, для явного приведения типов можно использовать функцию CAST, которая приводит типы внутри оператора SELECT, обычно в предложении WHERE-. CAST (<значение> I NULL AS ТипДанных) Совместимыми считаются только типы DATE, CHAR и NUMERIC. Вот как можно привести значение DateData (тип DATE) к типу CHAR и сравнить его со значением Char Date (типа CHAR): SELECT ... WHERE CHAR_DATE <= CAST(DATE_DATE AS CHAR);
Глава 19 СОЗДАНИЕ ДОМЕНОВ 19.1. ПОНЯТИЕ ДОМЕНА Если в таблице БД или в нескольких таблицах БД присутствуют столбцы, обладающие одними и теми же характеристиками, можно предварительно описать тип такого столбца и его поведение с помощью домена, а затем поставить в соответствие каждому из одинаковых столбцов имя домена. Например, создать домен POL_TYPE и затем использовать его при создании таблицы SOTR как тип столбца POL: CREATE DOMAIN POLJTYPE AS CHAR(3) COLLATE PXW_CYRL; CREATE TABLE SOTR( FIO CHAR(20) NOT NULL, POL POLJTYPE, OTDEL CHAR(10), DOLJ CHAR(20), PRIMARY KEY(FIO) ) ; Как можно заметить, домен есть описание какого-либо столбца. В языке Object Pascal близким по назначению является описание типа. Домен определяется оператором CREATE DOMAIN, имеющим такой формат: CREATE DOMAIN домен [AS] <тип_данных> [DEFAULT {литерал| NULL | USER)] [NOT NULL] [CHECK (<усл_поиска_домена>)] [COLLATE collation]; Предложение COLLATE задает порядок сортировки символов, например: CREATE DOMAIN POLJTYPE AS CHAR(3) COLLATE PXW_CYRL; Предложение DEFAULT определяет выражение, которое по умолчанию заносится в колонку, ассоциированную с доменом, при создании записи таблицы. Это значение будет присутствовать в соответствующем столбце записи до тех пор, пока пользователь не изменит его каким-либо образом. Значения по умолчанию могут быть выражены как литерал-значение (числовое, строковое или дата), NULL - специфицирует пустое значение или USER - имя текущего пользователя. Заметим, что значения по умолчанию, присваиваемые столбцу и объявленные в операторах CREATE TABLE или ALTER TABLE, переопределяют значения, присваиваемые по умолчанию тому же столбцу согласно директивам, содержащимся в CREATE DOMAIN. Предложение NOT NULL указывает, что столбцы, ассоциированные с доменом, обязательно должны содержать какое-либо значение, отличное от пустого. Нужно
Создание доменов 327 следить за тем, чтобы в объявлении домена не было противоречий, например, в описании значения по умолчанию как NULL и объявлении директивы NOT NULL. 19.2. ОГРАНИЧЕНИЯ НА ЗНАЧЕНИЯ СТОЛБЦОВ, АССОЦИИРОВАННЫХ С ДОМЕНОМ Предложение CHECK определяет требования к значениям каждого столбца, ассоциированного с доменом. Столбцу не могут быть присвоены значения, не удовлетворяющие ограничениям, наложенным в предложении CHECK. Формат ограничения, накладываемого на значения полей, ассоциированных с доменом: <огранич_домена> = { VALUE <оператор> <значение> | VALUE [NOT] BETWEEN <значение1> AND <значение2> | VALUE [NOT] LIKE <значение> [ESCAPE <значение>] I VALUE [NOT] IN (<значение1> [, <значение2> ...]) I VALUE IS [NOT] NULL I VALUE [NOT] CONTAINING <значение> I VALUE [NOT] STARTING [WITH] <значение> I (<огранич_домена>) I NOT <огранич_домена> I <огранич_домена> OR <огранич_домена> I <огранич_домена> AND <огранич_домена> ] где <оператор> = {= I < I > I <= I >= I !< I !> | о I ! = } (помимо обычных для Object Pascal операций отношения используются также операции !< - не меньше, !> - не больше, != - не равно). Ключевое слово VALUE далее означает все правильные значения, которые могут быть присвоены столбцу, ассоциированному с доменом. • VALUE <оператор> <значение> - значение домена находится с параметром значение во взаимоотношениях, определяемых параметром оператор. Например, значение, которое может быть записано в столбец, ассоциированный с доменомID_TYPE, должно быть больше или равно 100: CREATE DOMAIN IDJTYPE AS INTEGER CHECK(VALUE >= 100); • BETWEEN <значение1> AND <значение2> - значение домена должно находиться в промежутке между значение! и значение?, включая их. • LIKE <значение1> [ESCAPE <значение2>] - значение домена должно «походить » на параметр значение!. При этом символ «%» употребляется для указания любого значения любой длины и символ подчеркивания «_» - для указания любого единичного символа. Например, like "%usd" — вводимое значение должно оканчиваться символами «USD» независимо от того, какие символы и сколько расположены перед ними; like "___94" - вводимое значение может содержать 4 символа, из которых первые два - любые и последние два - «94». ESCAPE <значение2> используется, если в операторе LIKE служебные символы «%» или «_» должны использоваться в шаблоне подобия. В этом случае выбирается некоторый символ, например, «!», после которого служебные символы теряют свой специальный статус и входят в поисковую строку как
328 Глава 19 обычные символы. Символ «!» указывается после слова ESCAPE, например (значения столбца SUMMA должны заканчиваться символом «%»): CREATE DOMAIN SUMMA AS CHAR(IO) CHECK(LIKE "%!%" ESCAPE • IN (<значение1> [, <значение2> ...]) - значение домена должно совпадать с одним из приведенных в списке параметров значениеХ, например: CREATE DOMAIN POL_TYPE AS CHAR(3) CHECK(VALUE IN ("Муж","Жен")); • CONTAINING <значение> - значение домена должно содержать вхождение параметра значение, не важно, в каком месте. Например, в наименовании отдела вхождение «041» может встретиться где угодно («Отдел-041002», «003404192», и т.д.): CREATE DOMAIN OTDEL_TYPE AS VARCHAR(10) CHECK(VALUE CONTAINING "041") COLLATE PXW_CYRL; • STARTING [И7777] <значение> - значение домена должно начинаться параметром значение. Например, название отдела должно начинаться с «041»: CREATE DOMAIN OTDEL_TYPE AS VARCHAR(IO) CHECK(VALUE STARTING WITH "041") COLLATE PXW_CYRL; Может быть задана комбинация условий, которым должно соответствовать значение домена. В этом случае отдельные условия соединяются операторами AND или OR. Например: CREATE DOMAIN OTDEL_TYPE AS VARCHAR(IO) CHECK(VALUE STARTING WITH "041" AND VALUE CONTAINING "-12") COLLATE PXW_CYRL; Для большинства условий можно указать слово NOT, которое-изменит условие с точностью до наоборот. Например: CHECK(VALUE NOT BETWEEN 1 AND 100); 19.3. ИЗМЕНЕНИЕ ОПРЕДЕЛЕНИЯ ДОМЕНА Оператор ALTER DOMAIN имя { [SET DEFAULT {литерал| NULL | USER}] I [DROP DEFAULT] I [ADD [CONSTRAINT] CHECK (<огранич_домена>)] | [DROP CONSTRAINT] }; позволяет изменить параметры домена, определенного ранее оператором CREATE DOMAIN. Однако нельзя изменить тип данных и определение NOT NULL. Следует помнить, что все уделанные изменения будут учтены для всех столбцов, определенных с использованием данного домена (в том случае, если параметры домена не были переопределены при создании столбцов таблицы или впоследствии).
Создание доменов 329 • SET DEFAULT устанавливает значения по умолчанию подобно тому, как это делается в операторе CREATE DOMAIN. • DROP DEFAULT отменяет текущие значения по умолчанию, назначенные домену. • [ADD [CONSTRAINT] CHECK (<огранич_домена>)] добавляет условия, которым должны соответствовать значения столбца, ассоциированного .с доменом. При этом возможно определение условий, рассмотренных выше для предложения CHECK оператора CREATE DOMAIN. • DROP CONSTRAINT удаляет условия, определенные для домена в предложении CHECK оператора CREATE DOMAIN или предыдущих операторов ALTER DOMAIN. Например, пусть определен домен ID_TYPE~. REATE DOMAIN ID_TYPE AS INTEGER CHECK(VALUE >= 100); и в дальнейшем он использован при создании таблицы ААА: CREATE TABLE AAA( ID ID_TYPE NOT NULL, FIO VARCHAR(20), PRIMARY KEY(ID) ); Изменить условие CHECK так, чтобы значение было больше или равно 100 и меньше или равно 500, можно за два шага. Сначала нужно удалить старое условие: ALTER DOMAIN IDJTYPE DROP CONSTRAINT; после чего добавить новое (которое на самом деле есть модифицированное старое): ALTER DOMAIN IDJTYPE CHECK (VALUE >= 100 AND VALUE <= 500); Заметим, что изменять определение таблицы ААА нет необходимости, и отныне в столбец ID этой таблицы можно занести значения, большие или равные 100 и меньшие или равные 500.
Глава 20 СОЗДАНИЕ ТАБЛИЦ 20.1. ОБЩИЙ ФОРМАТ ОПЕРАТОРА CREATE TABLE Перед созданием таблиц БД необходимо продумать определение всех столбцов таблицы и характеристик каждого столбца (таких как тип, длина, обязательность для ввода, ограничения, накладываемые на значения и пр.), индексов, ограничений цело- стности по отношению к другим таблицам. Если при определении столбцов исполь- зуются домены, эти домены должны быть предварительно созданы оператором CREATE DOMAIN. Та БД, в которую будет добавлена создаваемая таблица, должна быть открыта, т. е. с ней должно быть установлено активное соединение. Создание таблицы БД осуществляется оператором CREATE TABLE ИмяТаблицы [EXTERNAL [FILE] "<имя файла>"] (<опр_столбца> [, <опр_столбца> | <ограничение> ...]); [EXTERNAL [FILE] "< имя файла >"] относится к внешним, т. е. расположенным отдельно от БД, таблицам БД. <опр_столбца> - определение столбца БД. Основные сведения о типах столбцов см. выше в п. 18. Определение столбца имеет формат: <опр_столбца> = опр_столбца{тип_данных | COMPUTED [BY] (<выражение>) | домен] [DEFAULT {литерал| NULL | USER}] [NOT NULL] [<огранич_столбца>] [COLLATE collation] Здесь: опр_столбца - имя столбца; тип-данных - тип столбца и, возможно, размерность массива, если столбец - мас- сив; для символьных столбцов может быть указан набор символов, отличный от принятого по умолчанию, при помощи предложения CHARACTER SET- COMPUTED [BY] (<выражение>) - служит для определения столбца вычисляе- мых значений (подробнее см. ниже); domain — имя домена, т. е. ранее описанного типа столбца; DEFAULT определяет значение, которое по умолчанию заносится в столбец, ассоцииро- ванный с доменом, при создании записи таблицы; это значение будет присутствовать в соответствующем столбце данной записи до тех пор, пока пользователь не изменит его каким-либо образом; значения по умолчанию (см. п. 19.1); ограничстолбца - ограничения, накладываемые на значения столбца (подробно рассматриваются ниже); COLLATE collation - определяет порядок сортировки символов (для символьных столбцов). 20.2. СТОЛБЦЫ ВЫЧИСЛЯЕМЫХ ЗНАЧЕНИЙ Столбцы вычисляемых значений описываются с помощью предложения COMPUTED [BY] (<выражение>)
Создание таблиц 331 Значение таких столбцов не вводится пользователем, а вычисляется автоматически согласно выражению. Тип результирующего значения и будет служить типом вычис- ляемого столбца. Например, показанная на рис. 20.1 таблица SALJHIST содержит но- мер квартала (QUORTER), количество продаж в данном квартале, в прошлом (LASTYEAR) и текущем году (THISYEAR) и прирост продаж за квартал (GROWTH). Она создана таким образом: CREATE TABLE SAL_HIST ( QUORTER INTEGER NOT NULL, LAST_YEAR INTEGER, THIS_YEAR INTEGER, GROWTH COMPUTED BY (THIS_YEAR - LAST_YEAR), PRIMARY KEY (QUORTER)); Puc. 20.1. Столбец вычисляемого значения. 20.3. ОГРАНИЧЕНИЯ ЦЕЛОСТНОСТИ Ограничения целостности бывают двух уровней: ограничения, накладываемые на отдельный столбец и на всю таблицу. В случае наложения ограничений целостности на отдельный столбец описание ог- раничения приводится следом за именем столбца и его типом: TOVAR TOVAR VARCHAR(20) NOT NULL PRIMARY KEY, ... В данном случае столбец TOVAR составляет первичный ключ, что объявлено в предложении PRIMARY KEY. В случае наложения ограничений на таблицу описание ограничений указывается после объявлений отдельных столбцов, например: CREATE TABLE ...( TOVAR TOVAR VARCHAR(20) NOT NULL, PRIMARY KEY(TOVAR) ); 20.4. ПЕРВИЧНЫЙ КЛЮЧ Если по столбцу строится первичный ключ, столбцу может быть приписан атрибут PRIMARYKEY: CREATE TABLE SAL_HIST ( QUORTER INTEGER NOT NULL PRIMARY KEY,
332 Глава 20 Заметим, что первичный ключ может быть построен и путем включения имени (имен) ключевого столбца (столбцов) в качестве параметров отдельного предложения PRIMARYKEY-. CREATE TABLE SAL_HIST ( QUORTER INTEGER NOT NULL, PRIMARY KEY (QUORTER) ); Однако и в том, и в другом случае ключевой столбец (столбцы), входящий (входя- щие) в состав первичного ключа, должен быть помечен как NOT NULL, поскольку первичный ключ не может быть построен даже по одному пустому значению. Первичный ключ, если он служит для обеспечения ссылочной целостности, должен корреспондировать с внешним ключом (FOREIGN KEY) другой (дочерней) таблицы. Определение ссылочной целостности между родительской и дочерней таблицами опи- сано ниже (см. п. 20.6). 20.5. УНИКАЛЬНЫЙ КЛЮЧ Атрибут UNIQUE, если он приписан столбцу, означает, что в столбце не могут со- держаться два одинаковых значения. Уникальный ключ строится по столбцу (столбцам), когда столбец не входит в состав первичного ключа, но тем не менее его значение долж- но всегда быть уникальным. Например, для таблицы VLADLIM («владельцы бюджетных лимитов») первичный ключ строится по коду владельца KODVLAD, введенному для со- кращения объема первичного ключа и времени поиска по нему (объем ключа по столбцу типа INTEGER много меньше объема ключа по символьному полю с максимальной дли- ной в 50 символов). Однако и название владельца лимита NAZWLAD должно быть уни- кальным, для чего ему приписан атрибут UNIQUE: CREATE TABLE VLADLIM ( KODVLAD INTEGER NOT NULL PRIMARY KEY, NAZWLAD VARCHAR (50) NOT NULL UNIQUE ); Уникальность может быть приписана и на уровне таблицы: CREATE TABLE VLADLIM ( KODVLAD INTEGER NOT NULL PRIMARY KEY, NAZWLAD VARCHAR (50) NOT NULL, UNIQUE (NAZWLAD) ); Столбец, объявленный с атрибутом UNIQUE, как и первичный ключ, может при- меняться для обеспечения ссылочной целостности между родительской и дочерней таблицами. В этом случае столбец с атрибутом UNIQUE должен принадлежать к роди- тельской таблице и должен корреспондировать с внешним ключом (FOREIGN KEY) другой (дочерней) таблицы. 20.6. ВНЕШНИЙ КЛЮЧ И ОПРЕДЕЛЕНИЕ ССЫЛОЧНОЙ ЦЕЛОСТНОСТИ Внешний ключ строится в дочерней (детальной) таблице для соединения родитель- ской (главной) и дочерних таблиц БД. Формат определения:
Создание таблиц 333 FOREIGN KEY (<список столбцов внешнего ключа») REFERENCES <имя родительской таблицы» [<список столбцов родительской таблицы»] [ON DELETE (NO ACTION | CASCADE | SET DEFAULT I SET NULL}] [ON UPDATE (NO ACTION | CASCADE | SET DEFAULT I SET NULL}] Список столбцов внешнего ключа определяет столбцы дочерней таблицы, по кото- рым строится внешний ключ. Имя родительской таблицы определяет таблицу, в которой описан первичный ключ (или столбец с атрибутом UNIQUE). На этот ключ (столбец) должен ссы- латься внешний ключ дочерней таблицы для обеспечения ссылочной целостно- сти. Список столбцов родительской таблицы необязателен при ссылке на первичный ключ родительской таблицы. При ссылке в родительской таблице на столбец с атрибутом UNIQUE этот список лучше привести. Параметры ON DELETE, ON UPDATE определяют способы изменения подчинен- ных записей дочерней таблицы при удалении или изменении поля связи в записи ро- дительской таблицы. Перечислим эти способы: • NO action - запрет удаления/изменения родительской записи при наличии подчиненных записей в дочерней таблице; • cascade - для оператора ON DELETE: при удалении записи родительской таб- лицы происходит удаление подчиненных записей в дочерней таблице; для ON UPDATE: при изменении поля связи в записи родительской таблицы происхо- дит изменение на то же значение поля внешнего ключа у всех подчиненных за- писей в дочерней таблице; • set default - в поле внешнего ключа у записей дочерней таблицы заносится значение этого поля по умолчанию, указанное при определении поля (параметр DEFAULT): если это значение отсутствует в первичном ключе, возбуждается исключение; заметим, что используется значение по умолчанию, имевшее место на момент определения ссылочной целостности; если впоследствии это значе- ние будет изменено, ссылочная целостность при SET DEFAULT все равно будет использовать прежнее значение; • set NULL - в поле внешнего ключа у записей дочерней таблицы заносится зна- чение NULL. Пример. Определим две таблицы: • родительскую «Справочник товаров» SPRTOVAR с полями TOVAR (наименование товара), ZENA_ED (цена за единицу измерения); первичный ключ по полю TOVAR-, • дочернюю «Приход товара на склад» PRIHOD с полями ID PRIHOD (номер прихода), DATAPRIH (дата прихода), TOVAR (товар), KOLVO (количество при- хода, ед.). Первичный ключ по полю ID_PRIHOD, внешний - по полю TOVAR для обеспечения ссылочной целостности с таблицей SPR TOVAR. Лря определения этих таблиц необходимо выполнить такие операторы: CREATE TABLE SPR_TOVAR( TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, ZENA_ED INTEGER NOT NULL,
334 Глава 20 PRIMARY KEY(TOVAR) ) ; CREATE TABLE PRIHOD( ID_PRIHOD INTEGER NOT NULL PRIMARY KEY, DATAPRIH DATE NOT NULL, TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, KOLVO INTEGER NOT NULL, FOREIGN KEY(TOVAR) REFERENCES SPR_TOVAR ) ; Теперь для таблицы SPR_TOVAR будет установлена блокировка удаления или из- менения значения в столбце TOVAR, если в таблице PRIHOD имеются записи о прихо- де этого товара. Заметим, что определение общих полей родительской и дочерней таблиц (полей связи) должно в точности совпадать. Если в родительской таблице объявить TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, а в дочерней объявить TOVAR VARCHAR(20) NOT NULL, то из-за различий в порядке сортировки символов эти столбцы не будут фактически идентичными, из-за чего будут постоянно возникать ошибки нарушения ссылочной целостности. Пример. Пусть созданы родительская таблица Р и дочерняя таблица F: CREATE TABLE Р( PK_FIELD INTEGER NOT NULL, OTHER_FIELD INTEGER, PRIMARY KEY(PK_FIELD) ) ; CREATE TABLE F(. PK_FIELD1 INTEGER NOT NULL, PK_FIELD2 INTEGER NOT NULL, SOME_FIELD INTEGER, PRIMARY KEY(PK_FIELD1,PK_FIELD2), FOREIGN‘KEY (PK_FIELD1) REFERENCES P ON UPDATE CASCADE ); Содержимое таблиц: Таблица P Таблица F PK_FIELD OTHER_FIELD PK_FIELD1 PK_FIELD2 SOME_FIELD 1 100 2 777 1 1 1000 1 2 8769 2 10 345 2 20 3
Создание таблиц 335 Тогда после выполнения оператора UPDATE Р SET PK_FIELD = 999 WHERE PK_FIELD = 1 содержимое таблицPhFбудет следующим: Таблица P Таблица F PK_FIELD OTHER_FIELD PK_FIELD1 PK_FIELD2 SOME_FIELD 999 100 999 1 1000 2 777 999 2 8769 2 10 345 2 20 3 Замечание. Если по какой-либо причине удаляется ограничение ссылочной целостности между двумя таблицами, сам внешний ключ в дочерней таблице может и не удаляться в том случае, если он используется в запросах (оператор SELECT) оптимизатором запросов InterBase. В этом случае он определяется как индекс (оператором CREATE INDEX). 20.7. ИМЕНОВАНИЕ ССЫЛОЧНОЙ ЦЕЛОСТНОСТИ Ссылочная целостность может именоваться следующим образом: [CONSTRAINT <имя ссылочной целостности»] FOREIGN KEY (Ссписок столбцов внешнего ключа») REFERENCES <имя родительской таблицы» [<список столбцов родительской таблицы»] Необязательное имя ссылочной целостности присутствует в системных сообщени- ях относительно нарушения целостности, а также может использоваться при анализе структуры БД и изменении структуры таблиц. В случае если это имя опущено, InterBase установит его сам. Например, назначим в приведенной выше таблице PRIHOD имя ссылочной цело- стности: CREATE TABLE PRIHOD( ID_PRIHOD INTEGER NOT NULL PRIMARY KEY, DATAPRIH DATE NOT NULL, TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, KOLVO INTEGER NOT NULL, CONSTRAINT PO_TOVARU FOREIGN KEY(TOVAR) REFERENCES SPR_TOVAR); Имя ссылочной целостности может назначаться также и при определении первич- ного ключа и уникального набора атрибутов: CREATE TABLE VLADLIM ( KODVLAD INTEGER NOT NULL PRIMARY KEY, ’ NAZWLAD VARCHAR (50) NOT NULL, CONSTRAINT PO_NAZV UNIQUE (NAZWLAD) ) ;
336 Глава 20 20.8. ТРЕБОВАНИЯ К ЗНАЧЕНИЯМ СТОЛБЦОВ Требования к значениям отдельных столбцов могут определяться как на уровне конкретного столбца, так и на уровне таблицы. Например, для таблицы PERSON PA- RAMS (параметры человека) рост (HEIGHT) должен быть больше веса (WIEGHT). То- гда данное ограничение на уровне столбца определяется следующим образом: CREATE TABLE PERSON_PARAMS( ID INTEGER NOT NULL PRIMARY KEY, HEIGHT INTEGER NOT NULL, WIEGHT INTEGER NOT NULL CHECK(HEIGHT > WIEGHT)); а на уровне таблицы - так: CREATE TABLE PERSON_PARAMS( ID INTEGER NOT NULL, HEIGHT INTEGER NOT NULL, WIEGHT INTEGER NOT NULL, PRIMARY KEY(ID), CHECK(HEIGHT > WIEGHT)); Ограничения, накладываемые на столбцы таблицы, определяются при помощи предложения CHECK, общий формат которого приводится ниже. CHECK (<условия_поиска>) <условия_поиска> = (<значение> <оператор> {<значение1> | (<выбор_одного>)} I <значение> [NOT] BETWEEN <значение1> AND <значение2> | <значение> [NOT] LIKE <значение> [ESCAPE <значение>] | <значение> [NOT] IN (<значение1> [, <значение2> ...] I <выбор_многих>) | <значение> IS [NOT] NULL | <значение> {[NOT] {= I < I >} I >- I <=} (ALL | SOME I ANY] (<выбор_многих>) | EXISTS (<выражение_выбора>) | SINGULAR (<выражение_выбора>) I <значение> [NOT] CONTAINING <значение1> I <значение> [NOT] STARTING [WITH] <значение1> I (<условия_поиска>) | NOT <условия_поиска> | <условия_поиска> OR <условия_поиска> I <условия_поиска> AND <условия_поиска>} <значение> = (столбец | <константа> | <выражение> | <функция> I NULL I USER | RDB$DB_KEY } [COLLATE collation] <константа> = число | "строка" <функция> = ( COUNT (* I [ALL] <значение> | DISTINCT <значение>) I SUM ([ALL] <значение> | DISTINCT <значение>) I AVG ([ALL] <значение> | DISTINCT <значение>) | MAX ([ALL] <значение> I DISTINCT <значение>) I MIN ([ALL] <значение> | DISTINCT <значение>) I CAST (<значение> AS <тип_данных>) I UPPER (<значение>)
Создание таблиц 331 I GEN_ID (генератор, <значение>) } <оператор> = {= | < | > | <= | >= | !< | !> | <> | !=}. <выбор_одного> = оператор SELECT, возвращающий одно значение или не воз- вращающий ничего. <выбор_многих> = оператор SELECT, который может возвращать более одного значения (список значений) или ни одного. <выражение_выбора> = оператор SELECT, который может возвращать более од- ного значения (список значений) или ни одного. <значение> <оператор> <значение!> определяет, что значение столбца находится со значением! во взаимоотношениях, определяемых оператором, который мо- жет принимать одно из следующих значений: <оператор> = {= | < | > | <= | >= | !< | !> | <> | !=} Например, значение столбца STOLBEZ не должно быть меньше 100: CREATE TABLE TBL( CHECK(STOLBEZ >= 100); ); <значение> <onepamop> (<выбор_одного>) устанавливает, что значение столбца находится во взаимоотношениях, определяемых оператором оператор, с ре- зультатом выполнения запроса SELECT к одной или нескольким таблицам БД, причем в качестве результата выполнения запроса возвращается единичное зна- чение (выбор_одного). Пример. Пусть существует таблица TOVAR (остаток товара на складе). CREATE TABLE TOVAR ( TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, OSTATOK INTEGER NOT NULL, PRIMARY KEY (TOVAR)); Требуется создать таблицу RASHOD (расход товара со склада) и для столбца KOLVO R (количество расхода) предусмотреть ограничение: количество расхода дан- ного товара не может быть больше его текущего остатка (значение столбца OSTATOK для данного значения товара) в таблице TOVAR)'. CREATE TABLE RASHOD ( ID_RS INTEGER NOT NULL, TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, DATE_RASH DATE NOT NULL, KOLVO_R INTEGER NOT NULL, PRIMARY KEY (ID_RS) , CONSTRAINT RASH_TOVAR FOREIGN KEY (TOVAR) REFERENCES TOVAR(TOVAR), CONSTRAINT POJDSTATKU CHECK(KOLVO_R <= (SELECT TOVAR.OSTATOK FROM TOVAR WHERE TOVAR.TOVAR = RASHOD.TOVAR)));
338 Глава 20 <значение> BETWEEN <значение!> AND <значение2> — значение столбца должно находиться в диапазоне от значение! до значение}, например: CREATE TABLE TBL( CHECK(STOLBEZ BETWEEN 100 AND 200); . -.) ; LIKE <значение> [ESCAPE <значение>] - содержимое столбца должно «походить » на значение (подробнее см. п. 19.2). <значение> {= | < | >} | >= | <=} {ALL | SOME | ANY} (<выбор_многих>) - значе- ние столбца больше, меньше и т. д. всех {ALL) или некоторых {SOME или ANY) значе- ний в списке выбор многих. Список значений выбор_многих выдается как результат выполнения оператора SELECT по отношению к одной или нескольким таблицам БД. Пример. Пусть определены таблицы TOVAR и PRIHOD: CREATE TABLE TOVAR ( TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, PRIMARY KEY (TOVAR)); CREATE TABLE PRIHOD ( ID_PR INTEGER NOT NULL, TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, DATE_PRIH DATE NOT NULL, KOLVO_P INTEGER NOT NULL, PRIMARY KEY (ID_PR) CONSTRAINT PRIHOD_TOVAR FOREIGN KEY (TOVAR) REFERENCES TOVAR(TOVAR)); Определим таблицу RASHOD, у которой дата расхода товара DATE_RASH больше всех дат прихода данного товара DATE_PRIHb таблице PRIHOD: CREATE TABLE RASHOD ( ID_RS INTEGER NOT NULL, TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, DATE_RASH DATE NOT NULL, KOLVO_R INTEGER NOT NULL, PRIMARY KEY (ID_RS), CONSTRAINT RASH_TOVAR FOREIGN KEY (TOVAR) REFERENCES TOVAR(TOVAR), CONSTRAINT PO_DATE_RASH CHECK (DATE_RASH > ALL (SELECT DATE_PRIH FROM PRIHOD WHERE PRIHOD.TOVAR = RASHOD.TOVAR))); Обратите внимание: в операторе SELECT в предложении WHERE указана только таб- лица PRIHOD. Таблица RASHOD не указана, но подразумевается, поскольку именно на нее наложено ограничение PO_DATE. Кроме того, в этом случае условие выборки WHERE PRIHOD.TOVAR = RASHOD.TOVAR подразумевает текущее значение RASHOD.TOVAR, т. е. значение, введенное в добав- ляемую запись таблицы RASHOD, - запись, значение столбца DATE RASH которой и
Создание таблиц 339 проверяется на соответствие условию, заданному в CHECK в ограничении CONST- RAINT PO DA ТЕ RASH. Указание в предложении WHERE обеих таблиц FROM PRIHOD, RASHOD приведет к тому, что условие WHERE PRIHOD.TOVAR = RASHOD.TOVAR будет воспринято как внутреннее соединение таблиц PRIHOD и RASHOD по столбцу TOVAR. В этом случае соединение будет произведено для всех значений столбца TOVAR в таблице RASHOD, а не для текущего значения той записи таблицы RASHOD, чье значение DATE RASH проверяется на соответствие условию. EXISTS (<выражение_выбора>) — возвращает True, если список выраже- ние_выбора не пустой, т. е. содержит хотя бы одну строку. Список выраже- ниевыбора выдается как результат выполнения оператора SELECT по отноше- нию к одной или нескольким таблицам БД. В приводимом ниже примере обязательно существование хотя бы одной записи в таблице PRIHOD с таким же значением столбца TOVAR, что и в поле TOVAR записи таблицы RASHOD. CREATE TABLE RASHOD (ID_RS INTEGER NOT NULL, TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, DATE_RASH DATE NOT NULL, CONSTRAINT RASH_TOVAR FOREIGN KEY (TOVAR) REFERENCES TOVAR(TOVAR), CONSTRAINT PO_DATE_RASH CHECK (EXISTS (SELECT TOVAR FROM PRIHOD WHERE PRIHOD.TOVAR = RASHOD.TOVAR))); SINGULAR (<выражение_выбора>) - возвращает True, если список выраже- ние_выбора содержит только одну строку. Список выражение выбора выдается как результат выполнения оператора SELECT по отношению к одной или не- скольким таблицам БД. В приводимом ниже примере обязательно существование единственной записи в таблице PRIHOD с таким же значением столбца TOVAR, что и в поле TOVAR записи таблицы RASHOD. CREATE TABLE RASHOD (ID_RS INTEGER NOT NULL, TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, DATE_RASH DATE NOT NULL, CONSTRAINT RASH_TOVAR FOREIGN KEY (TOVAR) REFERENCES TOVAR(TOVAR), CONSTRAINT PO_DATE_RASH CHECK (SINGULAR(SELECT TOVAR FROM PRIHOD WHERE PRIHOD.TOVAR = RASHOD.TOVAR))); <значение> CONTAINING <значение!> - значение столбца должно содержать вхо- ждение значение!, не важно, в каком месте.
340 Глава 20 В приводимом ниже примере значение поля STOLBEZ должно содержать вхожде- ние символов «USD» независимо от того, какие символы и в каком количестве распо- ложены перед ними или после них. CREATE TABLE TBL( CHECK (STOLBEZ CONTAINING "USD") ; ...) ; <значение> STARTING [WITH] <значение1> определяет, что значение столбца должно начинаться с символов значение!. В приводимом ниже примере поле значение STOLBEZ должно начинаться с симво- лов USD. CREATE TABLE TBL( CHECK (STOLBEZ STARTING WITH "USD") ; ...) ; Может быть задана комбинация условий, которым должно соответствовать значе- ние. В этом случае отдельные условия соединяются операторами AND или OR. Для многих из приведенных условий (см. CHECK) может быть выдано отрицание при помощи слова NOT. Например: CHECK(NOT STOLBEZ1 > STOLBEZ2); В качестве <значение> можно указывать имя столбца, константу, выражение, функцию. Могут использоваться следующие стандартные функции: COUNT - счетчик повторения, SUM — сумма, A VG - среднее значение, МАХ - максимальное значение, MIN - минимальное значение, CAST - приведение типов, UPPER - приведение всех букв к заглавным, GEN ID - возвращает уникальное значение генератора. 20.9. ИЗМЕНЕНИЕ ОБЪЯВЛЕНИЯ ТАБЛИЦЫ Оператор ALTER TABLE позволяет: • добавить определение нового столбца; • удалить столбец из таблицы; • удалить атрибуты целостности таблицы или отдельного столбца; • добавить новые атрибуты целостности. Перед изменением каких-либо атрибутов столбца данные, которые хранятся в нем, нужно сохранить. Для этого в таблице определяют временный столбец, в точности повто- ряющий все характеристики того столбца, который планируется изменить. Затем данные из изменяемого столбца копируют во временный столбец (используя, например, оператор UPDATE). После этого столбец, подлежащий изменению, попросту удаляют из таблицы, а на его месте создают новый, одноименный столбец с желаемыми атрибутами. В заключе- ние в него копируют данные из временного столбца, а временный столбец уничтожают. Пример. Пусть имеется таблица CREATE TABLE SOTR( ID_SOTR INTEGER NOT NULL PRIMARY KEY, FIO CHAR(10) COLLATE PXW_CYRL, OTDEL VARCHAR(10) COLLATE PXW_CYRL, DOLJNOST CHAR(10) COLLATE PXW_CYRL);
Создание таблиц 341 Пусть необходимо изменить характеристики столбца FIO, изменив тип столбца с CHAR(IO) на VARCHAR(25). 1. Добавляем в таблицу новый временный столбец FIOTMP, полностью повто- ряющий характеристики изменяемого столбца FIO: ALTER TABLE SOTR ADD FIO_TMP CHAR(IO) CHARACTER SET WIN1251 COLLATE PXW_CYRL; 2. Копируем данные из FIO в FIO TMP-. UPDATE SOTR SET FIO_TMP = FIO; 3. Удаляем столбец FIO: ALTER TABLE SOTR DROP FIO; 4. Создаем новый столбец FIO с необходимыми характеристиками: ALTER TABLE SOTR ADD FIO VARCHAR(25) COLLATE PXW_CYRL; 5. Переписываем данные из временного столбца FIO TMP в новый столбец FIO: UPDATE SOTR SET FIO = FIO_TMP; 6. Удаляем временный столбецFIO_TMP: ALTER TABLE SOTR DROP FIO_TMP; Замечание. Следует помнить, что изменение характеристик столбца, а также уда- ление столбца может закончиться неудачей, если: • столбец приобретает атрибуты PRIMARY KEY или UNIQUE, но старые значения в столбце нарушают требования уникальности данных; • удаляемый столбец входил как часть в первичный или внешний ключ, что при- вело к нарушению ссылочной целостности между таблицами; • столбцу были приписаны ограничения целостности CHECK на уровне таблицы; • столбец использовался в иных компонентах БД - в просмотрах, триггерах, в выражениях для вычисляемых столбцов. Все вышесказанное свидетельствует о том, что в случае необходимости изменения атрибутов столбца или в случае удаления столбца сначала необходимо тщательно про- анализировать, какие последствия для таблицы и базы данных в целом может повлечь такое изменение или удаление. 20.10. ИЗМЕНЕНИЕ АТРИБУТОВ СТОЛБЦА Добавление нового столбца в таблицу БД производится оператором ALTER TABLE <имя таблицы» ADD Сопределения столбца»; Добавление новых ограничений целостности: ALTER TABLE <имя таблицы» ADD [CONSTRAINT <имя ограничения»] Сопределения целостности»;
342 Глава 20 Удаление столбца (столбцов) из таблицы: ALTER TABLE <имя таблицы» DROP <имя столбца!»[,<имя столбца2».. . ] ; Удаление ограничений целостности (уровень таблицы): ALTER TABLE <имя таблицы» DROP <имя ограничения целостности»; Пример. Для таблицы PRIHOD CREATE TABLE PRIHOD( ID_PRIHOD INTEGER NOT NULL PRIMARY KEY, DATAPRIH DATE NOT NULL, TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, KOLVO INTEGER NOT NULL, CONSTRAINT PO_TOVARU FOREIGN KEY(TOVAR) REFERENCES SPR_TOVAR); удалить целостность PO_TOVARU: ALTER TABLE PRIHOD DROP PO_TOVARU; Замечание. Для удаления непоименованной целостности придется использо- вать ее системное имя. Его можно узнать из системной таблицы БД с именем RDBSRELATIONCONSTRAINTS. 20.11. УДАЛЕНИЕ ТАБЛИЦЫ Удаление таблицы целиком производится оператором DROP TABLE <имя таблицы»; Удаление может быть блокировано для родительских таблиц, для которых в до- черних таблицах (на данный момент не удаленных) имеются ссылки по внешнему ключу этих таблиц. Действительно, удаление родительской таблицы разрушило бы ссылочную целостность. Поэтому следует либо удалить ограничения ссылочной цело- стности во всех дочерних таблицах, либо - по необходимости - сначала удалить сами дочерние таблицы, а затем уже удалять родительскую.
Глава 21 РАБОТА С ИНДЕКСАМИ 21.1. ЛОГИЧЕСКОЕ РАЗДЕЛЕНИЕ НА КЛЮЧИ И ИНДЕКСЫ В InterBase разделено понятие ключей и индексов. Это разделение, впрочем, имеет логическую окраску и не затрагивает физическую организацию данных. Первичный [PRIMARYKEY) и внешний [FOREIGN KEY) ключи строятся для обес- печения ссылочной целостности реляционно связанных таблиц. Первичный ключ, по- мимо этого, выполняет функции поддержания уникальности своих значений, что обу- словлено его основным назначением - однозначно характеризовать запись в таблице. Для таких же целей может использоваться и просто уникальный ключ [UNIQUE). В отличие от ключей индексы, создаваемые оператором CREATE INDEX, служат для сортировок и оптимизации доступа к данным. В конечном счете ключи и индексы преобразуется в физические индексы - специальный механизм, обеспечивающий бы- стрый доступ к данным. Однако если индексам можно назначить имя, то физические индексы, реализованные на основе определений ключей, строятся и именуются систе- мой автоматически. Например, для таблицы SOTR будут построены два физических индекса - по первичному ключу и по полю DLJ: CREATE TABLE SOTR (ID_SOTR INTEGER NOT NULL, OTDEL VARCHAR(IO) CHARACTER SET WIN1251 COLLATE PXW_CYRL, DOLJNOST CHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL, FIO VARCHAR(25) CHARACTER SET WIN1251 COLLATE PXW_CYRL, PRIMARY KEY (ID_SOTR)); CREATE INDEX DLJ ON SOTR (DOLJNOST); но именоваться они будут следующим образом: DLJ INDEX ON SOTR(DOLJNOST) RDB$PRIMARY18 UNIQUE INDEX ON SOTR(ID_SOTR) Заметим, что вывести определения всех индексов БД можно оператором SHOW INDEX; а проделать то же для конкретной таблицы можно оператором SHOW INDEX <имя таблицы>; 21.2. НЕОБХОДИМОСТЬ СОЗДАНИЯ ИНДЕКСОВ Индексы необходимо создавать в случае, когда по столбцу или группе столбцов: • часто производится поиск в БД (столбец или группа столбцов часто перечисля- ются в предложении WHERE оператора SELECT)', • часто строятся объединения таблиц; • часто производится сортировка (то есть столбец или столбцы часто использу- ются в предложении ORDER BY оператора SELECT).
344 Глава 21 Не рекомендуется строить индексы по столбцам или группам столбцов, которые: • редко используются для поиска, объединения и сортировки результатов запро- сов; • часто меняют значение, что приводит к необходимости часто обновлять индекс и способно существенно замедлить скорость работы с БД; • содержат небольшое число вариантов значения. В случае, когда при выполнении запросов используется сортировка по одним и тем же столбцам, необходимо помнить, что создание единого индекса по этим столбцам способно ускорить выполнение запросов. Однако: • при использовании в запросах не всех столбцов из этого индекса следует ис- пользовать только непрерывную их последовательность; например, если индекс построен по столбцам Pl, Р2, РЗ, Р4, то в некотором операторе SELECT можно указать SELECT ... ORDER BY Р1,Р2,РЗ но никак не SELECT... ORDER BY Р1,Р2,Р4 ИЛИ SELECT... ORDER BY P1,P3,P4 поскольку в последних двух случаях индекс не будет использован; • последовательность указания в предложении ORDER BY столбцов является важной; так, для индекса, построенного по столбцам Pl, Р2, РЗ, Р4, указание SELECT ... ORDER BY Р2,Р1,РЗ не приведет к использованию индекса для сортировки результирующего набора данных; • при частом использовании в условной части WHERE, оператора SELECT не- скольких столбцов, связанных между собой операцией «или» (OR): SELECT ... WHERE Pl = значение! OR P2 = значение2 OR РЗ = ... вместо индекса по столбцам Pl, Р2, РЗ лучше создать несколько индексов, постро- енных по каждому из этих полей, поскольку в противном случае будет осуществлен последовательный просмотр всей таблицы. Это неудивительно, поскольку индексно- последовательный доступ для индекса по столбцам Pl, Р2, РЗ может быть осуществ- лен только для столбца Р1; значения столбцов Р2 и РЗ в этом случае спонтанно раз- бросаны по индексу. 21.3. СОЗДАНИЕ ИНДЕКСА Индекс может быть создан оператором CREATE [UNIQUE] [ASC[ENDING] I DESC[ENDING]] INDEX ИмяИндекса ON ИмяТаблицы (столбец! [,столбец2 ...]); • UNIQUE - требует создания уникального индекса, не допускающего одинако- вых значений индексных полей для разных записей таблицы;
Работа с индексами 345 • ASC[ENDING] - указывает на необходимость сортировки значений индексных полей по возрастанию (режим принят по умолчанию); • DESCENDING] - указывает на необходимость сортировки значений индекс- ных полей по убыванию; • ИмяИндекса - имя создаваемого индекса; • table - имя таблицы, для которой создается индекс; • столбецИ - имена столбцов, по которым создается индекс. Например, для таблицы PRIHOD CREATE TABLE PRIHOD( ID_PRIHOD INTEGER NOT NULL PRIMARY KEY, DATAPRIH DATE NOT NULL, TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, KOLVO INTEGER NOT NULL); создать индекс в порядке убывания значений DATAPRIHи TOVAR'. CREATE DESC INDEX D_P ON PRIHOD (DATAPRIH, TOVAR); 21.4. УЛУЧШЕНИЕ ПРОИЗВОДИТЕЛЬНОСТИ ИНДЕКСА После многократного внесения изменений в таблицу БД индексы этой таблицы мо- гут быть разбалансированы. Разбалансировка приводит к тому, что «глубина» индекса (depth) возрастает сверх критического значения. «Глубина» индекса — параметр, пока- зывающий максимальное количество операций, необходимых для нахождения иско- мого значения в таблице БД с использованием данного индекса. В случае разбаланси- ровки индекса его ценность при выполнении запросов снижается из-за увеличения времени выполнения запроса. Поэтому время от времени необходимо производить одно из перечисленных ниже действий: • выполнять перестройку индекса оператором ALTER INDEX', • пересчитать показатель «выбираемости» индекса оператором SET STATISTICS', • уничтожать индекс оператором DROP INDEX и заново создавать его операто- ром CREATE INDEX. 21.4.1. Перестройка индекса Перестройка индекса заключается в пересоздании и балансировке индекса. Эти операции начинаются после деактивизации индекса ALTER INDEX <имя индекса> DEACTIVATE; и последующей его активизации ALTER INDEX <имя индекса> ACTIVATE; Деактивизация индекса полезна также в том случае, когда в таблицу БД вставляет- ся большое число записей одновременно: при активном индексе изменения в него вносятся при добавлении каждой записи, что замедляет доступ к данным. Следует помнить, что: • нельзя перестроить индекс, если он используется в данный момент в запросах других пользователей;
346 Глава 21 • нельзя перестроить индекс, созданный в результате определения первичного и внешнего ключей, а также уникальности значений столбца или группы столб- цов (PRIMARY KEY, FOREIGN KEY, UNIQUE), - для этой цели следует приме- нять оператор ALTER TABLE-, • для выполнения оператора ALTER INDEX нужно иметь соответствующие при- вилегии доступа к БД. 21.4.2. Повторное вычисление показателя «полезности» индекса Показатель «полезности» индекса основан на сведениях о числе повторяющихся строк индекса. Он используется InterBase при доступе к таблице для выработки опти- мального плана удовлетворения запроса. Пересчет показателя «полезности» произво- дится оператором SET STATISTICS INDEX <имя индекса>; Заметим, что SET STATISTICS не перестраивает индекс - для этой цели необходи- мо использовать оператор ALTER INDEX. Для выполнения оператора SET STATISTICS нужно иметь соответствующие привилегии доступа к БД. 21.5. УДАЛЕНИЕ СУЩЕСТВУЮЩЕГО ИНДЕКСА Для удаления индекса, ранее созданного оператором CREATE INDEX, используется оператор DROP INDEX <имя индекса>; Нельзя удалить индекс, созданный в результате определения первичного и внешне- го ключей, а также уникальности значений столбца или группы столбцов (PRIMARY KEY, FOREIGN KEY, UNIQUE). Для этой цели следует использовать оператор ALTER TABLE. Нельзя также удалить индекс, используемый в данный момент в других запросах. Кроме этого, для удаления индекса нужно иметь соответствующие привилегии досту- па к БД.
Глава 22 КОМПОНЕНТ TDATABASE Компонент TDataBase создается для каждого факта соединения сессии с отдельной БД. Если для факта конкретного соединения в программе не предусмотрен компонент TDatabase, создается временный компонент TDatabase. Таким образом, каждая откры- тая БД имеет свой компонент TDataBase. Список активных БД данной сессии содер- жится в коллекции TSession.Databases, каждый элемент которой имеет тип TDatabase. Число активных БД сессии определяется через свойство TSession.DatabaseCount. Случаи явного использования в приложении компонента TDatabase чаще всего связаны с работой в архитектуре клиент-сервер. Явно определенный в программе компонент TDatabase облегчает управление БД, создавая постоянные соединения с ней, настраивая сеанс соединения с сервером, управляя транзакциями и создавая ло- кальные псевдонимы BDE. Разумеется, каждый набор данных, работающий с таблицами одной и той же БД, может иметь с этой БД отдельное соединение. Однако это нерационально, поскольку на каждое соединение затрачиваются системные ресурсы. Для минимизации их ис- пользования рекомендуется создавать единственное соединение с БД при помощи компонента TDatabase, а все наборы данных и компоненты, реализующие действия над БД, соединять с этим компонентом. Свойство property AliasName: TSymbolStr; указывает псевдоним BDE, ассоциированный с компонентом TDatabase. Если запол- нено свойство DriverName, значение свойства AliasName очищается. Свойство property DatabaseName: TFileName; определяет локальный псевдоним приложения, который может использоваться при доступе к БД вместо псевдонима BDE, пути к файлам БД или имени БД. Значение, определяемое данным свойством, показывается в выпадающем списке свойства DatabaseName компонентов ТТаЫе, TQuery и TStoredProc при разработке приложения. Свойства соединения компонента TDatabase с удаленной БД определяются: • параметрами псевдонима БД; • параметрами драйвера БД (например, InterBase)-, • общесистемными установками в утилите BDE Administrator. Эти установки могут изменяться во время разработки приложения в редакторе ба- зы данных. Для этого нужно щелкнуть по компоненту TDatabase правой кнопкой мы- ши и во вспомогательном меню выбрать опцию Database Editor. В диалоговом окне редактора БД (рис. 22.1) в поле Parameter overrides можно переустановить некоторые параметры БД и драйвера.
348 Глава 22 Рис. 22.1. Редактор компонента TDatabase. Чтобы вернуться к параметрам псевдонима, принятым по умолчанию, нужно на- жать кнопку Defaults. Не рекомендуется явно указывать значение параметра SERVER NAME, например, SERVER NAME=D:\BOOK\IB_SKLAD\Ib_sklad.gdb поскольку при изменении местонахождения БД придется изменять значения, свойства DatabaseName, возможно, в нескольких компонентах TDatabase, вместо того чтобы изменить путь к БД в единственном определении псевдонима с помощью утилиты BDE Administrator. На панели Options имеется два поля. Снятие отметки с поля Login Prompt приводит к тому, что при соединении с БД блокируется диалог запроса имени пользователя и пароля. В этом случае имя пользователя и пароль нужно указать в параметрах соеди- нения (в поле Parameter), например: USER_NAME=SYSDBA PASSWORD=masterkey Напоминание. Еще раз напоминаем, что знак подчеркивания между слова- ми USER name нужно ставить только при связи с сервером версии 5.5 (поставляется с Delphi 5). Д ля более ранних версий этот знак ставить нельзя! Отметка поля Login Prompt равносильна присваиванию значения True свойству property LoginPrompt: Boolean; Поле отметки Keep Inactive Connection в отмеченном состоянии означает, что соеди- нение с БД будет храниться даже в том случае, если ни один набор данных, работающий с этой БД, не открыт. Отметка поля аналогична установке значения True в свойство property KeepConnection: Boolean;
Компонент TDatabase 349 Свойство property Connected: Boolean; указывает, имеет ли компонент TDatabase активное соединение с БД. Свойства соединения с БД могут быть переустановлены во время работы програм- мы. Например: Databasel.Params[1 USER NAME '] := 'PASHA'; Databasel.Params['PASSWORD'] := 'ppp'; При этом следует помнить, что в момент смены параметров не должно быть активного соединения с БД, то есть в свойство Connected должно быть установлено значение False. Рассмотрим другие свойства компонента TDatabase. Свойство Назначение property DatasetCount: Integer; Определяет число активных НД, связанных с настоящей БД (компоненты ТТаЫе и TQuery). property Datasets[Index: Integer]: TDBDataSet; n Представляет собой коллекцию активных НД (компоненты TDBDataSet), связанных с на- стоящей БД. Число компонентов определяется свойством DatasetCount. Минимальный ин- декс 0, максимальный DatasetCount - 1. property DriverName: TSymbolStr; Указывает имя драйвера BDE, такого как STANDARD (dBASE и Paradox), ORACLE, INTERBASE и т. п. Значение свойства очища- ется, если устанавливается свойство AliasName, и наоборот. property IsSQLBased: Boolean; Указывает, ассоциирован ли данный компо- нент TDatabase с ^^.-ориентированной БД. property SessionName: String; Указывает компонент TSession, с которым связана БД. property Temporary: Boolean; Указывает, создавать ли временный компонент TDatabase для БД, для которых компоненты TDatabase не определены в приложении явно. property Translsolation: TTransIsolation; Устанавливает уровень изоляции транзакций для 50£-сервера. property InTransaction: Boolean; Возвращает True, если для компонента в дан- ный момент существует активная транзакция. Методы: Метод Назначение procedure StartTransaction; Инициирует начало транзакции. Если в этот момент активна некоторая транзакция, возбу- ждается исключение. Транзакционные изме- нения в наборах БД, имевшие место после выполнения метода StartTransaction, либо подтверждаются методом Commit, либо отме- няются методом Rollback. До подтверждения или отмены изменений транзакция, начатая StartTransaction, считается активной.
350 Глава 22 procedure Commit; Подтверждает текущую транзакцию, т. е. под- тверждает все модификации в БД, имевшие место с момента последнего вызова метода StartTransaction. Если ни одна транзакция не активна, возбуждается исключение. procedure Rollback; Откатывает текущую транзакцию, т. е. отме- няет все модификации в БД, имевшие место с момента последнего вызова метода StartTransaction. procedure ApplyUpdates(const DataSets: array of TDataSet); Применяется для подтверждения кэширован- ных изменений сразу в нескольких наборах данных. Список НД определяется параметром DataSets. В случае указания нескольких НД их имена разделяются запятыми. procedure Close; Закрывает БД и все связанные с ней открытые НД. procedure CloseDatasets; Закрывает открытые НД, связанные с БД, но не закрывает саму БД. procedure Open; Открывает БД, соединяя компонент TDatabase с сервером или BDE для Paradox или dBASE. Событие TLoginEvent = procedure(Database: TDatabase; LoginParams: TStrings) of object; property OnLogin: TLoginEvent; возникает, когда компонент TDatabase начинает сессию соединения с ^/.-сервером.
Глава 23 ОПЕРАТОР SELECT Оператор SELECT - наиболее часто используемый оператор SQL. Он позволяет производить выборки данных из ТБД и преобразовывать к нужному виду полученные результаты. С его помощью можно реализовать весьма сложные условия выбора дан- ных из различных таблиц. В самом общем виде он имеет следующий формат: SELECT [DISTINCT | ALL] {* | <значение1> [, <значение2> ...]} FROM <таСлица1> [, < таблица2> ...] [WHERE <условия_поиска>] [GROUP BY столбец [COLLATE collation] [, столбец! [COLLATE collation] ...] [HAVING < условия_поиска >] [UNION <onepaTop_select>] [PLAN <план_ выполнения_запроса>] [ORDER BY <список_столбцов>] 23.1. ПРОСТЕЙШИЙ ВИД ОПЕРАТОРА SELECT (SELECT...FROM) В простейшем случае, когда требуется создание НД, состоящего из всех записей одной или нескольких таблиц, оператор SELECT имеет такой вид: SELECT {* I <значение1> [, <значение2> ...]} FROM <таблица1> [, < таблица2> ...] После ключевого слова SELECT приводится список значений, каждое из которых определяет столбец возвращаемого оператором результирующего набора данных. В большинстве случаев - это имена столбцов таблиц, перечисленных после слова FROM. Звездочка «*» указывает, что в результат выполнения запроса нужно включить все столбцы той или иной таблицы. После FROM указывается список ТБД, из которых будет происходить выборка данных. Например, создать набор данных, состоящий из всех столбцов и всех записей таб- лицы RASHOD, можно с помощью такого оператора: SELECT * FROM RASHOD Такой же НД можно получить, если вместо звездочки перечислить все столбцы табли- цы: SELECT N_RASH, DAT_RASH, KOLVO, TOVAR, POKUP FROM RASHOD Результат выборки показан на рис. 23.1.
352 Глава 23 NRASH (DAT.RASH |KOLVO | TOVAR н н I POKUP Ш ► ЖЖШ 10.01-97 20 Сахар Лира, ТОО J 3 10.01.97 509 Сахар 1 4 10.01.97 3000 Ставрида консерв. Адмирал, АО 5 10.01.97 4000 Кока-кола Саяны, ИЧП Б 20.01.97 30 Сахар Саяны, ИЧП 7 20.01.97 20 Кока-кола 1 1 8 20.01.97 1000 Кока-кола Адмирал, АО 1 10.01.97 100 Кока-кола Адмирал, АО Рис. 23.1. Выборка всех столбцов таблицы RASHOD. 23.2. ИСПОЛЬЗОВАНИЕ ПРЕДЛОЖЕНИЯ WHERE Предложение WHERE используется для включения в НД лишь нужных записей. В этом случае оператор SELECT имеет следующий формат: SELECT {* | <значение1> [, <значение2> ...]} FROM <таблица1> [, < таблица2> ...] WHERE <условия_поиска> В набор данных, возвращаемый оператором SELECT, будут включаться только те записи, которые удовлетворяют условию поиска, указанному после WHERE. Далее будут рассмотрены варианты формирования разнообразных условий поиска, а пока разберем два простейших условия. 23.2.1. Сравнение значения столбца с константой При сравнении значения столбца с константой условие поиска имеет такой вид: <имя столбца> <оператор> <константа> где <оператор> - одна из следующих операций отношения: равно меньше больше меньше или равно больше или равно не меньше (т. е. больше или равно) не больше (т. е. меньше или равно) не равно не равно В качестве константы явно указываются строковые или числовые значения. Например, выбрать из таблицы RASHOD все операции отпуска товаров объемом 20 единиц можно так (рис. 23.2): SELECT * FROM RASHOD R WHERE KOLVO =20 Puc. 23.2. Выборка фактов отпуска товаров объемом 20 единиц.
Оператор SELECT 353 23.2.2. Внутреннее соединение таблиц При сравнении значения столбца одной таблицы со значением столбца другой таб- лицы условие поиска имеет следующий вид: Симя столбца таблицы 1> <оператор> Симя столбца таблицы 2> Чтобы выбрать все записи о расходе товара из таблицы RASHOD и для каждого то- вара указать его цену из таблицы TOVARY, можно использовать такой оператор (рис. 23.3): SELECT RASHOD.*, TOVARY.ZENA FROM RASHOD, TOVARY WHERE RASHOD.TOVAR = TOVARY.TOVAR Puc. 23.3. Соединение таблиц RASHOD и TOVARY. При выполнении оператора для каждой записи из таблицы RASHOD ищется запись в таблице TOVARY, у которой значение в поле TOVAR совпадает со значением в поле TOVAR текущей записи таблицы RASHOD. При этом безразлично, в каком порядке перечислять таблицы в условии поиска, т. е. безразлично, какая из таблиц будет упомянута слева, а какая справа. Следующие условия поиска идентичны: RASHOD.TOVAR = TOVARY.TOVAR TOVARY.TOVAR = RASHOD.TOVAR Такой способ соединения таблиц называется внутренним соединением (о внешних соединениях см. п. 23.15). При внутреннем соединении двух таблиц АтлВ логический порядок формирования результирующего НД можно представить себе следующим образом. 1. Из столбцов, которые указаны после слова SELECT, составляется промежуточ- ный НД путем сцепления результирующих столбцов каждой записи из таблицы А и результирующих столбцов записи из таблицы В. 2. Из получившегося НД отбрасываются все записи, не удовлетворяющие условию поиска в предложении WHERE. Замечание. Фактический порядок выполнения запроса конкретным SQL- сервером может быть другим.
354 Глава 23 Пример. Пусть имеются такие таблицы А и В: Таблица А ______________ Таблица В Столбец Р1 Столбец Р2 Столбец РЗ а X 400 ь X 200 с У 500 d Столбец Р1 Столбец Р2 X 1 У 2 Z 2 Тогда выполнение оператора SELECT A.Pl, А.Р2, В.Р2 FROM А, В WHERE А.Р2 = В.Pl с логической точки зрения приведет к формированию такого промежуточного НД: Столбец А.Р1 Столбец А.Р2 Столбец В.Р1 Столбец В.Р2 а X X 1 а X У 2 а X Z 2 ь X X 1 ь X У 2 ь X Z 2 с У X 1 с У У 2 с У Z 2 Примечание. Столбец В.Р1, который не включен в результат запроса, показан для наглядности. Затем путем исключения из промежуточного набора записей, не удовлетворяющих условию А.Р2 = В.Р1, будет получен такой результирующий НД: Столбец А.Р1 Столбец А.Р2 Столбец В.Р2 а X 1 ь X 1 с У 2 Пример. Получить все записи о расходе товара из таблицы RASHOD. Jijin каждого покупателя указать его адрес из таблицы POKUPATELI: SELECT RASHOD.*, POKUPATELI.ADRES FROM RASHOD, POKUPATELI WHERE POKUPATELI.POKUP = RASHOD.POKUP Как можно видеть из рис. 23.4, в результирующий НД не включены: 1. записи из таблицы RASHOD с пустым наименованием покупателя, т. к. нет ни од- ной записи с пустым наименованием покупателя в таблице POKUPATELI', 2. запись из таблицы POKUPATELI для покупателя Геракл, поскольку для этого покупателя нет записей в таблице RASHOD.
Оператор SELECT 355 N_RASH |DAT_RASH|KOLVO jTOVAR | POKUP Iadres ► 10.01.97 20 Сахар Лира, ТОО пр-т. Стромынка, 21 □ 4 10.0197 3000 Ставрида консерв. Адмирал, АО ул. Строителей, 2 5 10.01.97 4000 Кока-кола Саяны, ИЧП пр-д Сталеваров, 8 6 20.01.97 30 Сахар Саяны, ИЧП пр-д Сталеваров, 8 8 20.01.97 1000 Кока-кола Адмирал, АО ул. Строителей, 2 1 10.01.97 100 Кока-кола Адмирал, АО ул. Строителей, 2 d Рис. 23.4. Соединение таблиц RASHOD и POKUPATELI по полю POKUP. Чтобы получить внутреннее соединение таблицы RASHOD со столбцом ADRES из таблицы POKUPATELI, нужно удалить из оператора SELECT предложение WHERE'. SELECT RASHOD.*, POKUPATELI.ADRES FROM RASHOD, POKUPATELI Результат выполнения запроса не приводим из-за его величины (8 * 4 = 32 записи). Заметим, что внутреннее соединение таблиц БД, реализованное при помощи логи- ческих условий в предложении WHERE оператора SELECT, можно также реализовать с помощью предложения INNER JOIN. SELECT СпиСокСтолбцов FROM Таблица! Т1 INNER JOIN Таблица2 Т2 ON Условие Пример. Каждый из приводимых ниже запросов реализует внутреннее соединение таблиц TOVARY и RASHOD по столбцу POKUP и возвращает одинаковый результи- рующий НД: SELECT R.DAT_RASH,R.TOVAR, R.KOLVO, P.POKUP FROM RASHOD R, POKUPATELI P WHERE R.POKUP = P.POKUP ORDER BY P.POKUP SELECT R.DAT_RASH,R.TOVAR, R.KOLVO, P.POKUP FROM RASHOD R INNER JOIN POKUPATELI P ON R.POKUP = P.POKUP ORDER BY P.POKUP 23.3. ИСПОЛЬЗОВАНИЕ ПСЕВДОНИМОВ ТАБЛИЦ В рассмотренных выше примерах связывания таблиц в перечнях возвращаемых столбцов после слова SELECT и в условии поиска после WHERE перед именем столб- ца через точку пишется название таблицы, например: WHERE POKUPATELI.POKUP = RASHOD.POKUP Указание имени таблицы перед именем столбца необходимо в том случае, когда в разных таблицах есть одноименные столбцы (как в нашем примере). Использование имен таблиц для идентификации столбцов неудобно из-за своей громоздкости. Намно- го лучше присвоить каждой таблице какое-нибудь краткое имя. Такие имена называ- ются псевдонимами таблиц. Они отделяются пробелом от фактического имени табли- цы в списке таблиц после слова FROM'. SELECT ... FROM <таблица1 псевдоним1> [, < таблица2 псевдоним2> ...] WHERE ...
356 Глава 23 Например, запрос SELECT RASHOD.*, POKUPATELI.ADRES FROM RASHOD, POKUPATELI WHERE POKUPATELI.POKUP = RASHOD.POKUP после введения в него псевдонимов таблиц выглядит намного компактнее: SELECT R.*, Р.ADRES FROM RASHOD R, POKUPATELI P WHERE P.POKUP = R.POKUP 23.4. ПРЕДЛОЖЕНИЕ ORDER BY - ОПРЕДЕЛЕНИЕ СОРТИРОВКИ Результирующий НД можно отсортировать с помощью предложения ORDER BY <список_столбцов> Список столбцов содержит имена столбцов, по которым будет производиться сор- тировка. Если указаны два и более столбцов, первый столбец будет использован для глобальной сортировки, второй столбец - для сортировки внутри группы, определяе- мой единым значением первого столбца, и т. д. Пример. Показать все записи отпуска товара «Кока-кола», отсортировав их по ка- ждому покупателю (рис. 23.5): SELECT POKUP, DAT_RASH, TOVAR, KOLVO FROM RASHOD WHERE TOVAR = "Кока-кола" ORDER BY POKUP В следующем примере все записи из таблицы RASHOD, сортируются по имени по- купателя (рис. 23.6): | POKUP |DAT_RASH | TOVAR | KOLVO 3 1 i 20.01.97 Кока-кола 1000 □ Адмирал. AO 10.01.97 Кока-кола 100 Саяны, ИЧП 10.01.97 Кока-кола 4000 20.01.97 Кока-кола 20 Рис. 23.5. Сортировка по полю POKUP для товара «Кока-кола». I POKUP DAT_RA9H | TOVAR KOLVO 1-1 ► ;Адмирал, AO 20.01.97 Кока-кола юооЗ Адмирал. AO 10.01.97 Ставрида консерв. 3000 Адмирал, AO Лира, TOO 10.01.97 10.01.97 Кока-кола Сахар 100 20 Саяны, ИЧП 20.01.97 Сахар 30 Саяны. ИЧП 10.01.97 Кока-кола 4000 20.01.97 Кока-кола 20 10.01.97 Сахар 509 Рис. 23.6. Сортировка записей таблицы RASHOD по имени покупателя. SELECT POKUP, DAT_RASH, TOVAR, KOLVO FROM RASHOD ORDER BY POKUP
Оператор SELECT 357 Выбрать все записи из таблицы RASHOD, отсортировав их по именам покупателей, а для каждого покупателя - по дате (рис. 23.7): SELECT POKUP, DAT_RASH, TOVAR, KOLVO FROM RASHOD ORDER BY POKUP, DAT_RASH I pokup |DAT_RASH|TOVAR Ikolvo Ш ► Адмирал, АО 10.01.97 Кока-кола 100 □ Адмирал, АО 10.01.97 Ставрида консерв. 3000 Адмирал. АО 20.01.97 Кока-кола 1000 Лира, ТОО 10.01.97 Сахар 20 Саяны, ИЧП 10.01.97 Кока-кола 4000 Саяны. ИЧП 20.01.97 Сахар 30 10.01.97 Сахар 509 20.01.97 Кока-кола 20 d Puc. 23.7. Сортировка no имени и no дате. В следующем запросе сортировка осуществляется сначала по имени покупателя, затем по названию товара и, наконец, по дате (рис. 23.8): SELECT POKUP, TOVAR, DAT_RASH, KOLVO FROM RASHOD ORDER BY POKUP, TOVAR, DAT_RASH POKUP | TOVAR |dat_rash Ikolvo Ы ► Адмирал, АО ЯК Кока-кола 10.01.97 100 _J Адмирал, АО Кока-кола 20.01.97 1000 Адмирал, АО Ставрида консерв. 10.01.97 3000 Лира, ТОО Сахар 10.01.97 20 Саяны. ИЧП Кока-кола 10.01.97 4000 Саяны, ИЧП Сахар 20.01.97 30 Кока-кола 2001.97 20 -f Сахар 10.01.97 509 >[ Puc. 23.8. Сортировка no имени покупателя, наименованию товара и дате отпуска. 23.5. УСТРАНЕНИЕ ПОВТОРЯЮЩИХСЯ ЗНАЧЕНИЙ Часто в результирующий НД необходимо включать не все записи с одинаковым значением какого-либо столбца (комбинации столбцов), а только одну из них. В этом случае после SELECT указывают ключевое слово DISTINCT, с помощью которого уст- раняются повторяющиеся записи: SELECT [DISTINCT | ALL] {* | <значение1> [, <значение2> ...]} FROM <таблица1> [, < таблица2> ...] Повторяющимися считаются записи, содержащие идентичные значения во всех столбцах результирующего НД. Наоборот, если в результирующий запрос нужно включить все записи, после SELECT указывают слово ALL. В InterBase, как и во мно- гих других серверах, нет необходимости использовать ALL явно, поскольку это значе- ние действует по умолчанию. Чтобы получить только наименования всех отпущенных со склада товаров, можно использовать такой запрос (рис. 23.9):
358 Глава 23 SELECT DISTINCT TOVAR FROM RASHOD Puc. 23.9. Справка о наименовании отпущенных товаров. Следует заметить, что использование DISTINCT может существенно замедлять вы- полнение запросов, т. к. сервер будет тратить дополнительные ресурсы на проверку каждой записи. Обычно DISTINCT применяется в запросах, использующих агрегатные функции. 23.6. РАСЧЕТ ЗНАЧЕНИЙ ВЫЧИСЛЯЕМЫХ СТОЛБЦОВ Для расчета значений вычисляемых столбцов результирующего НД используются арифметические выражения. При этом в списке возвращаемых столбцов после SELECT вместо имени вычисляемого столбца указывается выражение: SELECT [DISTINCT | ALL] {* | <столбец1> [, <выражение1> ...]} FROM <таблица!> [, < таблица2> ...] Например, следующий запрос возвращает все записи об отпуске товаров из табли- цы RASHOD и для каждого факта отпуска товара рассчитывает общую стоимость от- пущенного товара (рис. 23.10): SELECT R.*, T.ZENA, R.KOLVO * T.ZENA FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR N RASH I DAT-RASH 1 KOLVO | TOVAR | POKUP [ZENA | COLUMN?. | та ► В 10.01.97 20 Сахар Лира, ТОО 4 80 □ 3 10.01.97 509 Сахар 4 2036 4 10.01 97 3000 Ставрида консерв. Адмирал, АО 5 15000 5 10.01.97 4000 Кока-кола Саяны, ИЧП 3 12000 6 20.01.97 30 Сахар Саяны, ИЧП 4 120 7 20.01.97 20 Кока-кола 3 60 8 20.01.97 1000 Кока-кола Адмирал, АО 3 3000 1 10.01 97 >100 Кока-кола Адмирал, АО 3 300 d Puc. 23.10. Вычисление стоимости отпущенного товара Как видно из рисунка, результат вычисления выражения R.KOLVO*T.ZENA для ка- ждой записи из таблицы RASHOD помещен в столбец, которому по умолчанию при- своено имя COLUMN7 (7 - порядковый номер столбца в результирующем НД). Если вычисляемому столбцу нужно присвоить нестандартное имя, это имя указывается по- сле выражения вслед за ключевым словом AS: SELECT ... {* I <значение1> [, <выражение1 [AS Симя столбца:»]> ...]] Например, использованному в предыдущем примере вычисляемому столбцу мож- но присвоить имя STOIM следующим образом: SELECT R.*, T.ZENA, R.KOLVO * T.ZENA AS STOIM FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR
Оператор SELECT 359 23.7. АГРЕГАТНЫЕ ФУНКЦИИ Агрегатные функции предназначены для вычисления итоговых значений операций над всеми зацисями НД. К агрегатным относятся следующие функции: • COUNT (<выражение>) - подсчитывает число вхождений значения выражения во все записи результирующего НД; • 5иМ(<выражение>) - суммирует значения выражения-, • А УС(<выражение>) - находит среднее значение; • МАХ(<выражение>) - определяет максимальное значение; • МШ(<выражение>) - определяет минимальное значение. Если из группы одинаковых записей нужно учитывать только одну, перед выра- жением в скобках включают слово DISTINCT: COUNT(DISTINCT POKUP) Чаще всего в качестве выражения выступают имена столбцов, при этом выражение может вычисляться и по значениям нескольких таблиц. В следующем запросе подсчитывается количество покупателей, приобретавших товары на складе (рис. 23.11): SELECT COUNT(DISTINCT POKUP) AS COUNT_POKUP FROM RASHOD Puc. 23.11. Количество покупателей, приобретавших товары на складе. Вычислить общую стоимость отпущенных товаров за 10.01.97 (рис. 23.12): SELECT SUM(R.KOLVO * T.ZENA) AS OBS_ZENA FROM RASHOD R, TOVARY T WHERE (R.TOVAR = T.TOVAR) AND ( R.DAT_RASH = "IO-JAN-1997") | OBS_ZENA | J 29416 Puc. 23.12. Общая стоимость отпущенных товаров 23.8. ГРУППИРОВКА ЗАПИСЕЙ Иногда требуется получить агрегированные значения (минимум, максимум, сред- нее) не по всему результирующему НД, а по каждой из входящих в него групп запи- сей, характеризующихся одинаковым значением какого-либо столбца. Например, вы- дать общее количество отпущенного товара по каждому товару. В этом случае в опе- ратор SELECT перед предложением WHERE вводится предложение GROUP BY столбец [,столбец! ...] При этом необходимо, чтобы один из столбцов результирующего НД был представлен агрегатной функцией. Вот как можно получить общее количество отпуска по каждому из товаров (рис. 23.13):
360 Глава 23 SELECT R.TOVAR, SUM(R.KOLVO) AS OTPUSK FROM RASHOD R GROUP BY R.TOVAR Puc. 23. J 3. Общее количество отпуска no каждому из товаров. Общая цена каждого их отпущенных из товаров (рис. 23.14): SELECT R.TOVAR, SUM(R.KOLVO * T.ZENA) FROM RASHOD R, TOVARY T WHERE T.TOVAR = R.TOVAR GROUP BY R.TOVAR Puc. 23.14. Общая цена каждого отпущенного товара. Общая цена по каждому из отпущенных товаров на каждую дату (рис. 23.15): SELECT R.TOVAR, R.DAT_RASH, SUM(R.KOLVO * T.ZENA) FROM RASHOD R, TOVARY T WHERE T.TOVAR = R.TOVAR GROUP BY R.TOVAR, R.DAT_RASH TOVAR | DAT.RASH SUM |_d ►] Ц 10.01.97 12300 LI Кока-кола 20.01.97 30Б0 Сахар 10.01.97 211G Сахар 20.01.97 120 I Ставрида консерв. 10.01.97 15000 ж| Puc. 23.13. Общая цена no каждому из отпущенных товаров на каждую дату. Количество покупателей на каждую дату (рис. 23.16): SELECT DAT_RASH, COUNT(DISTINCT POKUP) FROM RASHOD GROUP BY DAT_RASH Puc. 23.16. Количество покупателей на каждую дату.
Оператор SELECT 361 23.9. ПРЕДЛОЖЕНИЕ HAVING - НАЛОЖЕНИЕ ОГРАНИЧЕНИЙ НА ГРУППИРОВКУ ЗАПИСЕЙ Если нужно в результирующем НД выдавать агрегацию не по всем группам,' а только по тем из них, которые отвечают некоторому условию, после предложения GROUP BY указывают предложение HAVING < агрегатная функция > < отношение > < значение > где • агрегатная функция - одна из функций MIN, MAX, AVG, SUM; • отношение — одна из операций отношения =, о, <, <= , >, >=; • значение - константа, результат вычисления выражения или единичное значение, возвращаемое вложенным оператором SELECT. Таким образом, после НА VING указываются условия, которые отличаются от условий, определяемых в предложении WHERE, одним важным обстоятельством: в HAVING обязательно должна быть указана одна из агрегатных функций, в то время как в пред- ложении WHERE такие функции указывать нельзя. Минимальные покупки товара в единицах для всех покупателей, у которых мини- мальное количество покупаемого товара не меньше 100 единиц (рис. 23.17): SELECT POKUP, MIN(KOLVO) FROM RASHOD GROUP BY POKUP HAVING MIN(KOLVO) >= 100 Puc. 23.17. Минимальные покупки товара. Если не указывать HAVING MIN(KOLVO) >= 100 будут выданы все группы (рис. 23.18). Рис. 23.18. Минимальные покупки товара (без НА V1NG). Можно указывать различные агрегатные функции для возвращаемого столбца и ус- ловия в НА VING. Общее количество купленного товара (в единицах) для всех покупателей, у кото- рых минимальное количество покупаемого товара не меньше 100 единиц (рис. 23.19):
362 Глава 23 SELECT POKUP, SUM(KOLVO) FROM RASHOD GROUP BY POKUP HAVING MIN(KOLVO) >= 100 | POKUP |SUM 1 -1 П:Адмирал, АО М 4100 jJ, Puc. 23.19. Общее количество купленного товара. Заметим, что условия в HAVING и в WHERE имеют такие различия: • HAVING исключает из результирующего НД группы с результатами агрегиро- ванных значений; • WHERE исключает из расчета агрегатных значений по группировкам записи, не удовлетворяющие условию; • в условии поиска WHERE нельзя указывать агрегатные функции. Количество фактов отпуска товаров на каждую дату, в которых отпускалось не ме- нее 1000 единиц товара (рис. 23.20): SELECT DAT_RASH,COUNT(*) FROM RASHOD WHERE KOLVO >= 1000 GROUP BY DAT_RASH Puc. 23.20. Количество фактов отпуска товаров на каждую дату. Показать даты отпуска товаров, в которых количество отпускаемого товара было не меньше 1000 единиц. В результирующий НД включить только те группы, по кото- рым количество таких отпусков товаров больше 1 (рис. 23.21): SELECT DAT_RASH,COUNT(*) FROM RASHOD WHERE KOLVO >= 1000 GROUP BY DAT_RASH HAVING COUNT(*) > 1 Puc. 23.21. Даты отпусков товаров, в которых количество отпускаемого товара было не меньше 1000 единиц. 23.10. ЗАДАНИЕ СЛОЖНЫХ УСЛОВИЙ ПОИСКА Ранее были рассмотрены простые варианты задания условия поиска в предложении WHERE (сравнение столбца с константой и внутреннее соединение). Однако эти усло- вия могут быть достаточно сложными, чему способствует и сам синтаксис оператора SELECT. Рассмотрим основные конструкции для построения сложных условий поиска.
Оператор SELECT 363 23.10.1. Использование логических выражений Сложные логические выражения строятся при помощи операторов AND, OR и NOT. Их использование, а также построение из них сложных выражений подчиняется стандартным правилам, принятым для большинства алгоритмических языков (в том числе - в Object Pascal для Delphi), с одним важным исключением: операции отноше- ния в них имеют больший приоритет, чем логические операции, что избавляет от не- обходимости расстановки многочисленных скобок. В следующем запросе будут получены все записи из таблицы RASHOD, причем для каждого товара будет указана его цена из таблицы TOVARY, а для каждого покупателя - город из таблицы POKUPATELI (рис. 23.22): SELECT R.*, T.ZENA, P.ADRES FROM RASHOD R, TOVARY T, POKUPATELI P WHERE R.TOVAR = T.TOVAR AND R.POKUP = P.POKUP N_RASH |DAT_RASH KOLVO |TOVAR | POKUP |ZENA Iadres та ►1 @10.01.97 20 Сахар Лира, ТОО 4 пр-т. Стромынка, 21 □ 4 10.01.97 3000 Ставрида консерв. Адмирал, АО 5 ул Строителей, 2 5 10.01.97 4000 Кока-кола Саяны, ИЧП 3 пр-д Сталеваров, 8 6 20.01.97 30 Сахар Саяны, ИЧП 4 пр-д Сталеваров, 8 8 20.01.97 1000 Кока-кола Адмирал. АО 3 ул. Строителей, 2 1 10.01.97 100 Кока-кола Адмирал, АО 3 ул. Строителей, 2 Puc. 23.22. Результат сложного запроса к таблицам RASHOD, TOVARYи POKUPATELI. Получить все записи из таблицы RASHOD, для которых указано имя покупателя, плюс соответствующую каждому товару цену из таблицы TOVARY. При этом количе- ство отпущенного товара должно быть не больше 30 или не меньше 3000 единиц (рис. 23.23): SELECT R.*, T.ZENA FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR AND (R.KOLVO <= 30 OR R.KOLVO >= 3000) AND R.POKUP IS NOT NULL ORDER BY R.KOLVO Puc. 23.23. Результат сложного запроса к таблицам RASHOD и TOVARY. 23.10.2. Сравнение столбца с результатом вычисления выражения . Условие поиска в предложении WHERE может быть сформулировано при помощи выражения: <выражение> <оператор> <столбец>
364 Глава 23 Может использоваться и другой способ написания условия поиска: <столбец> <оператор> <выражение> Однако этот способ чаще применяется при использовании механизма вложенных под- запросов (вложенных операторов SELECT), речь о которых пойдет ниже. В обоих случаях результат вычисления выражения сравнивается с содержимым указанного столбца. Получить из таблицы RASHOD дату, товар и стоимость отпущенного товара. При этом учитывать только те записи, в которых стоимость отпущенного товара больше 120 единиц (рис. 23.24): SELECT R.DAT_RASH, R.TOVAR, (R.KOLVO * T.ZENA) AS STOIM FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR AND (R.KOLVO * T.ZENA) > 120 ORDER BY R.DAT_RASH |DAT-RASH|TOVAR STOIM | ► j10.01.97 | Кока-кола 300 □ 10.01.97 Кока-кола 12000 10.01.97 Сахар 203Б 10.01.97 Ставрида консерв. 15000 20.01.97 Кока-кола 3000 Puc. 23.24. Записи, в которых стоимость отпущенного товара больше 120 единиц. 23.10.3. Использование BETWEEN В условие поиска можно указать, что некоторое значение (столбец или вычислен- ное значение) должно находиться в интервале между значением! и значением2'. <значение> [NOT] BETWEEN <значение1> AND <значение2> Показать сведения обо всех отпусках товара, в которых количество (в единицах) отпущенного товара лежит в диапазоне 1000...3000 (рис. 23.25): SELECT * FROM RASHOD WHERE KOLVO BETWEEN 1000 AND 3000 |n_rash I DAT-RASH I KOLVO TOVAR | POKUP В 10.01.97 3000 Ставрида консерв. Адмирал, АО 8 20.01.97 1000 Кока-кола Адмирал, АО ▼ Puc. 23.25. Сведения обо всех отпусках товара, в которых количество отпущенного товара лежит в диа- пазоне 1000...3000 23.10.4. Использование IN Если нужно, чтобы значение какого-либо столбца (или результат вычисления неко- торого выражения) совпадало с одним из дискретных значений, в условии поиска ука- зывается предложение <значение> [NOT] IN (<значение1> [, <значение2> ...])
Оператор SELECT 365 В этом случае в результирующий НД будут включены только те записи, для кото- рых значение, стоящее слева от IN, равно одному из значений, указанных в списке (<значение1> [, <значение2>...]). Показать сведения обо всех отпусках товара, в которых количество отпущенного товара (в единицах) равно 100, 1000 или 3000 (рис. 23.26): SELECT * FROM RASHOD WHERE KOLVO IN (100, 1000, 3000) N_RASH |DAT_RASH KOLVO |TOVAR (pokup Ы ►1 ^^ШВЮ.01.97 3000 Ставрида консерв. Адмирал, АО | e 20.01.97 1000 Кока-кола Адмирал. АО — 1 10.01.97 100 Кока-кола Адмирал. АО | Puc. 23.26. Отпуски товара, в которых количество отпущенного товара равно 100, 1000 или 3000 единиц. Замечание. Существует вторая форма использования IN, в которой список возможных значений возвращается вложенным запросом SELECT. Этот вари- ант рассматривается в подразделе, посвященном вложенным запросам. 23.10.5. Использование STARTING Если в условии поиска нужно, чтобы значение какого-либо символьного столбца или выражения начиналось с определенной подстроки, в запрос включается предло- жение <значение> [NOT] STARTING [WITH] <подстрока> Вот как, например, можно получить список товаров, начинающихся с буквы «С» (рис. 23.27): SELECT * FROM TOVARY WHERE TOVAR STARTING WITH "C" Puc. 23.27. Наименования товаров, начинающиеся на «С». 23.10.6. Использование CONTAINING Если нужно, чтобы значение какого-либо символьного столбца или выражения включало в себя (не важно, начиная с какого символа) определенную подстроку, в запрос включается предложение <значение> [NOT] CONTAINING <значение> Показать список всех покупателей, чей адрес содержит подстроку «Стр» (рис. 23.28): SELECT * FROM POKUPATELI WHERE ADRES CONTAINING "dp
366 Глава 23 | | POKUP 1 GOROD и IADRES | ~ Лира, ТОО ^ЦцМОСКВА 'пр-т. Стромынка,21 л |_| Адмирал, АО С-Петербург ул. Строителей, 2 Рис. 23.28. Адреса содержат подстроку «Стр». 23.10.7. Использование функции UPPER Функция UPPER(<значение>) преобразует все буквы аргумента <значение> (содержимого столбца, результата вы- числения выражения) к заглавным. Обычно эта функция используется в условиях по- иска, когда необходимо игнорировать возможную разницу в высоте букв. Функция UPPER может фигурировать как в списке столбцов результирующего набора данных (после SELECT), так и в условии поиска в предложении WHERE. Пример. Пусть необходимо найти всех покупателей из Москвы. В таблице POKUPATELI два покупателя имеют в столбце GOROD имя города Москвы, однако в одном случае это значение «Москва», а в другом - «МОСКВА». Если выполнить такой запрос: SELECT * FROM POKUPATELI WHERE GOROD = 'Москва' то в результирующем НД будет только один покупатель (рис. 23.29). | POKUP JGOROD Саяны, ИЧП пр-д Сталеваров. 8 Рис. 23.29. Неполный список покупателей из г.Москвы. Проблема решается приведением обеих составляющих условия поиска к одному виду - строкам, состоящим из заглавных букв (рис. 23.30): SELECT * FROM POKUPATELI WHERE UPPER(GOROD) = 'МОСКВА' I POKUP I GOROD ' ADRES - | КЩ МОСКВА пр-т. Стромынка, 21 2] Саяны, ИЧП Москва пр-д Сталеваров, 8 zJ Puc. 23.30. Полный список покупателей из г. Москвы. 23.10.8. Использование LIKE Предложение LIKE определяет шаблоны сравнения строковых значений. Если не- обходимо, чтобы сравниваемое значение (значение столбца или результат вычисления строкового выражения) удовлетворяло шаблону, в условии поиска необходимо указать <значение> [NOT] LIKE < шаблон> [ESCAPE <подшаблон>] В шаблоне используются специальные символы — «%» и «_». Символ «%» (процент) означает, что на его месте может быть строка любой длины, а символ «_» (подчеркивание) используется для указания любого единичного символа. Например, LIKE "%USD
Оператор SELECT 367 требует, чтобы строковое значение оканчивалось символами «USD» независимо от то- го, какие символы (и сколько их) предшествуют этому окончанию. LIKE "___94" указывает, что сравниваемое значение может содержать 4 символа, из которых первые два - любые, а два последних - «94». Если в предложении LIKE символы «%» или «_» должны использоваться в шабло- не подобия как обычные символы (без учета их специальных функций), в запрос включается предложение ESCAPE <символ> ESCAPE определяет символ <символ>, появление которого в шаблоне отменяет специальные функции следующего за ним символа: WHERE STOLBEZ LIKE "%!%" ESCAPE "!" (строки STOLBEZ должны заканчиваться символом «%»). Пусть нужно найти информацию о покупателе, имя которого мы забыли, и назва- ние улицы помним неточно — то ли «Стромынка», то ли «Стормынка». Тогда можно выполнить такой запрос (рис. 23.31): SELECT * FROM POKUPATELI WHERE ADRES LIKE "%мынка%" | POKUP Igorod I adres JZ П^Лира, ТОО МОСКВА пр-т. Стромынка. 21 d Puc. 23.31. Использование шаблона поиска "%мынка%”. Шаблон заканчиваем символом «%», т. к. после названия улицы в адресе идут дру- гие реквизиты. 23.10.9. Использование функции CAST Иногда возникает потребность трактовать значение одного типа как значение дру- гого типа. Например, использовать числовое значение как символьную строку или наоборот. В этом случае применяют функцию CAST (<значение> AS <тип данных>) Функция CAST делает копию значения, преобразуя его к указанному типу данных. При этом не следует забывать о множестве типов данных, в которое может быть пре- образовано значение: Тип данных Можно привести к типам NUMERIC CHARACTER, DATE CHARACTER NUMERIC, DATE DATE CHARACTER, NUMERIC Пусть требуется найти покупателя, который делал закупки то ли 209, то ли 309 единиц товара. Тогда приводим значения столбца KOLVO к типу CHARACTER и к ре- зультату применяем LIKE (рис. 23.32):
368 Глава 23 SELECT * FROM RASHOD WHERE CAST(KOLVO AS CHAR(4)) LIKE "%09% |n_rash |dat_rash|kolvo |TOVAR 10.01.97 509 Сахар | POKUP 3: Puc. 23.32. Покупатели, закупившие x09 единиц товара. Замечание. Значение типа DATE не нужно приводить к строковому типу, по- скольку эти два типа являются совместимыми и значение DATE в InterBase может трактоваться как строковое. Например, в приводимом ниже запросе объединяются значения DATRASHтипа DATE со строковой константой, и ре- зультат помещается в вычисляемый столбец COLUMN1 (рис. 23.33). SELECT "Дата расхода: " I I DAT_RASH, POKUP, KOLVO FROM RASHOD WHERE KOLVO > 1000 (символы | | означают операцию сцепления строк). |COLUMN1 I POKUP | KOLVO | ± П. Дата расхода: 10-JAN-1997 1 Адмирал. АО 3000 _ 2] Дата расхода: 10-JAN-1997 Саяны, ИЧП 4000 -| Рис. 23.33. Пример преобразования типов. Выдать все покупки товара за 20-е число (предположим, каждого месяца). В InterBase нет встроенных функций для разделения даты на число, месяц и год. Вари- анты решения: • в клиентской программе анализировать даты при помощи стандартной проце- дуры DecodeDate', • написать UDF {User Defined Function - определенную пользователем функцию), которая реализует выделение номера дня из даты, и использовать имя этой функции в операторе SELECT-, • привести значение даты к типу CHAR (или, что лучше, трактовать значение да- ты как строковое значение) и применить к полученному значению LIKE, CONTAINING или STARTING WITH. Воспользуемся последним способом (результат показан на рис. 23.34): SELECT DAT_RASH, TOVAR, POKUP, KOLVO FROM RASHOD WHERE CAST(DAT_RASH AS CHAR(6)) STARTING WITH "20" или, устранив ненужное приведение типов, SELECT DAT_RASH, TOVAR, POKUP, KOLVO FROM RASHOD WHERE DAT_RASH STARTING WITH "20" Puc. 23.34. Даты на 20-е число каждого месяца
Оператор SELECT 369 23.11. ИСПОЛЬЗОВАНИЕ ПОДЗАПРОСОВ Часто невозможно решить поставленную задачу путем использования единствен- ного запроса. Например, в тех случаях, когда при использовании условия поиска <сравниваемое значение» <оператор> <значение, с которым сравнивать» в предложении WHERE параметр <значение, с которым сравнивать> заранее не оп- ределен и должен вычисляться в момент выполнения оператора SELECT или пред- ставляет собой не одно, а несколько значений. В такого рода случаях используются подзапросы (вложенные запросы). Оператор SELECT с подзапросом имеет такой вид: SELECT ... FROM ... WHERE <сравниваемое значение» <оператор> (SELECT ...) Синтаксис вложенного запроса ничем не отличается от синтаксиса основного за- проса, и, следовательно, вложенный запрос может, в свою очередь, содержать подза- прос. Заметим, что ^Г-синтаксис сервера InterBase требует заключать подзапрос в круглые скобки. Следующий запрос возвращает дату, на которую приходится максимальный отпуск товаров (рис. 23.35): SELECT KOLVO, DAT_RASH FROM RASHOD WHERE KOLVO = (SELECT MAX(KOLVO) FROM RASHOD) |KOLVcT J DAT RASH | 4000 Puc. 23.35. Дата отгрузки максимальной партии товара. Усложним предыдущий пример. Определить дату, когда со склада было отгружено максимальное количество товара, и реквизиты покупателя, который этот товар купил (рис. 23.36): SELECT R.KOLVO, R.DAT_RASH, Р.POKUP, P.GOROD, P.ADRES FROM RASHOD R, POKUPATELI P WHERE (R.POKUP = P.POKUP) AND KOLVO =(SELECT MAX(KOLVO) FROM RASHOD) Puc. 23.36. Дата отгрузки максимальной партии товара и реквизиты покупателя. По сравнению с предыдущим примером в запрос включено внутреннее соединение таблиц RASHOD и POKUPA TELL Замечание. Распространенной ошибкой является использование вложенного оператора SELECT, который вместо единичного значения способен возвра- щать список значений. Пусть, например, требуется найти в таблице POKUPATELI покупателя, у которого поле GOROD содержит «Москва», и по- казать из таблицы RASHOD все осуществленные им покупки товаров. Может быть написан следующий ошибочный запрос:
370 Глава 23 SELECT R.DAT_RASH, R.TOVAR, R.KOLVO, R.POKUP FROM RASHOD R WHERE R.POKUP = (SELECT POK.POKUP FROM POKUPATELI POK WHERE UPPER(POK.GOROD) = 'МОСКВА') Ошибка заключается в том, что столбец GOROD содержит неуникальные значения и таких покупателей - два. Таким образом, единственному значению RASHOD.POKUP в условии поиска требуется поставить в соответствие (оператор «-») сразу два значения PORUPATELI.POKUP. Попытка выполнения такого запро- са вызовет сообщение об ошибке. Выходом из положения является замена знака ра- венства в условии поиска на выбор из нескольких возможных значений (IN): SELECT R.DAT_RASH, R.TOVAR, R.KOLVO, R.POKUP FROM RASHOD R WHERE R.POKUP IN (SELECT POK.POKUP FROM POKUPATELI POK WHERE UPPER(POK.GOROD) = 'МОСКВА') Теперь условие поиска сразу предполагает, что одному значению RASHOD.POKUP может соответствовать множество значений PORUPATELI.POKUP, и запрос будет выполнен без ошибки (результат выполнения показан на рис. 23.37). Рис. 23.37. Результат выполнения подзапроса с несколькими возвращаемыми записями и оператором IN. 23.12. ВЛОЖЕНИЕ ПОДЗАПРОСОВ Как уже говорилось, синтаксис вложенного запроса ничем не отличается от син- таксиса основного запроса, что означает возможность вложения в подзапрос своего подзапроса, в тот - своего и т. д. Эта возможность широко используется на практике. Пример. Составим список отгрузки товаров покупателю, который в свое время ку- пил максимальную партию какого-либо товара (рис. 23.38): SELECT RRR.* FROM RASHOD RRR WHERE RRR.POKUP IN (SELECT R.POKUP FROM RASHOD R WHERE KOLVO = (SELECT MAX(RSH.KOLVO) FROM RASHOD RSH)) Puc. 23.38. Результат вложения подзапросов.
Оператор SELECT 371 Поясним логический порядок выполнения запроса. Сначала определяется самая крупная покупка - отыскивается максимальное значение в столбце KOLVO («самый вложенный» подзапрос SELECT МАХ). Далее в «среднем» подзапросе SELECT R.POKUP определяется покупатель, осуществивший эту покупку, а затем основной запрос SELECTRRR выбирает все записи с найденным покупателем. 23.13. ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ ИСПОЛЬЗОВАНИЯ ПОДЗАПРОСОВ, ВОЗВРАЩАЮЩИХ ЕДИНИЧНОЕ ЗНАЧЕНИЕ 23.13.1. Использование EXISTS Бывают случаи, когда в условии поиска нужно указать, что из таблицы требуется отобрать только те записи, для которых подзапрос возвращает одно или более значе- ний. В этом случае в условии поиска указывается предложение EXISTS (<подзапрос>) Пример. Составить список всех покупателей, которые хотя бы один раз получали товар со склада (рис. 23.39): SELECT Р.POKUP FROM POKUPATELI Р WHERE EXISTS (SELECT R.POKUP FROM RASHOD R WHERE R.POKUP = P.POKUP) jpOKUP . . - .'Л Лира.. TLHJ H Саяны. ИЧП Адмирал. АО _____ d Рис. 23.39. Список покупателей, совершивших хотя бы одну покупку. 23.13.2. Использование SINGULAR Если в условии поиска нужно указать, что из таблицы требуется выбрать лишь те записи, для которых подзапрос возвращает только одно значение, указывается пред- ложение SINGULAR (<onepaiop_select >) Пример. Составить список всех покупателей, купивших только один товар (рис. 23.40): SELECT Р.* FROM POKUPATELI Р WHERE SINGULAR (SELECT * FROM RASHOD R WHERE R.POKUP = P.POKUP)
372 Глава 23 Рис. 23.40. Результат использования SINGULAR. 23.14. ИСПОЛЬЗОВАНИЕ ПОДЗАПРОСОВ, ВОЗВРАЩАЮЩИХ МНОЖЕСТВО ЗНАЧЕНИЙ 23.14.1. Использование ALL, SOME Если в условиях поиска необходимо указать, что сравниваемое значение (значение столбца, результат вычисления выражения) должно находиться в определенных отно- шениях со всеми или некоторыми значениями из множества значений, возвращаемых подзапросом, применяют предложение <сравниваемое значение> {[NOT] <оператор> {ALL | SOME | ANY} (<подзапрос>) Отношение сравниваемого значения и значений, возвращаемых подзапросом, уста- навливается словами ALL и SOME (ANY): • ALL указывает, что условие поиска будет истинно только тогда, когда сравни- ваемое значение находится в нужном отношении со всеми значениями, возвра- щаемыми подзапросом; например, условие WHERE STOLBEZ > ALL (SELECT POLE FROM TABLIZA) требует, чтобы текущее значение столбца STOLBEZ было больше любых значе- ний в столбце TABLIZA.POLE', • SOME (вместо него можно указать ANY) - условие поиска истинно, когда срав- ниваемое значение находится в нужном отношении хотя бы с одним значени- ем, возвращаемым подзапросом, например: WHERE STOLBEZ > SOME (SELECT POLE FROM TABLIZA) определяет, что текущее значение столбца STOLBEZ должно быть больше хотя бы одного значения в столбце POLE из таблицы TABLIZA. Определим все факты отгрузки товаров со склада, в которых количество единиц отгружаемого товара превышает среднее значение (рис. 23.41): SELECT * FROM RASHOD RI WHERE RI.KOLVO > ALL (SELECT AVG(R2.KOLVO) FROM RASHOD R2 GROUP BY POKUP) Puc. 23.41. Результат использования ALL. Перечислим все факты отгрузки товаров со склада, в которых количество отгружае- мых единиц превышает среднее значение отгрузки хотя бы одного товара (рис. 23.42):
Оператор SELECT 373 SELECT * FROM RASHOD R1 WHERE Rl.KOLVO > SOME (SELECT AVG(R2.KOLVO) FROM RASHOD R2 GROUP BY POKUP) Puc. 23.42. Результат использования SOME. 23.14.2. Использование HAVING и агрегатных функций для вложенных подзапросов Если в условиях поиска для вложенного запроса нужно указать агрегатную функ- цию, используется предложение HAVING. Определим покупателя, у которого средняя покупка больше средней покупки дру- гих покупателей, и среднее количество покупок этого покупателя (рис. 23.43): SELECT Rl.POKUP, AVG(Rl.KOLVO) FROM RASHOD Rl GROUP BY Rl.POKUP HAVING AVG(Rl.KOLVO) >= ALL (SELECT AVG(R2.KOLVO) FROM RASHOD R2 GROUP BY R2.POKUP) Puc. 23.43. Результат использования в подзапросе предложения НА VING. Определим адрес покупателя, который приобрел наибольшее количество товаров (рис. 23.44): SELECT Р.* FROM POKUPATELI Р WHERE Р.POKUP = (SELECT RR.POKUP FROM RASHOD RR GROUP BY RR.POKUP HAVING SUM(RR.KOLVO) >= ALL (SELECT SUM(RRR.KOLVO) FROM RASHOD RRR GROUP BY RRR.POKUP)) I POKUP I GOROD Г!: Адмирал. AO С-Петербург |adres ул. Строителей, 2 Рис. 23.44. Адрес покупателя, который приобрел наибольшее количество товаров.
374 Глава 23 23.15. ВНЕШНИЕ СОЕДИНЕНИЯ Выше в п. 23.2.2 были рассмотрены внутренние соединения таблиц базы данных. Напомним, что внутренние соединения имеют место, если в предложении WHERE указано условие <имя столбца таблицы 1> <оператор> <имя столбца таблицы 2> Существует также и другой вид соединения таблиц - внешнее соединение. Оно оп- ределяется в предложении FROM согласно такой спецификации: SELECT {* | <значение1> [, <значение2> ...]} FROM <таблица1> <вид соединения> JOIN < таблица2> ON <условие поиска> Внешнее соединение отличается от внутреннего тем, что в результирующий НД включаются также записи ведущей таблицы соединения, которые объединяются с. пус- тым множеством записей другой таблицы. Какая из таблиц будет ведущей, определяет вид соединения'. • LEFT - (левое внешнее соединение), когда ведущей является таблица! (расположена слева от вида соединения)', • RIGHT - (правое внешнее соединение), когда ведущей является таблица2 (расположена справа от вида соединения)', • FULL - (полное внешнее соединение), когда ведущими таблицами являются и таблица!, и таблица2. В результирующий НД включаются все записи обеих таблиц по следующему алгоритму. Если для записи таблицы 1 имеются записи таблицы 2, удовлетворяющие условию соединения, в результирующий НД бу- дут включены все комбинации соединения таких записей таблиц 1 и 2; в про- тивном случае в результирующий НД будет включена запись таблицы 1, соеди- ненная с пустой записью. То же относится и к записям таблицы 2: если для за- писи таблицы 2 имеются записи таблицы 1, удовлетворяющие условию соеди- нения, в результирующий запрос будут включены все комбинации соединения таких записей таблиц 2 и 1; в противном случае в результирующий НД будет включена запись таблицы 2, соединенная с пустой записью. Пусть имеем такие таблицы: Таблица А___________________________ Таблица В_________________________ Столбец Р1 Столбец Р2 X 1 У 2 Z 2 Столбец Р1 Столбец Р2 Столбец РЗ а X 400 ь X 200 ( с У 500 d Тогда выполнение оператора SELECT A.Pl, А.Р2, В.Р2 FROM A LEFT JOIN В ON А.Р2 = В.Pl реализующего внешнее левое соединение, приведет к созданию такого результирую- щего НД:
Оператор SELECT 375 Столбец A.Pl Столбец A.P2 Столбец B.P2 a X 1 b X 1 c У 2 d Как видно из результата, для записи таблицы А, где столбец А.Р1 имеет значение «d», нет парных записей в таблице В, для которых удовлетворялось бы условие поиска А. Р2 = в. Р1. Поэтому данная запись таблицы А показана в соединении с пустой за- писью. В то же время выполнение оператора SELECT A.Pl, А.Р2, В.Р2 FROM A RIGHT JOIN В ON А.Р2 = В.Pl реализующего внешнее правое соединение, приведет к выдаче такого результирующе- го НД: Столбец A.Pl Столбец А.Р2 Столбец В.Р2 а X 1 ь X 1 с У 2 2 Для записи таблицы В, где столбец В.Р1 имеет значение «z» и столбец В.Р2 имеет значение «2», нет парных записей в таблице А, поэтому данная запись таблицы В пока- зана в соединении с пустой записью. Выполнение оператора полного внешнего соединения таблиц Л и В по условию А.Р2 = В.Р1 SELECT A.Pl, А.Р2, В.Pl, В.Р2 FROM A FULL JOIN В ON А.Р2 = В.Pl приведет к выдаче такого результата: Столбец А.Р1 Столбец А.Р2 Столбец В.Р1 Столбец В.Р2 а X X 1 ь X X 1 с У У 2 d Z 2 Построим внешнее соединение таблицы RASHOD с таблицей POKUPATELI, т. е. найдем покупателя, соответствующего каждой покупке (рис. 23.45): SELECT R.DAT_RASH, R.TOVAR, R.KOLVO, P.POKUP, P.GOROD FROM RASHOD R LEFT JOIN POKUPATELI P ON R.POKUP = P.POKUP
376 Глава 23 ИЛИ SELECT R.DAT_RASH, R.TOVAR, R.KOLVO, P.POKUP, P.GOROD FROM POKUPATELI P RIGHT JOIN RASHOD R ON R.POKUP = P.POKUP DAT RASH ITOVAR Ikolvo POKUP Igorod 10.01.97 Сахар 20 Лира. ТОО МОСКВА 10.01.97 Сахар 509 10.01.97 Ставрида консерв. 3000 Адмирал, АО С-Петербург ► П 0.01.97 1 Кока-кола 4000 Саяны, ИЧП Москва I 20.01.97 Сахар 30 Саяны, ИЧП Москва 20.01.97 Кока-кола 20 20.01.97 Кока-кола 1000 Адмирал, АО С-Петербург 10.01.97 Кока-кола 100 Адмирал, АО С-Петербург Puc. 23.45. Покупатели, соответствующие каждой покупке. Построим внешнее соединение таблицы POKUPATELI с таблицей RASHOD, т. е. найдем все покупки по каждому покупателю (рис. 23.46): SELECT Р.POKUP, P.GOROD, R.DAT_RASH, R.TOVAR, R.KOLVO FROM POKUPATELI P LEFT JOIN RASHOD R ON R.POKUP = P.POKUP ИЛИ SELECT P.POKUP, P.GOROD, R.DAT_RASH, R.TOVAR, R.KOLVO FROM RASHOD R RIGHT JOIN POKUPATELI P ON R.POKUP = P.POKUP POKUP GOROD |DAT_RASH TOVAR KOLVO ► Лира, ТОО МОСКВА 10.01.97 Сахар 20 □ Саяны, ИЧП Москва 10.01.97 Кока-кола 4000 Саяны, ИЧП Москва 20.01.97 Сахар 30 Адмирал, АО С-Петербург 1Q01.97 Ставрида консерв. 3000 Адмирал, АО С-Петербург 20.01.97 Кока-кола 1000 Адмирал, АО С-Петербург 10.01.97 Кока-кола 100 Г еракл Уфа Puc. 23.46. Покупки, произведенные каждым покупателем. Пример. Выполним полное внешнее соединение данных таблиц POKUPATELI и RASHOD по столбцу POKUP (рис. 23.47): SELECT Р.POKUP, R.DAT_RASH, R.TOVAR, R.KOLVO FROM POKUPATELI P FULL JOIN RASHOD R ON P.POKUP = R.POKUP
Оператор SELECT 377 POKUP DAT_RASH TOVAR KOLVO ► Лира, TOO 10.01.97 Сахар 20 10.01.97 Сахар 509 Адмирал, AO 10.01.97 Ставриаа консерв 3000 Саяны, ИЧП 10.01.97 Кока-кола 4000 Саяны, ИЧП 20.01.97 Сахар 30 20.01.97 Кока-кола 20 Адмирал, АО 20.01.97 Кока-кола 1000 Адмирал, АО 10.01.97 Кока-кола 100 Г еракл Рис. 23.47. Полное внешнее соединение таблиц POKUPATELI и RASHOD по столбцу POKUP. В предложении FROM оператора SELECT могут использоваться вложенные соеди- нения, реализованные при помощи конструкции JOIN. Рассмотрим пример: SELECT R.DAT_RASH,R.TOVAR, T.ED_IZM, R.KOLVO, P.ADRES FROM (RASHOD R LEFT JOIN POKUPATELI P ON R.POKUP = P.POKUP) LEFT JOIN TOVARY T ON R.TOVAR = T.TOVAR Результат выполнения запроса показан на рис. 23.48. Рис. 23.48. Результат выполнения двойного внешнего соединения. С результатом внешнего соединения можно производить не только внешнее, но и внутреннее соединение. Пример. Сначала выполняется внешнее соединение таблиц POKUPATELI и RASHOD (при этом главной таблицей является POKUPATELI). Затем над результатом выполняется внутреннее соединение с таблицей TOVARY. Условием внутреннего со- единения служит равенство столбцов R.TOVAR (результат внешнего соединения) и TOVAR (таблица TOVARY). SELECT Р.POKUP, R.DAT_RASH, R.TOVAR, T.ED_IZM, R.KOLVO FROM (POKUPATELI P LEFT JOIN RASHOD R ON P.POKUP = R.POKUP) INNER JOIN TOVARY T ON R.TOVAR = T.TOVAR
378 Глава 23 Результат показан на рис. 23.49. | POKUP Idat_rash TOVAR |ED IZM IKOLVO Ы D i Саяны, ИЧП в 10.01.97 Кока-кола банки 4000 .J Адмирал, АО 10.01.97 Кока-кола банки 100 Адмирал, АО 20.01.97 Кока-кола банки 1000 Лира, ТОО 10.01.97 Самар кг 20 Саяны ИЧП 20.01.97 Сахар кг 30 Адмирал, АО 10.01.97 Ставрида консерв. банки 3000 у| Рис. 23.49. Результат выполнения внешнего и последующего внутреннего соединения. 23.16. UNION - ОБЪЕДИНЕНИЕ РЕЗУЛЬТАТОВ ВЫПОЛНЕНИЯ НЕСКОЛЬКИХ ОПЕРАТОРОВ SELECT Иногда бывает полезным объединять два или более результирующих НД, возвра- щаемых в результате выполнения операторов SELECT. Такое объединение произво- дится при помощи оператора UNION. Результирующие НД должны иметь одинаковую структуру, т. е. одинаковый состав возвращаемых столбцов. Если в результирующих НД имеется одна и та же запись, в сводном НД она не дублируется. Соединим результаты выполнения трех запросов: SELECT R.* FROM RASHOD R WHERE R.TOVAR CONTAINING 'Ставрида' Puc. 23.50. Первый НД. SELECT R.* FROM RASHOD R WHERE R.KOLVO >= 3000 |n_rash ИННН1 |pATJASH|^OLVO | TOVAR 110.01.97 3000 Ставрида консерв. 10.01.97 4000 Кока-кола | POKUP Адмирал, АО Саяны, ИЧП Рис. 23.51. Второй НД. SELECT R.* FROM RASHOD R WHERE R.POKUP = 'Лира, TOO' Puc. 23.52. Третий НД. • Произведем объединение трех результирующих наборов данных (результат объе- динений на рис. 23.53): SELECT R.* FROM RASHOD R WHERE R.TOVAR CONTAINING 'Ставрида'
Оператор SELECT 379 UNION SELECT R.* FROM RASHOD R WHERE R.KOLVO >= 3000 UNION SELECT R.* . FROM RASHOD R WHERE R.POKUP = 'Лира, TOO' Puc. 23.53. Результат объединения трех запросов. 23.17. ИСПОЛЬЗОВАНИЕ IS NULL Если требуется выдать все записи, в которых некоторый столбец (или результат вычисления выражения) имеет значение NULL (т. е. не имеет никакого значения), дос- таточно в условии поиска указать предложение <значение> IS [NOT] NULL Следующий запрос возвращает все факты отгрузки товаров со склада, для которых не указан покупатель (результат показан на рис. 23.54): SELECT * FROM RASHOD WHERE POKUP IS NULL Puc. 23.54. Покупки без покупателя. 23.18. ИСПОЛЬЗОВАНИЕ ОПЕРАЦИИ СЦЕПЛЕНИЯ СТРОК Операция || соединяет два строковых значения, которые могут быть представлены выражениями: <строковое выражение1> || <строковое выражение2> Эту операцию можно использовать как после слова SELECT для указания возвращае- мых значений, так и в предложении WHERE. Следующий запрос возвращает в* одном столбце имена покупателей и названия их городов (рис. 23.55): SELECT POKUP || ' (' || GOROD || ')' FROM POKUPATELI
380 Глава 23 jcOLUMNI Саяны. ИЧП (Москва) Адмирал, АО (С-Петербург) Г еракл (Уфа) Рис. 23.55. Результат сцепления строк. 23.19. РАБОТА С РАЗНЫМИ БД В ОДНОМ ЗАПРОСЕ В одном запросе можно использовать таблицы из разных БД. В этом случае имя таблицы указывается в формате :ПсевдонимБД:ИмяТаблицы Под псевдонимом БД понимается псевдоним BDE. Ниже приведен пример обращения в одном запросе к таблицам БД InterBase (псевдоним MONITOR) и Oracle (псевдоним DWH)-. SELECT U.* FROM ":MONITOR:NLS" N, ":DWH:OLAP_UPE" U WHERE U.SC_CODE = N.COD_SCENARIO ORDER BY U.SC_CODE Заметим, что утилиты InterBase, такие как WISQL, осуществляют соединение с БД без использования псевдонима БД. Поскольку приведенный выше пример одновре- менного обращения к нескольким разным БД в одном запросе использует псевдонимы этих БД, такой пример может быть выполнен только в приложении или утилите, ис- пользующей BDE.
Глава 24 ДОБАВЛЕНИЕ, ИЗМЕНЕНИЕ, УДАЛЕНИЕ ЗАПИСЕЙ Язык SQL ориентирован на выполнение операций над группами записей, хотя в не- которых случаях операция может проводиться и над отдельной записью. Поэтому не- удивительно, что операторы добавления, изменения и удаления записей в общем слу- чае вызывают соответствующие операции над группами записей. Все приводимые ниже примеры выполнялись с помощью утилиты WISQL. Резуль- таты выполнения операторов также показываются в виде, возвращаемом этой утили- той. Следует обратить внимание на представление дат - в WISQL они показываются в строковом виде. Не стоит беспокоиться о совместимости столбцов типа дата, когда выполняются операторы INSERT, UPDATE или DELETE при посредстве компонента TQuery из программы Delphi: тип даты в InterBase полностью совместим с типом TDateTime в Delphi. 24.1. ОПЕРАТОР INSERT Оператор INSERT применяется для добавления записей в объект. В качестве объек- та может выступать ТБД или просмотр (VIEW), созданный оператором CREATE VIEW (см. гл. 25). В последнем случае записи могут добавляться сразу в несколько таблиц. Формат оператора INSERT: INSERT INTO <объект> [(столбец! [, столбец2 ...])] {VALUES (<значение1> [, <значение2> ...]) I <оператор SELECT>} Список столбцов указывает столбцы, которым будут присвоены значения в добав- ляемых записях. Список столбцов может быть опущен. В этом случае подразумевают- ся все столбцы объекта, причем в том порядке, в котором они определены в данном объекте. Поставить в соответствие столбцам списки значений можно двумя способами. Первый состоит в явном указании значений после слова VALUES, второй - в форми- ровании значений при помощи оператора SELECT. 24.1.1. Явное указание списка значений В этом случае оператор INSERT применяется для добавления одной записи и имеет формат INSERT INTO <объект> [(столбец! [, столбец2 ...])] VALUES (<значение1> [, <значение2> ...]) Значения присваиваются столбцам по порядку следования тех и других в операто- ре: первому по порядку столбцу присваивается первое значение, второму столбцу — второе значение и т. д. Вот как можно добавить в таблицу TOVARYновую запись: INSERT INTO RASHOD (N_RASH,DAT_RASH,KOLVO,TOVAR,POKUP) VALUES (45, "20-FEB-1997", 100, "Сахар", "Саяны, ИЧП")
382 Глава 24 Поскольку столбцы таблицы RASHOD указаны в полном составе и именно в том порядке, в котором они перечислены при создании таблицы RASHOD, оператор можно упростить: INSERT INTO RASHOD VALUES (45, "20-FEB-1997", 100, "Сахар", "Саяны, ИЧП") Для установки уникального значения поля первичного ключа N_RASH можно вос- пользоваться генератором: INSERT INTO RASHOD VALUES (GEN_ID(RASHOD_N_RASH, 2), "20-FEB-1997", 100, "Сахар", "Саяны, ИЧП") 24.1.2. Указание значений при помощи оператора SELECT Второй формой оператора INSERT является INSERT INTO <объект> [(столбец! [, столбец2 ...])] <оператор SELECT> При этом значениями, которые присваиваются столбцам, являются значения, возвра- щаемые оператором SELECT. Порядок их назначения столбцам аналогичен предыдущей форме оператора INSERT: значение первого по порядку столбца результирующего набора данных оператора SELECT присваивается первому столбцу оператора INSERT, второй — второму и т. д. Следует обратить внимание на важную особенность: поскольку оператор SELECT в общем случае возвращает множество записей, то и оператор INSERT в данной форме приведет к добавлению в объект аналогичного количества новых записей. Пусть в БД определена таблица RASHODDATA, по составу и порядку следования полей аналогичная таблице RASHOD: CREATE TABLE RASHOD_DATA( N_RASH INTEGER NOT NULL, DAT_RASH DATE NOT NULL, KOLVO INTEGER NOT NULL, TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, POKUP VARCHAR(20) COLLATE PXW_CYRL, PRIMARY KEY(N_RASH); Пусть в эту таблицу нужно ежедневно копировать все записи о расходах товара со склада за текущую дату. Представим, что эти сведения в дальнейшем ежедневно переправляются в территориально удаленную бухгалтерию. Тогда ежедневная выгрузка записей из таблицы RASHOD в таблицу RASHOD DATA будет реализовываться такого рода оператором: INSERT INTO RASHOD_DATA SELECT * FROM RASHOD WHERE DAT_RASH = "20-JAN-1997" Пусть перед выполнением оператора таблица RASHOD DATA пуста. Тогда после его выполнения она будет иметь следующее содержимое: N_RASH DAT_RASH KOLVO TOVAR POKUP 8 20-JAN-1997 1000 Кока-кола Адмирал, АО 7 20-JAN-1997 20 Кока-кола <null> 6 20-JAN-1997 30 Сахар Саяны, ИЧП
Добавление, изменение, удаление записей 383 24.2. ОПЕРАТОР UPDATE Оператор UPDATE используется для изменения значения в группе записей или - в частном случае - в одной записи объекта. В качестве объекта могут выступать ТБД или просмотр, созданный оператором CREATE VIEW. В последнем случае могут изме- няться значения записей из нескольких таблиц. Формат оператора UPDATE'. UPDATE <объект> SET столбец! = <значение1> [,столбец2 = <значение2>...] [WHERE <условие поиска >] При корректировке каждому из перечисленных столбцов присваивается соответст- вующее значение. Корректировка выполняется для всех записей, удовлетворяющих условию поиска, которое задается так же, как в операторе SELECT. Обратите внимание: если опустить WHERE <условие поиска>, в объекте будут изменены все записи! Заменить дату на «24.01.97» и увеличить количество отпущенного товара на 2 еди- ницы для всех записей с датой «20.01.97» в таблице RASHOD можно таким образом: UPDATE RASHOD SET DAT_RASH = "24-JAN-1997", KOLVO = KOLVO + 2 WHERE DAT_RASH = "20-JAN-1997" Содержимое таблицы RASHOD до выполнения оператора UPDATE: N_RASH DAT_RASH KOLVO TOVAR ’ POKUP 5 IO-JAN-1997 4000 Кока-кола Саяны, ИЧП 1 IO-JAN-1997 100 Кока-кола Адмирал, АО 2 IO-JAN-1997 20 Сахар Лира, ТОО 3 IO-JAN-1997 509 Сахар <null> 4 IO-JAN-1997 3000 Ставрида консерв. Адмирал, АО 8 20-JAN-1997 1000 Кока-кола Адмирал, АО 7 20-JAN-1997 20 Кока-кола <null> 6 20-JAN-1997 30 Сахар Саяны, ИЧП Содержимое той же таблицы после выполнения UPDATE: N_RASH DAT_RASH KOLVO TOVAR POKUP 5 10-JAN-1997 4000 Кока-кола Саяны, ИЧП 1 10-JAN-1997 100 Кока-кола Адмирал, АО 2 10-JAN-1997 20 С^хар Лира, ТОО 3 10-JAN-1997 509 Сахар <null> 4 10-JAN-1997 3000 Ставрида консерв. Адмирал, АО 8 24-JAN-1997 1002 Кока-кола Адмирал, АО 7 24-JAN-1997 22 Кока-кола <null> 6 24-JAN-1997 32 Сахар Саяны, ИЧП
384 Глава 24 24.3. ОПЕРАТОР DELETE Оператор DELETE предназначен для удаления группы записей из объекта. В каче- стве объекта могут выступать ТБД или просмотр VIEW. В частном случае может быть удалена только одна запись. Формат оператора DELETE'. DELETE FROM <объект> [WHERE <условие поиска>]; Удаляются все записи из объекта, удовлетворяющие условию поиска. Как и в случае оператора UPDATE, если опустить WHERE <условие поиска>, из объекта будут удалены все записи! Чтобы удалить из таблицы RASHOD все записи о расходе товара «Кока-кола» за дату «10.01.97», используем такой оператор: DELETE FROM RASHOD WHERE (TOVAR = "Кока-кола") AND (DAT_RASH = "IO-JAN-1997")
Глава 25 РАБОТА С ПРОСМОТРАМИ VIEW 25.1. ПОНЯТИЕ ПРОСМОТРА КАК ВИРТУАЛЬНОЙ ТАБЛИЦЫ В БД может быть определен просмотр, являющий собой виртуальную таблицу, в которой представлены записи из одной или нескольких таблиц. Порядок формирова- ния записей в просмотре определяется оператором SELECT. Для создания просмотра применяется оператор CREATE VIEW ИмяПросмотра [(CTon6eu_viewl [,столбеЦ—View ...])] AS <onepaTop_select> [WITH CHECK OPTION]; в котором после ИмениПросмотра следует необязательный список столбцов, опера- mop select есть полнофункциональный оператор SELECT, а необязательный параметр WITH CHECK OPTION определяет, допускать ли для обновляемых просмотров ввод записей, не удовлетворяющих условию формирования просмотра. Для удаления просмотра используется оператор DROP VIEW ИмяПросмотра; Создать просмотр, содержащий дату расхода, наименование товара, количество расхода товара из таблицы RASHOD и цену товара из таблицы TOVAR, можно таким образом: CREATE VIEW FULL_RASHOD AS SELECT R.DAT_RASH, R.TOVAR, R.KOLVO, T.ZENA FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR; После этого к нему можно обращаться как к обычной таблице БД:. SELECT * FROM FULL_RASHOD; Преимущества создания просмотров: • однажды определив просмотр, не нужно всякий раз формировать оператор SELECT, это важно для сложных операторов, выполняющих соединение не- скольких таблиц; • просмотр может предоставлять подмножество столбцов из таблицы, что важно для обеспечения сохранности данных и, возможно, усиления безопасности. 25.2. СПОСОБЫ ФОРМИРОВАНИЯ ПРОСМОТРОВ Просмотр может создаваться как: 1) вертикальный срез таблицы, когда в просмотр включается подмножество столб- цов таблицы, например: CREATE VIEW RASH_VERT AS SELECT DAT_RASH, TOVAR, KOLVO FROM RASHOD 2) горизонтальный срез таблицы, когда в просмотр включаются все столбцы, но не все записи таблицы, например: CREATE VIEW RASH_HORIZ
386 Глава 25 AS SELECT * FROM RASHOD WHERE TOVAR = "Кока-кола"; 3) вертикально-горизонтальный срез таблицы, когда в просмотр включается под- множество столбцов и подмножество строк, например: CREATE VIEW RASH_VERT_HORIZ AS SELECT DAT_RASH, TOVAR, KOLVO FROM RASHOD WHERE TOVAR = "Кока-кола"; 4) подмножество строк и столбцов соединения разных таблиц, например: CREATE VIEW FULL_RASHOD AS SELECT R.DAT_RASH, R.TOVAR, R.KOLVO, T.ZENA FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR; 25.3. УКАЗАНИЕ СТОЛБЦОВ ПРОСМОТРА В ОПЕРАТОРЕ CREATE VIEW Имена столбцов просмотра должны указываться в операторе CREATE VIEW в том случае, когда в качестве столбца просмотра определяется выражение, например: CREATE VIEW STOIM_RASHOD (DAT_RASH, TOVAR, STOIM) AS SELECT R.DAT_RASH, R.TOVAR, R.KOLVO * T.ZENA FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR; В противном случае имена столбцов просмотра указывать не обязательно. В случае если список имен столбцов опущен, имена столбцов считаются идентичными именам полей, возвращаемых оператором SELECT. 25.4. ОБНОВЛЯЕМЫЕ И НЕОБНОВЛЯЕМЫЕ ПРОСМОТРЫ Чтобы просмотр можно было обновлять, то есть применять к нему операции до- бавления, изменения и удаления записей, необходимо одновременное выполнение трех условий: • просмотр должен формироваться из записей только одной таблицы; • в просмотр должен быть включен каждый столбец таблицы, имеющий атрибут NOTNULD, • оператор SELECT просмотра не должен использовать агрегирующих функций, режима DISTINCT, предложения НА VING, соединения таблиц, хранимых про- цедур и функций, определенных пользователем. Если просмотр удовлетворяет этим условиям, к нему могут применяться операто- ры INSERT, UPDATE и DELETE. В следующем просмотре можно добавлять, корректировать и удалять записи: CREATE VIEW UPDATABLE_RASH AS SELECT N_RASH,DAT_RASH, TOVAR, KOLVO FROM RASHOD;
Работа с просмотрами VIEW 387 Чтобы к просмотру можно было применить операторы UPDATE и DELETE, для не- го одновременно должны выполняться два условия: • просмотр должен формироваться из записей только одной таблицы; • оператор SELECT просмотра не должен использовать агрегатных функций, ре- жима DISTINCT, предложения HAVING, соединения таблиц, хранимых проце- дур и функций, определенных пользователем. В следующем просмотре можно корректировать и удалять записи, но нельзя до- бавлять, т. к. в него не включено поле N_RASH, определенное с атрибутом NOT NULL'. CREATE VIEW LESSUPDAPTABLE_RASH AS SELECT DAT_RASH, TOVAR, KOLVO, POKUP FROM RASHOD; В следующем просмотре нельзя добавлять, корректировать и удалять записи, т. к. он соединяет две таблицы: CREATE VIEW А AS SELECT R.TOVAR, T.ZENA FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR; 25.5. ИСПОЛЬЗОВАНИЕ CHECK OPTION Если для обновляемого просмотра указан параметр CHECK OPTION, будут отвер- гаться все попытки добавления новых или изменения существующих записей таким образом, чтобы нарушалось условие WHERE оператора SELECT данного просмотра. В следующий просмотр нельзя добавить записи со значением поля KOLVO, мень- шим 1000: CREATE VIEW RASH_1000_CHECK AS- -SELECT * FROM RASHOD WHERE KOLVO > 1000 - tWITH CHECK OPTION; 25.6. КОМПОНЕНТЫ DELPHI И ИСПОЛЬЗОВАНИЕ ПРОСМОТРОВ Просмотр есть виртуальная таблица БД. «Виртуальная» означает, что эта таблица физически не существует, но имеет все свойства обычной таблицы. Поэтому компо- нент ТТаЫе может содержать имя просмотра в свойстве TableName, а компонент TQuery использовать 5б£-запрос, в котором наравне с обычными таблицами произво- дится обращение и к просмотру. Поведение просмотра в приложении аналогично по- ведению обычной таблицы БД с учетом особенностей, определяемых параметром CHECK OPTION и возможностью полного (добавление, удаление, изменение), час- тичного (изменение, удаление записей) обновления просмотра или невозможностью такового. Следует помнить, что подобно НД, возвращаемому оператором SELECT, НД, возвращаемый просмотром, показывает записи в состоянии, в котором они были на момент открытия набора данных компонента ТТаЫе или TQuery. Для того чтобы внесенные после открытия НД изменения стали актуальны в просмотре, компонент, использующий просмотр, должен быть переоткрыт.
Глава 26 РАБОТА С ХРАНИМЫМИ ПРОЦЕДУРАМИ 26.1. ПОНЯТИЕ ХРАНИМОЙ ПРОЦЕДУРЫ Хранимая процедура - это модуль, написанный на процедурном языке InterBase и хранящийся в базе данных как метаданные (то есть как данные о данных). Хранимую процедуру можно вызывать из программы. Существует две разновидности хранимых процедур: • процедуры выбора; • процедуры действия. Процедуры выбора могут возвращать более одного значения. В приложении имя хранимой процедуры выбора подставляется в оператор SELECT вместо имени табли- цы или просмотра. Процедуры действия вообще могут не возвращать данных и используются для реализации каких-либо действий. Хранимым процедурам можно передавать параметры и получать обратно значения параметров, измененные в соответствии с алгоритмами работы хранимых процедур. Преимущества использования хранимых процедур: • одну процедуру можно использоваться многими приложениями; • разгрузка приложений клиента путем переноса части кода на сервер и вследст- вие этого - упрощение клиентских приложений; • при изменении хранимой процедуры все изменения немедленно становятся дос- тупны для всех клиентских приложений; при внесении же изменений в прило- жение клиента требуется повторное распространение новой версии клиентского приложения между пользователями; • улучшенные характеристики выполнения, связанные с тем, что хранимые про- цедуры выполняются сервером, в частности - уменьшенный сетевой трафик. 26.2. СОЗДАНИЕ ХРАНИМОЙ ПРОЦЕДУРЫ Хранимая процедура создается оператором CREATE PROCEDURE ИмяПроцедуры [(входной_параметр тип_данных [,входной_параметр тип_данных...])] [RETURNS (выходной_параметр тип_данных [,выходной_параметр тип_данных ...])] AS <тело процедуры»; Входные параметры служат для передачи в процедуру значений из вызывающего приложения. Изменять значения входных параметров в теле процедуры бессмыслен- но: эти изменения будут забыты после окончания работы процедуры. Выходные параметры служат для возврата результирующих значений. Значения выходных параметров устанавливаются в теле процедуры и после окончания ее рабо- ты передаются в вызывающее приложение.
Работа с хранимыми процедурами 389 И входные, и выходные параметры могут быть опущены, если в них нет необходи- мости. Тело процедуры имеет следующий формат: [«объявление локальных переменных процедуры»] BEGIN < оператор» [«оператор» .. . ] END Следующая хранимая процедура FINDMAXKOLVO возвращает в выходном па- раметре MAX KOLVO максимальное количество отгруженного со склада товара, на- именование которого передается во входном параметре IN_TOVAR-. CREATE PROCEDURE FIND_MAX_KOLVO (IN_TOVAR VARCHAR(20)) RETURNS(MAX_KOLVO INTEGER) AS BEGIN SELECT MAX(KOLVO) FROM RASHOD WHERE TOVAR = :IN_TOVAR INTO :MAX_KOLVO; SUSPEND; END Заметим, что в приведенной процедуре за ненадобностью отсутствует определение локальных переменных. 26.3. АЛГОРИТМИЧЕСКИЙ ЯЗЫК ХРАНИМЫХ ПРОЦЕДУР Для написания тела хранимой процедуры применяют особый алгоритмический язык. Этот язык применяется также для написания триггеров (см. гл. 27). Рассмотрим конструкции алгоритмического языка хранимых процедур и триггеров. 26.3.1. Объявление локальных переменных Локальные переменные, если они определены в процедуре, имеют срок жизни от начала выполнения процедуры и до ее окончания. Вне процедуры такие локальные переменные неизвестны, и попытка обращения к ним вызовет ошибку. Локальные пе- ременные используют для хранения промежуточных значений. Формат объявления локальных переменных: DECLARE VARIABLE <имя переменной > <тип данных»; Пример объявления: CREATE PROCEDURE FULL_ADR (TOVARCHIK VARCHAR(20)) RETURNS (GOROD_ADRES VARCHAR(40)) AS DECLARE VARIABLE NAIDEN_POKUPATEL VARCHAR(20); DECLARE VARIABLE MAX_KOLVO INTEGER; BEGIN END 26.3.2. Операторные скобки BEGIN... END Операторные скобки BEGIN... END, во-первых, ограничивают тело процедуры, а во-вторых, могут использоваться для указания границ составного оператора.
390 Глава 26 Под простым оператором понимается единичное разрешенное действие, например: РОК = "Покупатель не указан"; Под составным оператором понимается группа простых или составных операторов, заключенная в операторные скобки BEGIN... END. 26.3.3. Оператор присваивания Оператор присваивания служит для занесения значений в переменные. Его формат: Имя переменной = выражение; где в качестве выражения могут выступать переменные, арифметические и строковые выражения, в которых можно использовать встроенные функции, функции, опреде- ленные пользователем, а также генераторы. Пример: OUT_TOVAR = UPPER(TOVAR); 26.3.4. Оператор IF ... THEN... ELSE Условный оператор IF. . . THEN. . . ELSE имеет такой же формат, как и в Object Pascal'. IF (<условие>) THEN < оператор 1> [ELSE < оператор 2>] В случае, если условие истинно, выполняется оператор 1, если ложно - оператор 2. В отличие от Object Pascal условие должно заключаться в круглые скобки. 26.3.5. Оператор SELECT Оператор SELECT используется в хранимой процедуре для выдачи единичной строки. По сравнению с синтаксисом обычного оператора SELECT в процедурный оператор добавлено предложение INTO :переменная [, переменная...] Оно служит для указания переменных или выходных параметров, в которые долж- ны быть записаны значения, возвращаемые оператором SELECT (те результирующие значения, которые перечисляются после ключевого слова SELECT). Приводимый ниже оператор SELECT возвращает среднее и сумму по столбцу KOLVO и записывает их соответственно в A VG_KOLVO и SUM KOLVO, которые мо- гут быть как локальными переменными, так и выходными параметрами процедуры. Расчет среднего и суммы по столбцу KOLVO производится только для записей, у ко- торых значение столбца TOVAR совпадает с содержимым IN_TOVAR (входной пара- метр или локальная переменная). SELECT AVG(KOLVO), SUM(KOLVO) FROM RASHOD WHERE TOVAR = :IN_TOVAR INTO :AVG_KOLVO, ;SUM_KOLVO;
Работа с хранимыми процедурами 391 26.3.6. Оператор FOR SELECT... DO Оператор FOR SELECT... DO имеет следующий формат: FOR <оператор SELECT> DO < оператор>; Оператор SELECT представляется в расширенном синтаксисе для алгоритмическо- го языка хранимых процедур и триггеров, то есть в нем может присутствовать пред- ложение INTO. Алгоритм работы оператора FOR SELECT. . . DO заключается в следующем. Вы- полняется оператор SELECT, и для каждой строки полученного результирующего на- бора данных выполняется оператор, следующий за словом DO. Этим оператором час- то бывает SUSPEND (см. ниже), который приводит к возврату выходных параметров в вызывающее приложение. Следующая процедура выдает все расходы конкретного товара, определяемого со- держимым входного параметра IN_TOVAR. CREATE PROCEDURE RASHOD_TOVARA(IN_TOVAR VARCHAR(20)) RETURNS (OUT_DAT DATE, OUT_POKUP VARCHAR(20), OUT_KOLVO INTEGER) AS BEGIN FOR SELECT DAT_RASH, POKUP, KOLVO FROM RASHOD WHERE TOVAR = :IN_TOVAR INfO :OUT_DAT, :OUT_POKUP, :OUT_KOLVO DO SUSPEND; END Рассмотрим логику работы оператора FOR SELECT... DO этой процедуры. Сначала выполняется оператор SELECT, который возвращает дату расхода, наименование поку- пателя и количество расхода товара для каждой записи, у которой столбец TOVAR со- держит значение, идентичное значению во входном параметре IN_TOVAR. Указанные значения записываются в выходные параметры (соответственно OUT_DAT, OUT_POKUP, OUTKOLVO). Имени параметра в этом случае предшествует двоеточие. После выдачи каждой записи результирующего НД выполняется оператор, следующий за словом DO. В данном случае это оператор SUSPEND. Он возвращает значения выход- ных параметров вызвавшему приложению и приостанавливает выполнение процедуры до запроса следующей порции выходных параметров от вызывающего приложения. Такая процедура является процедурой выбора, поскольку она может возвращать множественные значения выходных параметров в вызывающее приложение. Обычно запрос к такой хранимой процедуре из вызывающего приложения осуществляется при помощи оператора SELECT, например: SELECT MAX_KOLVO FROM FIND_MAX_KOLVO("Сахар") 26.3.7. Оператор SUSPEND Оператор SUSPEND передает в вызывающее приложение значения результирую- щих параметров (перечисленных после слова RETURNS в описании функции), имею-
392 Глава 26 щие место на момент выполнения SUSPEND. После этого выполнение хранимой про- цедуры приостанавливается. Когда от оператора SELECT вызывающего приложения приходит запрос на следующее значение выходных параметров, выполнение храни- мой процедуры возобновляется. Процедура POK LIST возвращает имена всех покупателей, которые сделали по- купки товара IN_TOVAR в количестве, превосходящем средний размер покупки по этому товару. В случае если наименование покупателя — пустое, вместо имени покупа- теля выводится «Покупатель не указан». CREATE PROCEDURE POK_LIST (IN_TOVAR VARCHAR(20)) RETURNS(РОК VARCHAR(20)) AS DECLARE VARIABLE AVG_KOLVO INTEGER; BEGIN SELECT AVG(KOLVO) FROM RASHOD WHERE TOVAR = :IN_TOVAR INTO :AVG_KOLVO; FOR SELECT POKUP FROM RASHOD WHERE KOLVO > :AVG_KOLVO INTO :POK DO BEGIN IF (:POK IS NULL) THEN РОК = "Покупатель не указан"; SUSPEND; END END 26.3.8. Оператор WHILE... DO Оператор имеет такой формат: WHILE (<условие>) DO < оператор> Алгоритм выполнения оператора: в цикле проверяется выполнение условия; если оно истинно, выполняется оператор. Цикл продолжается до тех пор, пока условие не перестанет выполняться. Рассмотрим процедуру SUM_0_N, которая подсчитывает сумму всех чисел от 0 до числа, определяемого входным параметром N. Вычисление суммы реализовано в цик- ле с использованием оператора WHILE...DO. CREATE PROCEDURE SUM_0_N (N INTEGER) RETURNS(S INTEGER) AS DECLARE VARIABLE TMP INTEGER; BEGIN S = 0; TMP = 1; WHILE (TMP <= N) DO BEGIN S = S + TMP; TMP = TMP + 1; END END
Работа с хранимыми процедурами 393 26.3.9. Оператор EXIT Оператор EXIT инициирует прекращение выполнения процедуры и выход в вызы- вающее приложение. Процедура MAX VALUE возвращает максимум из двух чисел, передаваемых как входные параметры; в случае, если одно из чисел имеет значение NULL, процедура завершается (в этом случае выходной параметр содержит значение NULL)-. CREATE PROCEDURE MAX_VALUE(A INTEGER, В INTEGER) RETURNS(M_V INTEGER) AS BEGIN IF (:A IS NULL OR :B IS NULL) THEN EXIT; IF (:A > :B) THEN M_V = :A; ELSE ’M_V = :B; END 26.3.10. Оператор EXECUTE PROCEDURE Оператор EXECUTE PROCEDURE имя [параметр [, параметр ...]]; [RETURNING_VALUES параметр [, параметр ...]]; вызывает другую хранимую процедуру. При этом после имени вызываемой процеду- ры перечисляются входные параметры, если они есть, а после RETURNING_VALUES - выходные параметры. Перепишем приведенную выше процедуру POK LIST таким образом, чтобы из ее тела вызывалась другая процедура, AVG KOLVO, возвращающая среднее количество отпущенного товара: CREATE PROCEDURE AVG_KOLVO (TVR VARCHAR(20)) RETURNS(OUT_AVG_KOLVO INTEGER) AS BEGIN SELECT AVG(KOLVO) FROM RASHOD WHERE TOVAR = :TVR INTO :OUT_AVG_KOLVO; SUSPEND; END CREATE PROCEDURE POK_LIST1 (INJTOVAR VARCHAR(20)) RETURNS(POK VARCHAR(20)) AS DECLARE VARIABLE AVG_KOLVO INTEGER; BEGIN EXECUTE PROCEDURE AVG_KOLVO(:INJTOVAR) RETURNING_VALUES :AVG_KOLVO; FOR SELECT POKUP FROM RASHOD WHERE KOLVO > :AVG_KOLVO INTO :POK
394 Глава 26 DO BEGIN IF (:POK IS NULL) THEN РОК = "Покупатель не указан"; SUSPEND; END END 26.3.11. Оператор POST_EVENT Оператор POST_EVENT "Имя события"; применяется для посылки сервером клиентским приложениям сообщения о наступле- нии какой-либо ситуации, связанной с именем события,. Приложение должно зареги- стрироваться на сервере для получения уведомления о наступлении событий и указать список интересующих приложение событий. Более подробно см. гл. 32. 26.4. ВЫЗОВ ПРОЦЕДУР ВЫБОРА В ПРИЛОЖЕНИИ КЛИЕНТА Процедуры выбора могут возвращать несколько значений одного и того же выход- ного параметра или группы выходных параметров. Возврат текущего значения выход- ных параметров соответствует возврату строки. Таким образом, процедура выбора возвращает НД, состоящий в общем случае из нескольких строк. Для передачи каждо- го значения выходных параметров в вызывающее приложение в хранимой процедуре выбора применяется оператор SUSPEND. Формирование строки результирующего НД (т. е. текущих значений выходных параметров) производится операторами SELECT... INTO или FOR...SELECT. Для обращения к хранимой процедуре выбора в приложении клиента используется компонент TQuery. Вызов хранимой процедуры производится в предложении FROM оператора SELECT с указанием входных параметров процедуры. Выходные параметры процедуры (все или часть) указываются в качестве возвращаемых значений оператора SELECT. Замечание. Для обращения к хранимой процедуре выбора может использоваться и компонент TStoredProc, открываемый при помощи метода Open. Однако в общем слу- чае возможность обращения к процедуре выбора из компонента TStoredProc опреде- ляется особенностями 5£2£-сервера. К хранимым процедурам выбора, определенным в InterBase, рекомендуется обращаться из оператора SELECT, который помещается в свойство SQL компонента TQuery. Выше приводилась процедура выбора RASHOD TOVARA, которая выдает все рас- ходы товара, определяемого содержимым входного параметра IN_TOVAR\ CREATE PROCEDURE RASHODJTOVARA(INJTOVAR VARCHAR(20)) RETURNS (OUT_DAT DATE, OUT_POKUP VARCHAR(20), OUT_KOLVO INTEGER) AS BEGIN FOR SELECT DAT_RASH, POKUP, KOLVO FROM RASHOD WHERE TOVAR = : IN_TOVAR INTO :OUT_DAT, :OUT_POKUP, :OUT_KOLVO DO SUSPEND; END
Работа с хранимыми процедурами 395 Чтобы обратиться к данной процедуре из клиентского приложения, создадим фор- му (рис. 26.1), разместив в ней: 1. компонент TDatabase, осуществляющий управление соединением с серверной БД; 2. компонент ТТаЫе, ассоциированный с таблицей TOVARY, для выбора названия товара, который будет передаваться в хранимую процедуру в качестве входного пара- метра; 3. компонент TQuery-, 4. компонент TButton для инициации доступа к процедуре. Рис. 26.1. Вид формы, в которой происходит обращение к хранимой процедуре. В свойстве SQL компонента TQuery необходимо определить такой SgZ-aanpoc: SELECT * FROM RASHOD_TOVARA(:PARAMI) а в свойстве Params компонента TQuery - указать тип параметра PARAM1 как String. В обработчике нажатия кнопки происходит присваивание параметру значения текущего названия товара из компонента ТТаЫе, после чего компонент TQuery активизируется: procedure TForml.ButtonlClick(Sender: TObject); begin Queryl.Close; Queryl.ParamByName('paraml').Value := Tablel.FieldByName('TOVAR').Value; Queryl.Open; end; Окно работающей программы показано на рис. 26.2.
396 Глава 26 Хранимые процедуры - использование TQuery ИС В Выберите товар Расчет Рис. 26.2. Данные в нижнем TDBGrid возвращаются хранимой процедурой. 26.5. ОБРАЩЕНИЕ К ПРОЦЕДУРАМ ДЕЙСТВИЯ. КОМПОНЕНТ TSTOREDPROC Процедура действия возвращает один экземпляр значения выходного параметра или группы параметров. Этим она отличается от хранимой процедуры выбора, которая за одно выполнение способна возвратить несколько экземпляров выходных параметров. В утилите WISQL InterBase хранимую процедуру действия можно вызвать при по- мощи оператора EXECUTE PROCEDURE имя [параметр [, параметр ...]]; Например, вызов процедуры STOIM с указанием двух входных параметров приве- дет к выдаче значения выходного параметра в окне результатов WISQL (рис. 26.3). При помощи WISQL удобно отлаживать хранимые процедуры действия, а также произ- водить пробные запуски хранимых процедур выбора, которые в этом случае возвращают только первую строку результирующего набора данных (то есть первый экземпляр выход- ных параметров). После отладки синтаксиса и получения правдоподобных результатов по- следующую отладку процедур выбора следует производить в приложении клиента. Из приложения клиента процедуру действия вызывают при помощи компонента TStoredProc. Для этого в его свойство DatabaseName помещают псевдоним серверной БД, в которой расположена хранимая процедура. Затем выбирают имя соответствую- щей хранимой процедуры из выпадающего списка в свойстве StoredProcName. При этом происходит считывание имен входных и выходных параметров и их типов. Их можно увидеть, нажав кнопку (...) в свойстве Params (рис. 26.4). Выбрав в окне вход- ной параметр, с помощью окна Инспектора Объектов можно присвоить ему нужное умалчиваемое значение (свойство Value).
Работа с хранимыми процедурами 397 При динамическом создании или редактировании компонента TStoredProc следует помнить, что имена, типы и значения параметров хранит его свойство property Params: TParams; Рис. 26.3. Выполнение процедуры действия в среде утилиты W1SQL. Рис. 26.4. Параметры процедуры действия (компонент TStoredProc). Объекты класса TParams имеют свойство Count, определяющее количество парамет- ров, и свойство property ParamValues[const ParamName: String]: Variant; открывающее доступ к значению конкретного параметра по его имени ParamName. Индексированное свойство
398 Глава 26 property Items[Index: Word]: TParam; объектов TParams ссылается на объекты класса TParam, описывающие каждый пара- метр. Свойство type TParamType = (ptUnknown, ptinput, ptOutput, ptlnputOutput, ptResult); property ParamType: TParamType; этих объектов определяет тип параметра: ptUnknown - тип неизвестен; ptinput — входной параметр; ptOutput - выходной параметр; ptlnputOutput - параметр используется как входной и выходной одновременно; ptResult - параметр предназначен для возврата ре- зультата обращения в процедуре; в нем процедура указывает свой статус завершения или код ошибки; у каждой процедуры может быть только один параметр этого типа. Определенные в компоненте TStoredProc имена параметров и порядок их следова- ния могут не совпадать с именами и порядком следования параметров в хранимой процедуре. В этом случае указать соответствие параметров компонента и процедуры можно, используя свойство property ParamBindMode: TParamBindMode; компонента TStoredProc, для которого допустимы значения: pbByName - (по умолча- нию) соответствие параметров ведется по именам (имена одинаковых параметров в компоненте и процедуре должны совпадать); pbByNumber - соответствие параметров производится по их порядковым номерам: производится попытка первый по порядку параметр компонента соотнести с первым по порядку параметром процедуры на сер- вере, второй - со вторым и т. д. Установить значения входных параметров и получить значения выходных пара- метров процедуры можно, используя свойство Params.ParamValues (см. выше) или метод function ParamByName(const Value: String): TParam; Например: // входной параметр StoredProcl.Params.ParamValues['IN_TOVAR'] := Tablel['TOVAR'].Value; // выходной параметр Labell.Caption := IntToStr(StoredProcl.ParamByName('MAX_KOLVO').Value); Перед выполнением процедуры нужно произвести связывание параметров компо- нента TStoredProc и параметров хранимой процедуры при помощи метода procedure Prepare; Вызов хранимой процедуры действия осуществляется методом procedure ЕхесРгос; Пусть на сервере определены хранимые процедуры действия FIND_MAX_KOLVO и STOIM. Осуществим их вызов из клиентского приложения.
Работа с хранимыми процедурами 399 Процедура FINDMAXKOLVO возвращает в выходном параметре MAX_KOLVO максимальную партию отгруженного товара, наименование которого передается во входном параметреIN_TOVAR\ CREATE PROCEDURE FIND_MAX_KOLVO (INJTOVAR VARCHAR(20)) RETURNS(MAX_KOLVO INTEGER) AS BEGIN SELECT MAX(KOLVO) FROM RASHOD WHERE TOVAR = : INJTOVAR INTO : MAX_KOLVO; SUSPEND; END Процедура STOIM возвращает стоимость количества товара. Параметр IN KOLVO определяет количество единиц товара, IN_TOVAR - название товара. CREATE PROCEDURE STOIM (INJTOVAR VARCHAR(20), IN_KOLVO INTEGER) RETURNS(OUT_STOIM INTEGER) AS DECLARE VARIABLE ZENA_ED INTEGER; BEGIN SELECT ZENA FROM TOVARY WHERE TOVAR = :INJTOVAR INTO :ZENA_ED; OUT_STOIM = ZENA_ED * IN_KOLVO; END Расположим на форме: 1. компонент ТТаЫе, ассоциированный с таблицей TOVARY, для выбора названия товара; 2. компонент TStoredProc (имя StoredProcI) для выполнения процедуры STOIM', 3. компонент TStoredProc (имя StoredProc2) для выполнения процедуры FIND_MAX_KOL VO; 4. компонент TButton для инициации выполнения хранимых процедур; 5. два компонента TLabel для визуализации результатов выполнения процедуры. Напишем такой обработчик нажатия кнопки, в котором происходит присваивание значений входных параметров и получение значений выходных параметров процедур: procedure TForml.ButtonlClick(Sender: TObject); begin StoredProc2.UnPrepare; StoredProcI.UnPrepare; StoredProcI.ParamByName('INJTOVAR').Value := Tablel.FieldByName('TOVAR').Value; StoredProcI.ParamByName('IN_KOLVO').Value := StoredProc2.ParamByName('MAX_KOLVO').Value; StoredProcI.Prepare; StoredProcI.ExecProc; Label1.Caption:= IntToStr(StoredProcI.ParamByName( 'OUT_STOIM').Value); StoredProc2.ParamByName('INJTOVAR').Value := Tablel.FieldByName('TOVAR').Value; StoredProc2.Prepare;
400 Глава 26 StoredProc2.ExecProc; Label2.Caption := IntToStr(StoredProc2.ParamByName( •MAX_KOLVO').Value); end; Окно работающей программы показано на рис. 26.5. Рис. 26.5. Значения максимального отпуска товара (количество и стоимость) возвращаются хранимой процедурой выбора как выходные параметры 26.6. ИЗМЕНЕНИЕ И УДАЛЕНИЕ ХРАНИМЫХ ПРОЦЕДУР Изменение хранимой процедуры производится оператором ALTER PROCEDURE ИмяПроцедуры [(входной_параметр тип_данных [, входной_параметр тип_данных...])] [RETURNS . (входной_параметр тип_данных [,входной_параметр тип_данных ...])] AS <тело процедуры>; Принципы построения оператора аналогичны изложенным выше принципам по- строения оператора CREATE PROCEDURE. После выполнения оператора ALTER PROCEDURE предыдущее определение процедуры заменяется на новое определение параметров, переменных и тела процедуры. Для удаления хранимой процедуры из базы данных используется оператор DROP PROCEDURE ИмяПроцедуры;
Глава 27 РАБОТА С ТРИГГЕРАМИ Триггер - это процедура БД, автоматически вызываемая 50Т-сервером при обнов- лении, удалении или добавлении новой записи в ТБД. Непосредственно из программы к триггерам обратиться нельзя. Нельзя и передавать им входные параметры и получать от них значения выходных параметров. Триггеры всегда реализуют действие. По событию изменения ТБД триггеры различаются на вызываемые при: • добавлении новой записи; • изменении существующей записи; • удалении записи. По отношению к событию, влекущему их вызов, триггеры различаются на: • выполняемые до наступления события; • выполняемые после наступления события. Преимущества использования триггеров: • Автоматическое обеспечение каскадных воздействий в дочерних таблицах при изменении, удалении записи в родительской таблице выполняется на сервере. Пользователю нет необходимости заботиться о программной реализации кас- кадных воздействий. Поскольку каскадные воздействия выполняет сервер, нет необходимости пересылать изменения в таблицах БД из приложения на сервер, что снижает загрузку сети. • Изменения в триггерах не влекут необходимости изменения программного кода в клиентских приложениях и не требуют распространения новых версий кли- ентских приложений. Замечание. При откате транзакции откатываются также и все изменения, вне- сенные в БД триггерами. 27.1. СОЗДАНИЕ ТРИГГЕРОВ Триггер создается оператором CREATE TRIGGER ИмяТриггера FOR ИмяТаблицы [ACTIVE | INACTIVE] [BEFORE | AFTER] {DELETE | INSERT | UPDATE] [POSITION номер] AS Стело триггера> Для определения тела триггера используется процедурный язык, рассмотренный в п. 26.3. В него добавляется возможность доступа к старому и новому значениям столбцов изменяемой записи OLD и NEW - возможность, недоступная при определе- нии тела хранимых процедур. Структура тела триггера-. [собъявление локальных переменных>] BEGIN < оператор> END
402 Глава 27 27.2. ОПРЕДЕЛЕНИЕ ЗАГОЛОВКА ТРИГГЕРА Заголовок триггера имеет формат CREATE TRIGGER ИмяТриггера FOR ИмяТаблицы [ACTIVE | INACTIVE] {BEFORE | AFTER) {DELETE | INSERT I UPDATE) [POSITION номер] • ACTIVE | INACTIVE — указывает, активен триггер или нет. Можно определить триггер «про запас», установив для него INACTIVE. В дальнейшем можно пере- определить триггер как активный. По умолчанию действует A CTIVE. • BEFORE | AFTER - указывает, будет выполняться триггер до (BEFORE) или по- сле (AFTER) запоминания изменений в БД. • DELETE | INSERT | UPDATE - указывает операцию над ТБД, при выполнении которой срабатывает триггер. • POSITION номер - указывает, каким по счету будет выполняться триггер в слу- чае наличия группы триггеров, обладающих одинаковыми характеристиками операции и времени (до, после операции) вызова триггера. Значение номера за- дается числом в диапазоне 0..32767. Триггеры с меньшими номерами выполня- ются раньше. Например, если определены триггеры CREATE TRIGGER A FOR RASHOD BEFORE INSERT POSITION 1 . CREATE TRIGGER C FOR RASHOD BEFORE INSERT POSITION 0.. CREATE TRIGGER D FOR RASHOD BEFORE INSERT POSITION 44 CREATE TRIGGER В FOR RASHOD AFTER INSERT POSITION 1 .. CREATE TRIGGER E FOR RASHOD AFTER INSERT POSITION 44 . для операции добавления новой записи в таблицу RASHOD они будут выполнены в последовательности С, A, D, В, Е. 27.3. ЗНАЧЕНИЯ OLD И NEW Значение ОЬО.ИмяСтолбца позволяет обратиться к состоянию столбца, имевшему место до внесения возможных изменений, а значение НЕЖИмяСтолбца - к состоя- нию столбца после внесения изменений. В том случае, если значение в столбце не изменилось, ОЬО.ИмяСтолбца будет равно NEW-ИмяСтолбца. Следующий триггер вносит соответствующие изменения в таблицу RASHOD, если в записи таблицы TOVARYизменилось значение столбца TOVAR'. CREATE TRIGGER BUJTOVARY FOR TOVARY ACTIVE BEFORE UPDATE AS BEGIN IF (OLD.TOVAR <> NEW.TOVAR) THEN UPDATE RASHOD SET TOVAR = NEW.TOVAR WHERE TOVAR = OLD.TOVAR; END
Работа с триггерами 403 27.4. ОБЕСПЕЧЕНИЕ КАСКАДНЫХ ВОЗДЕЙСТВИЙ Если между двумя или более ТБД установлены отношения ссылочной целостности (отношения «один-ко-многим», «один-к-одному»), при изменении столбца связи в родительской таблице должно быть изменено значение столбца связи у записей соот- ветствующих дочерних таблиц. Такое воздействие на дочернюю таблицу носит назва- ние каскадного обновления. Если в родительской таблице удалена запись, должны быть удалены все связанные с ней записи в дочерней таблице. Такое воздействие на дочернюю таблицу носит название каскадного удаления. Ограничение ссылочной целостности таблиц по внешнему ключу приводит к бло- кировке изменения и удаления записи в родительской таблице, если для нее есть до- черние записи в дочерней таблице: CREATE TABLE TOVARY( TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, PRIMARY KEY(TOVAR)); CREATE TABLE RASHOD( N_RASH INTEGER NOT NULL, TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, PRIMARY KEY(N_RASH), FOREIGN KEY(TOVAR) REFERENCES TOVARY); Для реализации автоматического выполнения каскадных обновлений и изменений необходимо, во-первых, в определении БД удалить ссылочные целостности, блоки- рующие такие изменения, и, во-вторых, определить сами триггеры для родительской таблицы. Триггер, реализующий каскадное обновление в дочерней таблице, будет в числе прочих содержать оператор IF (OLD.ПолеСвязиРодителя О NEW.ПолеСвязиРодителя) THEN UPDATE ДочерняяТаблица SET ПолеСвязиДочернейТаблицы = N£W.ПолеСвязиРодителя WHERE ПолеСвязиДочернейТаблицы = OLD.ПолеСвязиРодителя; Триггер, реализующий каскадное удаление в дочерней таблице, будет содержать оператор DELETE FROM ДочерняяТаблица WHERE ПолеСвязиДочернейТаблицы = ПолеСвязиРодителя; Напишем триггеры, выполняющие каскадные обновления и каскадные удаления в дочерней таблице RASHOD после соответственно изменения значения столбца связи или удаления записи в родительской таблице TOVARY-. CREATE TRIGGER BU_TOVARY FOR TOVARY ACTIVE BEFORE UPDATE AS BEGIN
404 Глава 27 IF (OLD.TOVAR <> NEW.TOVAR) THEN UPDATE RASHOD SET TOVAR = NEW.TOVAR WHERE TOVAR = OLD.TOVAR; END CREATE TRIGGER AD_TOVARY FOR TOVARY ACTIVE AFTER DELETE AS BEGIN DELETE FROM RASHOD WHERE RASHOD.TOVAR = TOVARY.TOVAR; END 27.5. ВЕДЕНИЕ ЖУРНАЛА ИЗМЕНЕНИЙ Журнал изменений в БД представляет собой таблицу БД, в которой фиксируются действия над всей базой данных или отдельными ее таблицами. В многопользователь- ских системах ведение такого журнала позволяет определить источник недостоверных или искаженных данных. Определим в базе данных таблицу TOVARY_LOG: CREATE TABLE TOVARY_LOG( DAT_IZM DATE, DEISTV CHAR(3), OLD_TOVAR VARCHAR(20), NEW_TOVAR VARCHAR(20) /* дата изменения */ /* операция */ /* старое значение TOVAR */ /* новое значение TOVAR */); в которую будем автоматически записывать любые изменения, добавления, удаления в таблице TOVARY. При этом будем фиксировать дату, операцию (INS, UPD, DEL) над таблицей TOVARY, а также старое и новое значение столбца TOVAR. Для операции удаления новое значение столбца TOVAR будет пустым. Для операции добавления пустым будет старое значение столбца TOVAR. CREATE TRIGGER TOVARY_ADD_LOG FOR TOVARY ACTIVE AFTER INSERT AS BEGIN INSERT INTO TOVARY—LOG(DAT—IZM, DEISTV, OLD—TOVAR, NEW_TOVAR) VALUES ("NOW","ADD","",NEW.TOVAR); END CREATE TRIGGER TOVARY_UPD_LOG FOR TOVARY ACTIVE AFTER UPDATE AS BEGIN INSERT INTO TOVARY—LOG(DAT—IZM, DEISTV, OLD—TOVAR, NEW_TOVAR) VALUES ("NOW","UPD",OLD.TOVAR,NEW.TOVAR); END
Работа с триггерами 405 CREATE TRIGGER TOVARY_DEL_LOG FOR TOVARY ACTIVE AFTER UPDATE AS BEGIN INSERT INTO TOVARY_LOG(DAT_IZM, DEISTV, OLD_TOVAR, NEW_TOVAR) VALUES ("NOW","DEL",OLD.TOVAR,""); END Пусть в таблицу TOVARY внесены некоторые изменения. Тогда, выполнив оператор SELECT * FROM TOVARY_LOG; получим историю изменений таблицы TOVARY: DAT_IZM DEISTV OLD_TOVAR NEW_TOVAR 30-JUN-1997 ADD 30-JUN-1997 UPD ОГУРЦЫ 30-JUN-1997 DEL ОГУРЦЫ ОГУРЦЫ Огурцы 27.6. ИСПОЛЬЗОВАНИЕ ТРИГГЕРОВ ДЛЯ РЕАЛИЗАЦИИ БИЗНЕС-ПРАВИЛ Триггеры активно используются для реализации бизнес-правил. В частности, это может быть установка с помощью генераторов уникальных значений индексных по- лей, накапливание статистики в других таблицах и многое другое. К сложностям, воз- никающим при реализации бизнес-правил при помощи триггеров, следует отнести неразвитость средств отладки логики кода, составляющего тело триггеров. Покажем на примерах реализацию некоторых бизнес-правил при помощи триггеров. Пусть столбец N RASH в таблице RASHOD должен содержать уникальное значе- ние. Для этой цели определим генератор RASHOD N RASH и установим его началь- ное значение 20. , CREATE GENERATOR RASHOD_N_RASH; SET GENERATOR RASHOD_N_RASH TO 20; При добавлении новой записи будем присваивать столбцу N_RASH уникальное значе- ние, полученное при помощи генератора: CREATE TRIGGER BI_RASHOD_GEN FOR RASHOD ACTIVE BEFORE INSERT BEGIN NEW.N_RASH = GEN_ID(RASHOD_N_RASH,1); END Пусть в БД имеется таблица STAT_TOVARY, в которой на каждую дату накаплива- ется количество отпущенного товара: CREATE TABLE STAT_TOVARY( DAT_RASH DATE NOT NULL,
406 Глава 27 TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, KOLVO INTEGER NOT NULL); Тогда следующие триггеры реализуют автоматическое заполнение корректных значе- ний в эту таблицу после добавления, изменения и удаления записи в таблице RASHOD: CREATE TRIGGER AI_RASHOD FOR RASHOD ACTIVE AFTER INSERT AS DECLARE VARIABLE CNT INTEGER; DECLARE VARIABLE OLD_KOLVO_VAL INTEGER; BEGIN /* выбрать число записей в таблице STAT_TOVARY */ /* по данному товару за дату расхода */ SELECT COUNT(*) FROM STAT_TOVARY WHERE (STAT_TOVARY.DAT_RASH = NEW.DAT_RASH) AND (STAT_TOVARY.TOVAR = NEW.TOVAR) INTO :CNT; /* если число записей = 0, добавить запись в */ /* таблицу STAT_TOVARY по данному */ /* товару и дате */ IF (:CNT = 0) THEN INSERT INTO STAT_TOVARY (DAT_RASH, TOVAR, KOLVO) VALUES(NEW.DAT_RASH, NEW.TOVAR, NEW.KOLVO); ELSE /* иначе добавить новое количество товара */ /* в уже существующей записи для этого товара */ /* и этой даты в STAT_TOVARY */ BEGIN SELECT KOLVO FROM STAT_TOVARY WHERE (9TAT_TOVARY.DAT_RASH = NEW.DAT_RASH) AND (STAT_TOVARY.TOVAR = NEW.TOVAR) INTO :OLD_KOLVO_VAL; UPDATE STAT_TOVARY SET KOLVO = :OLD_KOLVO_VAL + NEW.KOLVO WHERE (STAT_TOVARY.DAT_RASH = NEW.DAT_RASH) AND (STAT_TOVARY.TOVAR = NEW.TOVAR); END END CREATE TRIGGER AU_RASHOD FOR RASHOD ACTIVE AFTER UPDATE AS DECLARE VARIABLE CNT INTEGER; DECLARE VARIABLE OST_KOLVO INTEGER; DECLARE VARIABLE OLD_KOLVO_VAL INTEGER; BEGIN /* в таблице статистики STAT_TOVARY */ /* найти общее количество расхода */ /* старого товара по старой дате */
Работа с триггерами 407 /* из таблицы RASHOD */ SELECT KOLVO FROM STAT_TOVARY WHERE (STAT_TOVARY.DAT_RASH = OLD.DAT_RASH) AND (STAT_TOVARY.TOVAR = OLD.TOVAR) INTO :OLD_KOLVO_VAL; /* в таблице статистики STAT_TOVARY */ /* уменьшить общее количество прихода старого товара */ /* на старое значение количества расхода */ /* из таблицы RASHOD */ UPDATE STAT_TOVARY SET KOLVO = :OLD_KOLVO_VAL - OLD.KOLVO WHERE (STAT_TOVARY.DAT_RASH = OLD.DAT_RASH) AND (STAT_TOVARY.TOVAR = OLD.TOVAR); OST_KOLVO = OLD_KOLVO_VAL - OLD.KOLVO; /* если оставшееся количество расхода за */ /* эту дату по этому товару равно 0, удалить */ /* запись из таблицы STAT_TOVARY */ IF (:OST_KOLVO = 0) THEN DELETE FROM STAT_TOVARY WHERE (STAT_TOVARY.DAT_RASH = OLD.DAT_RASH) AND (STAT_TOVARY.TOVAR = OLD.TOVAR); /* выбрать число записей в таблице STAT_TOVARY */ /* по новому товару за новую дату расхода */ SELECT COUNT(*) FROM STAT_TOVARY WHERE (STAT_TOVARY.DAT_RASH = NEW.DAT_RASH) AND (STAT_TOVARY.TOVAR = NEW.TOVAR) INTO :CNT; /* если число записей = 0, добавить запись */ /* в таблицу STAT_TOVARY по новому */ /* товару и новой дате */ IF (:CNT = 0) THEN INSERT INTO STAT_TOVARY (DAT_RASH, TOVAR, KOLVO) VALUES(NEW.DAT_RASH, NEW.TOVAR, NEW.KOLVO); ELSE /* иначе добавить новое количество товара */ /* в уже- существующей записи для данного товара */ /* и новой даты в STAT_TOVARY */ BEGIN SELECT KOLVO FROM STAT_TOVARY WHERE (STAT_TOVARY.DAT_RASH = NEW.DAT_RASH) AND (STAT_TOVARY.TOVAR = NEW.TOVAR) INTO :OLD_KOLVO_VAL; UPDATE STAT_TOVARY SET KOLVO = :OLD_KOLVO_VAL + NEW.KOLVO WHERE (STAT_TOVARY.DAT_RASH = NEW.DAT_RASH) AND (STAT TOVARY.TOVAR = NEW.TOVAR); END END
408 Глава 27 CREATE TRIGGER ADL_RASHOD FOR RASHOD ACTIVE AFTER DELETE AS DECLARE VARIABLE OST_KOLVO INTEGER; DECLARE VARIABLE OLD_KOLVO_VAL INTEGER; BEGIN /* в таблице статистики STAT_TOVARY */ /* найти общее количество расхода */ /* товара за дату */ /* из таблицы RASHOD */ SELECT KOLVO FROM STAT_TOVARY WHERE (STAT_TOVARY.DAT_RASH = OLD.DAT_RASH) AND (STAT_TOVARY.TOVAR = OLD.TOVAR) INTO :OLD_KOLVO_VAL; /* в таблице статистики STAT_TOVARY*/ /* уменьшить общее количество прихода товара */ /* на значение количества расхода товара */ /* из удаленной таблицы RASHOD */ UPDATE STAT_TOVARY SET KOLVO = :OLD_KOLVO_VAL - OLD.KOLVO WHERE (STAT_TOVARY.DAT_RASH = OLD.DAT_RASH) AND (STAT_TOVARY.TOVAR = OLD.TOVAR); OST_KOLVO = OLD_KOLVOj_VAL - OLD.KOLVO; /* если оставшееся количество расхода по товару за */ /* эту дату равно 0, удалить */ /* запись из таблицы STAT_TOVARY */ IF (:OST_KOLVO = 0) THEN DELETE FROM STAT_TOVARY WHERE (STAT_TOVARY.DAT_RASH = OLD.DAT_RASH) AND (STAT_TOVARY.TOVAR = OLD.TOVAR); END Покажем состояние таблицы STATTOVARY после добавления в таблицу RASHOD новых записей по расходу за 12 и 14 января: SELECT * FROM STAT_TOVARY ORDER BY DAT_RASH, TOVAR DAT_RASH TOVAR KOLVO 12-JAN-1997 Кока-кола 23 12-JAN-1997 Сахар 100 14-JAN-1997 Кока-кола 3 Пусть после изменения в таблице RASHOD информации о расходе товара за 12 ян- варя: • вместо 11 единиц товара «Кока-кола» в действительности пришла 21 единица товара «Ставрида консерв.»; • вместо 100 единиц товара «Сахар» пришло 98 единиц этого товара.
Работа с триггерами 409 Покажем состояние таблицы STATTOVARY после внесения в таблицу RASHOD указанных изменений: SELECT * FROM STAT_TOVARY ORDER BY DAT_RASH, TOVAR DAT_RASH TOVAR KOLVO 12-JAN-1997 12-JAN-1997 12-JAN-1997 14-JAN-1997 Кока-кола 12 Сахар 98 Ставрида консерв. 21 Кока-кола 3 Покажем состояние таблицы STAT TOVARY после удаления расхода за 12 января товара «Сахар» в размере 98 ед. и за 14 января товара «Кока-кола» в размере 2 ед.: SELECT * FROM STAT_TOVARY ORDER BY DAT_RASH, TOVAR DAT_RASH TOVAR KOLVO 12-JAN-1997 Кока-кола 12 12-JAN-1997 Ставрида консерв. 21 14-JAN-1997 Кока-кола 1 27.7. ИЗМЕНЕНИЕ И УДАЛЕНИЕ ТРИГГЕРА Изменить существующий триггер можно при помощи оператора ALTER TRIGGER ИмяТриггера FOR ИмяТаблицы [ACTIVE | INACTIVE] (BEFORE | AFTER} (DELETE | INSERT I UPDATE} [POSITION номер] AS <тело триггера> После выполнения этого оператора все старые определения триггера заменяются на определения, указанные в операторе ALTER TRIGGER. Для удаления триггера следует воспользоваться оператором DROP TRIGGER ИмяТриггера;
Глава 28 ИСПОЛЬЗОВАНИЕ ГЕНЕРАТОРОВ Часто в состав первичного или уникального ключа входят цифровые поля, значения ко- торых должны быть уникальны, то есть не повторяться ни в какой другой записи таблицы. В одних случаях такое значение является семантически значимым и формируется пользовате- лем по определенному алгоритму - например, номер лицевого счета в банке. В других слу- чаях лучше предоставить выработку такого значения приложению или серверу БД. Для локальных СУБД (например, Paradox) для указанной цели применяются авто- инкрементные поля. При добавлении новой записи BDE автоматически устанавливает значение автоинкрементного поля так, чтобы оно было уникальным и не совпадало со значением данного автоинкрементного поля в других записях таблицы - не только существующих, но и удаленных. Иными словами, ранее использовавшееся значение автоинкрементного поля, даже если оно освободилось в результате удаления записи, никогда не назначается вновь. Изменить значение автоинкрементного поля нельзя. В InterBase отсутствует аппарат автоинкрементных столбцов. Вместо этого для ус- тановки уникальных значений столбцов можно использовать аппарат генераторов. Генератором называется хранимый на сервере БД механизм, возвращающий уни- кальные значения, никогда не совпадающие со значениями, выданными тем же самым генератором в прошлом. Для создания генератора используется оператор CREATE GENERATOR ИмяГенератора; Для генератора необходимо установить стартовое значение при помощи оператора SET GENERATOR ИмяГенератора ТО СтартовоеЗначение; При этом СтартовоеЗначение должно быть целым числом. Для получения уникального значения к генератору нужно обратиться с помощью функции GEN_ID (ИмяГенератора, шаг) ; Эта функция возвращает увеличенное на шаг предыдущее значение, выданное генера- тором (или увеличенное на шаг стартовое значение, если ранее обращений к генерато- ру не было). Значение шага должно принадлежать диапазону -231...+231 -1. Замечание. Не рекомендуется переустанавливать стартовое значение генера- тора или менять шаг при разных обращениях к GEN_ID. В противном случае генератор может выдать неуникальное значение, и как следствие будет возбу- ждено исключение при попытке запоминания новой записи в ТБД. Пусть в БД определен генератор, возвращающий уникальное значение для столбца N_RASH в таблице RASHOD: CREATE GENERATOR RASHOD_N_RASH; SET GENERATOR RASHOD_N_RASH TO 20; Обращение к генератору непосредственно из оператора INSERT: INSERT INTO RASHOD (N_RASH, DAT_RASH, KOLVO, TOVAR, POKUP) VALUES(GEN_ID(RASHOD_N_RASH,1),"IO-JAN-1997", 100,"Сахар", "Лира, TOO")
Использование генераторов 411 Присваивание ключевому столбцу уникального значения может быть реализовано с помощью триггера, вызываемого перед запоминанием новой записи в БД : CREATE TRIGGER BI_RASHOD FOR RASHOD ACTIVE BEFORE INSERT AS BEGIN NEW.N_RASH = GEN_ID(RASHOD_N_RASH, 1); END При этом в клиентском приложении, реализующем добавление новых записей в таблицу, столбец с уникальными значениями (в нашем случае NRASH) в программе не заполняется и оператор INSERT имеет вид INSERT INTO RASHOD (DAT_RASH, KOLVO, TOVAR, POKUP) VALUES(:DAT_RASH, :KOLVO, :TOVAR, :POKUP) Как можно заметить, столбец N RASH в операторе не упоминается: все необходимые дей- ствия по заполнению этого столбца уникальным значением выполняет триггер BI RASHOD. В ряде случаев для доступа к содержимому таблиц серверных БД в клиентских прило- жениях используют компонент ТТаЫе, который применяется для добавления, удаления и корректировки данных в этих таблицах. Здесь использование триггера может натолкнуться на неожиданное препятствие: при добавлении записи (метод Post компонента TTable} BDE отслеживает адекватность введенных значений столбцов добавляемой записи наложенным на эти столбцы ограничениям. Такой анализ осуществляется при выполнении метода Post в клиентском приложении (и, стало быть, до активизации триггера на сервере БД). При этом столбец с уникальным значением не заполнен, что, с точки зрения BDE, есть ошибка (поскольку данный столбец описан при создании с атрибутом NOT NULL, что обязательно для всех столбцов, входящих в первичный или просто уникальный ключ). «Обмануть» BDE можно, присваивая столбцу N_RASH любое значение, которое затем будет заменяться триггером на значение, полученное при помощи функции GEN_ID. Од- нако более корректным будет в данном случае использование не триггера, а процедуры: CREATE PROCEDURE GET_N_RASH RETURNS (NR INTEGER) AS BEGIN NR = GEN_ID(RASHOD_N_RASH,1); END При добавлении новой записи в таблицу в клиентском приложении вызов этой проце- дуры реализуется при помощи компонента TStoredProc, непосредственно после пере- вода компонента ТТаЫе в состояние dslnsert-. procedure TForml.RashodTableAfterlnsert(DataSet: TDataSet); begin StoredProcl.Close; StoredProcl.ExecProc; RashodTable.FieldByName('N_RASH').Value := StoredProcl.ParamByName(’NR’).Value; StoredProcl.Close; end; Заметим, что приведенные выше действия могут быть также реализованы в обра- ботчике события OnNewRecord компонента ТТаЫе.
Глава 29 ТРАНЗАКЦИИ 29.1. ОТКАТ ИЗМЕНЕНИЙ И ЦЕЛОСТНОСТЬ БД Существует несколько способов внесения изменений в таблицы БД. Пусть в НД (например, ТТаЫе), ассоциированном с какой-либо ТБД, выполнено удаление, добавление или корректировка записи. Для локальных БД (Paradox, dBase и т. д.) характерен подход немедленного ото- бражения изменений. Когда выполняется метод Post, изменения, внесенные в запись НД, немедленно физически запоминаются в ТБД, ассоциированной с этим НД. То же верно и для метода Delete - после него запись немедленно физически удаляется из ТБД. Отказаться от изменения таблицы БД в этом случае невозможно - ведь измене- ния уже физически внесены в нее. Необходимость отката изменений обусловливается тем обстоятельством, что БД всегда должна находиться в целостном состоянии. Классическим примером перехода БД из одного целостного состояния в другое является бухгалтерская проводка, когда некоторая сумма 5 должна быть списана со счета К и зачислена на счет D. Только ус- пешное выполнение этих двух операций гарантирует целостность информации в БД. Но целостность будет нарушена, если в результате сбоя сумма 5 будет списана со сче- та К, но не будет зачислена на счет D или, наоборот, зачислена на D, но не списана с К. Поэтому в случае ошибки списания/зачисления суммы результаты предыдущей операции зачисления/списания должны быть отменены. Существуют механизмы отката изменений в БД в случае невыполнения условия успешного завершения всех операций в составе группы. Один из таких механизмов носит название обработка транзакций. Обычно обработка транзакций реализуется серверами БД. Однако Delphi позволяет управлять транзакциями и для таблиц локаль- ных СУБД (Paradox, dBase). Кроме того, как для серверных, так и локальных БД Delphi предоставляет дополнительный механизм управления откатами изменений в БД - так называемые кэшированные изменения (см. гл. 30). 29.2. ПОНЯТИЕ ТРАНЗАКЦИИ Транзакция - это единичное или чаще групповое изменение БД, которое или вы- полняется полностью, или не выполняется вообще. Результаты выполнения транзак- ции записываются в БД только в том случае, если вся транзакция завершилась успеш- но. Таким образом, транзакция переводит БД из одного целостного состояния в дру- гое. Пусть нужно добавить запись в таблицу «Приход», прибавить количество прихода товара в запись для данного товара в ТБД «Остаток товара на складе» и прибавить стоимость поступившего товара и дату прихода в ТБД «Обороты по складу»: Prihod.Insert; Prihod.Post;
Транзакции 413 Ostatok.Edit; Ostatok.Post; Oborot.Edit; Oborot.Post; Если произошел сбой при выполнении любого метода Post, нужно отменить изме- нения, внесенные другими методами Post, иначе логическая целостность информации в БД будет разрушена. Управление транзакциями на уровне приложения реализуется методами компонен- та TDataBase. Начало транзакции инициируется методом procedure StartTransaction; После выполнения этого метода все изменения, внесенные в БД, считаются принадле- жащими к текущей активной транзакции. Подтвердить транзакцию, т. е. санкциониро- вать физическое запоминание сделанных изменений в БД, можно с помощью метода procedure Commit; Отказаться от физического запоминания сделанных изменений в БД («откатить» изме- нения), можно, выполнив метод procedure Rollback; Выполнение методов Commit или Rollback завершает активную транзакцию, нача- тую методом StartTransaction. Метод StartTransaction нельзя выполнить, если для БД в текущий момент времени имеется активная транзакция, т. е. транзакция, не завершен- ная методами Commit или Rollback. В этом случае возбуждается исключение. Проверить, имеются ли на текущий момент в БД активные незавершенные тран- закции, можно при помощи свойства property InTransaction: Boolean; Это свойство содержит True, если для БД имеется активная транзакция, и False, если не имеется. Предыдущий пример изменения таблиц Prihod, Ostatok и Oborot в рамках одной транзакции реализуется следующим образом: procedure SomeProcedure; begin Databasel.StartTransaction; Prihod.Insert; try Prihod.Post; except Databasel.Rollback; Exit; end; //try
414 Глава 29 Ostatok.Edit; try Ostatok.Post; except Databasel.Rollback; Exit; end; //try Oborot.Edit; try Oborot.Post; except Databasel.Rollback; Exit; end; //try try Databasel.Commit; except Databasel.Rollback; end; //try end; //procedure 29.3. УРОВНИ ИЗОЛЯЦИИ ТРАНЗАКЦИЙ: ПРИЛОЖЕНИЕ КЛИЕНТА 29.3.1. Уровни изоляции транзакций При одновременной работе нескольких клиентов с одной и той же БД возникают проблемы одновременного изменения данных. Пусть пользователь А получил данные из таблицы RASHOD и впоследствии изме- нил их. В это время с той же записью в таблице RASHOD работает пользователь В. Он также изменил данные в той же записи, что и Л, и пытается подтвердить их. Пользова- тель С работает с таблицей RASHOD в режиме только для чтения. Сразу же возникает группа вопросов - позволять или не позволять В изменять запись, если А еще не под- твердил ее изменение? Позволять ли С видеть изменения, внесенные Л и В? Может ли А видеть изменения, внесенные В, и наоборот? Для разрешения указанных проблем на стороне клиента определены три уровня изоляции (разграничения) транзакций. При уровне изоляции транзакций Dirty Read (в буквальном переводе — грязное чтение) конкурирующие транзакции видят изменения, внесенные, но не подтвер- жденные текущей транзакцией. Если текущая транзакция откатит сделанные измене- ния, другие транзакции будут располагать недостоверными данными. Этот уровень изоляции может привести к серьезным ошибкам и применяется редко - фактически он не изолирует конкурирующие транзакции. Уровень Dirty Read реализует BDE при ра- боте с локальными и файл-серверными БД. При уровне Read Committed (чтение подтверждений) конкурирующие транзакции оперируют только подтвержденными изменениями. Если транзакция А не завершена и в этот момент стартует транзакция В, она получит данные в том состоянии, которое
Транзакции 415 было на момент старта А. Однако, если транзакция А внесла изменения в запись и под- твердила их, а незавершившаяся транзакция В вновь прочитала эти данные, она уви- дит изменения. Уровень изоляции транзакций Repeatable Read (повторяющееся чтение) заставляет конкурирующие транзакции оперировать с собственными (локальными) версиями од- ной и той же записи. Если транзакция А читает какую-то запись, она, как и при уровне Read Committed, получит последнюю подтвержденную ее версию. Однако, если другая транзакция изменит эту запись, а транзакция А вновь прочитает ее, она не увидит вне- сенных изменений, т. е. А видит всегда одну и ту же запись. Изменения, которые вносят конкурирующие транзакции в одну и ту же запись, могут конфликтовать. В этом случае фактически изменит данные первая завершив- шаяся транзакция, а попытки подтвердить свои изменения второй и всеми другими незавершенными транзакциями, изменившими те же записи, будут отвергнуты (подробнее об этом см. п. 29.4). 29.3.2. Установка уровней изоляции транзакций в Delphi Уровень изоляции транзакций определяется свойством property Translsolation: TTranslsolation; компонента TDatabase. Возможные значения: tiDirtyRead, tiReadCommitted, tiRepeat- ableRead. Разные серверы БД различным образом интерпретируют уровни изоляции транзак- ций, установленные в свойстве Translsolation'. Сервер Translsolation Интерпретируется как Oracle tiDirtyRead tiReadCommitted tiRepeatableRead tiReadCommitted tiReadCommitted tiRepeatableRead (Только для НД, открытых в режиме Readonly) Sybase, MS-SQL tiDirtyRead tiReadCommitted tiRepeatableRead tiReadCommitted tiReadCommitted Не поддерживается DB2 tiDirtyRead tiReadCommitted tiRepeatableRead tiDirtyRead tiReadCommitted tiRepeatableRead Informix tiDirtyRead tiReadCommitted tiRepeatableRead tiDirtyRead tiReadCommitted tiRepeatableRead InterBase tiDirtyRead tiReadCommitted tiRepeatableRead tiReadCommitted tiReadCommitted tiRepeatableRead Paradox, dBASE tiDirtyRead tiReadCommitted tiRepeatableRead tiDirtyRead He поддерживается He поддерживается 29.4. СВОЙСТВО UPDATEMODE И ОБНОВЛЕНИЕ ЗАПИСЕЙ При наиболее популярном для серверных БД уровне изоляции транзакций Read Committed конкурирующие транзакции читают только подтвержденные данные. Это
416 Глава 29 означает, что сразу несколько транзакций могут изменить прочитанные данные и по- пытаться подтвердить эти изменения. Первая успешно завершившаяся транзакция фактически изменит данные, а что произойдет при подтверждении изменений другими транзакциями? Чтобы ответить на этот вопрос, следует изучить режимы, указываемые в свойстве UpdateMode набора данных в приложении клиента. Приложение изменяет копию записи в локальном НД на компьютере, где выполня- ется клиентское приложение. После выдачи подтверждения корректировки происхо- дит отправка к серверу БД запроса на изменение значений существующей в ТБД запи- си на новые значения, внесенные приложением в локальную копию. Для этого сервер должен найти в ТБД запись, которая еще сохраняет свои старые, неоткорректирован- ные значения. По существу сервер пытается выполнить оператор UPDATE, где в пред- ложении WHERE перечислены старые значения полей. Если запись, которую пытается изменить сервер, уже была изменена другой тран- закцией, операция UPDATE не пройдет из-за того, что записи со старыми значениями в БД физически не существует. Свойство набора данных property UpdateMode; определяет, по каким полям ищется запись для обновления на сервере. Возможные значения этого свойства: • WhereAII (по умолчанию) - поиск записи, которая должна быть физически из- менена, ведется по всем полям; • WhereKeyOnly - поиск записи ведется только по значениям ключевых полей; • WhereChanged - поиск ведется по ключевым полям и по тем из неключевых по- лей, которые были изменены при корректировке записи в приложении. Наиболее жесткое условие поиска - WhereAII. Оно предотвращает запоминание из- менений, внесенных в запись, если конкурирующая транзакция успела изменить хотя бы одно поле и подтвердить это изменение. Менее жесткое условие - WhereChanged. Оно предотвращает запоминание изменений, внесенных в запись, если конкурирующая тран- закция успела изменить хотя бы одно ключевое поле или поле, которое было изменено в текущей транзакцией. Вероятность ошибок при этом выше, чем при WhereAII. Наименее жесткое и с наибольшей вероятностью ведущее к ошибке условие — WhereKeyOnly. Оно предотвращает запоминание изменений, если конкурирующая транзакция успела изме- нить хотя бы одно ключевое поле, которое пытается также изменить и текущая транзак- ция. Изменения других полей в расчет не принимаются. Пусть в ТБД, состоящей из 4 полей (столбцов): Firma, Familia, Doljnost и Oklad, - две транзакции из разных приложений пытаются изменить запись Firma Familia Doljnost Oklad Янтарь Ивонов Директор 1000 Пользователи А и В оба знают, что эти данные недостоверны. Во-первых, Ивонов не директор, а бухгалтер. А Иванов - действительно директор — получает не 1000, а 2000. 1000 получает Ивонов. Поэтому каждый из пользователей А и В решает изменить за- пись. Пусть пользователь А изменил фамилию на Иванов, а оклад на 2000 и подтвер- дил транзакцию. Запись была изменена так:
Транзакции 417 Firma Familia Doljnost Oklad Янтарь Иванов Директор 2000 Несколько мгновений спустя пользователь В изменил должность на Бухгалтер и так- же подтвердил транзакцию: Firma Familia Doljnost Oklad Янтарь Ивонов Бухгалтер 1000 Однако записи [«Янтарь», «Ивонов», «Бухгалтер», 1000] физически не существует. Дальнейшие действия BDE зависят от значения свойства UpdateMode набора дан- ных. При значении WhereAll обновления, сделанные пользователем В, будут отклонены (в приложении этого пользователя возникнет исключительная ситуация), и запись ос- танется в том виде, в который она перешла после завершения транзакции пользовате- лем Л. При значении WhereKeyOnly BDE будет идентифицировать в ТБД ту запись, кото- рую нужно обновить, по значению индексного поля (полей). Пусть такое поле в дан- ном случае - Firma. Пользователь А не изменял значение этого поля - как было « Янтарь», так и осталось. Поэтому обновление записи для пользователя В будет разре- шено. В результате запись, о которой идет речь, будет в ТБД иметь вид Firma Familia Doljnost Oklad Янтарь Иванов Бухгалтер 1000 то есть станет недостоверной. При значении WhereChanged идентификация в ТБД записи, которая должна быть замещена, ведется на соответствие ключевого поля и поля, которое изменено. В дан- ном случае это поля Firma и Doljnost. Поскольку пользователь А не менял этих полей, запись будет найдена и данные в ТБД вновь окажутся недостоверными. 29.5. ЯВНО И НЕЯВНО СТАРТУЕМЫЕ ТРАНЗАКЦИИ Явная транзакция начинается методом StartTransaction, а завершается методами Rollback или Commit компонента TDatabase. В рамках одного компонента TDatabase невозможно запустить две параллельные транзакции. Неявная транзакция начинается методами Insert, Edit, Delete, Append и т. д. Она за- вершается после выполнения метода Post и откатывается в случае выполнения метода Cancel. Неявная транзакция запускается и после посылки к серверу 5£?£-оператора (компонент TQuery), то есть после выполнения метода ExecSQL. Последний способ называется PassThroughSQL. Выдача автоматического подтверждения неявной тран- закции определяется значением параметра SQLPASSTHRU для псевдонима БД (утилита BDE Administrator). Этот параметр определяет: • могут ли вызовы PassThroughSQL и стандартные вызовы BDE использовать од- но и то же соединение с БД; • видят ли друг друга транзакции, порожденные PassThroughSQL, и транзакции, порожденные вызовами BDE. Значения параметра SQLPASSTHRU'. • SHARED AUTOCOMMIT — после выполнения ExecSQL, равно как и метода Post (компоненты ТТаЫе, TQuery), автоматически выдается Commit. При этом Post,
418 Глава 29 Delete и PassThroughSQL могут использовать одно и то же соединение с БД (один и тот же компонент TDatabase). • SHARED NO AUTOCOMMIT - неявная транзакция, порожденная Pass- ThroughSQL, автоматически не завершается (Commit автоматически не выдает- ся), и подтверждение транзакции необходимо выполнять программно. При этом BDE и PassThroughSQL могут использовать одно и то же соединение с БД (один и тот же компонент TDatabase). • NOT SHARED - транзакции каждого типа должны использовать отдельное со- единение с БД каждая. Транзакции, начатые явно при помощи TDatabase.Start- Transaction, игнорируются. Подтверждение транзакции должно осуществляться явно посылкой COMMIT. J\o этих пор изменения, внесенные PassThroughSQL, не будут видны в компонентах ТТаЫе и TQuery, работающих с БД через другое соединение. Режим NOT SHARED следует применять только тогда, когда для управления тран- закциями на сервере применяется PassThroughSQL, поскольку в этом случае игнори- руются действия по управлению транзакциями компонента TDatabase. В противном случае одновременное управление транзакциями при помощи компонента TDatabase и PassThroughSQL может привести к непредсказуемым последствиям. Режим SHARED AUTOCOMMIT не рекомендуется к применению для приложений, работающих с удаленными БД в архитектуре «клиент-сервер». В этом случае при по- строчных операциях с НД происходит подтверждение транзакции для каждой записи, что способно существенно увеличить сетевой трафик и как следствие - привести к существенному замедлению работы. 29.6. УПРАВЛЕНИЕ ТРАНЗАКЦИЯМИ НА SQL-СЕРВЕРЕ INTERBASE InterBase управляет транзакциями при помощи SQL-операторов SET TRAN- SACTION (начать транзакцию), COMMIT (подтвердить транзакцию) и ROLLBACK (откатить транзакцию). Оператор SET TRANSACTION имеет формат SET TRANSACTION [READ WRITE |READ ONLY] [WAIT I NO WAIT] [[ISOLATION LEVEL] {SNAPSHOT [TABLE STABILITY] I READ COMMITTED [[NO] RECORD_VERSION]}] [RESERVING Ссписок таблиц> [FOR [SHARED I PROTECTED][READ | WRITE]], [< список таблиц > ...]; где • READ WRITE | READ ONLY устанавливает уровень доступа к данным (по умол- чанию READ WRITE)-, • WAIT | NO WAIT определяет поведение при возникновении конфликта по об- новлению записи данной транзакции с другой транзакцией, ранее сделавшей изменение в той же записи: WAIT (по умолчанию) побуждает данную транзак- цию ожидать завершения конкурирующей транзакции; NO WAIT определяет аварийное завершение данной транзакции;
Транзакции 419 • ISOLATION LEVEL определяет уровни изоляции транзакций на сервере (по умолчанию SNAPSHOT, что соответствует уровню REPETABLE READ)-, • READ COMMITTED разрешает стартующей транзакции читать только подтвер- жденные данные: NO RECORD VERSION (по умолчанию) - читается последняя версия записи, даже если она не подтверждена другой транзакцией; при этом если указан параметр WAIT, стартующая транзакция будет ожидать подтвер- ждения последней версии; RECORD VERSION - читается последняя подтвер- жденная версия записи, даже если на сервере есть более поздняя, но не под- твержденная версия; • RESERVING в рамках стартующей транзакции блокирует конкурирующим транзакциям доступ к таблицам, перечисленным в списке таблиц. В последнем случае каждому элементу списка таблиц ставятся в соответствие па- раметры: • PROTECTED READ - конкурирующие транзакции могут читать данные, но не могут изменять; • PROTECTED WRITE - читать данные могут только транзакции с уровнями SNAPSHOT или READ COMMITTED, и никакая конкурирующая транзакция не может их изменять. Следующий оператор стартует транзакцию без имени с уровнем изоляции READ COMMITTED, транзакция будет ожидать подтверждения обновления последней вер- сии нужной записи: SET TRANSACTION WAIT ISOLATION LEVEL READ COMMITTED; Следующий оператор стартует транзакцию Т1 и блокирует таблицы TABLE1 и TABLE2 для чтения, a TABLE3 - для записи: SET TRANSACTION NAME Т1 WAIT ISOLATION LEVEL READ COMMITTED RESERVED TABLEI, TABLE2 FOR PROTECTED READ, TABLE3 FOR PROTECTED WRITE;
Глава 30 КЭШИРОВАННЫЕ ИЗМЕНЕНИЯ 30.1. ИСПОЛЬЗОВАНИЕ КЭШИРОВАННЫХ ИЗМЕНЕНИЙ Кэширование изменений заключается в том, что создается локальная копия данных в буфере (кэше). Все последующие корректировки данных, включая изменение, уда- ление, добавление новых записей, происходят в этом локальном буфере. Внесенные изменения затем могут быть физически перенесены в БД, или может быть произведен отказ от изменений («откат»). К преимуществам кэширования изменений можно отнести: • минимизацию сетевого трафика при работе с удаленным сервером; • отсутствие блокировок на изменяемые записи. Кэширование изменений реализуется для наборов данных (компоненты ТТаЫе, TQuery, TStoredProc) путем присваивания значения True свойству property CachedUpdates: Boolean; Установка значения этого свойства может быть произведена как на этапе разработки приложения, так и во время прогона программы. Установка значения False в это свой- ство отменяет режим буферизации, при этом произведенные над кэшированными дан- ными изменения должны быть либо отменены, либо подтверждены. Для отмены изме- нений используется метод procedure CancelUpdates; 30.2. ПОДТВЕРЖДЕНИЕ КЭШИРОВАННЫХ ИЗМЕНЕНИЙ Подтверждение кэшированных изменений реализуется в две фазы. Двухфазный подход имеет своей целью обеспечить защиту БД от ошибок, особенно в случае вне- сения изменений в несколько ТБД. Например, при кэшированных изменениях не об- наруживаются нарушения ограничений ссылочных целостностей таблиц. Они обнару- живаются только при попытке запомнить такие изменения. 1 фаза Запускается транзакция БД; Кэшированные изменения записываются в БД. 2 фаза В случае успешной записи кэшированных изменений в БД: • происходит подтверждение транзакции БД; • подтверждаются кэшированные изменения. В случае неуспешной записи: • происходит откат транзакции БД; • происходит откат кэшированных изменений с восстановлением или без восстановления первоначальных значений записей. Пользователь может скорректировать ситуацию, вызвавшую ошибку, и повторить попытку за- поминания кэшированных изменений в БД.
Кэшированные изменения 421 30.2.1 . Использование метода ApplyUpdates компонента TDatabase Метод компонента TDatabase procedure ApplyUpdates(const DataSets: array of TDataSet); применяется для подтверждения кэшированных изменений сразу в нескольких НД. Список НД определяется параметром DataSets. В случае указания нескольких НД их имена разделяются запятыми. Двухфазное запоминание в этом случае производится неявно: метод ApplyUpdates стартует транзакцию и пытается запомнить измененные записи в БД. В случае неудачи он откатывает транзакцию, но не изменяет статуса кэшированных изменений. Поэто- му, если необходимо отменить кэшированные изменения, это необходимо сделать яв- но при помощи метода НД CancelUpdates. Управление ошибкой запоминания кэширо- ванного изменения может осуществляться в обработчике события OnUpdateError. В случае успешного запоминания результатов кэшированных изменений в БД метод ApplyUpdates подтверждает транзакцию и подтверждает кэшированные изменения. Пример. Пусть требуется подтвердить кэшированные изменения НД Tovary и Rashod. В случае неудачи необходимо откатить кэшированные изменения: try DataBasel.ApplyUpdates([Tovary, Rashod]); except ShowMessage('Изменения сохранить нельзя: они '+ 'влекут нарушение целостности базы данных'); Tovary.CancelUpdates; Rashod.CancelUpdates ; end; //try 30.2.2 . Использование метода ApplyUpdates набора данных Метод procedure ApplyUpdates; компонентов TTable, TQuery, TStoredProc применяется для подтверждения кэширо- ванных изменений отдельного НД. Отсутствие ошибок при выполнении метода гаран- тирует успешность операции подтверждения. Метод procedure CommitUpdates; применяется для обновления в БД кэшированных изменений, успешно подтвержден- ных методом ApplyUpdates. Двухфазное подтверждение в случае использования методов ApplyUpdates и Cow- mitUpdates необходимо реализовывать явно. Например: Databasel.StartTransaction; try Tovary.ApplyUpdates; Rashod.ApplyUpdates; Databasel.Commit; Tovary.CommitUpdates; Rashod.CommitUpdates;
422 Глава 30 except Databasel.Rollback; ShowMessage('Изменения могут привести к нарушению 1 + 'целостности БД. Изменения отменены!' ); Tovary.CancelUpdates ; Rashod.CancelUpdates; end; //try В случае сбоя при выполнении Tovary.ApplyUpdates или Rashod.ApplyUpdates не произойдет подтверждения транзакции (Databasel.Commit) и подтверждения кэширо- ванных изменений для каждой из таблиц; значения измененных записей будут восста- новлены (Tovary.CancelUpdates, Tovary.CancelUpdates). Часто важно, чтобы даже в случае неудачной попытки запомнить изменения, зна- чения измененных записей не менялись - например, для того чтобы пользователь мог внести коррективы и попытаться снова подтвердить изменения. В этом случае из кода необходимо убрать вызов метода CancelUpdates. 30.3. ВИДИМОСТЬ ИЗМЕНЕННЫХ ЗАПИСЕЙ Свойство property UpdateRecordTypes: TUpdateRecordTypes; TUpdateRecordTypes = set of (rtModified, rtlnserted, rtDeleted, rtUnModified); набора данных указывает, какие из записей будут «видны» в НД после проведения кэшированных изменений. Множество TUpdateRecordTypes может содержать сле- дующие значения: rtModified — измененные записи; rtlnserted — добавленные записи; rtDeleted - удаленные записи; rtUnmodified - неизменявшиеся записи. Вот как можно показать в НД только удаленные записи: RashodTable.UpdateRecordTypes := [rtDeleted]; А так - показать только неизменявшиеся, добавленные и измененные записи (удаленные не показывать): RashodTable.UpdateRecordTypes := [rtUnmodified,rtModified, rtlnserted]; Замечание. Хотя в НД могут показываться и удаленные записи, тем не менее свойство НД RecordCount возвратит количество записей за вычетом удаленных. 30.4. ОБРАБОТЧИК СОБЫТИЯ ONUPDATEERROR 30.4.1. Использование обработчика OnUpdateError К наиболее часто встречающимся ошибкам, происходящим при попытках под- тверждения кэшированных изменений, относятся: • нарушение значения поля связи в родительской или дочерней таблицах; • нарушение уникальности индекса, построенного по первичному или уникаль- ному ключу таблицы БД; • попытка ввести в попе связи дочерней таблицы значение, отсутствующее в поле связи родительской таблицы; • нарушение требования ввода в одно из полей обязательного значения;
Кэшированные изменения 423 • нарушение ограничений, накладываемых на значение поля. Реагировать на ошибки, возникающие при подтверждении кэшированных измене- ний, можно в обработчике события OnUpdateError: procedure TForml.TovaryUpdateError(DataSet: TDataSet; E: DatabaseError; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); begin end; Параметры обращения имеют следующее назначение: DataSet - набор данных, для которого при попытке подтверждения кэшированных изменений произошла ошибка; UpdateKind - вид изменений, вызвавших ошибку; возможные значения: ukModify - запись была изменена (скорректировано значение хотя бы одного поля); uklnsert- запись была добавлена; ukDelete - запись была удалена. UpdateAction - возвращаемый параметр, с помощью которого обработчик указыва- ет, что нужно делать с ошибочной записью (см. п. 30.6.2); Е - указатель на исключительную ситуацию, возбужденную в результате ошибки; мо- жет использоваться для получения сообщения об ошибке или кода ошибки (см. п. 30.6.3). 30.4.2. Использование параметра UpdateKind Параметр UpdateKind позволяет в случае ошибки предпринять какие-либо дейст- вия, зависящие от способа изменения записи. Вот как, например, можно восстановить запись, если при попытке подтверждения ее удаления произошла ошибка: procedure TForml.TovaryUpdateError(DataSet: TDataSet; E: EDatabaseError; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); begin if UpdateKind = ukDelete then Tovary.RevertRecord; end; 30.4.3. Использование параметра UpdateAction Изменяемый параметр UpdateAction определяет действие, которое следует произ- вести в отношении ошибочной записи. Действие указывается перед выходом из обра- ботчика: procedure TForml.TovaryUpdateError(DataSet: TDataSet; E: EDatabaseError; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); begin // Какие-либо операторы, если нужны UpdateAction := uaSkip; end; Можно указать следующие действия:
424 Глава 30 Значение Смысл uaFail Отменить изменения для записи, выдать сообщение об ошибке. uaAbort Отменить изменения для записи, сообщение об ошибке не выдавать. Приме- няется также в случаях, когда генерируется сообщение об ошибке, отличное от выдаваемого системой. uaSkip Не отменять изменений для записи, но запись физически в таблицу БД не переносить (что, собственно, и невозможно без изменения значений записи). uaRetry Повторить попытку подтверждения изменений для записи. Предполагается, что на момент повторения попытки значения полей данной записи скорректирова- ны программным способом. В противном случае произойдет зацикливание. Заметим, что при выборе uaSkip в физической ТБД будут подтверждены изменения не всех записей, а только тех из них, для которых не произошла ошибка при подтверждении кэшированных изменений. Такой подход противоречит транзакционному подходу, согласно которому либо запоминаются все изменения, либо происходит их одновременный откат. Пусть имеется таблица «Товары» и дочерняя таблица «Расход товара». Пусть эти таблицы соединены по полю связи «Товар» (рис. 30.1). Кэшированные изменения OnUpdateError Т овары [7 Открыт? Подтвердить изменения в Тovary | Откат изменений в Тovary Г ираагечсгюгп I (• uaFail ! С uaAbort | | C uaSkip । C uaRetry Расход Рис. 30.1. Таблица «Товары» и дочерняя таблица «Расход товара». Как видно из рисунка, в таблице «Товары» для товаров «Сахар» и «Сыр гудаутский » имеются дочерние записи в таблице «Расход». Изменим в таблице «Товары» наиме- нования всех товаров и попытаемся подтвердить кэшированные изменения: Databasel.StartTransaction; try Tovary.ApplyUpdates; Databasel.Commit; Tovary. CommitUpdates;’ except Databasel.Rollback; end; //try Тогда изменение значения в поле «Товар» для товаров «Сахар» и «Сыр гудаутский» приведет к нарушению целостности БД, поскольку в таблице «Расход» наименования то- варов мы изменять не будем. Напишем такой обработчик OnUpdateError для НД Tovary.
Кэшированные изменения 425 procedure TForml.TovaryUpdateError(DataSet: TDataSet; E: EDatabaseError; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); begin case RadioGroupl.Itemindex of 0 : UpdateAction := uaFail; 1 : UpdateAction := uaAbort; 2 : UpdateAction := uaSkip; 3 : UpdateAction := uaRetry; end; //case end; Компонент RadioGroupl дает возможность выбора одного из Значений изменяемо- го параметра UpdateAction. Как показано на приводимых ниже рисунках 30.2, 30.3 и 30.4, в режимах uaFail и uaAbort результаты кэшированных изменений полностью отменены, а в режиме uaSkip частично подтверждены (для товаров Хек и Чавыча). В режиме uaRetry программа зависнет, т. к. в обработчике не корректируются ошибочные записи. Рис. 30.2. Таблицы «Товары» и «Расход» до подтверждения изменений. Рис. 30.3. После подтверждения в режимах uaFail и uaAbort.
426 Глава 30 Рис. 30.4. После подтверждения в режиме uaSkip. Введем в обработчик события OnUpdateError для режима UpdateAction = uaRetry возврат к старому значению поля «Товар» для записей, на которых попытка подтвер- ждения изменений вызывает ошибку: procedure TForml.TovaryUpdateError(DataSet: TDataSet; E: EDatabaseError; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); begin case RadioGroupl.Itemindex of 0 : UpdateAction := uaFail; 1 : UpdateAction := uaAbort; 2 : UpdateAction := uaSkip; 3 : begin Tovary.FieldByName('Tovar').NewValue := Tovary.FieldByName('Tovar').OldValue; UpdateAction := uaRetry; end; end; //case end; Теперь обработчик OnUpdateError будет действовать так, как если бы был уста- новлен режим uaSkip. 30.4.4. Использование параметра Е Используя параметр Е, можно определить причину возникновения ошибки и в со- ответствии с этой причиной предпринять какие-либо действия. Параметр Е содержит ссылку на исключительную ситуацию, возбужденную при диагностировании ошибки в процессе подтверждения кэшированных изменений. Тип исключения, на которое ссылается Е, - EDatabaseError. Код ошибки в этом типе от- сутствует, но имеется свойство property Message: String; которое содержит текст сообщения об ошибке, выводимого на экран при возбуждении исключительной ситуации. Анализируя этот текст, можно уточнить характер ошибки. Например:
Кэшированные изменения 427 procedure TForml.TovaryUpdateError(DataSet: TDataSet; E: EDatabaseError; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); var S: String; begin S := "; // Уточнение характера ошибки с помощью // анализа сообщения Message: if pos('FOREIGN',Е.Message) > 0 then S := 'Нарушение целостности БД. Для старого значения поля TOVAR ' + 'есть дочерние записи в таблице "Расход"' else if pos('Key violation',E.Message) > 0 then S := 'Нарушение уникальности поля ТОВАР' else S := 'Другая ошибка'; ShowMessage(S); end; Значительно удобнее анализировать код ошибки, однако, как уже говорилось, тип EDatabaseError не имеет поля с кодом ошибки. Если преобразовать параметр Е к типу EDBEngineError операцией приведения типов EBDEngineError(Е) можно воспользоваться свойствами этого класса и, в том числе, свойством property Errors[Index: Integer]: TDBError; которое возвращает стек ошибок BDE. В свойстве property ErrorCount: Integer; содержится количество сохраняемых в стеке ошибок. В подкаталоге DOC каталога размещения Delphi в файле BDE.INT описываются коды ошибок BDE. Код ошибки имеет тип Word и представляет собой категорию ошибки (в старшем байте) и саму ошибку (в младшем). Объекты класса TDBError содержат код ошибки в свойстве ErrorCode, а категорию ошибки - в свойстве Category. Вот как, например, можно воспользоваться описанными свойствами для уточнения характера ошибки: procedure TForml.TovaryUpdateError(DataSet: TDataSet; E: EDatabaseError; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); var S: String; begin with EDBEngineError(E) do case Errors[ErrorCount-1].Category of ERRCAT_INTEGRITY: S := 'Нарушение целостности базы'; ERRCAT_DATACORRUPT: S := 'Физическое разрушение данных'; ERRCAT_IO: S := 'Ошибка операции ввода-вывода'; {...}
428 Глава 30 ERRCAT_OTHER: S := 'Ошибка категории ERRCAT_OTHER'; end; //case ShowMessage(S); end; 30.4.5. Свойства OldValue и New Value При использовании кэшированных изменений доступны свойства OldValue и NewValue объектов-полей TField. Первое содержит исходное значение поля (столбца), то есть значение, имевшее место до каких-либо изменений. После того как кэширо- ванные изменения были успешно подтверждены, старое значение поля (столбца) ста- новится недоступным. Свойство NewValue содержит новое значение поля. Если при попытке подтверждения кэшированных изменений возникает ошибка, в обработчике OnUpdateError можно программно скорректировать значение NewValue и повторить попытку подтверждения изменений, указав UpdateAction - uaRetry. 30.5. КОМПОНЕНТ TUPDATESQL 30.5.1. Назначение компонента Компонент TUpdateSQL используется для подтверждения кэшированных измене- ний в НД, связанном с компонентом TQuery. Такой НД является результатом выпол- нения S^Z-оператора SELECT и может быть доступным для обновления или доступ- ным только для чтения. В последнем случае свойство НД property CanModify: Boolean; возвращает False, даже если в свойство property RequestLive: Boolean; компонента TQuery помещено значение True на этапе разработки приложения или программно во время выполнения. Компонент TUpdateSQL позволяет вносить изменения в НД, доступные только для чтения. Этот компонент объединяет в себе три компонента TQuery, содержащих SQL- операторы INSERT, UPDATE, DELETE для соответственно добавления, изменения или удаления записей. 30.5.2. Работа с компонентом на этапе конструирования Для работы с компонентом его нужно разместить на форме или в модуле данных, где размещен кэшированный компонент TQuery, и связать с этим компонентом. Заме- тим, что в компоненте TQuery уже должны быть определены свойства CachedUpdates, DatabaseName и SQL, т. е. Delphi должна знать текст оператора SELECT, который бу- дет выполняться в момент открытия TQuery (открывать TQuery на этапе конструиро- вания необязательно). Связь компонентов TQuery и TUpdateSQL осуществляется с по- мощью свойства UpdateObject компонента TQuery, в которое нужно поместить имя компонента TUpdateSQL. После связывания компонентов вызывается редактор компонента TUpdateSQL - двойным щелчком по компоненту или с помощью опции UpdateSQL Editor его ло- кального меню (рис. 30.5).
Кэшированные изменения 429 Рис. 30.5. Редактор TUpdateSQL. Если компонент связан с Гамету, в котором уже подготовлен оператор SELECT, редактор автоматически настроит поля Table Name, Key Fields и Update Fields, в про- тивном случае их нужно настроить вручную. После выбора обновляемой таблицы, ключевых и обновляемых полей нажимается кнопка Generate SQL, чтобы Delphi автоматически создала три S^L-onepaTopa для вставки, изменения и удаления записи. Тексты этих операторов можно увидеть и при необходимости отредактировать с помощью окна, связанного с закладкой SQL редак- тора компонента (рис. 30.6). Они хранятся в свойствах InsertSQL, ModifySQL и DeleteSQL компонента TUpdateSQL. Рис. 30.6. Сгенерированный SQL-оператор UPDATE.
430 Глава 30 30.5.3. Выполнение SQL-операторов Существует два способа выполнения SgZ-операторов, определенных в компоненте TUpdateSQL. Если с TQuery связан единственный компонент TUpdateSQL, следует использовать автоматический способ. Он заключается в том, что применение метода TDatabase.Ap- plyUpdates для каждой подтвержденной записи приводит к автоматическому выбору и выполнению соответствующего ^/.-оператора из компонента TUpdateSQL, ассоции- рованного с одним из изменяемых НД. В этом случае не нужно кодировать вызов SQL-операторов из компонента TUpdateSQL. Если с НД связано несколько компонентов TUpdateSQL (это возможно, если в опе- раторе SELECT компонента TQuery объединяются несколько таблиц), вызов SQL- оператора из нужного компонента TUpdateSQL производят в обработчике события OnUpdateRecord. Параметры обработчика OnUpdateRecord идентичны параметрам рассмотренного выше обработчика события OnUpdateError за исключением отсутст- вующего параметра Е: procedure TForml.QuerylUpdateRecord(DataSet: TDataSet; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); Для установки значений параметров изменяемой записи используют метод SetParams компонента TUpdateSQL: procedure SetParams(UpdateKind: TUpdateKind); который замещает параметры в SgZ-операторе компонента TUpdateSQL значениями соответствующих полей ТБД. Какой SQL -оператор выполнять, определяет значение UpdateKind'. ukModify — запись была изменена (скорректировано значение хотя бы од- ного поля); uklnsert - запись была добавлена; ukDelete - запись была удалена. Для выполнения соответствующего S^Z-оператора используют метод ExecSQL компонента TUpdateSQL: procedure ExecSQL(UpdateKind: TUpdateKind); Существует также метод Apply, который объединяет в себе функциональность ме- тодов SetParams и ExecSQL'. procedure Apply(UpdateKind: TUpdateKind); Метод Apply в соответствии co значением параметра UpdateKind определяет тре- буемый SgZ-оператор, изменяет параметры этого оператора и затем выполняет его. Например, обработчик события OnUpdateRecord, содержащий вызов соответст- вующих SQL-операторов компонента TUpdateSQL, может иметь такой вид: procedure TForml.QuerylUpdateRecord(DataSet: TDataSet; UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); begin with DataSet.UpdateObject as TUpdateSQL do begin SetParams(UpdateKind) ; ExecSQL(UpdateKind) ; end; //with UpdateAction := uaApplied; end;
Глава 31 РАБОТА С СОБЫТИЯМИ 31.1. ПОНЯТИЕ СОБЫТИЯ В SQL -языке сервера InterBase определен оператор POST_EVENT <Имя события>; После его выполнения наступает событие с указанным именем. Сервер БД уведом- ляет о наступлении этого события все активные приложения, зарегистрировавшие свой интерес к нему с помощью оператора EVENT INIT, который создается и выпол- няется компонентом TIBEventAlerter. Обмен событиями между сервером и приложениями может широко использовать- ся. Например, с его помощью можно создать клиентское приложение, которое будет видеть все изменения, вносимые в БД другими пользователями, — для этого лишь нужно, чтобы в соответствующих триггерах БД возбуждалось нужное событие. Полу- чив сообщение, программа-наблюдатель обновляет соответствующий НД, в котором тут же отображаются внесенные изменения. Может иметь место и обмен уведомлениями о наступлении каких-либо событий двумя одновременно работающими с одной и той же БД клиентскими приложениями, выполняющими по отношению к БД различные функции. В этом случае сервер БД выступает в качестве посредника. Так, например, может быть построена и почтовая система организации, где сервер БД выполняет функции рассылки сообщений, а одна из таблиц БД £или их группа) служит в качестве системы почтовых ящиков с различ- ными правами доступа. 31.2. КОМПОНЕНТ TIBEVENTALERTER Компонент TIB Event Alerter расположен на странице Samples палитры компонентов. Тот факт, что он расположен не на страницах Data Controls или Data Access, можно объяснить специализацией компонента на работу только с сервером InterBase. Для клиентских приложений, работающих с InterBase, этот компонент, во-первых, регистрирует на сервере приложение как приемник определенных событий и, во- вторых, позволяет эти события обрабатывать. В компоненте TIBEventAlerter опреде- лены следующие свойства, методы и события. Свойство Назначение property Database: TDatabase; Содержит имя компонента TDatabase, управ- ляющего соединением с БД. property Events: TStrings; Содержит список имен событий, о наступле- нии которых сервер будет информировать клиентское приложение. property Registered: Boolean; Возвращает True, если программа зарегистри- рована как приемник сообщений о наступле- нии событий.
432 Глава 31 С помощью метода procedure RegisterEvents; программа регистрируется как приемник сообщений, перечисленных в свойстве Events. С помощью procedure UnregisterEvents; регистрация отключается. Этот метод не может вызываться в обработчике события OnEventAlert, который получает управление в момент поступления уведомления от сервера: TEventAlert = procedure( Sender: TObject; EventName: String; Eventcount: Longlnt; var CancelAlerts: Boolean); procedure OnEventAlert: TEventAlert; Назначение параметров: EventName - имя события; EventCount - содержит количе- ство событий, имевших место на сервере с момента последней передачи клиентскому приложению уведомления о наступлении событий. CancelAlerts — возвращаемый па- раметр: значение False (по умолчанию) сообщает серверу о том, что клиентское при- ложение продолжает интересоваться событиями из списка, указанного в свойстве Events', True сообщает о том, что интерес приложения к уведомлению о событиях ис- сяк. Обработку события OnEventAlert можно возобновить с помощью вызова метода QueueEvents. 31.3. ИСПОЛЬЗОВАНИЕ КОМПОНЕНТА TIBEVENTALERTER Пусть для таблицы RASHOD определены следующие триггеры: CREATE TRIGGER AIEVENT_RASHOD FOR RASHOD ACTIVE AFTER INSERT AS - BEGIN POST_EVENT "INS_POSTED"; END CREATE TRIGGER’ AUEVENT_RASHOD FOR RASHOD ACTIVE AFTER UPDATE AS BEGIN POST_EVENT "UPD_POSTED"; END CREATE TRIGGER ADLEVENT_RASHOD FOR RASHOD ACTIVE • AFTER DELETE AS BEGIN POST_EVENT "DEL_POSTED"; END Для регистрации указанных сообщений в клиентском приложении разместим ком- понент TIBEventAlerter с именем IBEventAlerterl. Укажем в его свойстве Database имя
Работа с событиями 433 компонента TDatabase, управляющего соединением с удаленной БД. В списке свойст- ва Events определим события, при уведомлении о наступлении которых в клиентском приложении должны предприниматься какие-либо действия (рис. 31.1). Установим в свойство Registered этого компонента значение True. Рис. 31.1. Список свойства Events. Определим обработчик события OnEventAlert, в котором обновляется набор дан- ных RashodQuery (компонент TQuery), ассоциированный с таблицей RASHOD: procedure TForml.IBEventAlerterlEventAlert(Sender: TObject; EventName: String; EventCount: Longlnt; var CancelAlerts: Boolean); begin RashodQuery.Close; RashodQuery.Open; CancelAlerts := False end;
Глава 32 ФУНКЦИИ, ОПРЕДЕЛЯЕМЫЕ ПОЛЬЗОВАТЕЛЕМ 32.1. ПОНЯТИЕ ФУНКЦИИ, ОПРЕДЕЛЯЕМОЙ ПОЛЬЗОВАТЕЛЕМ Состав встроенных функций InterBase весьма небогат - в него входят функции вычисления агрегированных значений {MIN, MAX, SUM, A VG), функция преобразования букв UPPER и функция приведения типа CAST. Часто разработчики нуждаются в дополнительных функциях, которые можно было бы использовать так же, как и стандартные встроенные функции InterBase. Это могут быть функции определения длины строки, усечения хвостовых или ведущих пробелов в символьных значениях, извлечение из даты значения месяца, года и т. д. Специально для таких целей в InterBase предусмотрен аппарат функций, определяемых пользователем {User Defined Functions, UDF)1. Разработка UDF может осуществляться на любом алгоритмическом языке, позволяющем создавать динамически загружаемые библиотеки {DLL). Каждая UDF оформляется в виде функции, входящей в состав DLL. Таким образом, одна динамически загружаемая библиотека состоит минимум из одной функции. После того как DLL разработана, она либо перемещается в подкаталог BIN каталога размещения InterBase, либо располагается в ином каталоге, путь к которому известен в операционной системе. Каждая функция определяется оператором DECLARE EXTERNAL FUNCTION. Этот оператор устанавливает связь между функцией из DLL и ее описанием в БД. После этого функция может использоваться в 5(Д,-операторах наряду со стандартными функциями InterBase. Преимущества UDF. • динамически загружаемая библиотека и определенные в ней функции могут использоваться более чем одной БД и более чем одним приложением; таким образом осуществляется повторное использование однажды написанного кода; • пользователь может реализовать в UDF достаточно сложные алгоритмы. 32.2. РАЗРАБОТКА DLL И UDF В DELPHI 32.2.1. Общие положения Чтобы создать с помощью Delphi динамически загружаемую библиотеку, необходимо в главном меню выбрать режим New и затем на странице New выбрать пиктограмму DLL для построения шаблона библиотеки. Текст модуля DLL должен содержать: • заголовок Library имя, где имя — имя создаваемой DLL', 1 В версии InterBase, поставляемой с Delphi 4 и 5, реализована дополнительная библиотека IB_UDF.DLL, которая располагается в подкаталоге \L1B каталога расположения InterBase. Состав функций этой библиотеки описан в Приложении 4 в конце книги.
Функции, определяемые пользователем 435 • в разделе реализации модуля нужно разместить один или несколько блоков exports, в которых через запятую перечисляются имена экспортируемых функций; каждая функция должна описываться выше блока по тексту модуля; • каждая экспортируемая функция объявляется с использованием директив export и cdecl (последняя указывает компилятору, что функция использует соглашения для передачи параметров, принятые в C/C++). Пример определения функции: function SomeName(param: Integer): Integer; cdecl; export; begin end; exports SomeName; 32.2.2. Совместимость типов параметров При описании параметров в БД (оператор DECLARE EXTERNAL FUNCTION) и параметров функций в DLL следует помнить о совместимости типов Object Pascal и InterBase'. Тип InterBase INTEGER DOUBLE PRECISION CSTRING DATE Тип Object Pascal Integer Double PChar type IBDateTlme = record Days: Integer; Msec : Cardinal; end; . Соответствие типов верно для случая, когда результат функции передается в базу данных по значению. Чтобы результат передавался таким образом, необходимо в операторе DECLARE EXTERNAL FUNCTION после слова RETURNS указать BY VALUE. В следующем примере таким способом объявляется функция DENтипа DATE, содержащаяся в DLL с именем UDF_DLL\ DECLARE EXTERNAL FUNCTION DEN DATE RETURNS INTEGER BY VALUE ENTRY_POINT "Den" MODULE_NAME "udf_dl1"; Если результат работы UDF передается по ссылке, необходимо использовать указатели на соответствующие типы. Тип PChar всегда передается по ссылке. 32.2.3. Особенности использования в UDF параметров типа PChar Параметры типа PChar используются для совместимости с форматом представления строк C/C++, однако в Object Pascal со строками, передаваемыми как PChar, в теле функции лучше работать как с длинной строкой String, воспользовавшись преобразованием из типа PChar в String и обратно.
436 Глава 32 Следующая функция принимает строку PChar и отсекает хвостовые и ведущие пробелы: function TrimChar(InString: PChar): PChar; cdecl; export; begin Result := PChar(Trim(String(InString))); end; 32.2.4. Особенности использования в UDF параметров типа даты и времени Значения InterBase типа DATE в Object Pascal интерпретируются как запись, состоящая из двух полей — целочисленного знакового и беззнакового: type IBDateTime = record Days: Integer; Msec: Cardinal; end; Чтобы перевести значение из формата IBDateTime в формат даты и времени Delphi TDateTime, необходимо произвести следующее преобразование: flaia_Delphi = flaia_InterBase.Days - 15018 + Baia_InterBase.MSec / (MSecsPerDay * 10); где константа MSecsPerDay определена в Delphi как число миллисекунд в сутках. 32.3. ОБЪЯВЛЕНИЕ UDF Для объявления функции, определенной пользователем, в InterBase необходимо выполнить оператор DECLARE EXTERNAL FUNCTION ИмяФункции [<Тип данных> | CSTRING (длина) [, <Тип данных> I CSTRING (длина) ...]] RETURNS (<Тип данных> [BY VALUE] | CSTRING (длина)} [FREE_IT] ENTRY_POINT "<Имя функции в DLL>" MODULE_NAME "< Имя DLL >"; ИмяФункции - имя функции, под которой функция будет известна в БД. Это имя может отличаться от имени UDF в DLL. После имени функции следует список типов входных параметров функции. Это либо тип данных, разрешенный в InterBase, либо CSTRING (длина) для строковых значений. Длина определяет размер строкового значения в символах. Если длина меньше действительного размера строки, строка при передаче в UDF усекается. После слова RETURNS указывается тип возвращаемого параметра функции. Это либо тип данных, разрешенный в InterBase, либо CSTRING (длина) для строковых значений. Слова BY VALUE означают, что результат функции возвращается по значению, а не по ссылке. После ENTRY POINT в кавычках указывается имя функции в DLL, а после слов MODULE_NАМЕ — имя модуля DLL (без расширения). Для временных переменных в теле функций обычно определяют локальные переменные, описывая их в блоке var. Однако в UDF, с которыми одновременно могут
Функции, определяемые пользователем 437 работать более одного пользователя, следует динамически выделять память, вместо того чтобы использовать статические переменные. В этом случае параметр FREEIT гарантирует высвобождение памяти, выделенной динамически. Для динамического выделения памяти рекомендуют использовать функцию malloc из библиотеки msvcrt.dll, расположенной в каталоге WINDOWS\SYSTEM32. Ее определение в разделе interface модуля, реализующего UDF: function malloc(Size: Integer): PChar; cdecl; external 'msvcrt.dll'; Удалить из БД объявление UDF можно при помощи оператора DROP EXTERNAL FUNCTION ИмяФункции; 32.4. ПРИМЕР СОЗДАНИЯ DLL С НЕСКОЛЬКИМИ UDF И ОБЪЯВЛЕНИЯ ИХ В БД Создадим DLL с именем UDF DLL, в состав которой входят три функции: library udf_dll; uses SysUtils, Classes; type TIBDateTime = record Days, MSeclO: Cardinal; end; Plnteger = лInteger; function TrimChar(InString: PChar): PChar; cdecl; export;’ {Функция усекает ведущие и хвостовые пробелы у строкового значения, передаваемого как параметр} begin Result := PChar(Trim(AnsiString(InString))); end; function Den(var InDate: TIBDateTime): Integer; cdecl; export; {Функция возвращает по значению номер дня передаваемой в качестве параметра даты} var DT: TDateTime; Gd,Ms,Dn: Word; begin DT := InDate.Days - 15018 + InDate.MSeclO / (MSecsPerDay * 10); DecodeDate(DT,Gd,Ms,Dn); Result := Integer(Dn) ; end; function Mes(var InDate: TIBDateTime): PInteger; cdecl; export; {Функция возвращает по ссылке номер месяца передаваемой в качестве параметра даты}
438 Глава 32 var DT: TDateTime; Gd,Ms,Dn; Word; R: Integer; begin DT := InDate.Days - 15018 + InDate.MSeclO / (MSecsPerDay * 10); DecodeDate(DT,Gd,Ms,Dn); R := Ms; Result := @R; end; exports TrimChar, Den, Mes; begin end. После генерации модуля UDF DLL.DLL переместим его в подкаталог BIN каталога на диске, в котором расположен сервер InterBase. Затем объявим функции в БД: DECLARE EXTERNAL FUNCTION TRIMCHAR CSTRING(256) RETURNS CSTRING(256) ENTRY_POINT "TrimChar" MODULE_NAME "udf_dll"; DECLARE EXTERNAL FUNCTION DEN DATE RETURNS INTEGER BY VALUE ENTRY_POINT "Den" MODULE_NAME "udf_dl1"; DECLARE EXTERNAL FUNCTION MES DATE RETURNS INTEGER ENTRY_POINT "Mes" MODULE_NAME "udf_dll"; Примеры использования объявленных в БД функций пользователя: SELECT TRIMCHAR(POKUP) II ' ' II GOROD FROM POKUPATELI SELECT * FROM RASHOD WHERE DEN(DAT_RASH) > 10;
Глава 33 ОПРЕДЕЛЕНИЕ БИЗНЕС-ПРАВИЛ 33.1. РАЗМЕЩЕНИЕ БИЗНЕС-ПРАВИЛ Бизнес-праеила (БП) задают ограничения на значения данных в БД. Они также оп- ределяют механизмы, согласно которым при изменении одних данных изменяются и связанные с ними данные в той же или других таблицах БД. Таким образом, бизнес- правила определяют условия поддержания БД в целостном состоянии. Идеология архитектуры «клиент-сервер» требует переноса максимально возмож- ного числа БП на сервер. К преимуществам такого подхода относятся: • гарантия целостности БД, поскольку БП сосредоточены в едином месте (в базе данных); • автоматическое применение БП, определенных на сервере БД, для любых при- ложений; • отсутствие различных реализаций БП в разнотипных клиентских приложениях, работающих с БД; • быстрое срабатывание БП, поскольку они реализуются на сервере и, следова- тельно, нет необходимости посылать данные клиенту, увеличивая при этом се- тевой трафик; • доступность изменений, внесенных в БП на сервере, для всех клиентских при- ложений, работающих с настоящей БД, и отсутствие необходимости повторно- го распространения измененных приложений клиентов среди пользователей. К недостаткам хранения бизнес-правил на сервере можно отнести: • отсутствие у клиентских приложений возможности реагировать на некоторые ошибочные ситуации, возникающие на сервере при реализации БП (например, игнорирование приложениями, написанными на Delphi, ошибок при выполне- нии хранимых процедур на сервере); • ограниченность возможностей SQL и языка хранимых процедур и триггеров для реализации всех возникающих потребностей определения БП. На практике в клиентских приложениях реализуют лишь такие бизнес-правила, ко- торые трудно или невозможно реализовать с применением средств сервера. Все ос- тальные БП переносятся на сервер. 33.2. РЕАЛИЗАЦИЯ БИЗНЕС-ПРАВИЛ НА СЕРВЕРЕ 33.2.1. Ограничения значения столбца записи Ограничение первичного ключа Требует уникальности значения столбца или группы столбцов, входящих в первич- ный ключ. Например: CREATE TABLE SAL_HIST ( QUORTER INTEGER NOT NULL, PRIMARY KEY (QUORTER));
440 Глава 33 Ограничение уникального ключа Требует уникальности значения столбца или группы столбцов, входящих в уни- кальный ключ. Например: CREATE TABLE VLADLIM ( NAZWLAD VARCHAR (50) NOT NULL, UNIQUE (NAZWLAD)); Ограничение ссылочной целостности Определяет требование, согласно которому для каждой записи в дочерней таблице должны иметься записи в родительской таблице. При этом изменение значения столб- ца связи в записи родителя при наличии дочерних записей блокируется, равно как и удаление родительской записи (запрет каскадных изменений и удалений). Для разре- шения каскадных воздействий следует отменить ограничение ссылочной целостности и реализовать каскадные воздействия по отношению к дочерним записям (изменение поля связи, удаление) в триггерах. Например: CREATE TABLE SPR_TOVAR( TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, PRIMARY KEY(TOVAR)); CREATE TABLE PRIHOD( ID_PRIHOD INTEGER NOT NULL PRIMARY KEY, TOVAR VARCHAR(20) NOT NULL COLLATE PXW_CYRL, FOREIGN KEY(TOVAR) REFERENCES SPR_TOVAR); Ограничение требуемого значения Определяет, что в поле не может храниться пустое значение (NULL). Например: CREATE TABLE TOVAR ( OSTATOK INTEGER NOT NULL, ...) ; Значения по умолчанию Столбцу может быть присвоено значение по умолчанию. Это значение будет акту- ально в том случае, если пользователь не введет в столбец никакого иного значения. Например: CREATE TABLE SAL_HIST ( QUORTER INTEGER DEFAULT 1 , ...) ; Требование соответствия одному значению из списка К столбцу может быть предъявлено ограничение, согласно которому значение столбца должно содержать одну из величин, объявленных в списке, и никакое другое значение. Например: CREATE DOMAIN POLJTYPE AS CHAR(3) CHECK(VALUE IN ("Муж","Жен"));
Определение бизнес-правил 441 Ограничение диапазона возможных значений Такое требование определяет диапазон возможных значений столбца: CREATE TABLE TBL( CHECK(STOLBEZ BETWEEN 100 AND 200); ...) ; Ограничение максимума или минимума Указывает, что значения столбца должны быть не меньше некоторого минимума и/или не больше некоторого максимума. В приводимом ниже примере для столбца STOLBEZ в качестве минимального значения указано 100. CREATE TABLE TBL( CHECK(STOLBEZ >= 100); ...) ; Алгоритм вычисления значений Для столбца, чье значение вычисляется по значениям других столбцов, может быть установлен алгоритм вычисления значения. Например: CREATE TABLE SAL_HIST ( LAST_YEAR INTEGER, THIS_YEAR INTEGER, GROWTH COMPUTED BY (THIS_YEAR - LAST_YEAR), ...);' Ограничение отношения между столбцами Такое ограничение определяет некоторое отношение (больше, меньше, равно и т. д.) между значениями двух столбцов одной и той же записи. Например: CREATE TABLE PERSON_PARAMS( HEIGHT INTEGER NOT NULL, WIEGHT INTEGER NOT NULL CHECK(HEIGHT > WIEGHT)); Ограничение формата значения Указывает, что в значение столбца должна входить группа символов. В приводи- мом ниже примере значение столбца STOLBEZ должно оканчиваться символами «USD », независимо от того, какие символы и сколько расположены перед ними. CREATE TABLE TBL( CHECK (STOLBEZ LIKE "%USD") ; ...) ; Требование вхождения символов в значение Устанавливает, что в значение столбца должна входить группа символов (с неоп- ределенной позиции). В приводимом ниже примере значение столбца STOLBEZ долж- но содержать вхождение символов «USD» независимо от того, какие символы и сколько расположено перед ними и после них. CREATE TABLE TBL(
442 Глава 33 CHECK (STOLBEZ CONTAINING "USD") ; ...) ; Требования присутствия ведущих символов Значение столбца должно начинаться с определенной группы символов. В приво- димом ниже примере значение STOLBEZ должно начинаться с символов «USD». CREATE TABLE TBL( CHECK (STOLBEZ STARTING WITH "USD") ; ...) ; Требование отношения co значением в другой таблице Значение столбца находится в некотором отношении (=, >, < и т. д.) со значением, получаемым путем выполнения запроса к другой таблице. В приводимом ниже приме- ре значение в столбце KOLVO таблицы RASHOD не должно превышать значения столбца OSTATOK из записи таблицы TOVAR, причем поле TOVAR у обеих сравни- ваемых записей должно иметь одинаковое значение. CREATE TABLE RASHOD ( ID_RS INTEGER NOT NULL, TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, KOLVO_R INTEGER NOT NULL, CONSTRAINT PO_OSTATKU CHECK(KOLVO_R <= (SELECT TOVAR.OSTATOK FROM TOVAR WHERE TOVAR.TOVAR = RASHOD.TOVAR))); Требование отношения значения столбца со всеми или некоторыми значе- ниями в другой таблице Значение столбца находится в отношении (=, >, < и т. д.) со значением столбца всех (ALL) или некоторых (SOME) записей, получаемых путем выполнения запроса к дру- гой таблице. В приводимом ниже примере значение столбца DATE_RASH (таблица RASHOD) должно быть больше значений всех полей DATE PRIH в таблице PRIHOD. При этом сравниваемые записи в PRIHOD и RASHOD должны иметь одинаковое зна- чение столбца TOVAR. CREATE TABLE RASHOD (ID_RS INTEGER NOT NULL, TOVAR VARCHAR(20). CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, DATE_RASH DATE NOT NULL, CONSTRAINT RASH_TOVAR FOREIGN KEY (TOVAR) REFERENCES TOVAR(TOVAR), CONSTRAINT PO_DATE_RASH CHECK ( DATE_RASH > ALL (SELECT DATE_PRIH FROM PRIHOD WHERE PRIHOD.TOVAR = RASHOD.TOVAR))); Требование существования хотя бы одной записи в другой таблице Устанавливает, что в другой таблице должна существовать хотя бы одна запись, удовлетворяющая некоторому условию. В приводимом ниже примере обязательно
Определение бизнес-правил 443 существование хотя бы одной записи в таблице PRIHOD с таким же значением столб- ца TOVAR, что и в поле TOVAR записи таблицы RASHOD. CREATE TABLE RASHOD (ID_RS INTEGER NOT NULL, TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, DATE_RASH DATE NOT NULL, CONSTRAINT RASH_TOVAR FOREIGN KEY (TOVAR) REFERENCES TOVAR(TOVAR), CONSTRAINT PO_DATE_RASH CHECK (EXISTS (SELECT TOVAR FROM PRIHOD WHERE PRIHOD.TOVAR = RASHOD.TOVAR))); Требование существования единственной записи в другой таблице Устанавливает, что в другой таблице должна существовать только одна запись, удовлетворяющая некоторому условию. В следующем примере обязательно сущест- вование единственной записи в таблице PRIHOD с таким же значением столбца TOVAR, что и в столбце TOVAR записи таблицы RASHOD. CREATE TABLE RASHOD (ID_RS INTEGER NOT NULL, TOVAR VARCHAR(20) CHARACTER SET WIN1251 NOT NULL COLLATE PXW_CYRL, DATE RASH DATE NOT NULL, ~ i CONSTRAINT RASH_TOVAR FOREIGN KEY (TOVAR) REFERENCES TOVAR(TOVAR), CONSTRAINT PO_DATE_RASH CHECK (SINGULAR(SELECT TOVAR FROM PRIHOD WHERE PRIHOD.TOVAR = RASHOD.TOVAR))); 33.2.2. Запрет добавления записей в просмотре Для просмотра VIEW может быть включен режим WITH CHECK OPTION, предот- вращающий добавление записей, не удовлетворяющих условию WHERE оператора SELECT данного просмотра. Например, для приведенного ниже просмотра будет от- вергаться попытка добавления записи со значением поля KOLVO, меньшим 1000. CREATE VIEW RASH_1000_CHECK AS SELECT * FROM RASHOD WHERE KOLVO > 1000 WITH CHECK OPTION; 33.2.3. Использование триггеров для поддержания ссылочной целостности Для поддержания ссылочной целостности на сервере могут использоваться тригге- ры. Они автоматически запускаются при выполнении любого изменения таблицы БД (добавление новой записи, корректировка или удаление существующей). Время запус- ка - до или после события — определяется в заголовке триггера. Большим преимуще-
444 Глава 33 ством триггеров является возможность обращения (при изменении записи) к старому (OLD) и новому (NEW) значению столбца. Триггеры могут использоваться для: • реализации каскадных воздействий в дочерних таблицах при изменении значе- ния столбца связи в записи родительской таблицы или при удалении записи ро- дительской таблицы; • внесения уникального значения в столбец, по которому построен уникальный или первичный ключ; • внесения изменений в семантически связанные таблицы; • ведения журнала изменений БД. Приводимые ниже триггеры выполняют каскадные обновления в дочерней таблице RASHOD после изменения значения столбца связи в родительской таблице TOVARY: CREATE trigger bu_tovary for tovary ACTIVE BEFORE UPDATE AS BEGIN IF (OLD.TOVAR <> NEW.TOVAR) THfiN UPDATE RASHOD SET TOVAR = NEW.TOVAR WHERE TOVAR = OLD.TOVAR; END ( CREATE TRIGGER AD_TOVARY FOR TOVARY ACTIVE AFTER DELETE AS BEGIN DELETE FROM RASHOD WHERE RASHOD.TOVAR = TOVARY.TOVAR; END Следующий триггер при добавлении записи в таблицу RASHOD присваивает столбцу NJRASH уникальное значение, для чего используется генератор RASHODNRASH: CREATE TRIGGER BI_RASHOD_GEN FOR RASHOD ACTIVE BEFORE INSERT BEGIN NEW.N_RASH = GEN_ID(RASHOD_N_RASH,1); END Приводимый ниже триггер при добавлении новой записи в таблицу RASHOD (расход товара) прибавляет значение поля KOLVO (количество) вновь введенной запи- си к полю KOLVO в таблице STATTOVARY для записи с той же датой (DATJRASH) и названием товара (TOVAR). Если в таблице STAT_TOVARY такая запись отсутствует, она создается. CREATE TRIGGER AI_RASHOD FOR RASHOD ACTIVE AFTER INSERT
Определение бизнес-правил 445 AS DECLARE VARIABLE CNT INTEGER; DECLARE VARIABLE OLD_KOLVO_VAL INTEGER; BEGIN /* выбрать число записей в таблице STAT_TOVARY */ /* по данному товару за дату расхода */ SELECT COUNT(*) FROM STAT_TOVARY WHERE STAT_TOVARY.DAT_RASH = NEW.DAT_RASH AND STAT_TOVARY.TOVAR = NEW.TOVAR INTO :CNT; /* если число записей = 0, добавить запись в */ /* таблицу STAT_TOVARY по данному */ /* товару и дате */ IF (:CNT = 0) THEN INSERT INTO STAT_TOVARY (DAT_RASH, TOVAR, KOLVO) VALUES(NEW.DAT_RASH, NEW.TOVAR, NEW.KOLVO); ELSE /* иначе добавить новое количество товара */ /* в уже существующей записи для нового товара */ /* и новой даты в STAT_TOVARY */ BEGIN SELECT KOLVO FROM STAT_TOVARY WHERE STAT_TOVARY.DAT_RASH = NEW.DAT_RASH AND STAT_TOVARY.TOVAR = NEW.TOVAR INTO :OLD_KOLVO_VAL; UPDATE STAT_TOVARY SET KOLVO = :OLD_KOLVO_VAL + NEW.KOLVO WHERE STAT_TOVARY.DAT_RASH = NEW.DAT_RASH AND STAT_TOVARY.TOVAR = NEW.TOVAR; END END Следующий триггер реализует автоматическую фиксацию в таблице TOVAR Y_LOG добавлений, внесенных в таблицу TOVARY. , CREATE TRIGGER TOVARY_ADD_LOG FOR TOVARY ACTIVE AFTER INSERT AS BEGIN INSERT INTO TOVARY—LOG(DAT—IZM, DEISTV, OLD_TOVAR, NEW_TOVAR) VALUES ("NOW","ADD","",NEW.TOVAR); END 33.3. РЕАЛИЗАЦИЯ БИЗНЕС-ПРАВИЛ В ПРИЛОЖЕНИИ КЛИЕНТА Для реализации бизнес-правил в приложении клиента могут использоваться раз- личные компоненты. В первую очередь это компоненты типа «набор данных» (ТТаЫе, TQuery, TStoredProc) и компоненты-поля TField. Интерфейсные компоненты
446 Гпава 33 (например, TEdit и TButton) могут использоваться для контроля за вводом пользовате- ля и могут блокировать закрытие формы, если он ввел неправильные значения. 33.3.1. Реализация бизнес-правил в компонентах типа «набор данных» Компоненты типа «набор данных» позволяют реализовывать бизнес-правила в об- работчиках следующих событий: • OnNewRecord - возникает при добавлении новой записи сразу после перехода НД в состояние dslnsert из состояния dsBrowse, но перед выдачей полей новой записи пользователю для ввода значений; используется для присваивания полям значений по умолчанию; • BeforeOpen, BeforeClose, BeforeCancel, BeforeEdit, Beforeinsert - возникают до выполнения соответствующего метода; • AfterPost, AfterDelete, AfterOpen, AfterClose, AfterCancel, AfterEdit, AfterInsert — возникают после выполнения соответствующего метода; не возникают, если при выполнении метода произошел сбой; • OnCalcFields — наступает при необходимости заполнения вычисляемых полей; применяется для задания алгоритмов расчета значений вычисляемых полей; • OnDeleteError - наступает при ошибке удаления записи; • On UpdateError - происходит при ошибке редактирования записи; • OnPostError - происходит при возникновении ошибки в ходе выполнения ме- тода Post. Определение вычисляемых полей Обработчик события OnCalcFields применяется для определения значения вычис- ляемых полей. Например: procedure SomeTableCalcFields(DataSet: TDataSet); begin SomeTableVychPole.Value := SomeTablePolel.Value / SomeTablePole2.Value; end; i Присваивание значений полей по умолчанию В обработчике события OnNewRecord можно присвоить полям вновь добавляемой записи значения по умолчанию. Эти значения останутся актуальными, если пользова- тель перед добавлением записи не изменит их. Например: procedure SomeTableNewRecord(DataSet: TDataSet); begin with SomeTable do begin FieldByName('polelnt').Aslnteger := 0; FieldByName('poleStr').AsString := ' end; //with end;
Определение бизнес-правил 447 Автоматическое присваивание значения полям связи При добавлении новой записи в дочерний НД может понадобиться присвоить со- ответствующие значения полям связи с родительской таблицей. В дальнейшем поль- зователь обычно не изменяет поля связи. Например, при добавлении новой записи в таблицу ChildTable устанавливается значение поля связи, равное значению поля cod^parent текущей записи родительской таблицы ParentTable'. procedure ChildTableNewRecord(DataSet: TDataSet); begin ChildTable.FieldByName('cod_parent').Aslnteger := ParentTable.FieldByName('cod_parent').Aslnteger; end; Присваивание уникального значения столбцу таблицы Если столбцу таблицы при добавлении новой записи должно присваиваться уни- кальное значение, можно выполнить отдельный запрос к той же таблице, получить максимальное значение уникального столбца и увеличить его на 1. Например, при добавлении новой записи в RashodTable выполняется формируемый запрос (компонент WorkQuery, тип TQuery), возвращающий максимальное значение столбца cod_unique. После увеличения на 1 оно присваивается столбцу cod_unique вновь добавляемой записи: procedure RashodTableNewRecord(DataSet: TDataSet); var Max_cod_unique: Integer; begin with WorkQuery do begin SQL.Add('SELECT MAX(COD_UNIQUE) FROM RASHOD'); Open ; Max_cod_unique := Fields[0].Aslnteger + 1; Close; end; //with ChildTable['cod_unique'].Aslnteger := Max_cod_unique; end; Присваивание столбцу уникального значения удобнее производить в хранимой процедуре при помощи генератора. Например, если в БД определен генератор CREATE GENERATOR X; SET GENERATOR X TO 1; и определена процедура CREATE PROCEDURE GET_UNIQUE_VALUE RETURNS(UV INTEGER) AS BEGIN UV = GEN_ID(X,1); END
448 Глава 33 то в приложении достаточно определить компонент TStoredProc и связать его с хра- нимой процедурой GET UNIQUE VALUE, а в обработчике события OnNewRecord — вызвать процедуру и присвоить полю возвращаемое ею уникальное значение: procedure RashodTableNewRecord(DataSet: TDataSet); begin StoredProcl.ExecProc; ChildTable['cod_unique'].Aslnteger := StoredProcl.ParamByName('UV').Aslnteger; end; Внесение изменений в связанную таблицу БД Обычно изменения в связанные таблицы вносятся в обработчиках событий AfterPost, AfterDelete. Например, уменьшить значение поля Kolvo таблицы StatTable на значение поля Kolvo из удаленной записи таблицы RashodTable: > procedure RashodTableAfterDelete(DataSet: TDataSet); begin with StatTable do begin Edit; FieldByName('Kolvo').Value := FieldByName('Kolvo1)-Value - DeletedRashodValue; Post; end; //wi th end; 33.3.2. Свойство Constrained (компонент TQuery) Предотвратить ввод записей, не удовлетворяющих условиям, перечисленным в предложении WHERE оператора SELECT, можно путем установки значения True в свойство property Constrained: Boolean; компонента TQuery. Например, для НД, возвращенного оператором SELECT * FROM RASHOD WHERE KOLVO > 1000 при Constrained, содержащим True, будут блокироваться попытки запоминания запи- сей со значением поля KOLVO, меньшим или равным 1000. 33.3.3. Свойство Constraints Свойство property Constraints: TCheckConstraints; набора данных является коллекцией ограничений на значения столбцов НД. Указание ограничения производится в ^/.-подобном синтаксисе (рис. 33.1). В случае если зна- чение поля не удовлетворяет наложенным на него ограничениям, при попытке запо- минания записи возбуждается исключение.
Определение бизнес-правил 449 Рис. 33.1. Редактор ограничений окна Инспектора Объектов. 33.3.4. Реализация бизнес-правил в компоненте TField Анализ правильности введенного в поле значения Проверку правильности введенного в поле значения можно осуществить в обра- ботчиках событий OnValidate, OnSetText, OnChange объектов-полей. Пример. Значение поля Company не должно содержать символа «@»: procedure TForml.TablelCampanyValidate(Sender: TField); begin if pos('@',TablelCompany.AsString) > 0 then raise Exception.Create('Неверное значение'); end; Значение поля PUR PRICE не должно превышать 100: procedure TForml.TablelPUR_PRICESetText(Sender: TField; const Text: String); var Tmp: Real; begin Tmp := StrToFloat(Text); if Tmp > 100 then ShowMessage('Ошибочное значение') else TablelPUR_PRICE.Value := Tmp; end; или procedure TForml.TablelPUR_PRICEChange(Sender: TField) ; begin if TablelPUR_PRICE.Value > 100 then raise Exception.Create('Ошибочное значение'); end; Форматирование содержимого поля при показе и редактировании Значения могут храниться в виде, отличном от того, в котором они показываются пользователю. Преобразование значения поля к виду, пригодному для показа, осуще- ствляется свойствами EditMask (маска значения при редактировании записи), DisplayFormat (маска значения при показе в визуальных компонентах) и обработчиком
450 Глава 33 события OnGetText, который применяется, если возможностей DisplayFormat для пре- образования значения недостаточно. Пример. Значение поля Company при редактировании должно показываться в ка- вычках, при просмотре - с заменой всех букв на заглавные: procedure TForml.TablelCompanyGetText(Sender: TField; var Text: OpenString; DisplayText: Boolean); begin if DisplayText then Text := AnsiUpperCase(TablelCompany.AsString) else Text := + TablelCompany.AsString + '; end; Свойства CustomConstraint и ConstraintErrorMessage Свойство property CustomConstraint: String; поля TField позволяет наложить ограничения на значения поля (при помощи SQL- подобного синтаксиса). Если в поле занесено значение, не отвечающее указанному ограничению, возбуждается исключение с сообщением, определяемым свойством property ConstraintErrorMessage: String; 33.3.5. Другие способы реализации бизнес-правил Проверка правильности значения Приводимый ниже пример проверяет значение в Editl. Text на соответствие форма- ту даты. В дальнейшем это значение планируется записать в поле типа даты таблицы БД. Проверка производится в обработчике события OnExit и возникает в момент, ко- гда Editl теряет фокус ввода: procedure TToObrForm.EditlExit(Sender: TObject); begin try StrToDate(Editl.Text) ; except ShowMessage('Неверная дата'); Editl.SetFocus; 'Exit; end; //try end; Запрет подтверждения изменений в БД В случае невыполнения какого-либо условия в форме может быть запрещена кноп- ка, реализующая запоминание в БД внесенных изменений: procedure TToObrForm.EditlExit(Sender: TObject); begin try StrToDate(Editl.Text) ; PostButton.Enabled := True;
Определение бизнес-правил 451 except ShowMessage('Неверная дата'); ' Editl.SetFocus; PostButton.Enabled := False; .Exit; end; end; Выполнение действий при переходе на другую запись Событие OnDataChange компонента TDataSource происходит при изменении те- кущей записи в НД (в режиме dsBrowse) или при изменении какого-либо поля записи (режим dsEdit). В следующем примере подразумевается, что для текущей записи в ParentTable по запросу, связанному с нажатием кнопки SelectChildRecordsButton, в НД ChildTable фильтруются дочерние записи, которые отображаются в таблице ChildDBGrid. При переходе на новую запись в ParentTable содержимое ChildTable до нового нажатия кнопки SelectChildRecordsButton становится неактуальным. Поэтому таблица Child- DBGrid, показывающая дочерние записи, окрашивается в серый цвет и визуализирует- ся кнопка SelectChildRecordsButton'. procedure TForml.ParentDataSourceDataChange(Sender: TObject; Field: TField); begin if ParentTable.State <> dsBrowse then Exit; ChildDBGrid.Enadbled := True; ChildDBGrid.Color := clSilver; » SelectChildRecordsButton.Visible := True; end; Те же действия можно реализовать в обработчике события AfterScroll набора дан- ных, возникающем после перехода на новую запись в НД. 33.4. ИСПОЛЬЗОВАНИЕ СЛОВАРЯ ДАННЫХ ДЛЯ ОПРЕДЕЛЕНИЯ АТРИБУТОВ ПОЛЕЙ В том случае, когда для какого-либо поля (столбца) таблицы БД требуется опреде- лить характеристики его визуального представления, используют словарь данных. Словарь данных позволяет: • сформировать поименованный набор атрибутов поля без привязки его к кон- кретному столбцу таблицы БД; • по мере необходимости ставить поименованный набор атрибутов в соответст- вие конкретным столбцам тех или иных ТБД. В состав атрибутов, которые можно определить в поименованном наборе атрибу- тов, входят: выравнивание (Alignment), заголовок поля при показе (Display Label), ши- рина показа (Display Width), признак «только для чтения» (Read Only), признак обяза- тельного ввода значения (Required), признак видимого отображения (Visible), маска при редактировании (EditMask), маска при показе (Display Mask) и другие. Наличие такой возможности преследует вполне конкретную цель: однажды опре- делив в словаре данных некий абстрактный набор атрибутов, мы затем можем ставить его в соответствие полям в одной или нескольких БД и таким образом:
452 Глава 33 • устраняем повторное определение визуальных атрибутов поля (столбца) в лю- бом приложении, его использующем; • придаем полю (столбцу) единообразные характеристики во всех работающих с ним приложениях. Рассмотрим процесс назначения атрибутов полю и затем импорт их в приложение на следующем примере. Пусть существует таблица БД zpa_kn.ig.db (записная книжка), в состав которой входят поля NN (порядковый номер, автоинкрементное поле), FIO (фамилия, имя, отчество, символьное поле), Tel (телефон, символьное поле), Prim (примечание, символьное поле). Пусть поле Tel должно вводиться в формате (ххх) ххх-хх-хх и показываться с заголовком «Телефон». При этом известно, что поле будет использовано более чем в одном приложении, и потому его визуальному представлению желательно придать определенное единообразие. Процесс назначения полю набора атрибутов в словаре данных и последующего импорта атрибутов в приложение состоит из ряда этапов. 1. Выберем в главном меню Delphi элемент Database | Explore. 2. В появившемся окне утилиты SQL Explorer выберем закладку Dictionary. 3. Импортируем в словарь базу данных, содержащую таблицу zpaknig.db (псевдоним БД book). Для этого в меню SQL Explorer выберем элемент Dictionary | Import from Database и затем в появившемся окне запроса выберем с помощью выпадающего меню псевдоним book, после чего нажмем кнопку ОК. 4. Создадим новый набор атрибутов. Для этого установим указатель мыши на элементе дерева Attribute Sets, нажмем правую кнопку мыши и в появившемся •меню выберем элемент New. 5. В левом окне для вновь созданного набора атрибутов введем имя Tel attri- bute_set. В правом окне установим значения атрибутов Display Label, Edit Mask и Display Mask (рис. 33.2). 6. Подтвердим внесенные для набора атрибутов Tel_attribute_set изменения, вы- звав правой кнопкой мыши меню и выбрав элемент Apply. 7. Раскроем ветвь для БД book, раскроем ветвь Tables, выберем zap_knig, раскроем ее ветвь Fields и выберем поле Tel (левое окно). В правом окне для данного по- ля при помощи выпадающего списка установим значение Tel_attribute_set в оп- ределение Attribute set. 8. Подтвердим внесение изменений в набор атрибутов для поля Tel, для чего вы- зовем правой кнопкой мыши всплывающее меню и выберем элемент Apply. 9. В приложении Delphi разместим на форме компонент ТТаЫе, связанный с ТБД zap_knig, а также связанные между собой стандартным образом компоненты TDataSource и TDBGrid. Как видно из рис. 33.3, характеристики поля Tel пока не отражают набора атрибутов, установленных этому полю в словаре данных. 10. Используя редактор полей, явно определим для Tablel объекты-поля. Как вид- но из рис. 33.4, теперь свойства поля Tel отражают атрибуты, установленные этому полю в словаре данных (изменился заголовок и значение столбца Tel). В режиме выполнения приложения поле Tel будет использовать установленные в словаре данных маску ввода и маску показа (рис. 33.5).
Определение бизнес-правил 453 Рис. 33.2. Установка значений атрибутов. Рис. 33.3. До создания объектов-полей характеристики поля Tel не отражают набора атрибутов. Рис. 33.4. После создания объектов-полей.
454 Глава 33 " Импорт атрибутов полей таблицы из словаря НПЕЗ Г NN НО Телефон jPfim Я г 1 Иванов И.И. (095)111-22-33 дом Г“ 2 Петров П.П. (095) 222-33-44 раб н й 1 Сидоров С.С. (095) 777-88-JL 1 1 L чр Рис. 33.5. В работающей программе поле Tel использует маску ввода и маску показа, установленные в сло- варе данных.
Глава 34 ОПТИМИЗАЦИЯ РАБОТЫ С БД Оптимизация работы с БД заключается в создании таких условий, когда обеспечива- ется наибольшее быстродействие БД при минимально возможных затратах ресурсов. Оптимизация зависит от многих факторов, которые можно разбить на три группы: • оптимизация структуры БД; • оптимизация запросов; • оптимизация клиентского приложения. Декларировав известный факт, что чем мощнее аппаратура сервера, тем лучше производительность БД, рассмотрим такие факторы, которые не связаны с аппарату- рой и поддаются воздействию со стороны разработчиков и администраторов БД. 34.1. ОПТИМИЗАЦИЯ СТРУКТУРЫ БД 34.1.1. Нормализация таблиц: теория и практика На быстродействие БД непосредственно влияет то, каким образом была проведена нормализация таблиц, то есть каким образом устранена в таблицах избыточность дан- ных. Часто нормализация ухудшает быстродействие. Приведем пример. Пусть имеется таблица «Сотрудники» и таблица «Оклады»: Сотрудники Оклады ♦ИО Должность Иванов И.И. директор Петров П.П. инженер ... « « Яковлев Я.Я. инженер Должность Оклад директор 1000 бухгалтер 600 ... . ». инженер 650 С точки зрения теории, нормализация таблиц приводит к наиболее ясному отображе- нию сущностей из предметной области. Устраняя избыточность данных, она тем самым существенно экономит дисковое пространство. Например, если удалить таблицу «Оклады» и ввести поле «Оклад» в таблицу «Сотрудники», будет налицо явно нерацио- нальное расходование дискового пространства, что всегда критично при больших объе- мах данных: если в измененной таблице «Сотрудники» присутствуют сведения о 1000 инженерах, одно и то же значение оклада инженера будет дублироваться 999 раз. С точки зрения практики, оптимизация может существенно уменьшить быстродей- ствие. Например, при обращении к таблице «Сотрудники» часто требуется знать оклад сотрудника. Для этого необходимо обратиться к таблице «Оклады», найти нужную должность и уже затем получить оклад. Как видим, вместо одного обращения (к столбцу уже найденной записи в таблице «Сотрудники») необходимо дополнительно обратиться к таблице «Оклады», осуществить операцию поиска в ней, эффективность которого зависит от многих факторов, в первую очередь от правильного построения индексов, и уже затем считать нужное значение оклада. При обращении к одной, пусть большой, таблице БД затрачивается меньше времени, чем при обращении к несколь- ким более мелким таблицам. Кроме того, на практике высокая степень нормализации таблиц приводит к боль- шому количеству таблиц, в результате чего структура информации в БД не восприни-
456 Глава 34 мается разработчиком целостно. Невозможность целостного представления структуры данных в БД является одним из «человеческих факторов», способных внести серьез- ные ошибки в структуру БД уже на стадии разработки БД, что впоследствии может иметь самые серьезные - и всегда негативные - последствия. Поэтому при проектировании структуры БД следует учитывать как отрицательные, так и положительные стороны нормализации таблиц. Обычно жертвуют дополнитель- ным расходом дисковой памяти для хранения не полностью нормализованных таблиц, стремясь обеспечить максимальное быстродействие, что актуально при возрастании числа пользователей, одновременно работающих с системой. 34.1.2. Частичная зависимость структуры данных от методов доступа к ним Хотя из теории известно, что структура данных в БД должна быть независима от способов доступа к данным, на практике это обычно не так. Приведем пример. Пусть в БД, работающей под управлением InterBase, имеется родительская таблица «Бюджетные лимиты подразделения» и подчиненные ей таблицы «Корректировки лимита», «Суммы сверх лимита», «Расходы лимитированных средств». При каждом расходовании лимитных средств (например, на покупку компьютеров) необходимо вычислить текущий остаток по такой формуле: SI + S2 + S3 - S4 где S1 — начальный лимит (ТБД «Бюджетные лимиты подразделения»); S2 — сумма всех корректировок лимита по данному подразделению (таблица «Корректировки лимита»); S3 — сумма всех записей сверх лимита по данному подразделению (таблица «Суммы сверх лимита»); S4 - сумма всех предыдущих расходов подразделения по данному лимиту (таблица «Расходы лимитированных средств»). Пусть это соответственно значения 1 000 000 + 1 000 000 + 4 000 000 - 600 000 = 5 400 000 (рублей). Требуется записать сведения о расходе 3 000 000 рублей. Однако нужно быть уве- ренным в том, что другой пользователь в этот же самый момент не изменит ни одну из записей в указанных таблицах, результатом чего будет изменение остатка лимитных средств 5 400 000 (наверняка в меньшую сторону). Чтобы блокировать текущий лимит данного подразделения, добавим в таблицу «Бюджетные лимиты подразделения» поле STATUS CHAR(1) и каждую транзакцию на изменение таблиц «Бюджетные лимиты подразделения», «Корректировки лимита», «Суммы сверх лимита», «Расходы лимити- рованных средств» в приложении будем начинать с изменения этого статуса: Databasel.StartTransaction; TQueryl.Params[0] := Code; // Code = код записи в ТБД «Бюджетные лимиты подразделения» try TQueryl.ExecSQL; {Текст запроса вида UPDATE ... SET STATUS = 'A' WHERE CodeZap = :Parl...}
Оптимизация работы с БД 457 {Вычисление текущего остатка бюджетного лимита по данному подразделению} {Выполнение действий по изменению записей в дочерних таблицах} except end; Тогда всякая другая транзакция, желающая изменить записи в дочерних таблицах (подчиненных текущей записи в таблице «Бюджетные лимиты подразделения»), при выполнении TQueryI.ExecSQL возбудит исключение (deadlock) в связи с тем, что за- пись изменена другой транзакцией, пока не подтвердившей и не отменившей сделан- ных ею изменений. Однако эта блокировка не распространяется на другие бюджетные лимиты данного подразделения или других подразделений. Как видим, введение дополнительного поля способно обеспечить желаемый режим разграничения доступа к данным. Таким образом, проектируя логическую структуру данных в БД, невозможно абст- рагироваться от того, каким образом эти данные будут обрабатываться на сервере и в клиентском приложении. Заметим, что зависимость между структурой запросов к БД (в основном в операто- ре SELECT), структурой и составом индексов таблиц также свидетельствует о связи между структурой данных и методами доступа к ним. 34.1.3. Физические характеристики БД Для быстрого доступа к записям таблицы БД необходимо, чтобы таблица физиче- ски занимала в БД непрерывный блок страниц. Известно, что при выделении новых страниц в БД InterBase не делает никаких попыток выделять смежные страницы для хранения одной и той же таблицы БД. Поэтому данные, относящиеся к одной страни- це в БД, могут быть фрагментированы. Хранение множественных поколений записей также приводит к сильному «загряз- нению» БД и замедляет работу с ней. Напомним, что при всяком изменении записи фактически создается новая версия записи. Версии записей, измененные или добав- ленные транзакциями, которые впоследствии отменяются, из БД не удаляются. Кроме того, при удалении записей из БД не происходит перемещения оставшихся записей с тем, чтобы удалить образовавшиеся «дыры» на страницах БД. Решением указанных проблем является периодическое создание резервной копии и восстановление из нее БД. При этом: • собирается «мусор», т. е. версии записей, которые далее не будут востребованы; • устраняются «дыры» на страницах БД, образовавшиеся после удаления записей; • каждая таблица размещается в непрерывном блоке страниц. Немаловажным является и размер самой страницы БД. Запись таблицы БД должна раз- мещаться максимум на одной странице. Как известно, чтение-запись в БД InterBase осуще- ствляются страницами. Поэтому, если размер страницы мал для хранения одной записи и она располагается на более чем одной странице, для чтения такой записи нужно выполнить несколько физических операций чтения. С другой стороны, размер страницы не должен быть слишком велик, поскольку в этом случае будут считываться ненужные записи. Размер буфера ввода-вывода также способен оказывать влияние на быстродействие при работе с базой. Для БД, к которым чаще применяются операции чтения, рекомен-
458 Глава 34 дуется увеличить размер буфера ввода-вывода. Для БД, в которых чаще выполняются операции записи данных, размер буфера можно уменьшить. 34.2. ОПТИМИЗАЦИЯ ЗАПРОСОВ Оптимизация запросов к БД связана с построением адекватной запросам и опти- мальной структуры индексов таблиц БД и оптимизацией собственно текстов запросов. 34.2.1. Оптимальная структура индексов От структуры индексов таблиц БД в огромной степени зависит эффективность вы- полнения запросов. При выполнении запросов InterBase сначала просматривает спи- сок индексов, определенных для участвующих в запросе таблиц. Затем выбирается одна из двух схем выполнения запроса - использовать имеющиеся индексы или по- следовательно просмотреть таблицы. Оптимизатор InterBase стремится выполнить запрос с максимальным быстродействием и с минимальными накладными расходами. Он всегда оптимизирует запрос при его первом использовании, основываясь на теку- щем состоянии БД. Повторно параметрические запросы, у которых меняются только значения запросов, не оптимизируются. Происходит лишь предварительное связыва- ние формальных и фактических параметров, после чего запрос выполняется. Хотя состояние БД может меняться и поэтому полностью предсказать, по какой схеме оптимизатор InterBase будет выполнять запрос, нельзя, существуют общие по- ложения, которые следует учитывать при проектировании запросов. 34.2.2. «Полезность» индексов Эффективность использования индекса при поиске информации в таблице БД сильно зависит от того, построен ли индекс по уникальным значениям и, если нет, на- сколько отличаются данные, по которым он построен. Пусть необходимо выбрать из таблицы RASHOD все записи о расходе товара за 10.01.1997, у которых количество расходуемого товара превышает 300 единиц: SELECT * FROM RASHOD WHERE DAT_RASH = "10/01/1997" AND KOLVO > 300 При выполнении запроса InterBase определяет - есть ли индексы, построенные од- новременно по столбцам DAT_RASH и KOLVO, столбцам KOLVO и DATRASH, или индексы, в которые указанные столбцы входят в качестве ведущих (например, индекс, построенный по столбцам DAT RASH, KOLVO, TOVAR, но не индекс, построенный по столбцам DAT_RASH, TOVAR, KOLVO). При отсутствии таких индексов проверяется наличие индексов отдельно по столбцам DAT_RASHи KOLVO. В случае отсутствия индексов поиск записей, удовлетворяющих запросу, осущест- вляется путем перебора всех записей в таблице, т. е. путем последовательного досту- па, что обеспечивает наименьшую эффективность выполнения запроса. В случае не- скольких индексов, по которым можно осуществить поиск (например, индекс по столбцам DAT RASH, KOLVO и по столбцам KOLVO, DAT_RASH), выбирается для использования тот, у которого выше показатель полезности индекса (selectivity).
459 Оптимизация работы с БД II I I—Ч-=1' —.-. Показатель полезности индекса рассчитывается как число различающихся значе- ний индексных полей внутри индекса, отнесенное к среднему количеству записей. Этот показатель рассчитывается при создании индекса. После внесения изменений в таблицу, по которой построен индекс, меняется степень отличия значений столбцов, по которым построен индекс. Поэтому рассчитанный показатель полезности может не отражать реального состояния индекса и значение показателя рекомендуется принуди- тельно пересчитывать: время от времени — при внесении небольших изменений и все- гда - при внесении существенных изменений. Пересчет реализуется оператором SET STATISTIC INDEX ИмяИндекса Среднее количество записей — показатель, который рассчитывается всякий раз при оптимизации запроса как количество страниц БД, занятых этой таблицей, деленное на максимальное число записей на странице. Уменьшение числа страниц, занятых БД, и уничтожение на них «дыр» ведут к уменьшению показателя среднего числа записей и как следствие - к повышению показателя полезности индексов. Это еще один аргу- мент в пользу периодического сжатия БД путем создания резервной копии и восста- новления из нее БД. Для участия в выполнении запроса выбираются индексы с максимальным показа- телем полезности. Такие индексы обеспечивают более быстрый поиск. Максимальным показателем полезности обладают уникальные индексы, т. е. индексы, построенные по определениям первичных и уникальных ключей. 34.2.3. Просмотр плана выполнения запросов При выполнении запросов к БД в утилите WISQL установим режим показа плана выполнения запроса (выбрав элемент меню Session | Basic Settings и отметив режим Display Query Plan). Тогда при выполнении запросов будет выводиться и план их вы- полнения. Под планом выполнения запроса понимается перечень индексов, исполь- зуемых InterBase при выполнении запроса. Слово NATURAL означает последователь- ный перебор таблицы. Например: SELECT RASHOD.*, TOVARY.ZENA FROM RASHOD, TOVARY WHERE RASHOD.TOVAR = TOVARY.TOVAR PLAN JOIN (RASHOD NATURAL,TOVARY INDEX (RDB$PRIMARY8)) N_RASH DAT_RASH ZENA KOLVO TOVAR POKUP 2 IO-JAN-1997 20 Сахар Лира, ТОО 4 3 , IO-JAN-1997 509 Сахар <null> 4 4 IO-JAN-1997 3000 Ставрида консерв. Адмирал, АО 5 5 IO-JAN-1997 4000 Кока-кола Саяны, ИЧП 3 6 20-JAN-1997 30 Сахар Саяны, ИЧП 4 7 20-JAN-1997 20 Кока-кола <null> 3 8 20-JAN-1997 1000 Кока-кола Адмирал, АО 3 1 IO-JAN-1997 100 Кока-кола Адмирал, АО 3
460 Глава 34 Для принудительного выполнения запроса по тому или иному плану следует в опе- раторе SELECT использовать предложение PLAN <гшан _выполнения_запроса> < план _выполнения_запроса > = [JOIN | [SORT] MERGE] (<элемент_плана> | < план _выполнения_запроса > [, < элемент_плана > I < план _выполнения_запроса > ...]) <злемент_плана> = {таблица | псевдоним] NATURAL | INDEX ( <индекс> [, < индекс > ...]) I ORDER < индекс > Синтаксис предложения PLAN относится как к единичной таблице БД, так и к не- скольким таблицам. В последнем случае выполняется соединение таблиц для увеличе- ния скорости выполнения запроса, что определяется использованием необязательного ключевого слова JOIN. В том случае, если не существует индексов, по которым дан- ные таблицы могли бы быть соединены, для увеличения скорости выполнения запроса указывают ключевые слова SORT MERGE. <элемент_плана> является именем таблицы, в которой производится поиск дан- ных. В том случае, если одна и та же таблица несколько раз участвует в запросе, для сокращения текста плана полезно использовать ее псевдоним, то есть обозначение, указываемое в предложении FROM после имени таблицы. Следующие ключевые сло- ва определяют способ доступа к данным. NATURAL (по умолчанию) - указывает, что для поиска записей применяется по- следовательный доступ. Это единственный способ поиска записей в том случае, если нет подходящих индексов. INDEX- указывает один или несколько индексов, которые должны использоваться для поиска записей, удовлетворяющих условию запроса. ORDER - указывает, что <элемент_плана> должен быть отсортирован по указан- ному индексу. 34.2.4. Целесообразность создания индексов Индексы необходимо создавать в случае, когда по столбцу или группе столбцов часто: • производится поиск в БД (столбец или группа столбцов часто перечисляются в предложении WHERE оператора SELECT)-, • строятся объединения таблиц; • производится сортировка НД, возвращаемых в качестве результатов запросов к БД (т. е. столбец или столбцы часто используются в предложении ORDER BY оператора SELECT). Не рекомендуется строить индексы по столбцам или группам столбцов, которые: • редко используются для поиска, объединения и сортировки результатов запро- сов; • содержат часто меняющиеся значения, что приводит к необходимости частого обновления индекса и способно существенно замедлить скорость работы с БД; • содержат небольшое количество вариантов значения.
Оптимизация работы с БД 461 34.2.5. Частичное использование составного индекса Если запросы часто используют для поиска одни и те же столбцы, следует постро- ить по этим столбцам индекс (если это возможно) так, чтобы чаще используемые столбцы выступали в качестве ведущих полей индекса. Тогда при поиске может быть использована часть индексных полей. Пример. Пусть часто выполняются запросы SELECT * FROM SOMETABLE WHERE A = : ParamA AND В = :ParamB SELECT * FROM SOMETABLE WHERE A = : ParamA AND В = :ParamB AND C = :ParamC SELECT * FROM SOMETABLE WHERE A = : ParamA AND В = :ParamB AND C = :ParamC AND D = :ParamD SELECT * FROM SOMETABLE ORDER BY A,В Тогда, построив индекс по столбцам А, В, С, D, мы можем с большой долей уве- ренности утверждать, что данный индекс будет использован при оптимизации всех четырех запросов. В первом случае будет использовано подмножество индекса, т. е. значения А, В; во втором - значения А, В, С, в третьем - А, В, С, D (то есть все значе- ния индекса); в четвертом - А, В. Следует помнить, что при использовании в запросах не всех столбцов из индекса, можно использовать только непрерывную последовательность столбцов, что важно для указания порядка сортировки в предложении ORDER BY. Например, если индекс построен по столбцам А, В, С, D, этот индекс не может использоваться для выполне- ния запросов SELECT * FROM SOMETABLE ORDER BY А,С , SELECT * FROM SOMETABLE ORDER BY B,D Порядок следования условий по столбцам в предложении WHERE оператора SELECT не важен (если условия объединены с помощью AND). Например, для выпол- нения следующих запросов может использоваться один и тот же индекс: SELECT * FROM SOMETABLE WHERE A = LOO AND В = 200 SELECT * FROM SOMETABLE WHERE В = 200 AND A = 100
462 Глава 34 Однако при указании уровней сортировки в предложении ORDER BY оператора SELECT порядок следования столбцов является существенным. Например, для выпол- нения следующих запросов не может использоваться один и тот же индекс: SELECT * FROM SOMETABLE ORDER BY A, ВC SELECT * FROM SOMETABLE ORDER BY А, С, В 34.2.6. Многопоточность поиска no OR и IN При частом использовании в условной части WHERE оператора SELECT несколь- ких столбцов, связанных между собой операцией «или» (OR)\ SELECT * WHERE А = 100 OR В = 200 OR С = 300 вместо индекса по столбцам А, В, С лучше создать несколько индексов, построенных по каждому из этих полей, поскольку в противном случае будет осуществлен последо- вательный просмотр всей таблицы. Это неудивительно, так как индексно-последова- тельный доступ для индексов А, В, С может быть осуществлен только для столбца А\ значения столбцов В и С в этом случае спонтанно разбросаны по индексу. Важно пом- нить, что при использовании оператора OR в условной части оператора SELECT каж- дая часть условия влечет за собой отдельное сканирование участвующих в запросе таблиц. Так, например, при выполнении оператора SELECT с условной частью WHERE А = 100 OR В = 200 OR С = 300 будет осуществлено три отдельных сканирования таблицы (таблиц) для поиска значе- ний, удовлетворяющих условиям А = 100; в = 200; С = 300. Отдельный поток поиска порождает и каждый элемент в списке IN. Например, WHERE A IN (100,200,300) интерпретируется как WHERE А = 100 OR А'= 200 OR А = 300. Однако при указании диапазона BETWEEN WHERE A BETWEEN 100 AND 300 этого не происходит. Поэтому там. где возможно, следует заменять IN на BETWEEN. 34.2.7. Уменьшение общего количества индексов Следует стремиться к уменьшению количества индексов, поскольку при большом их количестве снижается скорость добавления, изменения и удаления записей в таб- лицах БД. Как правило, в БД определяется два вида индексов: индексы, фактически использующиеся запросами для доступа .к данным, и индексы, введенные в БД для обеспечения ссылочной целостности между родительскими и дочерними таблицами БД. Если индексы не используются при доступе к данным, их следует удалять, а ссы- лочную целостность обеспечивать с использованием триггеров.
Оптимизация работы с БД 463 34.3. Оптимизация клиентских приложений От того, каким образом организуется доступ к удаленной БД, во многом зависит, насколько эффективно будет работать с ней данное приложение. 34.3.1. Минимизация соединений с БД Для соединения с удаленной БД в клиентских ©е/рЛг-приложениях используется компонент TDatabase. Он служит для: • создания постоянного соединения с БД; • создания локального псевдонима БД; • изменения параметров соединения, установленных для псевдонима БД (в ути- лите BDE Administrator}, • управления транзакциями. Если не использовать компонент TDatabase, то соединение с БД может, в принци- пе, осуществлять каждый компонент типа «набор данных» (TTable, TQuery, TStoredProc). Однако следует помнить, что каждое соединение с БД потребляет сис- темные ресурсы и их чрезмерный расход может сказаться на эффективности доступа к БД. Кроме того, при соединении с удаленной БД «напрямую» (из компонентов типа «набор данных») невозможно изменять предустановленные параметры соединения. Поэтому рекомендуется снижать число соединений с удаленной БД к минимуму, а в идеале - иметь одно соединение с каждой БД. 34.3.2. Использование TQuery Хотя при доступе к таблицам БД могут использоваться два компонента типа «набор данных» - ТТаЫе и TQuery, для доступа к удаленным данным рекомендуется использовать компонент TQuery. Предпочтительность использования TQuery при доступе к удаленным данным оп- ределяется следующими причинами: • при доступе к табличным данным компонент ТТаЫе считывает все записи уда- ленной таблицы, в то время как TQuery - ровно столько, сколько нужно для те- кущих целей визуализации, например, для заполнения сетки TDBGrid-, при дос- тупе к таблицам большого объема использование ТТаЫе может привести к су- щественным временным задержкам; • компоненты ТТаЫе и TQuery имеют разную природу: ТТаЫе ориентирован на • навигационный метод доступа к данным, что более характерно для работы с ло- кальными СУБД; TQuery ориентирован на работу с множествами записей, что характерно при доступе к удаленным БД в архитектуре «клиент-сервер»; ТТаЫе позволяет обратиться к одной таблице БД, TQuery - к результатам выполнения запроса одновременно к нескольким ТБД; соответственно, подтверждение из- менений данных в ТТаЫе осуществляется для каждой записи, что существенно увеличивает сетевой трафик; изменение данных при использовании TQuery мо- жет производиться сразу над множеством записей с использованием операторов INSERT, UPDATE, DELETE-, • при помощи компонента TQuery можно выполнять разнообразные SQL- операторы, как возвращающие НД (SELECT), так и не возвращающие его (INSERT ит. п.).
464 Глава 34 34.3.3. Перенос тяжести вычислительной работы на сервер При работе с удаленными БД следует стремиться перенести всю тяжесть вычисли- тельной работы на сервер, по возможности оставив приложению клиента лишь работу по реализации интерфейса с пользователем, отсылке запросов к серверу и интерпрета- ции полученных от него данных. Не надо обращаться к серверу с запросом необоснованно большого объема данных, на которые приходится накладывать фильтры в самом клиентском приложении. Следует максимально использовать возможности сервера, памятуя о том, что он может выполнять большинство требований приложения быстрее и, кроме того, серве- ру не нужно пересылать данные самому себе по сети. Реализуйте ограничения на значения вводимых пользователем данных при помощи аппарата ограничений БД {CONSTRAINTS), а ссылочную целостность - при помощи триггеров. Запросы, требующие при своем выполнении ветвящихся или циклических алго- ритмов, а также всевозможные вычисления значений, основанные на текущих данных из БД, реализуйте при помощи хранимых процедур. Бизнес-правила, связанные с транзакционными изменениями ряда таблиц, реали- зуйте при помощи триггеров. Получайте уникальные значения числовых полей при помощи генераторов. Повторяющиеся действия, которые могут разделяться различными приложениями и использоваться в ^/.-операторах, реализуйте при помощи функций, определенных пользователем {UDF). Перенос тяжести вычислительной работы на сервер, во-первых, убыстряет работу клиентского приложения, а во-вторых, минимизирует возможность возникновения ошибок.
Глава 35 ПОДДЕРЖКА ORACLE8 35.1. ПОЧЕМУ ORACLE8 В Delphi 4 (и 5) включены средства, поддерживающие промышленный сервер Oracle версии 8 (далее Oracle8). В начале 1998 года Inprise заключила с производите- лем сервера - американской корпорацией Oracle - долгосрочное партнерское согла- шение, в рамках которого Oracle передала ей некоторые свои технологии, a Inprise включила поддержку этих технологий в Delphi 4. Взаимный интерес двух корпораций неслучаен. Подобно тому как вот уже на про- тяжении почти 20 лет Inprise {Borland) считается лучшим в мире производителем ин- струментальных средств, Oracle является производителем лучших в мире серверов. Серверам Oracle принадлежат впечатляющие рекорды как по суммарному объему БД - более 40 Тбайт (Тбайт - терабайт = 1024*1024*1024*1024 = 1 099 511 627 726 байт), так и по скорости - до 15000 транзакций в секунду1. Однако для этих серверов слабо развиты инструментальные средства создания клиентских мест. С другой стороны, Inprise не только выпускает высокого качества сервер InterBase, принятый на воору- жение (в буквальном смысле этого слова) армией США, но и производит Delphi - лучшую в мире среду разработки клиент-серверных систем. После выпуска (в середине 1997 года) версии 8 своего сервера Oracle заявила о долгосрочных планах, суть которых заключается в предоставлении своим клиентам возможности работать «без жестких дисков»: если вы открываете кран, то рассчиты- ваете, что из него польется вода, при этом вас совершенно не интересует, как эта вода попала в кран; точно так же, когда вы включаете компьютер, вы рассчитывает обна- ружить в нем нужные вам программы и данные, при этом для вас, собственно, совер- шенно не важно, как они туда попали, - примерно такими словами представитель Oracle предварил объявление плана, добавив, что новый сервер предоставляет пользо- вателям такие большие объемы данных и такие высокие скорости доступа к ним, что у них (пользователей) просто не будет необходимости в собственных локальных жест- ких дисках1 2. Понимая, что столь амбициозный план реализовать собственными силами невоз- можно, Oracle обратилась к ведущим американским и иностранным фирмам с пред- ложением о сотрудничестве в обмен на финансовую и маркетинговую поддержку3. Inprise одна из первых откликнулась на этот призыв, a Delphi 4, насколько нам извест- но, является первым в мире инструментальным средством с поддержкой Oracle8. (Заметим в скобках, что сближению корпораций в немалой степени способствовала их 1 Если у вас возникли сомнения по поводу приведенных цифр, сообщим, что серверами Oracle оснащены предприятия транснациональной корпорации American Telephone & Telegraph Company, регистрирующие в автоматическом режиме тысячи телефонных соединений в секунду. 2 В рамках этого плана Oracle предложила правительству США программу «Каждому американскому ребенку - сетевой компьютер». 3 Oracle принадлежит более трети мирового рынка серверов БД. По объему продаж и доходов она явля- ется вторым в мире (после Microsoft) производителем и продавцом программного обеспечения.
466 Глава 35 давняя нелюбовь к Microsoft-, недаром они являются основателями и активными уча- стниками OMG1, прекрасно понимая, что CORBA - единственная реальная альтерна- тива технологии DCOM и, следовательно, ее внедрение становится последним шансом избежать полной зависимости от Microsoft.) В этой главе описываются средства Delphi, рассчитанные на поддержку Oracle8, однако нам кажется необходимым сначала хотя бы вкратце охарактеризовать новые возможности этого сервера. 35.2. НЕКОТОРЫЕ НОВЫЕ ВОЗМОЖНОСТИ ORACLE8 Oracle8 разрабатывался несколько лет и рекламировался как первый в мире объ- ектно-ориентированный сервер. Однако даже сами разработчики сервера называют его объектно-реляционным сервером, подчеркивая тем самым, что в его основе лежит традиционная реляционная модель данных, а объектно-ориентированные средства лишь дополняют эту модель. Объектно-ориентированная модель данных предполагает использование вместо привычных реляционно-связанных таблиц БД неких объектов, которые бы объединяли в себе собственно табличные данные и методы доступа к ним (инкапсуля'ция), были способны порождать более совершенных потомков (наследова- ние) и распоряжаться их методами как своими собственными (полиморфизм). Приня- тие этой модели могло бы вызвать дружную неприязнь со стороны разработчиков и администраторов БД, многие из которых воспитаны на таких языках, как Кобол, FoxPro, Clipper. Видимо, опасение потерять потенциальных клиентов вынудило Oracle не спешить с внедрением новой идеологии. Тем не менее объектно-ориентированные средства введены в новый сервер. Их стержнем стал так называемый абстрактный тип данных ADT (Abstract Data Type), который декларирует некоторую структуру, содержащую не только данные, но и, воз- можно, код. Программист может объявлять сколько угодно абстрактных типов и затем использовать их в определении столбцов и доменов. Единственное, но существенное ограничение — абстрактный тип должен в конечном счете состоять только из атомар- ных (базовых) типов и не может ссылаться на другой абстрактный тип, т. е. запрещен иерархический механизм наследования и полиморфизма. К другим новшествам относятся наборы и новые типы. Набор - это множество ти- пов, рассматриваемое как единое целое. В Oracle8 можно использовать два сорта на- боров - массивы переменной длины VARRAYи вложенные таблицы (NESTED TABLE). Массивы предназначены для хранения нескольких однотипных компонентов в одном столбце. Они являются частью таблицы и вместе с другими столбцами считываются за одно обращение к ней. Вложенные таблицы хранятся отдельно от основной таблицы. Они рассчитаны на поддержку популярного реляционного отношения один-ко- многим. Вложенные таблицы считываются как минимум за два обращения к БД. В отличие от обычных таблиц они могут не иметь собственного имени, так как фактиче- ски рассматриваются как неотъемлемые составные части (столбцы) родительской таб- лицы. К новым типам относятся типы LOB, REF и BFILE. LOB (Large Object - большой объект) может иметь длину до 4 Гбайт и по сути является аналогом типа BLOB в таб- 1 OMG - Object Management Group - независимая организация, занимающаяся разработкой и внедрени- ем технологии распределенных вычислений CORBA (Common Object Require Broker Architecture.
Поддержка Oracle8 467 лицах Paradox. Для реализации конкретных разновидностей этого типа используются типы BLOB (трактует LOB как последовательность байтов), CLOB (длинная цепочка однобайтных символов) и NCLOB (последовательность двухбайтных символов). Тип REF содержит ссылку на другой объект (столбец или таблицу) в той же БД, а тип BFILE - ссылку на внешний (по отношению к БД) файл. 35.3. СРЕДСТВА DELPHI ДЛЯ ПОДДЕРЖКИ ORACLE8 Эти средства заключаются в изменении базовых типов TField (поле, см. гл. 5), TDBDataSet (набор данных, см. гл. 6) и введении специального компонента TNestedTable. Заметим, что утилиты BDE Administrator, Database Desktop и SQL Explorer не поддерживают Oracle8, т. е. с их помощью нельзя создать, просмотреть или отредактировать таблицы, содержащие новшества этого сервера. 35.3.1. Класс TObjectField и его наследники В состав допустимых значений класса TFieldType (свойство FieldType класса TField, см. п. 5.1) теперь могут входить значения ftFixedChar (тип CLOB), ftWideChar (тип NCLOB), ftLargelnt {BLOB),ftADT {ADT), ftArray (VARRAY), ftReference {REF) и ftDataSet {NESTED TABLE). Три первых лишь расшифровывают содержимое столбца в терминах новых типов Oracle8 и не требуют дополнительных изменений, т. к. лежа- щий в их основе фактический тип LOB представляет собой аналог «родного» для Delphi типа BLOB. Четыре других значения потребовали введения четырех новых для Delphi типов - TADTField, TArrayField, TReferenceField и TDataSetField. Их общие свойства и методы инкапсулирует родительский класс TQblectField, являющийся пря- мым наследником TField. Специфичные свойства класса TObjectField-. Свойство Назначение property AsString: String; Представляет вложенный столбец как после- довательность текстовых строк, разделенных запятыми. Это свойство предназначено только для чтения. property AsVariant: Variant; Представляет вложенный столбец как вари- антный массив. Это свойство может использо- ваться как для чтения, так и для записи. property DataSet: TDataSet; Указывает на НД, используемый в качестве вложенной таблицы (только для чтения). property FieldCount: Integer; Содержит количество полей во вложенной таблице. property Fields: TFields; Определяет столбць! вложенной таблицы. См. свойство ObjectView класса TDBDataSet (п. 35.3.2) и примечание 1. property Fieldvalues[Index: Integer]: Variant; Открывает индексированный доступ к столб- . цам вложенной таблицы.
468 Глава 35 property HasConstraints: Boolean; Содержит True, если вложенная таблица или хотя бы один ее столбец имеют ограничения, накладываемые сервером или BDE. property ObjectType: String; Возвращает строку, идентифицирующую тип столбца в терминах Oracle8. property Size: Word; В родительском классе TObjectField содержит количество полей в дочернем НД, в специали- зированных потомках - длину поля в байтах. property UnNamed: Boolean; Содержит True, если вложенная таблица не имеет собственного имени. Примечание 1. В свойстве Fields для ссылки на столбцы вложенного НД, объект- ные поля ADT, массивы VARRAY используется новый класс TFields, с помощью кото- рого программист может получить доступ к нужному столбцу, добавлять или удалять определения столбцов. Свойства класса TFields’. Свойство Назначение property Count: Integer; Содержит количество столбцов. property DataSet: TDataSet; Содержит ссылку на вложенный НД. Про- грамма не должна изменять значение этого свойства. property Fields[Index: Integer]: TField; Открывает индексированный доступ к столб- цам. Методы класса TFields’. Метод Назначение procedure Add(Field: TField); Добавляет к массиву Fields определение ново- го столбца. procedure CheckFieldName(const FieldName: String); Проверяет существование в списке Fields столбца с именем FieldName. Если такой стол- бец существует, возбуждается исключение EDatabaseError. procedure CheckFieldNames(const FieldNames: String); Проверяет существование в списке Fields столбцов с именами FieldNames (разделяются запятыми). Если хотя бы один столбец отсут- ствует в Fields, возбуждается исключение EDatabaseError. procedure Clear; Очищает список Fields. function FieldByName(const FieldName: String): TField; Открывает доступ к столбцу по его имени FieldName. function FieldByNumber(FieldNo: Integer): TField;' Открывает доступ к столбцу по его номеру. function FindField(const FieldName: String): TField; Ищет в списке Fields столбец с именем FieldName и возвращает ссылку на него или NIL, если столбец не найден.
Поддержка Oracle8 469 procedure GetFieldNames(List: TStrings); Заполняет список List именами столбцов. function IndexOf(Field: TField): Integer; Возвращает индекс столбца Field или -1, если столбец не найден. procedure Remove(Field: TField); Удаляет из списка Fields определение столбца Field. Все специализированные потомки TObjectField переопределяют его абстрактный конструктор Create и свойство Size. Класс TReferenceField изменяет свойство AsVariant, чтобы связать ссылочное поле с вариантным массивом байтов, и вводит дополнительное свойство property ReferenceTableName: String; Это свойство указывает имя таблицы, содержащей столбец, на который ссылается ро- дительский столбец. В классе TDataSetField введены два дополнительных свойства: property IncludeObjectField: Boolean; и property NestedDataSet: TDataSet; С помощью первого можно создать объекты-поля для вложенной таблицы. Для этого в него нужно поместить значение True и затем использовать редактор полей. Второе свойство содержит ссылку на вложенный НД. 35.3.2. Изменения в классе TDBDataSet Класс TDBDataSet (см. гл. 6) является родительским классом для компонентов-ис- точников ТТаЫе, TQuery, TStoredProc. Свойства этого класса изменены для поддерж- ки новых средств Oracle8. Новые свойства класса: Свойство Назначение property AggFields: TFields; Содержит массив агрегатных полей. property DataSetField: TDataSetField; Содержит массив вложенных таблиц. property Objectview: Boolean; Определяет способ представления столбцов в массиве TObjectField.Fields (см. ниже приме- чание 1). property SparseArrays: Boolean; Если содержит False, для каждого элемента массива PARRA Y создается объект класса TField. Примечание 1. Если свойство ObjectView содержит True, столбцы в свойстве TObjectField.Fields представляются как объекты новых классов TADTField, TReferenceField, TArrayField, TDataSetField, в противном случае - как объекты TField. Это свойство используется для возможности отображения столбцов новых типов ви- зуальными компонентами Delphi, которые не могут работать с этими типами данных.
470 Глава 35 35.3.3. Компонент TNestedTable Этот компонент расположен на странице Data Access палитры компонентов Delphi. Он является источником данных для вложенных таблиц. Все его свойства, методы и события унаследованы им от родительского класса TBDEDataSet, который несет в се- бе примерно такую же функциональность, как и класс TDBDataSet. Таким образом, компонент TNestedTable практически не отличается от других компонентов- источников. Он перекрывает конструктор Create своего родительского класса, чтобы устано- вить значение True в свойство ObjectView. Это означает, что по умолчанию с ним не смогут работать визуальные компоненты НД.
Часть 3 ТРЕХЗВЕННАЯ АРХИТЕКТУРА И РАСПРЕДЕЛЕННЫЕ ПРИЛОЖЕНИЯ
Глава 36 ВВЕДЕНИЕ В ТРЕХЗВЕННУЮ АРХИТЕКТУРУ 36.1. ОБЗОР ТРЕХЗВЕННОЙ АРХИТЕКТУРЫ 36.1.1. Преимущества трехзвенной архитектуры В п. 3.3.3 приведено краткое описание способа реализации и преимуществ трех- звенной архитектуры. Повторим, что основной целью этой технологии является все- мерное снижение требований к клиентским компьютерам, чтобы в идеале в качестве таковых моЬли использоваться дешевые бездисковые сетевые компьютеры. С этой целью значительная часть программных ресурсов клиента переносится в промежуточ- ное звено - сервер приложений. Сервер приложений располагается на компьютере сервера БД или на выделенном сетевом компьютере. Только он с помощью BDE непо- средственно взаимодействует с сервером БД, реализуя посредничество между ним и остальными клиентскими местами (рис. 36.1). Клиент Клиент Клиент Клиент Рис. 36.1. Трехзвенная архитектура в варианте расположения сервера приложений на машине сервера БД. В этой архитектуре клиентское программное обеспечение не обращается более к BDE и освобождается от значительного слоя компонентов-источников и реализации б олыпей части клиентских бизнес-правил, которые в этом случае переносятся на сервер приложения. В результате получается «облегченный» клиент, который не требует
больших ресурсов памяти и может загружаться с сетевого компьютера - это главное преимущество трехзвенной архитектуры1. К дополнительным достоинствам можно отнести: • централизованный доступ к большинству бизнес-правил, которые сосредоточе- ны теперь в одном месте - на сервере приложения; эта централизация позволяет гибко изменять бизнес-правила без необходимости их тиражирования во мно- гие клиентские приложения; • существенное уменьшение сетевого трафика за счет отложенного обновления и регулируемого объема пакетов данных; • простоту распространения новых версий клиентского программного обеспече- ния, т. к. нет необходимости устанавливать на клиентских местах BDE или по- добный механизм доступа к данным; • возможность отложенных обновлений НД: можно получить необходимую пор- цию данных, сохранить их в файле, работать с ними автономно (например, в другом городе), а затем вновь соединиться с сервером приложений и обновить на сервере сделанные в данных изменения. Замечание. В этом разделе упор делается на наиболеё популярные се- годня трехзвенные приложения (сервер БД - сервер приложений - кли- ент). На самом деле наличие в этой схеме сервера БД вовсе не обяза- тельно - вместо него с успехом могут выступать и невзыскательные файл-серверы с таблицами dBASE или Paradox. Преимущества «облегченного» клиента и централизации бизнес-правил будут в полной мере проявляться и в этом случае. Кромё того, в таких схемах сущест- венно снижается нагрузка на сеть, т. к. фильтрация и отбор данных реа- лизуются сервером приложений, а клиенту пересылается лишь резуль- тат запроса. 36.1.2. Программная реализация В Delphi поддерживаются несколько способов программной реализации трехзвенной архитектуры, в том числе с помощью технологий DCOM, MTS или CORBA. В зависимо- сти от принятой технологии сервер приложений реализуется с помощью компонентов TRemoteDataModule, TMTSDataModule или TCorbaDataModule. Каждый из этих компо- нентов представляет собой невидимое в работающем приложении окно-контейнер, по- добное окну модуля TDataModule. В этот контейнер помещаются компоненты для связи с удаленной БД (TSession, TDatabase), компоненты-источники (ТТаЫе, TQuery, TStoredProc) и, если необходимо, обработчики событий этих компонентов, в которых реализуются нужные бизнес-правила, а также объекты-поля для соответствующих НД. С каждым компонентом-источником связывается специальный компонент TDataSetProvider, обеспечивающий связь источника с клиентом (рис. 36.2). Сам модуль может получить информацию об ограничениях, накладываемых на значения столбцов НД, из словаря баз данных Delphi и (после определения полей) выступать в роли брокера ограничений, не допуская передачу неверных данных на сервер БД. 1 Минимальную экономию дисковой памяти можно оценить следующим образом. £>Л£-файлы BDE за- нимают более 8 Мбайт и размещаются на машине сервера приложений. Вместо них иа машине клиента устанавливается одна библиотека DBCLIENT.DLL, объемом менее 212 Кбайт.
Введение в трехзвенную архитектуру 475 На клиентской стороне располагается связной компонент, осуществляющий непо- средственную связь с удаленным сервером. В зависимости от используемого протоко- ла передачи данных это может быть TDCOMConnection, TCORBAConnection, TSocketConnection или TOLEnterpriseConnection (в роли связного компонента может также выступать TRemoteServer, включенный в Delphi 4 для совместимости с Delphi 3; в новых разработках этот компонент использовать не рекомендуется). Посредниками компонентов-источников сервера приложений служат компоненты TClientDateSet - по одному компоненту на каждый компонент-источник. Дальнейшая цепочка передачи данных ничем не отличается от обычной: через компоненты TDataSource к визуаль- ным компонентам (TDBGrid, TDBEdit и т. п.) и обратно. Рис. 36.2. Программная реализация трехзвенной архитектуры. Поскольку в трехзвенной архитектуре клиент и сервер приложений в общем случае располагаются на разных машинах, связь клиента с сервером приложений реализуется с помощью той или иной технологии удаленного доступа. Delphi поддерживает сле- дующие технологии удаленного доступа: • DCOM (Distributed Component Object Model - распределенная компонентная модель объектов) - рассчитана на локальную сеть, в которой сервер приложе- ний работает под управлением операционной системы Windows NT Server, • с помощью сокетов TCP/IP, позволяющих использовать сеть без Windows NT Server, • OLEnterprise (Object Linked and Embedding for Enterprise - связывание и вне- дрение объектов для предприятия) — рассчитана на локальную сеть, в которой сервер приложений работает под управлением любой ОС Windows 32; эта тех- нология — собственность Borland; в связи с бурным развитием DCOM и MTS она уже не включена в версию Delphi 5, хотя и входила в поставку версии 4; • MTS (Microsoft Transaction Server - сервер транзакций Microsoft) - основана на DCOM и включает в себя некоторые дополнительные возможности, связанные с управлением системными ресурсами (процессами, потоками, соединениями с ба- зами данных) и транзакциями, а также с повышенной защищенностью данных;
476 Глава 36 • CORBA (Common Object Request Broker Architecture — архитектура с брокером нужных общих объектов) - в отличие от DCOM не предъявляет специальных требований к ОС или аппаратной платформе клиентов и серверов. Если вы имеете опыт работы с локальной сетью, вы можете спросить: а разве нель- зя сделать доступным каталог размещения сервера приложений клиентским машинам и запускать на них сервер так, как это делается для любой другой исполняемой про- граммы, не прибегая к сложным технологиям удаленного доступа? Увы, такой широко используемый прием в данном случае не работает. Дело в том, что удаленная про- грамма загружается в адресное пространство клиента, а не машины сервера и, таким образом, лишается возможности работы с BDE. Наоборот, указанные технологии за- гружают сервер приложений в адресное пространство машины сервера, причем в большинстве случаев делают это автоматически, по требованию клиента. Такие резидентные программы обычно называются Service Control Manager (SCM). При работе с Windows NT Server в роли SCM выступает утилита DCOMCNFG. EXE. Хотя фирменная документация утверждает, что перенос этой утилиты на Windows 95 дает возможность рабо- тать с DCOM под управлением этой ОС, известные авторам попытки этого не увенчались успехом (то же самое отмечают авторы крайне полезной книги [5]). dcomcnfg.exe вклю- чена в состав Windows 98, но и в этом случае добиться ее нормальной работы авторам, на- пример, не удалось. Связано это с тем, что право запуска удаленной программы должно быть не всеобщим (на уровне ресурсов машины), а избирательным (на уровне пользователей или групп пользователей). Таким образом, фактически с DCOM пока можно работать только в том случае, когда сервер приложений установлен на машине с ОС Windows NT Server. Такая особенность DCOM дала толчок к развитию других технологий. В частности, Borland создала технологию OLEnterprise, в которой удаленный за- пуск сервера реализовывался на том основании, что сервер могли запускать только пользователи, имеющие на своем компьютере специальную клиентскую часть этой технологии. С помощью этих частей клиент мог обратиться к специальному межсете- вому объекту - Buisnes Object Broker, программе, которая умела экспортировать из реестра сервера запись о сервере приложений и, используя ее, запускать сервер. Любой знакомый с Windows и Internet специалист, разумеется, вспомнит о сущест- вовании сокетов - специального слоя низкоуровневых ЛР/-функций, реализующих связь машин на уровне протокола TCP/IP. Небольшой «довесок» к этим функциям - утилита scktsrvr.exe должна стартовать на машине сервера, после чего с ее помо- щью можно запускать любой зарегистрированный на этой машине сервер приложений (утилита scktsrvr.exe входит в поставку Delphi и располагается в каталоге BIN ката- лога расположения Delphi}. Таким образом, если вас не волнует разграничение прав пользо- вателей, вы с успехом можете использовать сокеты в роли SCM. В технологии CORBA центральную роль играет сочетание двух специальных объектов - Smart Agent и О AD. Первый играет роль сетевого каталога и «знает» расположение любого сервера. Второй умеет запускать сервер после, если это необходимо, проверки прав пользо- вателя (в CORBA используется сложный алгоритм аутентификации пользователя с привле- чением для этого «цифровой подписи»). 36.1.3. Пример реализации трехзвенной архитектуры Чтобы наглядно продемонстрировать, какими скромными средствами можно обой- тись при создании трехзвенных приложений, мы сознательно откажемся от DCOM
Введение в трехзвенную архитектуру 477 (чтобы не требовать наличия в сети Windows NT Server) и любого сервера БД (даже от поставляемого с Delphi сервера InterBase. 1. С помощью опции Database | Explorer вызовите SQL Explorer и убедитесь, что существует псевдоним DBDEMOS. Этот псевдоним обычно создается автоматически при установке Delphi, если вы потребовали скопировать также и учебные примеры. Если такого псевдонима нет, создайте его как Standard, установив в параметре Path строку C:\Program FilesXCommon Files \Borland SharedX Data (указан умалчивае- мый маршрут доступа); разумеется, если в указанном каталоге нет таблиц демонстра- ционной БД, нужно создать собственную БД и установить доступ к ней; 2. Закройте SQL Explorer и выберите File | New Application — мы начинаем создание сервера приложений. 3. С помощью-кнопки New инструментальной панели или опции File | New вызовите окно репозитория Delphi и на странице Multitier дважды щелкните по пиктограмме Remote Data Module. 4. В ответ на запрос эксперта введите имя класса Server. 5. Поместите в окно Server компоненты TDatabase и ТТаЫе. В компоненте Databasel в выпадающем списке свойства AliasName выберите псевдоним dbdemos и установите имя srv в его свойство DatabaseName. 6. Для Tablel выберите псевдоним srv в раскрывающемся списке DatabaseName и таблицу biolife в списке TableName. Откройте НД, установив в его свойство Active значение True. 7. Поместите на модуль компонент TDataSetProvider (страница Midas галереи компонен- тов Delphi). В его свойство DataSet поместите ссылку на таблицу Tablel. Замечание. В Delphi 4 программист должен был вручную экспортировать из сервера приложений любой НД, который мог ему понадобиться в клиент- ской программе. Для этого использовалось локальное меню НД или связанно- го с ним провайдера, из которого выбиралась опция Export Tablel from data module. В Delphi 5 набор данных автоматически экспортируется, если с ним связан компонент TDataSetProvider. 8. Сохраните модуль Unit2 под именем RS0, модуль Unit! под именем RS1, а сам проект под именем RServer. Запустите программу, чтобы зарегистрировать сервер в реестре Windows и закройте окно Forml сразу после того, как оно появится на экране. Наш сервер приложений готов к работе. 9. Теперь создадим клиента: выберите опцию File | New Application. 10. Поместите на форму компоненты TSocketConnection, а также компоненты TDataSource, TDBGrid, TButton и TDBImage (рис. 36.3). И. Щелкните по кнопке Host компонента SocketConnectionl и выберите имя сетевой машины, на которой расположен сервер приложений. 12. Работа с сокетами поддерживается специальной утилитой scktsrvr.exe, которая располагается в каталоге BIN каталога размещения Delphi. Запустите ее. 13. Раскройте список свойства ServerName компонента SocketConnectionl и выберите RServer.Server. Установите значение True в свойство Connected этого компонента. 14. Для компонента ClientDataSetl выберите в списке RemoteServer имя компонента SocketConnectionl, а в списке ProviderName - DataSetProviderl (если вы помните, этот компонент на сервере приложений экспортирует данные таблицы biolife).
478 Глава 36 15. Свяжите компонент DataSource 1 с ClientDataSetl, DBGridl - с DataSourcel, а DBImagel - с DataSourcel и с полем Graphic (свойство DataField). Откройте набор ClientDataSetl - в сетке должны появиться данные (рис. 36.3). Рис. 36.3. Вид окна клиента на этапе конструирования формы. 14. В свойство Caption кнопки TButton поместите Обновить и напишите для нее такой обработчик события OnClick. procedure TForml.ButtonlClick(Sender: TObject); begin with ClientDataSetl do begin ApplyUpdates(0); // Запоминаем изменения в БД Close; // Обновляем локальный НД, Open // чтобы убедиться в изменениях end end; После запуска программы вы можете просматривать, изменять, удалять и вставлять записи, однако все вносимые вами изменения будут касаться локальной копии таблич- ных данных до тех пор, пока вы не нажмете кнопку Обновить. В этот момент вызыва- ется метод ApplyUpdates (параметр обращения к нему определяет количество попыток обращения к серверной таблице, если первая попытка была неудачной), в ходе выпол- нения которого локальные данные переносятся на сервер. Используемые вслед за ним методы Close и Open нужны для считывания данных с сервера - таким способом мы убеждаемся в действительном сохранении внесенных изменений (разумеется, в реаль- ной программе этот прием необязателен). Локальные данные можно записать в файл и затем прочитать их из него с помощью методов SaveToFile и LoadToFile компонентов TClientDataSet. Таким способом можно сохранить нужные данные на диске ноутбука, затем взять его на выездное мероприя- тие (например, на презентацию или в мобильную торговую точку), изменять их авто- номно и, вновь подсоединившись к сети, запомнить изменения в серверной БД. Отложенная обработка позволяет значительно снизить загрузку сети. Этому же способствует и возможность регулирования объемов пересылаемых данных с помо-
Введение в трехзвенную архитектуру 479 щью свойства PacketRecords компонентов TClientDataSet (умалчиваемое значение -1 означает пересылку всех данных). > 36.1.4. Библиотека типов Вновь откройте проект RServer. Вы, очевидно, заметите, что на экране появилось новое окно, которого раньше в проекте не было (рис. 36.4). Если этого окна не видно, выберите опцию View | Type Library. Рис. 36.4. Окно редактора библиотеки типов. Это окно редактора библиотеки типов, которая автоматически создается в момент компиляции сервера приложений (точнее, в момент компиляции программы, которая содержит модуль TXXXDataModule). В библиотеку типов помещаются описания ин- терфейсов и связанных с ними глобально-уникальных идентификаторов (GUID). Уни- кальными идентификаторами снабжаются сама библиотека, интерфейс сервера и класс сервера. Если вы посмотрите исходный текст модуля RS0, то обнаружите в нем такое объявление класса TServer. type TServer = class(TRemoteDataModule, IServer) Sessionl: TSession; Databasel: TDatabase; Tablel: TTable; private { Private declarations } public { Public declarations } protected function Get_Tablel: IProvider; safecall; end;
480 Глава 36 Таким способом (т. е. с указанием не только родительского класса, но и интерфей- са) объявляются так называемые объекты СОМ. Объект Server - это объект СОМ. В момент старта программы такие объекты автоматически регистрируются в реестре Windows 32: в секцию HKEY CLASS ROOT помещается составное имя класса (имя программы, точка и имя класса) и GUID класса (рис. 36.5). Рис. 36.5. Фрагмент списка HKEY CLASS ROOT. После регистрации клиентам становится доступен объект СОМ, т. к. они «умёют» просматривать реестр в поисках нужных объектов. С помощью окна редактора библиотеки типов программист может добавлять к ин- терфейсу сервера приложений собственные методы и свойства, после чего они станут доступны клиентам. Таким образом клиентская программа может передать серверу приложений и получить от него информацию, которая может быть недоступна стан- дартному интерфейсу IProvider. 36.2. ИНТЕРФЕЙСЫ Интерфейсы играют главную роль в технологиях удаленного доступа. Их основная задача - описать свойства, методы и события удаленного объекта в терминах машины клиента, т. е. на используемом при разработке клиентского приложения языке про- граммирования. С помощью интерфейсов программа клиента обращается к удаленно- му объекту так, как если бы он был ее собственным объектом. 36.2.1. Создание и использование интерфейсов Интерфейсы - частный случай описания типов. Они описываются с помощью заре- зервированного слова interface. Например: type lEdit = interface procedure Copy; stdcall; procedure Cut; stdcall; procedure Paste; stdcall; function Undo: Boolean; stdcall; end;
Введение в трехзвенную архитектуру 481 Такое описание эквивалентно описанию абстрактного класса в том смысле, что про- возглашение интерфейса не требует расшифровки объявленных в нем свойств и методов. В отличие от классов интерфейс не может содержать поля и, следовательно, объяв- ляемые в нем свойства в разделах read и write могут ссылаться только на методы. Все объявляемые в интерфейсе члены размещаются в единственной секции public. Методы не могут быть абстрактными (abstract), виртуальными (virtual), динами- ческими (dynamic) или перекрываемыми (override). Интерфейсы не могут иметь конструкторов или деструкторов, т. к. описываемые в них методы реализуются только в рамках поддерживающих их классов, которые называются интерфейсными. Если какой-либо класс поддерживает интерфейс, имя этого интерфейса указывает- ся при объявлении класса в списке его родителей: TEditor = class(TInterfacedObject, lEdit) procedure Copy; stdcall; - procedure Cut; stdcall; procedure Paste; stdcall; function Undo: Boolean; stdcall; end; Интерфейсный класс может иметь более одного родительского интерфейса: type IMylnterface = interface procedure Delete; stdcall; end; TMyEditor = class(TInterfacedObiect, lEdit, IMylnterface) procedure Copy; stdcall; procedure Cut; stdcall; procedure Paste; stdcall; function Undo: Boolean; stdcall; procedure Delete; stdcall; end; B любом случае в разделе реализации интерфейсного класса необходимо описать соответствующие интерфейсные методы. Если, например, объявлен интерфейс IPaint = interface procedure CirclePaint(Canva: TCanvas; X,Y,R: Integer); procedure RectPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer); end; и использующий его класс TPainter = class(TInterfacedObject, IPaint) procedure CirclePaint(Canva: TCanvas; X,Y,R: Integer); procedure RectPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer); end; to в разделе implementation следует указать реализацию методов: procedure TPainter.CirclePaint(Canva: TCanvas; X,Y,R: Integer); begin with Canva do Ellipse(X, Y, X+2*R, Y+2*R); end;
482 Глава 36 procedure TPainter.RectPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer); begin with Canva do Rectangle (XI, Yl, X2,^ Y2) end; Теперь можно объявить интерфейсный объект класса TPainter, чтобы с его помо- щью нарисовать окружность и квадрат: procedure TForml.PaintBoxlPaint(Sender: TObject); var Painter: IPaint; begin Painter := TPainter.Create; Painter.CirclePaint(PaintBoxl.Canvas, 10,0,10) ; Painter.RectPaint(PaintBoxl.Canvas,40,0, 60,20) ; end; Несмотря на то, что интерфейс всегда объявляется до объявления использующего его интерфейсного класса и, следовательно, известен компилятору, его методы обязательно должны быть перечислены в объявлении класса. В нашем случае простое указание type TPainter = class (TlnterfacedObj-ect, IPaint) end; было бы ошибкой: компилятор потребовал бы вставить описание методов CirclePaint и RectPaint. Методы интерфейсного класса могут исполняться под именами, отличными от имен методов интерфейса: TPainter = class(TInterfacedObject,IPaint) procedure IPaint.CirclePaint = CPaint; procedure IPaint.RectPaint = RPaint; procedure CPaint(Canva: TCanvas; X,Y,R: Integer); procedure RPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer); end; Подобно тому как все классы в Object Pascal порождены от единственного родите- ля TObject, все интерфейсные объекты порождены от общего предка TInterfacedObject. Этот предок умеет распределять память для интерфейсных объектов и использует глобальный интерфейс lUnknow. type TinterfacedObject = class(TObject, IUnknown) private FRefCount: Integer; protected function Queryinterface( const IID: TGUID; out Obj): Integer; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public property RefCount: Integer read FRefCount; end;
Введение в трехзвенную архитектуру 483 Если бы в предыдущем примере класс TPainter был описан так: TPainter = class(IPaint) procedure CirclePaint(Canva: TCanvas; X,Y,R: Integer); procedure RectPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer); end; компилятор потребовал бы описать недостающие методы Query Interface, Add и Release класса TInterfacedObject. Поле FRefCount этого класса служит счетчиком вызовов интерфейсного объекта и используется по принятой в Windows схеме: при каждом обращении к методу Add интерфейса lUnknow счетчик наращивается на еди- ницу, при каждом обращении к Release - на единицу сбрасывается. Когда значение этого поля становится равно 0, интерфейсный объект уничтожается и освобождается занимаемая им память. Если интерфейс предполагается использовать в технологиях COM/DCOM или CORBA, его методы должны описываться с директивой stdcall или (для объектов Автоматизации) safecall. К интерфейсному объекту можно применить оператор приведения типов as, чтобы использовать нужный интерфейс: procedure Paintobjects(Р: TInterfacedObject) var X: IPaint; begin try X := P as IPaint; X.CirclePaint(PaintBoxl.Canvas,0,0,20) except ShowMessage('Объект не поддерживает интерфейс IPaint') end end; Встретив такое присваивание, компилятор создаст код, с помощью которого вызы- вается метод Queryinterface интерфейса lUnknow с требованием вернуть ссылку на интерфейс IPaint. Если объект не поддерживает указанный интерфейс, возникает ис- ключительная ситуация. Интерфейсы, рассчитанные на использование в удаленных объектах, должны снабжаться глобально-уникальным идентификатором (GUID). Например: IPaint = interface ['{A4AFEB60-7705-11D2-8B41-444553540000}'] procedure CirclePaint(Canva: TCanvas; X,Y,R: Integer); procedure RectPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer); end; Чтобы получить GUID в среде Delphi, достаточно нажать клавиши Ctrl+Shift+G. Для работы с GUID в модуле System объявлены следующие типы: type PGUID = ATGUID; TGUID = record DI: LongWord; D2: Word;
484 Глава 36 D3: Word; D4: array [0..7] of Byte; end; Программист может объявлять типизированные константы типа TGUID, например: const IID_IPaint: TGUID= ['{A4AFEB61-7705-11D2-8B41-444553540000} ' ]; Константы GUID могут использоваться вместо имен интерфейсов при вызове под- программ. Например, два следующих обращения идентичны: procedure Paint(const IID: TGUID); Paint(IPaint); Paint(IID_Paint); С помощью зарезервированного слова implements программист может делегиро- вать какому-либо свойству некоторого класса полномочия интерфейса. Это свойство должно иметь тип интерфейса или класса. Если свойство имеет тип интерфейса, имя этого интерфейса должно указываться в списке родителей класса, как если бы это был интерфейсный класс: type IMylnterface = interface procedure De- procedure P2; end; TMyClass = class(TObject, IMylnterface) FMylnterface: IMylnterface; property Mylnterface: IMylnterface read FMylnterface implements IMylnterface; end; Обратите внимание: в этом примере класс TMyClass не является интерфейсным классом, т. е. классом, в котором исполняются методы Р1 и Р2. Однако если из него убрать определение уполномоченного свойства Mylnterface, он станет интерфейсным и в нем должны быть описаны методы интерфейса IMylnterface. Уполномоченное свойство обязательно должно иметь часть read. Если оно имеет тип класса, класс, в котором оно объявлено, не может иметь других уполномоченных свойств. 36.2.2. Объекты Автоматизации и интерфейс IDispatch В технологии OLE активно используются так называемые объекты Автоматизации (Automation objects). Эти объекты представляют собой экземпляры интерфейсных классов, родительским интерфейсом которых является специальный интерфейс IDispatch. Отличительной особенностью этого интерфейса является то обстоятельство, что методы объекта Автоматизации никогда не вызываются напрямую, но всегда - с помощью метода Invoke интерфейса IDispatch. Управление объектами СОМ с помо- щью выполнения методов IDispatch называется маршализацией (marshaling). Для объявления класса Автоматизации используется специальное зарезервирован- ное слово dispinterface, а перечисляемые в нем методы и свойства должны снаб- жаться целочисленным идентификатором, который вставляется в конце описания ме- тода (свойства) после зарезервированного слова dispid:
Введение в трехзвенную архитектуру 485 type IStringsDisp = dispinterface ['{EE05DFE2-5549-11D0-9EA9-0020AF3D82DA}'] property ControlDefault[Index: Integer]: OleVariant dispid 0; default; function Count: Integer; dispid 1; property Item[Index: Integer]: OleVariant dispid 2; procedure Remove(Index: Integer); dispid 3; procedure Clear; dispid 4; function Add(Item: OleVariant): Integer; dispid 5; function _NewEnum: IUnknown; dispid -4; end; В отличие от обычного интерфейсного класса класс Автоматизации не может иметь родительского класса и поэтому за словом dispinterface нельзя указать список родителей. Идентификаторы методов (свойств) должны быть уникальными в пределах объявления класса. Все возвращаемые функциями и свойствами результаты, а также все параметры обращения к методам должны иметь один из следующих типов: Byte, Currency, Real, Double, Longlnt, Integer, Single, SmaUint, AnsiString, WideString, TDateTime, Variant, OleVariant, WordBool или любой интерфейсный тип. За исключением дирек- тивы default, которую можно указать для свойства-массива, никакие другие директивы доступа в объявлении методов и свойств не допускаются. Для доступа к объектам Автоматизации используются переменные вариантного типа. Инициация такой переменной осуществляется вызовом функции CreateOleObject, опре- деленной в модуле ComObj. Эта функция возвращает ссылку на интерфейс IDispatch, с помощью которой можно обращаться к методам и свойствам класса Автоматизации так, как если бы они были методами и свойствами варианта. Например: Uses ComObj; var Word: Variant; begin Word := CreateOleObject('Word.Basic'); Word.FileNew('Normal') ; Word.Insert('Первая строка'#13); Word.Insert('Вторая строка'#13); Word.FileSaveAs('c:\temp\test.txt', 3); end; Параметром обращения к CreateOleObject является имя сервера Автоматизации/ которое должно быть предварительно зарегистрировано в реестре Windows 32. Харак- терно, что методы сервера не известны на этапе компиляции программы, поэтому компилятор никак не контролирует правильность их вызовов. Названия методов не подчиняются правилам построения идентификаторов Delphi и в них могут использо- ваться символы национальных алфавитов. Передаваемые методам параметры могут быть позиционными и именованными. Позиционные параметры являются обычными для подпрограмм Object Pascal пара- метрами-значениями. Именованные параметры записываются в виде Имя_параметра := Значение
486 Глава 36 Например, при обращении к методу FileSaveAs (см. выше) были использованы два позиционных параметра. Это же обращение можно было бы записать с использовани- ем именованных параметров следующим образом: Word.FileSaveAs(Format := 3, Name := 'c:\temp\test.txt'); Именованные параметры могут перечисляться в произвольном порядке, но обязатель- но после позиционных параметров, если они указываются в одном и том же обращении. Интерфейсы-наследники от IDispatch называются дуальными (двойственными), так как они описываются на этапе компиляции сервера приложений, а связываются с кли- ентом на этапе прогона программы (для них недоступно раннее связывание, см. п. 38.2.6). Методы дуальных интерфейсов (кроме унаследованных от lUnknow и IDispatch) должны компилироваться в режиме safecall. 36.2.3. Стандартные интерфейсы В модуле StdVCL объявлены стандартные дуальные интерфейсы IDataBroker и IProvider, с помощью которых реализуется доступ к удаленным объектам. Интерфейс IDataBroker Интерфейсу IDataBroker следуют компоненты TXXXDataModule. Его единствен- ный метод function GetProviderNames: OleVariant; safecall; возвращает список имен компонентов-источников, размещенных на сервере при- ложений. Только одно из имен этого списка можно установить в свойство ProviderName компонента TClientDataSet. Интерфейс IProvider С помощью интерфейса IProvider реализуется обмен данными между сервером приложений и клиентами. Свойство property Constraints: WordBool; содержит True, если информация об ограничениях включается в пакет данных. Эта информация позволяет клиенту локально проверить наложенные ограничения и ис- ключить обновление записей, в которых эти ограничения нарушены. Свойство property Data: OleVariant; предназначено только для чтения. Оно содержит пакет данных, которые получены клиентом от сервера приложений. Методы интерфейса: Метод Назначение function ApplyUpdates(Delta: OleVariant; MaxErrors: Integer; out ErrorCount: Integer): OleVariant; safecall; Обновляет набор данных: Delta - содержит пакет измененных, вставленных и удаленных записей, которые необходимо поместить в НД; MaxErrors - максимальное количество ошибок, после кото- рого процесс обновления прекращается; ErrorCount- действительное количество ошибок. Функция возвращает пакет записей, которые по тем или иным причинам не были обновлены.
Введение в трехзвенную архитектуру 487 function DutuRequest(Input: OleVariant): OleVariant; safecall; Используется для вызова обработчика собы- тия OnDataRequest на сервере приложений. С помощью обработчика сервер приложений и клиент могут обменяться произвольной ин- формацией. function Get_Constraints: WordBool; safecall; Возвращает значение свойства Constraints. function Get_Data: OleVariant; safecall; Возвращает значение свойства Data. function GetMetaData: OleVariant; safecall; Возвращает метаданные, описывающие запи- си в пакете данных. function GetRecords(Count: Integer; out RecsOut: Integer): OleVariant; safecall; Передает клиенту нужное количество записей. Если Count=(-\), передаются все записи; если Count=0, передаются метаданные; если Count>0, передаются Count записей. Возвра- щаемый параметр RecsOut содержит истинное количество переданных записей. procedure Reset(MetaData: WordBool); safecall; Требует от сервера приложений переслать клиенту еще раз текущий набор данных. Если параметр обращения содержит True, требуется переслать предыдущую посылку метаданных. procedure Set_Constraints(Value: WordBool); safecall; Устанавливает значение Value в свойство Constraints. procedure SetParams(Values: OleVariant); safecall; Посылает параметры от клиентского прило- жения компоненту-источнику на сервере приложений. Интерфейс IDatalntercept В модуле sconnect определен низкоуровневый интерфейс IDatalntercept, исполь- зуемый для организации связи между клиентом и сервером приложений с помощью технологии сокетов (TCP/IP). Этот интерфейс является прямым потомком lUknow и декларирует два дополнительных метода: procedure Dataln(const Data: IDataBlock); stdcall; procedure DataOut(const Data: IDataBlock); stdcall; Первый используется для осуществления некоторых действий после получения данных (например, их расшифровка или декомпрессия), второй - перед посылкой данных (например, для их шифровки или сжатия). Оба метода в качестве параметра обращения получают ссылку на интерфейс IDataBlock, определенный в том же модуле sconnect. Интерфейс IDataBlock Свойства интерфейса IDataBlock-. Свойство Назначение property BytesReserved: Integer; Указывает, сколько байтов в свойстве Memory зарезервированы для внутреннего использова- ния. property Memory: Pointer; Содержит ссылку на пакет данных. Длина пакета определяется значением свойства Size.
488 Глава 36 property Signature: Integer; Определяет тип передаваемых данных. Значе- ние этого свойства сохраняется в первых байтах зарезервированной области свойства Memory. property Size: Integer; Определяет общую длину пакета данных. Методы интерфейса IDataBlock-. Метод Назначение procedure Clear; Очищает пакет данных, кроме зарезервиро- ванной области BytesReserved. function GetBytesReserved: Integer; Возвращает длину зарезервированной облас- ти. function GetMemory: Pointer; Возвращает указатель на свойство Memory. function GetSignature: Integer; Возвращает тип данных. function GetSize: Integer; Возвращает размер Memory. function Read(var Buffer; Count: Integer): Integer; Читает не более Count байт из Memory в буфер Buffer, начиная с байта BytesReserved+l. Count не может быть больше Size-BytesReserved. procedure Setsignature(Value: Integer); Устанавливает тип передаваемых данных. procedure Setsize(Value: Integer); Устанавливает значение свойства Size. function Write(const Buffer; Count: Integer): Integer; Копирует не более Count байт из буфера Buffer в свойство Memory и возвращает действитель- ную длину скопированного. Count не может быть больше Size-BytesReserved. Интерфейсным объектом для IDataBlock могут служить объекты- стандартного класса TDataBlocklnterpreter. Этот класс имеет следующие собственные методы: Метод Назначение constructor Create(SendDataBlock: ISendDataBlock); Создает объект класса TDataBlocklnterpreter (см. примечание). function CreateObject(Name: String): OleVariant; Создает удаленный объект СОМ класса Name на сервере приложений и возвращает ссылку на его интерфейс. Name содержит GUID клас- са. procedure InterpretData(Data: IDataBlock); Читает вызов удаленного объекта СОМ, ин- терпретирует его как IDispatch и позволяет приложению работать с этим объектом. type TVarFlag = (vfByRef, vfVariant); TVarFlags = set of TVarFlag; function Readvariant(out Flags: TVarFlags; Data: IDataBlock): OleVariant; Преобразует блок данных Data к типу вари- ант. Выходной параметр Flags указывает содержимое варианта: vfByRef- цепочка байт; vjVariant — вариантный массив. procedure WriteVariant(const Value: OleVariant; Data: IDataBlock); Копирует вариант Value в блок данных, свя- занный с интерфейсом Data так, чтобы его можно было отправить удаленному серверу.
Введение в трехзвенную архитектуру 489 Интерфейс ITransport Этот интерфейс используется для реализации транспортного уровня протокола TCP/IP. Его единственное свойство property Connected: Boolean; содержит True, если между клиентом и сервером приложений установлена сокетная связь. Методы интерфейса: Метод Назначение function GetConnected: Boolean; Возвращает значение свойства Connected. function Receive(WaitForlnput: Boolean): IDataBlock; Создает объект СОМ на сервере приложений и получает от него блок данных по протоколу TCP/IP. Если параметр WaitForlnput равен False, передача может быть ошибочной, если сервер не начнет немедленно передавать дан- ные. procedure Send(Data: IDataBlock); Посылает блок данных удаленному объекту СОМ. procedure Setconnection(Value: Boolean); Записывает значение Value в свойство Connected.
Глава 37 КОМПОНЕНТЫ DELPHI ДЛЯ ТРЕХЗВЕННОЙ АРХИТЕКТУРЫ 37.1. КОМПОНЕНТЫ ДЛЯ СОЗДАНИЯ СЕРВЕРА ПРИЛОЖЕНИЙ 37.1.1. TRemoteDataModule Этот модуль служит основой для создания серверов приложений, использующих технологию COM/DCOM или OLEnterprise. Обычно модуль создается на этапе проектирования программы. Ниже описывают- ся наиболее важные свойства, методы и события этого класса, которые могут пред- ставлять интерес при создании модуля на этапе прогона программы. Заметим, что все свои методы, свойства и события он унаследовал от родительского класса TDataMod- ule, от которого произошли также модули TMTSDataModule и TCorbaDataModule. Свойство property OldCreateOrder: Boolean; определяет, когда наступают события OnCreate и OnDestroy. Если оно содержит False (по умолчанию), OnCreate возникает после завершения работы всех дочерних конст- рукторов (вызывается методом AfterConstructiori), a OnDestroy - перед началом работы первого деструктора (вызывается методом BeforDestructiori). В версии 3 эти методы вызывались после вызова конструктора модуля и перед вызовом его деструктора. Этот порядок вызова устанавливается, если свойство содержит True. Конструктор constructor Create(AOwner: TComponent); создает экземпляр модуля во время прогона программы, если он не был создан на этапе ее конструирования. Он вызывает конструктор CreateNew и возбуждает исклю- чение в случае ошибки. Если экземпляр создан успешно, вызывает обработчик собы- тия OnCreate. Конструктор constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0) ; является основным конструктором объекта - он распределяет его в памяти и регистри- рует в глобальном объекте Screen. Параметр AOwner определяет собственника объекта (обычно - глобальный объект Application), умалчиваемый параметр Dummy не исполь- зуется в этой версии Delphi. Особенности эксперта модуля Сервер приложений, подобно серверу БД, может обслуживать одновременно не- сколько пользователей. По умолчанию для каждого запроса клиента создается свой экземпляр объекта TRemoteDataModule, что исключает конфликты в многопользова- тельской среде. С помощью эксперта модуля можно уточнить взаимодействие сервера приложений с множеством пользователей. На рис.37.1 показано окно эксперта.
Компоненты Delphi для трехзвенной архитектуры 491 Рис.37.1. Окно эксперта модуля TRemoteDataModule. С помощью списка Instancing можно выбрать один из следующих режимов насле- дования: • Internal - модуль создается как часть активной библиотеки DLL’, если выбраны другие значения, модуль представляет собой объект Автоматизации; • Single Instance - для каждого клиентского вызова создается один наследник сер- вера приложений и ему выделяется свой процесс на серверной машине; • Multiple Instance - то же, что и Single Instance, но все наследники разделяют один процесс. Если выбран режим Internal, с помощью списка Threading Model можно выбрать модель потоков команд: • Single - для DLL выделяется один поток, поэтому все запросы клиентов выпол- няются последовательно; • Apartment - каждый экземпляр объекта TRemoteDataModule обрабатывает один запрос клиента в отдельном потоке команд; экземпляры (точнее, связанные с ними данные) сохраняются автоматически, но программист должен исключить возможные конфликты, связанные с использованием потоками общей глобаль- ной памяти; • Free - один экземпляр модуля может обрабатывать несколько запросов в разных потоках команд; программист должен позаботиться о сохранности данных и ис- ключить возможные конфликты, связанные с общей памятью; • Both - то же, что Free, но вызов клиентских интерфейсов сервер приложений осуществляет последовательно (в одном потоке команд). 37.1.2. TMTSDateModule Этот модуль служит основой для создания серверов приложений, использующих технологию 'MTS. По сравнению с DCOM, MTS обеспечивает следующие дополни- тельные возможности: • управление системными ресурсами, включая процессы, потоки и соединения с базами данных; • управление транзакциями, в том числе старт, подтверждение или откат транзакции; • управление доступом к наборам данных, основанное на закреплении за НД той или иной роли; пользователь получит доступ к данным только в том случае, ко- гда его роль совпадает с ролью НД.
492 Глава 37 Компонент является наследником TRemoteDataModule и, таким образом, наследует все его свойства, методы и события. Специфичные методы компонента: Метод Назначение procedure DisableCommit; Запрещает подтверждение текущей транзак- ции вплоть до вызова метода EnableCommit или SetCommit. Если пользователь попытается подтвердить транзакцию после вызова этого метода, транзакция будет удалена. Использу- ется при явном управлении транзакциями. procedure EnableCommit; Отменяет действие метода DisableCommit. function IsCallerlnRole(const Role: WideString): WordBool; Возвращает True, если текущий пользователь имеет ту же роль, что и параметр Role. Ис- пользуется для блокировки доступа пользова- теля к части данных внутри текущего НД (доступ к самому НД блокируется автоматиче- ски, если роль пользователя не совпадает с ролью НД). function IsInTransaction: WordBool; Возвращает True, если под управлением моду- ля стартовала незавершенная транзакция. function IsSecurityEnabled: WordBool; Возвращает True, если модуль может выпол- нять функции защиты данных (эти функции блокируются, если модуль выполняется под управлением Windows 95). procedure SetAbort; Прекращает и удаляет текущую транзакцию. procedure SetComplete; Указывает, что модуль успешно завершил текущую работу и может деактивизироваться. Событие property OnActivate: TNotifyEvent; возникает в момент активизации модуля. Обработчик события должен установить все необходимые связи с БД (до активизации модуль не может иметь активных связей с БД). Событие property OnDeactivate: TNotifyEvent; возникает в момент деактивизации модуля. Его обработчик должен отключить все связи с БД. Особенности эксперта модуля Окно эксперта модуля TMTSDataModule показано на рис.37.2. В отличие от TRemoteDataModule, TMTSDataModule всегда создается в виде DLL, поэтому в окне эксперта отсутствует список Instancing, а модели потоков в списке Threading Model такие же, как и у модуля TRemoteDataModul (см. выше п.37.1.1). На- значение переключателей группы Transaction model: • Requires a transaction - каждый вызов клиентом интерфейса удаленного модуля включается в текущую транзакцию; если уже был создан контекстный объект транзакции, новая транзакция не создается;
Компоненты Delphi для трехзвенной архитектуры 493 • Requires a new transaction - каждый вызов клиентом интерфейса удаленного мо- дуля создает новую транзакцию; • Supports transactions - модуль может быть включен в транзакцию, но клиент сам поставляет ему контекстный объект транзакции; • Does not support transactions - модуль не включается в транзакцию. Рис.37.2. Окно эксперта модуля TMTSDataModule. 37.1.3. TCorbaDataModule Этот модуль служит основой для создания серверов приложений, использующих технологию CORBA. Обычно модуль создается на этапе проектирования программы. На этапе прогона программы модуль создается с помощью конструктора Create, а разрушается - с помощью деструктора Destroy. Особенности эксперта модуля Окно эксперта показано на рис.37.3. Рис.37.3. Окно эксперта модуля TCorbaDataModule. Список Instancing определяет количество создаваемых экземпляров модуля: • Instance-per-client - новый экземпляр модуля создается в ответ на любое кли- ентское соединение, установленное с сервером приложений, и разрушается по- сле отсоединения клиента;
494 Глава 37 • Shared Instance - все запросы клиентов обслуживаются единственным экземп- ляром модуля; это означает, что клиент не может работать с модулем, исполь- зуя стандартный интерфейс IProvider. Список Threading Model определяет принятую модель потоков: • Single-threaded - все экземпляры модуля используют единственный поток команд; • Multithreaded - каждый экземпляр использует свой поток. 37.1.4. TDataSetProvider Компонент TDataSetProvider служит связующим элементом между сервером приложений и клиентским набором данных. Он упаковывает данные, поставляемые связанным с ним компонентом-набором ТТаЫе, TQuery или TStoredProc в единый пакет и передает этот пакет клиентскому компоненту TClientDataSet. Последний рас- паковывает данные, сохраняет их в памяти и предоставляет пользователю. После за- вершения работы пользователя с локальной копией данных TClientDataSet упаковыва- ет все измененные данные и отправляет их компоненту TDataSetProvider для внесения необходимых изменений в соответствующий НД. Замечание. В версии Delphi 4 роль этого компонента играл BDE- ориентированный компонент TProvider. Компонент TProvider из по- ставки Delphi 5 исключен. Свойства компонентов TDataSetProvider. Свойство Назначение property Constraints: WordBool; Если содержит True, клиенту пересылается информация о наложенных на данные ограни- чениях, чтобы он смог организовать локаль- ный контроль данных. property Data: OleVariant; Содержит созданный компонентом пакет данных (свойство только для чтения). property DataSet: TDBDataSet; Содержит имя связанного с компонентом источ- ника данных (.ТТаЫе, TQuery, TStoredProc). property Exported: Boolean; Содержит True, если клиент может обращаться к провайдеру, используя интерфейс lAppServer. type TProviderOption = (poFetchBlobsOnDemand, poFetchDetailsOnDemand, poIncFieldProps, poCascadeDeletes, poCascadeUpdates, poReadOnly, poAllowMultiRecordUpdates, poDisablelnserts, poDisableEdits, poDisableDeletes, poNoReset, poAutoRefresh, poPropogateChanges, poAllowCommandText); TProviderOptions = set of TProvider- Option; property Options: TProviderOptions; Определяет, какие данные включаются в пакет и как они используются: poFetchBlobsOn- Demand - данные из полей BLOB не включа- ются в пакет; для доступа к полям клиент использует метод FetchBlobs\ poFetchDetails- OnDemand - данные из вложенных таблиц не включаются в пакет; для доступа к данным клиент использует метод FetchDetails', poIncFieldProps - в пакет включаются сле- дующие свойства полей: Alignment, DisplayLabel, DisplayWidth, Visible, Display- Format, EditFormat, MaxValue, MinValue, CurValue, EditMask, DisplayValues', poCascade-
Компоненты Delphi для трехзвенной архитектуры 495 Deletes - предписывает серверу производить каскадное удаление-записей в подчиненных таблицах, если пользователь уничтожил за- пись в главной таблице; poCascadeUpdates - предписывает серверу автоматически изме- нять записи в дочерних таблицах, если поль- зователь изменил ключевые поля в записи главной таблицы; poReadOnly - данные пре- доставляются только для чтения; poAllowMultiRecordUpdates - разрешает инди- видуальное обновление сразу нескольких записей; poDisablelnserts - запрещает вставку записей; poDisableEdits - запрещает редакти- рование записей; poDisableDeletes - запрещает удаление записей; poNoReset - игнорирует флаг наличия первой передаваемой записи в уже переданном ранее пакете; poAutoRefresh - разрешает автоматическое обновление запи- сей при их изменении; poPropogateChanges - изменения, сделанные в обработчиках After- или BeforeUpdateRecords возвращаются кли- енту в клиентском пакете; poAllowCommandText - разрешает посылать дополнительную команду в процессе выпол- нения текущей. property Resolver: TCustomResolver; Содержит ссылку на связанный с компонен- том объект класса TCustomResolver, с помо- щью которого осуществляется управление обновляемыми данными и разрешаются ошибки обновления (см. ниже примечание). property ResolveToDataSet: Boolean; Если содержит True, обновляются данные в НД, указанном в свойстве DataSet, в против- ном случае - непосредственно в серверной БД. type TUpdateMode = (upWhereAll, upWhereChanged, upWhereKeyOnly); property UpdateMode: TUpdateMode; Определяет, по каким полям ищется обнов- ляемая запись в ТБД: upWhereAll - по всем полям; upWhereChanged - по ключевым и измененным полям; upWhereKeyOnly - только по ключевым полям. Примечание. Объект класса TCustomResolver создается автоматически компонен- том TDataS'etProvider, если к моменту начала обновления данных его свойство Re- solver содержит NIL. Сам по себе класс TCustomResolver - абстрактный; в зависимости от значения свойства ResolveToDataSet компонент создает либо экземпляр его потомка TDataSetResolver для внесения изменений в компонент-источник, либо потомка TSQL- Resolver, с помощью которого изменения вносятся непосредственно в серверную БД. Ниже описываются основные свойства и методы базового класса TCustomResolver. Свойство property Provider: TBaseProvider; содержит ссылку на связанный компонент-провайдер. Свойство UpdateMode дублиру- ет аналогичное свойство компонента-провайдера.
496 Глава 37 Методы класса TCustomResolver. Метод Назначение function ApplyUpdates(const Delta: OleVariant; MaxErrors: Integer; out ErrorCount: Integer); OleVariant; virtual; Обновляет данные: Delta - измененные, вставлен- ные и удаленные записи до обновления их в ТБД; MaxErrors - максимальное количество ошибок, при котором процесс обновления прекращается; ErrorCount - действительное количество ошибок. Функция возвращает набор записей, которые по тем или иным причинам не были обновлены. procedure BeginUpdate; virtual; Вызывается в момент начала обновления и предназначен для перекрытия в специализиро- ванных потомках. procedure DoDelete(Tree: TUpdateTree); virtual; abstract; Вызывается для каждой записи в Delta, кото- рая должна быть удалена. procedure DoInsert(Tree: TUpdateTree); virtual; abstract; Вызывается для каждой вставляемой записи. procedure DoUpdate(Tree: TUpdateTree); virtual; abstract; Вызывается для каждой обновляемой записи. procedure EndUpdate; virtual; Вызывается в момент завершения обновлений. function FetchData(const Packet: OleVariant): OleVariant; virtual; Автоматически вызывается методом FetchData провайдера и возвращает требуемое BLOB-ticme или вложенную таблицу Oracle8. procedure FreeTreeData(Tree: TUpdateTree); virtual; Вызывается после завершения обновления. Предназначен для перекрытия в специализи- рованных потомках. function HandleUpdateError(Tree: TUpdateTree; E: EUpdateError; var MaxErrors, ErrorCount: Integer): Boolean; Реализует обработку ошибок, возникающих в процессе обновления: Tree - обновляемые записи; Е - класс обработки исключительной ситуации; MaxErrors - максимальное количе- ство ошибок; ErrorCount - действительное количество ошибок. Метод вызывает Initialize- ConflictBuffe, возбуждает событие OnUpdate- Error и возвращает True, если Resolver должен попытаться еще раз обновить ошибочную запись. procedure InitializeConflictBuffer(Tree: TUpdateTree); virtual; abstract; Метод размещает информацию об ошибочной записи во внутреннем буфере. Вызывается методом HandleUpdateError перед генерацией события OnUpdateError. type TResolverResponse = (rrSkip, rrAbort, rrMerge, rrApply, rrlgnore); procedure LogUpdateError(Tree: TUp- dateTree; E: EUpdateError; Response: TResolverResponse); Добавляет ошибочную запись в журнал оши- бок, который будет возвращен клиенту: Tree - обновляемые записи; Е - класс исключений; Response - результат обновления: rrSkip - запись пропущена; rrAbort - обновление пре- кращено; rrMerge - ошибочная запись замене- на исходной; rrApply - обновление прошло успешно; rrlgnore - ошибка игнорировалась.
Компоненты Delphi для трехзвенной архитектуры 497 Методы класса TDaaSetProvider: Метод Назначение function ApplyUpdates(Delta: OleVarant; MaxErrors: Integer; out ErrorCount: Integer); OleVariant; override; Обновляет данные: вызывает аналогичный метод ассоциированного объекта Resolver и возвращает его результат. procedure DoBeforeExecute(const CommandText: WideStijing, var Params, OwnerData: OleVariant); override; Создает событие OnBeforeExecute, в котором программист может изменить ££Д,-предложе- ние или имя хранимой процедуры и, если в опциях имеется флаг poAllowCommandText, выполнить вновь сформированную команду перед выполнением основной. function DataRequest(Input: OleVariant): OleVariant; virtual; Создает событие OnDataRequest. procedure Execute(const CommandText: WideString, var Params, OwnerData: OleVariant); Выполняет запрос, хранимую процедуру или произвольное SgL-предложкение. function FetchData(const Packet: OleVariant): OleVariant; override; Считывает BLOB-поле или дочерний НД: вызывает аналогичный метод ассоциирован- ного объекта Resolver и возвращает его ре- зультат. function GetMetaData: OleVariant; Считывает метаданные. Аналог вызова GetRecords с Count=0. function GetRecords(Count: Integer; out RecsOut: Integer); OleVariant; override; Считывает нужную информацию из ассоции- рованного компонента DataSet. Если Count = (-1), считываются все записи; если Count=0, считываются метаданные; если Count>0, счи- тывается Count записей. RecsOut - действи- тельное количество прочитанных записей. procedure Reset (MetaData: WordBool),- override; Повторно считывает предыдущий пакет запи- сей. Если MetaData=True, повторно считыва- ются метаданные. procedure SetParams(Values: OleVariant); override; Пересылает параметры компонентам- источникам. События компонента TDataSetProvider: Событие С чем связано type TAfterUpdateRecordEvent = pro- cedure (Sender : TObject; SourceDS: TDapaSet; DeltaDS: TClientDataSet; UpdateKind: TUpdateKind) of object; property AfterUpdateRecord: TAfterUp- dateRecordEvent; Возникает сразу после успешного обновления записей: Sender - провайдер, осуществивший обновление; SourceDS - исходный НД; DeltaDS - клиентский набор данных, инициировавший обновление; UpdateKind - характер обновления: запись обновлена (ukModify), вставлена (uklnsert) или уничтожена (ukDelete). type TBeforeUpdateRecordEvent = pro- cedure (Sender : TObject; SourceDS: TDataSet; DeltaDS: TClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean) of object; Возникает перед обновлением записи. В пара- метре Applied обработчик должен вернуть True, если к записям SourceDS можно приме- нить обновление. Назначение остальных па-
498 Глава 37 property BeforeUpdateRecord: TBefore- UpdateRecordEvent; раметров такое же, как в событии AfterUpdateRecord. type TDataRequestEvent = function(Sender: TObject; Input : OleVariant) : OleVariant of object; Возникает в ответ на вызов метода DataRequest. type TProviderDataEvent = proce- dure (Sender : TObject; DataSet: TCli- entDataSet) of object; property OnGetData: TProviderDa- taEvent; Возникает после того, как провайдер получил данные, но до того, как он перешлет их клиен- ту- type TGetDSProps = procedure(Sender: TObject; DataSet: TDataSet; out Prop- erties: OleVariant) of object; property OnGetDataSetProperties: TGetDSProps; Возникает в ходе выполнения метода GetOptionalParam клиентского НД и позволяет провайдеру передать этому компоненту произ- вольную информацию (см. ниже примечание). property OnUpdateData: TProviderDataEvent; Возникает в момент начала процедуры обнов- ления данных. С помощью параметра DataSet программа может проверить и/или зашифро- вать данные перед их посылкой в сервер БД. type TResolverErrorEvent = procedure (Sender: TObject; DataSet: TClient- DataSet; E: EUpdateError; UpdateKind: TUpdateKind; var Response: TResolverResponse) of object; property OnUpdateError: TResolverErrorEvent; Возникает при обнаружении ошибки в ходе выполнения обновления данных. С помощью параметра DataSet обработчик может получить доступ к значениям OldValue, NewValue и Cur- Value полей, чтобы скорректировать ошибку. В параметре Response обработчик может вернуть rrApply, если он устранил ошибку. Примечание. В обработчике события OnGetDataSetProperties программист полу- чает информацию о провайдере (Sender) и связанном с ним компоненте-источнике (DataSet). В выходном параметре Properties он может указать клиентскому НД ин- формацию в виде вариантного массива, элементами которого служат триады из имени параметра, его значения и логического признака, указывающего, нужно ли пересылать обратно провайдеру информацию по данному параметру. В качестве имен параметров не могут использоваться следующие имена, зарезервированные для внутренних нужд компонента: UNIQUE_KEY DEFAULT_ORDER CHANGE_LOG SERVER_COL CONSTRAINTS DATASET_CONTEXT DATASET_DELTA LCID BDERECORD_X TABLE_NAME MD_FIELDLINKS UPDATEMODE 37.2. КОМПОНЕНТЫ ДЛЯ СОЗДАНИЯ КЛИЕНТСКОГО ПРИЛОЖЕНИЯ 37.2.1. TClientDataSet Компонент TClientDataSet tlsl обеспечивает доступ к данным, но, в отличие от TDataSet, не использует BDE. Источниками данных для этих компонентов могут слу- жить обычные файлы, расположенные в локальных или сетевых каталогах, или копии данных, полученные из других НД. Однако главное назначение компонентов - слу- жить представителем удаленных данных, поставляемых им компонентами TProvider или компонентами-источниками, расположенными на сервере приложений.
Компоненты Delphi для трехзвенной архитектуры 499 Свойства компонента: Свойство Назначение property Active: Boolean; Открывает/закрывает набор данных. property ActiveAggs[Index: Integer]: TList; Указывает, каким образом осуществляется агрегирование значений: по всем данным (индекс 0), по первому групповому уровню (индекс 1) и т.д. Элементами ActiveAggs явля- ются списки возможных значений свойства Aggregate.?. Если какой либо элемент содержит NIL, агрегация на данном уровне отсутствует. property Aggregates: TAggregates; Содержит наборы объектов класса TAggregate, каждый из которых определяет формулу, по которой осуществляется вычисление агрегат- ного значения. property AggregatesActive: Boolean; Разрешает/запрещает агрегацию данных. property CanModify: Boolean; Содержит True, если данные можно изменять. property ChangeCount: Integer; Содержит количество изменений, сделанных в текущем НД. property Data: OleVariant; Содержит данные, полученные от провайдера. property DataSetField: TDataSetField; Открывает доступ к вложенным таблицам Oracle8. property DataSize: Integer; Определяет текущую длину данных в байтах. property DataSource: TDataSource; В дочерней таблице указывает имя главного НД (вариант только-для-чтения свойства MasterSource). property Delta: OleVariant; Содержит обновляемые записи. property FetchOnDemand: Boolean; Если содержит True, данные из BLOB-полей и вложенных таблиц Oracle8 не включаются в пакет данных. property Filter: String; Аналог свойства TDataSet.Filter (см. гл.6). property Filtered: Boolean; Аналог свойства TDataSet.Filtered (см. гл.6). property FilterOptions: TFilterOptions; Аналог свойства TDataSet.FilterOptions (см. гл.6). function GetOptionalParam(ParamName: String): OleVariant; Создает событие OnGetDataSetProperties лля соответствующего провайдера и возвращает значение параметра с именем ParamName. property GroupingLevel: Integer; Указывает количество уровней, на которых агрегируются данные. property HasProvider: Boolean; Если содержит True, компонент может взаи- модействовать с удаленным провайдером, используя интерфейс IProvider. property IndexDefs: TIndexDefs; Содержит информацию об индексах ТБД. property IndexFieldCount: Integer; Содержит количество полей в индексе. property IndexFieldNames: String; Содержит имена индексных полей, разделен- ных точками с запятыми. property IndexFields[Index: Integer]: TField; Открывает доступ к индексным полям. property IndexName: String; Содержит имя текущего индекса.
500 Глава 37 property KeyExclusive: Boolean; Аналог свойства TTable.KeyExclusive (см. гл.7). property KeyFieldCount: Integer; Содержит количество используемых для по- иска индексных полей в сложном индексе. property KeySize: Word; Содержит длину в байтах текущего ключа. property LogChanges: Boolean; Содержит True, если ведется протокол изме- нений в локальном наборе данных. property MasterFields: String; Аналог одноименного свойства ТТаЫе (гл.7). property Mastersource: TDataSource; Аналог одноименного свойства ТТаЫе (гл.7). property PacketRecords: Integer; Содержит максимальное количество записей в одном пакете данных. : property Params: TParams; Аналог одноименного свойства TQuery (гл.8). property Provider: IProvider; Содержит ссылку на интерфейс провайдера. property ProviderName: String; Содержит имя провайдера. property Readonly: Boolean; Содержит True, если данные нельзя изменять. property RecNo: Integer; Аналог одноименного свойства ТТаЫе (гл.7). property RecordCount: Integer; Содержит количество записей в локальном НД. property RemoteServer: TCustomRemote- Server; Содержит ссылку на удаленный сервер при- ложений. property SavePoint: Integer; Указывает позицию изменения в протоколе изменений (см. примечание). type TUpdateStatus = (usUnmodified, usModified, uslnserted, usDeleted); TUpdateStatusSet = set of TUpdateSta- tus; property StatusFilter: TUpdateStatusSet; Определяет записи, которые будут участво- вать в процедуре обновления: usUnmodified - это значение игнорируется; usModified - изме- ненные записи; uslnserted - вставленные запи- си; usDeleted - удаленные записи. property StoreDefs: Boolean; Если содержит True, определения таблиц и полей локального НД сохраняются в потоке при сохранении формы, на которой располо- жен НД. Примечание. С помощью свойства SavePoint можно легко восстановить НД после ошибочных изменений. Для этого нужно перед изменениями запомнить текущее зна- чение свойства, а для отмены изменений - поместить в него сохраненное значение. Например, пусть поле Age (возраст) НД не должно быть меньше 0 и больше 150. Тогда можно использовать такой фрагмент программы: var BeforeEditPos: Integer; procedure TForml.ClientDataSetlBeforeEdit(DataSet: TDataSet); begin // Перед началом изменений запоминаем позицию изменений // в глобальной переменной BeforeEditPos: BeforeEditPos := ClientDataSetl.SavePoint; end; procedure *TForml.ClientDataSetlAfterEdit(DataSet: TDataSet); var Age: Real; begin Age := DataSet['Age'].AsFloat; if (Age<0) or (Age>150) then
Компоненты Delphi для трехзвенной архитектуры 501 begin ShowMessage('Указан неверный возраст'); ClientDataSetl.SavePoint := BeforeEditPos end end; Методы TClientDataSet: Метод Назначение procedure Addindex(const Name, Fields: String; Options: TIndexOp- tions; const DescFields: String = ' const CaselnsFields: String = const GroupingLevel: Integer = 0 ); Создает новый индекс: Name - имя индекса; Fields - список индексных полей; Options - может включать 0,1 или 2 флага ixDescending (сортировка по убыванию) и ixCaselnsensitve (сортировка с учетом высоты букв); остальные значения типа TIndexOptions вызовут исключе- ние; DescFields - список полей, сортируемых по убыванию; игнорируется, если установлен флаг ixDescending; CaselnsFields - список по- лей, сортируемых с учетом высоты букв; игно- рируется, если установлен флаг ixCaselnsensitve; GroupingLevel - умалчиваемый уровень вложенности индекса. procedure AppendData(const Data: OleVariant; HitEOF: Boolean); Добавляет данные Data к существующему локальному НД. Вместо этого метода обычно используется GetNextPacket. procedure ApplyRartge; Включает механизм фильтрации записей по ключевым полям function ApplyUpdates(MaxErrors: Integer); Integer; Обновляет записи в удаленной БД: MaxErrors - максимально допустимое количество ошибок. Возвращает истинное количество ошибок. function BookmarkValid(Bookmark: TBookmark): Boolean; Возвращает True, если закладка Bookmark содержит правильную ссылку. procedure Cancel; override; Откатывает все неподтвержденные изменения текущей записи. procedure CancelRange; Отменяет режим фильтрации по ключевым полям. procedure CancelUpdates; Отменяет все неподтвержденные изменения из пакета Delta. procedure CloneCursor(Source: TCli- entDataSet; Reset: Boolean; KeepSet- tings: Boolean = False); Позволяет разделять данные двумя и более компонентами (см. ниже примечание 1). function CompareBookmarks(Bookmarkl, Bookmark2: TBookmark): Integer; Сравнивает две закладки и возвращает -1,0 или +1, если первая закладка соответственно меньше, равна или больше второй. function ConstraintsDisabled: Boolean; Разрешает или запрещает компоненту учиты- вать ограничения на значения полей. function CreateBlobStream(Field: TField; Mode: TBlobStreamMode): TStream; override; Создает поток для BLOB-иопя: Field - имя поля; Mode - режим потока: для чтения (bmRead), записи (bmWrite) или обновления (bmReadWrite) данных. procedure CreateDataSet; Создает пустой набор данных с умалчиваемы- ми параметрами.
502 Глава 37 procedure Deletelndex(const Name: String); Уничтожает индекс с именем Name. procedure DisableConstraints; Временно прекращает действие ограничений, наложенных на значения полей. procedure EditKey; Переводит НД в режим dsEditKey и сохраняет поисковый буфер. Этот метод полезно вызы- вать для комплексного индекса, когда между двумя поисками меняются лишь некоторые поля (см. примечание 2). procedure EditRangeEnd; Переводит НД в режим редактирования ниж- ней границы диапазона фильтрации. procedure EditRangeStart; Переводит НД в режим редактирования верх- ней границы диапазона фильтрации. procedure EmptyDataSet; Очищает буфер от всех записей. procedure EnableConstraints; Отменяет режим, установленный методом DisableConstraints. procedure FetchBlobs; Получает от сервера приложений текущее BLOB-поле. procedure FetchDetails; Получает от сервера приложений вложенную таблицу Oracle8. function FetchParams; Получает от сервера приложений текущие параметры TQuery или TStoredProc. function FindKey(const KeyValues: array of const): Boolean; Аналог одноименного метода ТТаЫе (см. при- мечание 2). procedure FindNearest(const KeyValues: array of const); Аналог одноименного метода ТТаЫе (см. при- мечание 2). function GetCurrentRecord(Buffer: PChar): Boolean; override; Копирует текущую запись в буфер Buffer. function GetFieldData(Field: TField; Buffer: Pointer): Boolean; override; Копирует поле Field текущей записи в буфер Buffer. type TGroupPosInd = (gbFirst, gbMid- dle, gbLast); TGroupPosInds = set of TGroupPosInd; function GetGroupState(Level: Inte- ger) : TGroupPosInds; Возвращает положение текущей записи в группе агрегирования Level'. gbFirst - первая в группе минимум из двух записей; gbMiddle - ни первая, ни последняя; gbLast - последняя в группе минимум из двух записей. procedure Getlndexlnfo(IndexName: String); Возвращает информацию о индексе с именем IndexName. function GetNextPacket: Integer; Требует от сервера приложений передать сле- дующий пакет записей. Длина пакета опреде- ляется свойством PacketRecords. Функция возвращает количество записей, добавленных в свойство Data. function GetOptionalParam(const ParamName: String): OleVariant; Возвращает из пакета данных пользователь- скую информацию с именем ParamName, кото- рая помещена туда методом SetOptionalParam. procedure GotoCurrent(DataSet: TClientDataset) ; Синхронизирует курсор текущего набора с курсором набора DataSet. function GotoKey: Boolean; Ищет запись, определенную предварительным вызовом SetKey или EditKey с последующим вызовом FieldByName и возвращает True, если запись найдена (см. примечание 2).
Компоненты Delphi для трехзвенной архитектуры 503 procedure GotoNearest; Ищет запись либо точно соответствующую поисковому значению, либо следующую за ней в порядке индекса. Поисковое значение задает- ся SetKey или EditKey с последующим вызовом FieldByName (см. примечание 2). procedure LoadFromFile(const FileName: String = ''); Загружает набор данных из файла FileName. Совместно с SaveToFile jsaert возможность отложенного обновления данных. procedure LoadFromStream(Stream: TStream); Загружает набор данных из потока Stream. typeTLocateOption - (loCaselnsensi- tive, loPartialKey); TLocateOptions = set of TLocateOption; function Locate(const KeyFields: String; const KeyValues: Variant; Options: TLocateOptions): Boolean; override; Ищет запись по значениям KeyValues полей KeyFields и возвращает True в случае успеха. Option уточняет условия поиска: loCaselnsensi- tive - игнорировать разницу в высоте букв; loPartialKey - искать по части ключа. function Lookup(const KeyFields: String; const KeyValues: Variant; const ResultFields: String): Variant; override; Ищет запись по значениям KeyValues полей KeyFields и возвращает значения ее полей ResultFields в случае успеха. procedure MergeChangeLog; Объединяет данные из протокола изменений с реальными данными с помощью вызова ApplayUpdates. procedure Post; override; Подтверждает изменения в текущей записи. function Reconcile(const Results: OleVariant): Boolean; Вызывается методом ApplayUpdates провайде- ра в случае возникновения ошибки: Results - пакет ошибочных записей. Метод возбуждает событие OnReconcileError для каждой записи пакета Results (см. примечание 3). procedure RefreshRecord; Обновляет текущую запись, считывая ее у провайдера. Протокол изменений записи не меняется. procedure RevertRecord; Восстанавливает текущую запись по протоколу ее изменений. procedure SaveToFile(const FileName: String = ’1); Сохраняет НД в файле FileName. procedure SendParams; Посылает серверу приложений свойство Par- ams для установки параметров TQuery или TStoredProc. procedure SetAltRecBuffers(Old, New, Cur: PChar); Устанавливает новые значения в поля Old- Value, NewValue и CurValue текущей записи. procedure SetKey; Переводит НД в режим dsSetKey для установки нового поискового значения, которое задается следующим вызовом FieldByName (см. приме- чание 2). procedure SetOptionalParam(const ParamName: String; const Value: OleVariant; IncludelnDelta: Boolean = False); Помещает в пакет данных пользовательскую информацию: ParamName - имя данных; Value - данные; IncludelnDelta содержит True, если данные помещаются в пакет Delta. procedure SetRange(const Startvalues, Endvalues: array of const); Устанавливает диапазон поиска по ключам (см. примечание 2).
504 Глава 37 procedure SetRangeEnd; Устанавливает нижнюю границу диапазона поиска (см. примечание 2). procedure SetRangeStart; Устанавливает верхнюю границу диапазона поиска (см. примечание 2). function UndoLastChange(FollowChange: Boolean): Boolean; Отменяет последнюю операцию изменения, удаления или вставки записи. Если FollowChange=True, после отмены текущей становится восстановленная запись. function UpdateStatus: TUpdateStatus; override; Возвращает статус обновления текущей запи- си. Примечание 1. С помощью метода CloneCursor компонент переводится в режим совместного владения данными с другим компонентом, ссылку на который содержит параметр Source. Параметры Reset и KeepSettings определяют, как будут использовать- ся свойства Filter, Filtered, FilterOptions, OnFilterRecord; IndexName; MasterSource, MasterFields; Readonly; RemoteServer, ProviderName, Provider, влияющие на совместное владение данными: если оба параметра имеют значение False, перечисленные свойства у текущего компонента будут такими же, как у компо- нента Source', если Reset=True, свойства очищаются, а если Reset=False и KeepSet- tings-True, сохраняют значения, какие они имели до обращения к методу. Поскольку оба компонента владеют одной и той жё копией данных, изменения, сделанные в ней одним компонентом, тут же становятся видны другому компоненту. Примечание 2. Подробнее об индексном поиске с помощью методов SetKey, Edit- Key, SetRange и т.п. см. п.7.4. Примечание 3. С компонентом связано специфичное событие type TReconcileAction = (raSkip, raAbort, raMerge, raCorrect, raCancel, raRefresh); TReconcileErrorEvent = procedure(DataSet: TClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction) of object; property OnReconcileError: TReconcileErrorEvent; которое возбуждается методом Reconcile для каждой ошибочной записи: DataSet - клиентский НД, содержащий ошибочные записи (текущая запись этого набора и явля- ется ошибочной); Е - объект исключения; UpdateKind - характер обновления ошибоч- ной записи. В параметре Action обработчик должен вернуть одно из следующих значе- ний: raSkip - пропустить ошибочную запись; raAbort - прекратить режим корректиров- ки; raMerge - слить ошибочную запись с исходной; raCorrect - запись была скорректи- рована в обработчике; raCancel - восстановить запись по протоколу изменений; raRefresh - восстановить запись, считав ее из БД.
Компоненты Delphi для трехзвенной архитектуры 505 Обработчик может использовать значения OldValue, NewValue и CurValue для каж- дого поля записи, но он не должен изменять текущее положение курсора НД. 37.2.2. TDCOMConnection |d£om| Компонент TDCOMConnection filtbh реализует связь DCOM с удаленным сервером приложений. Он устанавливает с ним соединение, регистрирует на сервере клиентские наборы данных TClientDataSet и дает им возможность обращаться к серверным ком- понентам TDataSetProvider с использованием интерфейса IProvider. Свойство компонента property ComputerName: String; содержит сетевое имя удаленной машины, на которой расположен сервер приложе- ний. Если это свойство пустое, сервер приложений располагается на машине клиента или зарегистрирован в его реестре Windows 32. Событие type TGetUsernameEvent = procedure(Sender: TObject; var Username: String) of object; property OnGetUsername: TGetUsernameEvent; возникает перед регистрацией клиентской машины на удаленном сервере. Его обра- ботчик должен вернуть регистрационное имя пользователя в параметре Username. Событие не возникает, если параметр LoginPrompt связного компонента TDatabase содержит False. Событие type TLoginEvent = procedure(Sender: TObject; Username, Password: String) of object; property OnLogin: TLoginEvent; возникает после установления связи с удаленным сервером с помощью установки в свойство ServerName имени сервера или его GUID. 37.2.3. TCorbaConnection IorbC Компонент TCorbaConnection ЯсЭ обслуживает связь с удаленным сервером при- ложений в рамках технологии CORBA. Свойства компонента: Свойство Назначение property AppServer: Variant; Предоставляет клиентским наборам данных воз- можность прямого обращения к методам динами- чески вызываемого интерфейса DII (Dynamic Invocation Interface), который поддерживается в технологии CORBA (см. примечание 1). property Cancelable: Boolean default False; Определяет реакцию клиентского приложения на задержку в установлении связи: если со-
506 Глава 37 держит True и если возникла более чем се- кундная задержка в установлении связи, воз- никает событие OnCancel (см. примечание 2). property HostName: String; Содержит имя или /P-адрес машины сервера приложений. Если свойство пусто, связь будет устанавливаться с первым сервером, чей ин- терфейс находится в репозитории интерфейсов. property ObjectName: String; Содержит имя объекта, на чей интерфейс ссылается свойство Repositoryld. Если есть единственный объект с указанным интерфей- сом, это свойство может быть пустым. type TRepositoryld = type String; property Repositoryld: TRepositoryld; Указывает идентификатор интерфейса СОЛВЛ-фабрики сервера приложений (см. примечание 3). Примечание 1. С помощью свойства AppServer клиентское приложение может об- ращаться к методам интерфейса DII. Например: CorbaConnactionl.AppServer.MyMethod; Чтобы повысить эффективность кода, используйте раннее (статическое) связывание с помощью оператора приведения типов as: with IUnknown(CorbaConnectionl.AppServer) as IMyCorbaDataModule do MyMethod; Примечание 2. Единственное специфичное событие компонента type TCancelEvent = procedure(Sender: TObject; var Cancel: Boolean; var DialogMessage: String) of object; property OnCancel: TCancelEvent; возникает, если свойство Cancelable=True и если в течение секунды модуль не может установить связь с сервером приложений. В параметрах Cancel и DialogMessage обра- ботчик возвращает результат следующим образом: • если Cancel=False и DialogMessageo'', это сообщение появляется в диалого- вом окне вместе с кнопкой Cancel-, пользователь должен закрыть диалог, нажав кнопку; в этом случае будет возбуждено исключение и попытки связи прекратятся; • если Cancel=False и DialogMessage=' ', окно не появляется и модуль ожи- дает установления связи еще секунду, после чего будет вновь возбуждено собы- тие OnCancel-, • если Cancel=True, будет возбуждена исключительная ситуация и попытки свя- зи будут прекращены немедленно. Примечание 3. Свойство Repositoryld должно быть определено до установления связи с сервером приложений. Его можно задать либо указав идентификатор CORBA- фабрики: CorbaConnectionl.Repositoryld := 'IDL:MyProject/MyCorbaDataModuleFactory:1.0'; либо идентификатор СОЛЙЛ-сервера: CorbaConnectionl.Repositoryld := 'MyProject/MyCorbaDataModule';
Компоненты Delphi для трехзвенной архитектуры 507 Сервер может быть предварительно определен с помощью свойств HostName и ObjectName 37.2.4. TSocketConnection Компонент TSocketConnection устанавливает связь между клиентом и серве- ром с помощью протокола TCP/IP на основе низкоуровневых ЛРЛфункций Windows, называемых сокетами. Такое соединение отличается максимальной скоростью обмена данными, но заставляет программиста приложить дополнительные усилия для органи- зации связи и защиты данных. Свойства компонента: Свойство Назначение property Address: String; Определяет /P-адрес машины сервера прило- жений. Если оно установлено, свойство Host очищается. property Host: String; Определяет сетевое имя машины сервера приложений. Если оно установлено, свойство Address очищается. property InterceptGUID: String; Указывает GUID объекта СОМ, который реа- лизует взаимодействие с сокетным соединени- ем по интерфейсу IDatalntercept. property Port: Integer; Определяет порт для связи с удаленной маши- ной. По умолчанию это порт 211, используе- мый в поставляемых с Delphi диспетчерских программах ScktSrvr.exe и ScktSrvc.exe. Методы компонента: Метод Назначение function CreateTransport: ITransport; virtual; Создает интерфейсный объект для доступа к транспортному интерфейсу ITransport и воз- вращает ссылку на его интерфейс. procedure DoConnect; override; Устанавливает сокетное соединение с удален- ной машиной сервера приложений. События компонента: Событие С чем связано type TGetUsernameEvent = procedure Sender: TObject; var Username: String) of object; property OnGetUsername: TGetUsernameEvent; Возникает перед новым соединением, если свойство LoginPrompt содержит True. В пара- метре Username обработчик должен ввести регистрационное имя пользователя. type TIipginEvent = procedure (Sender: TObject; Username, Password: String) of object; property OnLogln: TLoginEvent; Возникает сразу после установления связи.
508 Глава 37 37.2.5. TSimpleObj ectBroker Этот компонент предоставляет клиенту список всех доступных в сети серверов приложений. Используется в архитектуре с множеством связных компонентов, чтобы приблизительно сбалансировать загрузку серверов приложений. Это достигается уста- новкой значения True в его свойстро property LoadBalanced: Boolean; Собственно список серверов хранится в свойстве property Servers: TServerCollection; компонента. 37.2.6. TWebConnection Устанавливает и поддерживает связь с сервером приложений по протоколу HTTP. В этом случае предполагается, что сам сервер реализован средствами ActivX Page и экспо- нируется имеющимся в сети ИИьсервером. При этом на клиентской стороне должна быть запущена библиотека Wini.net.dll, а на машине сервера - HTTPsrvr.dll. Первая поставляется с Internet Explorer версий 2 и выше и, следовательно, находится на любой машине с Windows 98 (обычно располагается в папке C:\windows\system) Вторая входит в по- ставку Delphi (располагается в каталоге bin каталога размещения Delphi Свойство Назначение property Agent: String; Содержит строку для специального User-Agent | заголовка, содержащего имя вашего приложения. property AppServer: Variant; Содержит ссылку на интерфейс сервера при- ложений. property Connected: Boolean; Содержит True, если установлена связь с сер- вером. property DataSetCount: Integer; Содержит количество активных НД, связан- ных с компонентом и перечисленных в свой- стве DataSets. property DataSets[Index: Integer]: TDataSet; Открывает индексированный доступ к любому связанному с компонентом активному НД. property LoginPrompt: Boolean; Содержит True, если при установлении связи требуется диалоговое определение регистра- ционного имени и пароля пользователя. property ObjectBroker: TCustomObjectBroker; Содержит список доступных серверов в сети. property Password: String; Содержит пароль пользователя. property Proxy: String; Содержит список всех ргоху-серверов. property ProxyByPass: String; Содержит список серверов, к которым компо- нент будет обращаться непосредственно, даже если соответствующий сервер указан в свой- стве Proxy. property ServerGUID: String; Содержит GUID сервера приложений. property ServerName: String; Содержит имя сервера приложений. property UserName: String; Содержит имя пользователя.
Глава 38 ОБЩИЕ ВОПРОСЫ СОЗДАНИЯ ТРЕХЗВЕННЫХ ПРИЛОЖЕНИЙ 38.1. СОЗДАНИЕ СЕРВЕРА ПРИЛОЖЕНИЙ Для создания сервера приложений следует сначала определиться, какую именно технологию удаленного доступа вы собираетесь использовать. Ниже еще раз кратко перечисляются характерные черты технологий. DCOM Эта технология является развитием базовой технологии СОМ корпорации Microsoft. Роль СОМ трудно переоценить: на нее опираются остальные технологии Windows (рис.38.1). Соединение СОМ с протоколом RPC (Remote Procedure Call - вызов удаленной процедуры) и определяет DCOM. Безусловным достоинством DCOM является ее органичное единство с Windows и простота реализации, т.к. основные инструменты этой технологии.представляют собой неотъемлемые части этой ОС. Поскольку на сегодня Windows 32 - самая распростра- ненная ОС для персональных компьютеров, DCOM и базирующаяся на ней MTS в
510 Глава 38 ближайшие годы будут, судя по всему, наиболее популярными технологиями удален- ного доступа. К недостаткам DCOM можно отнести, во-первых, требование, чтобы серверная машина работала под управлением относительно дорогостоящей ОС Windows NT Server, и, во-вторых, полное отсутствие в ней средств, специфичных для многопользо- вательского доступа (координация работы нескольких серверов приложений, управле- ние транзакциями). Эти недостатки и стали причиной появления двух других техноло- гий - OLEnterprise и MTS. OLEnterprise Технология OLEnterprise использует тот же механизм СОМ и RPC, что и DCOM. Ее отличительная особенность - возможность работы под управлением Windows 95 и введение специального сетевого каталога - брокера объектов, осуществляющего функции регистрации серверов приложений, равномерной их загрузки и переключе- ния клиентских вызовов на работоспособный сервер в случае отказа сервера, с кото- рым работал клиент. Эта технология разработана Borland и поставлялась вместе с Delphi 4, однако в поставку Delphi 5 она не включена, т.к. фактически не имеет пре- имуществ перед MTS. MTS Технология MTS может реализовать удаленный доступ с помощью DCOM, OLEn- terprise или сокетов: она не является технологией удаленного доступа как таковой, но служит весьма полезным инструментом управления транзакциями, т.е. ориентирована на поддержку трехзвенной архитектуру. Дополнительной особенностью MTS является разграничение прав доступа к данным на основе ролей, приписываемых как пользова- телю, так и данным. Технология MTS является собственностью Microsoft. Она не вхо- дит в состав поставки Delphi и приобретается отдельно. CORBA Хотя CORBA, как и DCOM, использует механизм интерфейсов для удаленного дос- тупа, она реализуется на принципах, отличных от СОМ, что позволяет ей работать в смешанных (гетерогенных) системах, т.е. в сети, где одновременно обслуживаются машины разного типа, работающие под управлением разных ОС (например, сервер на «большой конторской» машине (мейн-фрейме), работающей под управлением ОС UNIX или VMS, и клиентские места на 7ВЛ/-совместимых ПК, работающих под управ- лением Windows 32, Linux или OS/2). CORBA, как и OLEnterprise, имеет гибкие средст- ва управления загрузкой нескольких серверов приложений и переключения клиентов на работоспособные серверы, а также возможность дополнительной защиты данных с помощью современных криптографических средств. Вариант реализации CORBA ком- панией Visigen' (так называемый VisiBroker) включен в комплект поставки Delphi. 38.1.1. Последовательность создания сервера приложений Сервер приложений реализуется следующим образом. 1. В зависимости от выбранной технологии удаленного доступа в репозитории Delphi на странице Multitier выбирается нужный модуль TXXXDataModule-. ' В мае 1998 г. Borland и Visigen объединились в рамках одной корпорации - Inprise.
Общие вопросы создания трехзвенных приложений 511 • TRemoteDataModule - для создания серверов СОМ Автоматизации и связи с помощью DCOM, OLEnterprise или сокетов; • TMTSDataModule - для создания Активных библиотек СОМ, связи с помо- щью DCOM, OLEnterprise или сокетов и использования механизма управле- ния транзакциями; • TCORBADataModule - для реализации технологии CORBA. После выбора модуля начинает работать соответствующий эксперт. Особенно- сти настройки экспертов модулей TXXXDataModule рассматриваются в п.37.1. 2. В окне модуля размещаются необходимые компоненты-наборы (ТТаЫе, TQuery, TStoredProc) и компоненты для организации связи с серверной БД (TSession, TDataBase). 3. Для каждого компонента-источника, к которому будет иметь доступ клиент, в окне модуля размещается компонент-провайдер TDataSetProvider. 4. Если вы хотите, чтобы сервер приложений выступал в роли брокера ограничений, т.е. не пропускал на сервер БД заведомо неправильные данные, создайте для ком- понентов-наборов объекты-поля и экспортируйте в них словарь базы данных. На- пишите необходимые обработчики событий, возникающих в размещенных в мо- дуле компонентах, для реализации бизнес-правил, проверки ограничений и защи- ты данных. В некоторых случаях вам, возможно, понадобится расширить интер- фейс модуля собственными методами и событиями. 5. Сохраните и откомпилируйте сервер приложений, после чего зарегистрируйте его или проинсталлируйте: • если для связи используется технология DCOM или сокеты, сервер реализу- ется в виде объекта Автоматизации СОМ и должен регистрироваться в рее- стре машины, на которой он размещен; • если используется технология MTS, сервер реализуется в виде Активной библиотеки DLL и не регистрируется, но инсталлируется в качестве MTS- объекта; • в случае CORBA необходимо инсталлировать сервер в репозитории интер- фейсов и зарегистрировать его в активном каталоге Smart Agent; этот каталог создается и поддерживается запущенной в корпоративной сети утилитой OSAGENT.EXE, входящей в поставляемый вместе с Delphi инструментарий VisiBroker. 6. Если не используется DCOM, для нормального функционирования сервера при- ложений перед его активизацией должны быть запущены вспомогательные ути- литы: • в случае сокетов - утилита scktsrvr . ехе; • в случае CORBA - Smart Agent (OSAGENT. EXE) и О AD (OAD. EXE) 38.1.2. Управление данными С помощью компонентов-провайдеров можно управлять информацией, передавае- мой клиенту в пакете данных. В том числе: • указывать поля, которые будут появляться в наборе данных; • изменять свойства пакета с помощью установки нужных флагов в свойство Op- tions; • передавать клиенту дополнительную информацию.
512 Глава 38 Указание полей Чтобы провайдер включил в передаваемый пакет только значения нужных полей, следует в модуле сервера приложений создать объекты-поля (см. п.5.2). Если для ком- понента-источника не созданы объекты-поля, все поля источника будут передаваться клиенту. Наоборот, если создан хотя бы один объект-поле, все поля, для которых та- кие объекты не созданы, будут недоступны клиентскому приложению. Следует помнить, что в состав объектов-полей нужно включать поля, которые од- нозначно определяют запись. Если, например, для списка сотрудников создан единст- венный объект поля «ФИО», может оказаться так, что два сотрудника будут иметь одинаковые фамилию и инициалы. В этом случае обновление такого НД станет невоз- можно. Для однозначного определения записи в НД следует включить соответствую- щее ключевое поле, например, табельный номер сотрудника. Если такого рода допол- нительные поля не должны отображаться в клиентском наборе, для них следует уста- новить флаг pfHidden в свойстве ProviderFlags объекта-поля TField. Управление свойствами пакета Управление свойствами пакета реализуется установкой нужных флагов в значение свойства Options компонента TDataSetProvider. • poFetchBlobsOnDemand - данные из полей BLOB не включаются в пакет; для доступа к таким полям клиент использует метод FetchBlobs\ • poFetchDetailsOnDemand - данные из вложенных таблиц не включаются в па- кет; для доступа к данным клиент использует метод FetchDetails (см. п.38.3); • poIncFieldProps - в пакет включаются следующие свойства полей: Alignment, DisplayLabel, DisplayWidth, Visible, Display Format, EditFormat, MaxValue, MinValue, CurValue, EditMask, DisplayValues (см. n.5.1.1); • poCascadeDeletes - предписывает серверу производить каскадное удаление за- писей в подчиненных таблицах, если пользователь уничтожил запись в главной таблице; • poCascadeUpdafes - предписывает серверу автоматически изменять записи в подчиненных таблицах, если пользователь изменил ключевые поля в записи главной таблицы; • poReadOnly - данные предоставляются только для чтения. Включение в пакет дополнительной информации Включение в пакет дополнительной информации реализуется с помощью обработ- чика события TDataSetProvider.OnGetDataSetProperties, которое возникает в ответ на вызов клиентом метода TClientDataSet.GetOptionalParam. Информация передается клиенту в виде вариантного массива, каждый элемент которого содержит три компо- нента: имя параметра, его значение и логический признак, указывающий, будет ли этот параметр включаться в пакет Delta записей для обновления их на сервере БД. На клиентской стороне вызывается функция GetOptionalParam компонента TCU- entDataSet, единственным параметром обращения к которой является имя интересую- щего параметра. Функция возвращает значение параметра в виде типа OleVariant. В следующем примере показан способ передачи клиенту дополнительной информации о полной длине НД: procedure TServer.ProviderlGetDataSetProperties(Sender: TObject; DataSet: TDataSet; out Properties: OleVariant);
Общие вопросы создания трехзвенных приложений 513 begin Properties : = VarArrayCreate([0,0], varVariant); Properties[0] := VarArrayOf(['TotalRecs', DataSet.RecordCount, False]) end; Получить и использовать эту информацию клиентское приложение может так: procedure TForml.btnOpenClick(Sender: TObject); var TotalRecs: Integer; begin Screen.Cursor := crHourGlass; with ClientDataSetl do begin try Open ; TotalRecs := GetOptionalParam('TotalRecs'); Labell.Caption := 'Всего записей: '+IntToStr(TotalRecs) except Screen.Cursor := crDefault; ShowMessage('Невозможно открыть НД '+ProviderName) end; end; Screen.Cursor := crDefault; end; 38.1.3. Выполнение запроса на получение данных При выполнении клиентского запроса на получение данных возникает событие OnGetData. В параметре DataSet обработчик события получает подготовленный к пересылке пакет записей, которые он может при необходимости изменять. Заметим, что глобальные свойства НД, такие как состав показываемых пользователю полей, в обработчике изменить нельзя, но можно изменить конкретные записи набора. Следующий обработчик события OnGetData шифрует строковые данные в столбце CUSTOMER. Для шифровки используется хорошо известный прием двойного наложе- ния битовой маски на один и тот же байт с помощью операции XOR: после первого наложения получаем зашифрованный байт, после второго - первоначальный байт. Для усложнения шифровки битовая маска генерируется псевдослучайным образом с по- мощью стандартной функции Random. Чтобы клиент смог создать такую же последо- вательность случайных масок для расшифровки принятого НД, стартовое значение «зерна» RandSeed генератора псевдослучайных чисел перед шифровкой очередной записи устанавливается равным заранее обусловленной величине 100. procedure TServer.ProviderlGetData(Sender: TObject; DataSet: TClientDataSet); var k: Integer; S: String; begin with DataSet do while not EOF do begin // Цикл no всем записям НД RandSeed := 100; // Опорное число генератора S := FieldByName('CUSTOMER').Value;
514 Глава 38 // Шифруем все байты строки случайными масками: for к := 1 to Length(S) do S[k] := Chr(ord(S[к]) xor (Random(255)+1)); // Редактируем запись: Edit; FieldByName('CUSTOMER').AsString := S; Post; Next; // Переходим к следующей записи end end; Вид зашифрованного НД в клиентском окне показан на рис.38.2. Использована таблица CUSTOMER из демонстрационной БД IB_EMPLOY, входящей в комплект поставки Delphi. Puc.38.2. Пример зашифрованных данных. Для расшифровки столбца CUSTOMER используется событие AfterOpen клиент- ского набора данных: procedure TForml.ClientDataSetlAfterOpen(DataSet: TDataSet); var k: Integer; S: String; begin with DataSet do begin First; while not EOF do begin RandSeed := 100; S := FieldByName('CUSTOMER').AsString;
Общие вопросы создания трехзвенных приложений 515 for к := 1 to Length(S) do S[k] := Chr(ord(S[к]) xor (Random(255)+1)); Edit; FieldByName('CUSTOMER').AsString := S; Post; Next; end end end; Вид того же НД после расшифровки показан на рис.38.3. Рис.38.3. Набор данных после расшифровки. Заметим, что событие AfterOpen не возникает после добавления к НД очередного пакета записей методом GetNextPacket и в этом случае для расшифровки нужно ис- пользовать более сложные приемы (например, метод DataRequest - см. п.38.1.7). Замечание. Получаемые в ходе шифровки НД в общем случае нельзя обнов- лять, т.к. они устанавливают зашифрованные значения в свойства OldValue, доступные клиенту только для чтения. Свойства OldValue используются при поиске на сервере записей, нуждающихся в обновлении. Так как на сервере БД зашифрованных записей нет, обновление такого НД невозможно. Однако с помощью флагов свойства ProviderFlags шифруемого поля (полей) это ог- раничение можно снять: создайте на сервере приложений объекты-поля и для шифруемого поля уберите умалчиваемый флаг pflhWhere, с помощью которого поле включается в предложение WHERE оператора UPDATE и, следовательно, участвует в поиске обновляемой записи. К сожалению, нельзя исключать из поиска поля, определяющие уникальность записи, поэтому, в частности, нельзя шифровать ключевые поля.
516 Глава 38 38.1.4. Сервер приложений как брокер ограничений На сервере приложений можно создать объекты-поля, в свойства CustomConstraint и ImportedConstraint которых можно поместить ограничения на значения соответст- вующих полей. После этого сервер приложений приобретает свойства брокера огра- ничений: он контролирует правильность получаемых от клиента наборов данных Delta и блокирует их обновление, если они содержат недопустимые значения. Ограничения можно импортировать из БД в словарь данных BDE и/или установить вручную. В первом случае вызывается утилита SQL Explorer (опция Database | Explore глав- ного меню Delphi), затем после щелчка правой кнопкой мыши по имени псевдонима БД в появившемся локальном меню выбирается Import to Dictionary (рис.38.4). При этом ограничения и другая вспомогательная информация переписываются из БД в словарь BDE, которая (BDE), таким образом, сможет следить за ними, не пересылая в базу данных заведомо неправильные данные. Definition □ Qj Databases ffl-Д DBDEMOS Ё oft DefaultDD Ё-Jft HYDRO E Ё-1®| IB_SK * Ё "ft IBLoc Close E "ft lb_s Refresh Ctrl+R E £ft PROB----------- hr.. I. -’I DeK- ' i41 >, Rp-iriDE? t '' ' , Ёрда . •' Save As. Ctrl+S Import to Dictionary Type / BATCH BLOB blob: ENABI ENABL LANGf MAX Bl OPEN I SCHEM SCHEM, SCHEM. SERVE! SQLPAf SQLQFT USER N _________________________ Version Information.., pMo., в-иг. _____________ServerManaget. Puc. 38.4. Импорт ограничений из БД IB_EMPLOY в словарь BDE. Заметим, что хотя BDE является неотъемлемой частью сервера приложений, сам по себе импорт ограничений в словарь BDE еще не делает сервер приложений брокером ограничений. Для этого нужно создать объекты-поля. Более того, для каадого поля необ- ходимо переписать ограничение из словаря BDE в свойство ImportedConstraint'. для этого в редакторе полей выделите все поля (опция Select all локального меню или клавиши Ctrl+L) и нажмите Ctrl+O или выберите Associate A ttributes в локальном меню. Теперь для каждого поля таблицы, имеющего соответствующее ограничение в словаре, будет вызва- но диалоговое окно Associate Attributes и вы сможете перенести импортированные огра- ничения в свойства ImportedConstraint объектов-полей. На рис.38.5 показан фрагмент экрана с окнами редактора полей и Associate Attributes сразу после нажатия Ctrl+O. Как видим, несмотря на то что в редакторе полей выделены все поля, в окне Associate Attrib-
Общие вопросы создания трехзвенных приложений 517 utes высвечено только поле BUDGET, так как только для него одного в таблице DEPART- MENT демонстрационной БД IBEMPLDY имеется ограничение value>100Q0. DEPT_NO DEPARTMENT HEAD_DEPT MNGR.NO BUDGET LOCATION PHONE, NO jBUDGETl for sL Attribute set name------- BUDGET Companii Continent COUNTRYCOUNTRY COUNTRYCURRENC CUSTNO CustNo CUSTOMERCUST_N( -i CUSTOMERCUSTOM Cancel Рис.38.5. Импортирование ограничений из словаря BDE. Во втором случае программист вручную вносит нужные ограничения в свойства CustomConstraint объектов-полей. При этом, как и в случае импорта ограничений из словаря, полезно определить свойства ConstraintErrorMessage полей, чтобы пользова- тель смог понять суть ошибки. Замечание. Если сервер приложений играет роль брокера ограничений, ошибочные данные при их попытке обновления немедленно блокируются сервером, который возбуждает исключение с сообщением ConstraintError- Message или стандартным сообщением, если это свойство - пустое. В этом случае все события, описываемые далее в этой главе и связанные с ошибками обновления, не возникают. 38.1.5. Выполнение запроса на обновление данных С помощью вызова метода ApplyUpdate клиент сохраняет в физической ТБД изме- нения, сделанные в локальной копии данных. Через интерфейс IProvider клиентское приложение вызывает одноименный метод провайдера, который получает пакет изме- ненных записей (т.е. записи с измененными полями, вставленные и удаленные записи) в параметре обращения Delta и создает событие OnUpdateData. В обработчике этого события программист может произвести редактирование Delta и/или изменить роль полей в процедуре обновления. Процесс обновления идет последовательно по записям, причем перед обновлением очередной записи создается событие BeforeUpdateRecord. Если обновление записи привело к возникновению ошибки, создается событие OnUpdateError, обработчик которого может скорректировать запись и повторить попытку подтверждения. Ошиб- ки, в основном, вызываются изменением ключевых полей, вставкой записи с недопус- тимыми значениями полей или изменением записи другим клиентом. Если обработчик ошибок не устранил причину появления ошибки, ошибочная запись сохраняется во временном пакете, который по окончании процесса передается клиенту.
518 Глава 38 Следует заметить, что описанный процесс нельзя использовать для обновления данных, полученных из хранимой процедуры. Чтобы все-таки обновить такие данные, используется обработчик события BeforeUpdateRecord, в котором изменения переад- ресуются компоненту ТТаЫе, если хранимая процедура получила данные из одной таблицы, либо динамически формируемому запросу TQuery с соответствующими предложениями INSERТ, UPDATE, DELETE. ' Обработчик OnUpdateData позволяет работать сразу со всеми измененными запися- ми. Если в обработчике осуществляется редактирование лишь вновь вставленных или измененных записей (как правило, удаленные записи не редактируются), можно исполь- зовать свойство StatusFilter для отбора таких записей. В следующем обработчике встав- ляется текущая дата в поле Date_of-Update всех измененных и вставленных записей: procedure TServer.ProviderlUpdateData(Sender: TObject; DataSet: TClientDataSet); begin with DataSet do begin StatusFilter := [usModified, uslnserted]; First; while not EOF do begin Edit; FieldByName('Date_of_Update').AsDateTime := Now; Post; Next end end end; Эту же задачу решает такой обработчик события BeforeUpdateRecord’. procedure TServer.ProviderlBeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean); begin if UpdateKind in [ukModify, uklnsert] then with DeltaDS do begin Edit; FieldByName('Date_of_update').AsDateTime := Now; Post; end; Applied := True end; 38.1.6. Обработка ошибок обновления Если обновление записи привело к ошибке, провайдер создает событие OnUp- dateError (см. замечание в п.38.1.4): type TResolverErrorEvent = procedure(Sender: TObject; DataSet: TClientDa- taSet; E: EUpdateError; UpdateKind: TUpdateKind; var Response: TResolverResponse) of object; property OnUpdateError: TResolverErrorEvent;
Общие вопросы создания трехзвенных приложений 519 В параметре Response обработчик должен вернуть одно из следующих значений: • rrSkip - пропустить ошибочную запись и поместить ее в пакет ошибок; • rrAbort - прекратить процесс обновления без сообщения об ошибке; • rrMerge - заменить ошибочные значения полей исходными; • rrApply - повторить попытку обновления после коррекции записи (в этом случае обработчик должен устранить причину ошибки); • rrlgnore - игнорировать ошибку и не помещать ошибочную запись в клиентский пакет. Параметр UpdateKind содержит информацию о характере ошибки: • ukModify - запись была изменена и содержит недопустимые значения полей; • uklnsert - вставляется новая ошибочная запись; • ukDelete - эту запись нельзя удалить; Для набора данных DataSet текущая запись - ошибочная. Обработчик не может осуществлять навигацию по этому НД, но может использовать свойства полей Old- Value, NewValue и CurValue, для распознавания причины ошибки и ее устранения. 38.1.7. Событие OnDataRequest Клиент может вызвать метод DataRequest интерфейса IProvider, чтобы передать провайдеру или получить от него произвольную информацию. Этот метод создает для провайдера следующее событие: type TDataRequestEvent = function(Sender: TObject; Input : OleVariant): OleVariant of object; property OnDataRequest: TDataRequestEvent; Обработчик события получает входной параметр Input и может вернуть данные как результат, возвращаемый функцией. Поскольку и параметр, и результат имеют тип OleVariant, может осуществляться обмен произвольной информацией. В следующем примере клиент передает провайдеру маску для фильтрации набора данных на сервере приложений, т.е. до передачи их клиентскому НД: procedure TForml.ButtonlClick(Sender: TObject); begin // Передаем провайдеру маску и получаем от него отфильтрованный НД: ClientDataSetl.Data := ClientDataSetl.Provider.DataRequest(FilterEdit.Text); end; Провайдер может использовать эту информацию следующим образом: function TForml.ProviderlDataRequest(Sender: TObject; Input: OleVariant): OleVariant; begin with (Sender as TProvider) do begin // Фильтруем набор: DataSet.Filter := Input; DataSet.Filtered := True; // Передаем его клиенту: Reset(True); Result := Data end end;
520 Глава 38 38.1.8. Расширение интерфейса сервера приложений В некоторых случаях бывает необходимо снабдить сервер приложений нестан- дартными функциями. Например, он может проверить идентификатор подключенного пользователя и соответственно разрешить или запретить доступ к удаленным данным. Для включения в интерфейс сервера приложений нестандартного метода или свой- ства удобно воспользоваться опцией Edit | Add to Interface главного меню Delphi (этот элемент меню будет недоступным, если активный модуль проекта не является моду- лем удаленного доступа TXXXDataModule). Разворачивающееся после этого диалого- вое окно (рис.38.6) содержит два строковых параметра - Interface и Declaration. Стро- ка Interface содержит имя модуля данных, снабженное стандартным префиксом I. Она на самом деле определяет интерфейс так называемого со-класса (CoClass), исполняю- щего дуальный интерфейс IXXXDisp (XXX - имя класса, которое запрашивает эксперт в момент создания удаленного модуля данных). Этот интерфейс является наследником стандартного интерфейса IDataBroker и создается экспертом удаленного модуля. Для показанного на рис.38.6 окна имя класса модуля сервера приложений - Ser, имя дуаль- ного интерфейса - ISerDisp, имя со-класса - TSer, а имя его интерфейса - ISer. . Рис. 38.6 Окно расширения интерфейса. В строке Declaration следует ввести заголовок метода или свойства так, как он оп- ределяется в описании класса. Например: procedure Proc(var X: Integer); function Func: Variant; property UserID: Integer; При определении свойства достаточно указать только его имя и тип - эксперт окна автоматически добавит к нему методы Set_XXX и Get XXX, где XXX - имя свойства. После ввода определения метода (свойства) эксперт сделает соответствующие изме- нения в описаниях интерфейса и со-класса и создаст необходимые скелеты методов. Пусть, например, мы хотим расширить интерфейс сервера, чтобы клиент мог пере- давать ему некоторое целое число, которое сервер будет трактовать как идентифика- тор пользователя. Введем текст property UserID: Integer в строке Declaration (можно не вводить завершающую точку с запятой). После нажа- тия Enter эксперт добавит к определению интерфейса ISer объявления свойства Us- erID, функции Get_UserID и процедуры Set_UserID, а к определению дуального ин- терфейса ISerDisp - только свойство UserID. Таким образом, в рамках класса TSer сервера приложений мы можем исполнять методы Get_UserID и Set_UserID, в то вре- мя как для клиентов доступно только одно дополнительное свойство UserID.
Общие вопросы создания трехзвенных приложений 521 Чтобы завершить пример, покажем практическую реализацию новрк методов, а также введем в сервер обработчик события OnActivate таблицы TTablel, в котором проверим значение свойства UserID: если это свойство не содержит заранее обуслов- ленное число 123, открытие таблицы будет блокировано: type TSer = class(TRemoteDataModule, ISer) procedure TablelBeforeOpen(DataSet: TDataSet); private { Private declarations } FUserlD: Integer; public { Public declarations } protected function Get_UserID: Integer; safecall; procedure Set_UserID(Value: Integer); safecall; end; function TSer.Get_UserID: Integer; begin Result := FUserlD end; procedure TSer.Set_UserID(Value: Integer); begin FUserlD := Value end; procedure TSer.TablelBeforeOpen(DataSet: TDataSet); begin if Get_UserID<>123 then raise Exception.Create(' В доступе отказано') end; Ha клиентской стороне может использоваться такой обработчик нажатия кнопки But- ton! для доступа к таблице Table! (предполагается, что в компоненте SpinEditl поль- зователь ввел свой идентификатор): procedure TForml.ButtonlClick(Sender: TObject); begin DCOMConnectionl.AppServer.UserID := SpinEditl.Value; ClientDataSetl.Open; end; Подробнее о доступе к интерфейсу сервера см. п.38.2.6. 38.1.9. Поставка сервера приложений Если на машине сервера приложений установлена Delphi, никаких дополнительных действий для установки на этой машине сервера приложений не требуется. Но если Delphi нет, вам следует: 1. установить и зарегистрировать BDE-, 2. установить в каталоге WINDOWS\SYSTEM машины сервера файлы DBCLI- ENT.DLL и STDVCL40.DLL.
522 Глава 38 Проще всего для этого воспользоваться программой InstallShield Express, постав- ляемой вместе с Delphi. Однако создаваемая ею программа установки использует не русифицированные диалоговые окна. Если вы захотите создать собственную про- грамму установки, для переноса BDE необходимо: 1. установить в отдельном каталоге все файлы BDE (около 16 Мбайт); по умолча- нию при установке Delphi файлы BDE помещаются в каталог Program Files\Common Files\Borland Shared\BDE', 2. создать в реестре машины узел HKEY_CLASSES_ROOT | Borland.Data- base Engine. 4 и записать в него единственный умалчиваемый параметр Bor- land Database Engine (рис.38.7); Рис.38.7. УзелHKEY_CLASSES_ROOT| Borland.Database_Engine.4. 3. в узле HKEY_CLASSES_ROOT | Borland. Database_Engine. 4 создать подузел CLSID и записать в него регистрационный идентификатор BDE - {FB99D710- 18B9-11D0-A4CF-00A024C91936}; 4. создать в узле hkey_classes_ROOT | clsid узел с этим идентификатором, а также с идентификатором {FB99D700-18B9-11D0-A4CF-00A024C91936}; 5. в узел FB99D700.. . записать параметр Borland Database Engine, Query Edit Object, ав узел FB99D710.. . -Borland Database Engine (рис.38.8); Редактор реестра див1 реестр Дравка £ид Справка Е Cl (F935DС22-1 CFO-11D O-ADB9-OOC04 Е EJ :935DC26-1CF0-11D0ADB9-00C04.<, ECJ {F9A3F405-8068-11CF-858C-95A235 С ф Q {FA77A74E -Е109-11DOAD6E-OOC04"-! ф С :AEE09F0-8A5B-11 СЕ-8379-524153Й| ф Г'| {FAEE09F1-8А5В-11СЕ-8379-52415?',: j| П, i*?] (По умолчанию) "Borland Database Engine. Query Edit Object" Е П {FB99D710-18B9-11D0-A4CF-00A02-/-/I Е П {FBF23B40-E3F0-1B1B-B488-O0AA0tt\. I Ё П {FBF23B41-E3FO-101B-8488-OOAAOOF'-jl Е П {FBF23B42-E3FO-1D1B-8488-OOAAOG Л ЁС] (FC2AC420-62e4-11CF-ABBD-0040Ci_J m Г'-» frr.-ir’xm rmn -terser nrrn Мой <OMnb»rep\HKEY_ClASSES_R00T\CLSID4FB99 700-18В9-11ООА44Е-аМ24СЭ1936) Рис.38.8. Узел HKEY_CLASSES_ROOT\ {FB99D700-18B9-11D0-A4CF-00A024C91936}. 1 а И
523 Общие вопросы создания трехзвенных приложений *—а 6. создать в обоих узлах подузлы inprocServer32 и для FB99D700... в умалчи- ваемый параметр записать путь к каталогу размещения BDE на машине сервера и ссылку на библиотеку idsql32.dll, а в такой же параметр узла FB99D710... - ссылку на библиотеку IDAPI32. DLL (рис.38.9); Рцс.38.9. Узел \HKEY_CLASSES_ROO1\..\InprocServer32. 7. дополнить обе регистрационные записи параметрами ThreadingModel со зна- чениями Apartment. 38.2. СОЗДАНИЕ КЛИЕНТСКОГО ПРИЛОЖЕНИЯ 38.2.1. Особенности клиента в трехзвенной архитектуре Клиентское приложение в трехзвенной архитектуре имеет некоторую специфику, связанную с необходимостью установки определенного протокола связи с сервером приложений и использования полномочного представителя удаленного НД - компо- нента TClientDataSet. Общая схема создания клиентского приложения такова. 1. В приложение включается модуль данных TDataModule, который затем будет служить контейнером невизуальных компонентов {TXXXConnection, TClientDa- taSet, TSource). Разумеется, использование TDataModule необязательно, и вы можете расположить эти компоненты просто на пустой форме. Однако модуль данных обеспечивает концентрацию специфичных компонентов в одном месте и освобождает от них обычные формы. 2. В модуль данных помещается связной компонент TXXXConnection. Тип связно- го компонента зависит от выбранного протокола связи: • TDCOMConnection - для установления связи COM/DCOM\ • TCORBAConnection - для установления связи CORBA', • TSocketConnection - для установления связи с помощью сокетов; • TWebConnection - для установления связи с помощью протокола HTTP. Для модуля TMTSDataModule можно выбрать любой из перечисленных прото- колов. Если вы предполагаете использовать НД из разных серверов приложе- ний, установите дополнительные связные компоненты. 3. Установите специфичные свойства связных компонентов (см. ниже пп.38.2.2-38.2.4). 4. С помощью свойства ObjectBroker компонентов TXXXConnection выберите нужный сервер приложений, но не активизируйте связь (свойство Connection) -
524 Глава 38 оно активизируется автоматически при активизации хотя бы одного клиентско- го НД. 5. Разместите в модуле данных необходимое количество компонентов TClientDa- taSet и настройте их свойства RemoteServer на связь с нужным связным компо- нентом (с нужным сервером приложений), a ProviderName - для работы с нуж- ным провайдером (источником данных) сервера приложений. 6. Дальнейшие шаги для создания клиентского приложения не отличаются от созда- ния одно- (локальных) или двухзвенных (клиент/серверных) приложений за ис- ключением обеспечения дополнительных действий, связанных с отложенной об- работкой, обновлением данных и управлением связью с сервером приложений. 7. При установке программы на машине клиента скопируйте в каталог windows \ system библиотеку dbclient . dll. 38.2.2. Связь COM/DCOM Эта связь используется наиболее эффективно, если машина сервера приложений работает под управлением ОС Windows NT Server, сконфигурированной в режиме сервера доменов, а машина клиента - под управлением Windows NT Work Station. Только связь DCOM делает доступным разграничение прав доступа к данным на осно- ве ролей при использовании MTS. Для организации связи на машине клиента устанавливается компонент TDCOMCon- nection. В его свойство ComputerName следует установить имя удаленного сервера при- ложений из автоматически создаваемого выпадающего списка £>СОЛ/-серверов. Если это свойство останется незаполненным, вместо DCOM будет работать технология СОМ, т.е. будет считаться, что сервер располагается на машине клиента в виде сервера СОМ. Если сервер располагается на другой машине, он может быть зарегистрирован в реестре ма- шины клиента или его имя должно содержаться в свойстве ComputerName. В свойстве BrokerName компонент создает и поддерживает список доступных серве- ров приложений на компьютере с именем ComputerName. Он периодически посылает в сеть специальные запросы, на которые откликаются все активные серверы приложений. Это несколько увеличивает нагрузку на сеть, но позволяет компоненту определить дос- тупные в данный момент серверы, так что список BrokerName с течением времени может меняться. Свойство BrokerName отсутствует у компонентов TCORBAConnection, так как у них есть собственные средства контроля за несколькими серверами приложений. 38.2.3. Связь CORBA Связь CORBA реализует компонент TCORBAConnection. В этой технологии роль посредника между клиентом и сервером приложений играет Smart Agent, запускаемый перед первым стартом сервера приложений. В момент старта происходит автоматиче- ская регистрация сетевого адреса сервера приложений, поэтому для установления связи клиенту достаточно указать идентификационный номер библиотеки типов сер- вера приложений. Получив этот номер, Smart Agent отыскивает его в специальном репозитории интерфейсов и затем просматривает свой каталог доступных (активных) серверов приложений, имеющих указанный идентификационный номер. Таким обра- зом, для обычной (нормальной) связи в технологии CORBA программист должен уста- новить значение в свойство RepositorylD компонента TCORBAConnection. Если программист по каким-либо причинам желает установить связь с конкретным сервером приложений, он может указать сетевое имя машины сервера (/P-адрес) в
Общие вопросы создания трехзвенных приложений 525 свойстве HostName, а с помощью свойства ObjectName - указать нужное имя экземп- ляра сервера приложений. Заметим, что свойства Repository ID, HostName, и Object- Name дополняют, но не исключают друг друга. 38.2.4. Связь с помощью сокетов Перед установлением связи на машине сервера приложений должна быть установ- лена и запущена утилита scktsrvr.exe (поставляется с Delphi и располагается в его каталоге BIN). Поскольку не существует специальных средств запуска этой утилиты, а она должна работать перед установлением связи, полезно организовать ее автоматический за- пуск с помощью средств Windows (например, поместив ее в каталог автозапуска). Для установления связи программист вначале с помощью раскрывающегося списка свойства Host выбирает нужный компьютер в доступном сетевом окружении, затем с помо- щью списка свойства ServerName - имя сервера приложений (если при обращении к этому свойству возникнет ошибка, еще раз поверьте, запущена ли утилита scktsrvr . EXE). 38.2.5. Управление связью Для установки связи клиентского набора данных с провайдером он должен просто обратиться к одному из методов интерфейса IProvider - обычно к методу Open (напрямую или установкой значения True в свойство Active). К этому моменту клиент- ский НД должен содержать в свойстве RemoteServer имя связного компонента, а в свой- стве ProviderName - имя провайдера. Перед открытием связи клиентский НД проверяет состояние соответствующего связного компонента и автоматически активизирует его, поэтому предварительная активизация связного компонента не имеет смысла. Непосредственно перед установкой связи возникает событие OnBeforeConnect, а сразу после установки - OnAfterConnect. Jlflu разрыва связи достаточно установить False в свойство Connection связного ком- понента. Заметим, что разрыв связи происходит автоматически в момент окончания работы клиентского приложения. Связь разрывается также при изменении любого клю- чевого свойства связного компонента - ComputerName, ServerGUID, ServerName и т.п. В момент разрыва связи создаются события OnBeforeDisconnect и OnAfterDisconnect. 38.2.6. Раннее и позднее связывание с интерфейсом сервера В большинстве случаев клиентское приложение обращается к интерфейсу сервера приложений опосредованно, с помощью обращений к свойствам и методам клиент- ского НД. В некоторых специальных случаях, а также если интерфейс сервера прило- жений расширен нестандартными свойствами и методами, обращение к нему реализу- ется с помощью свойства AppServer связного компонента. Существуют два способа доступа к интерфейсу сервера: позднее (динамическое) и раннее (статическое) связывание. В первом случае программа обращается к свойству AppServer, не указывая нужного интерфейса. Например, так: MyConnection.AppServer.MyMethod(X,Y); Поскольку к этому моменту компилятору не известен интерфейс сервера (этот интер- фейс описан в другой программе - в сервере приложений), он не может проверить правильность обращения к методу MyMethod и принимает это обращение «на веру».
526 Глава 38 Во втором случае осуществляется явное приведение интерфейса к нужному, типу с помощью оператора as. Например, для DCOM это выглядит так: MyConnection.AppServer as IMyDCOMServer do MyMethod(X,Y); Поскольку CORBA не исполняет интерфейс IDispatch, для раннего связывания с CORBA-сервером используется обращение к универсальному интерфейсу lUnknow. (lUnknow(MyConnection.AppServer) as IMyCORBAServer).MyMethod(X,Y); При раннем связывании включается в действие механизм Code Insight среды Delphi, а также проверка количества и типов параметров обращения, поэтому раннее связыва- ние более предпочтительно, тем более что оно осуществляется на этапе компиляции и не требует дополнительного времени в момент исполнения программы (при позднем связывании клиент первоначально обращается к интерфейсу lUnknow сервера, чтобы получить ссылку на нужный интерфейс). При раннем связывании компилятор каким-то образом должен знать интерфейс сервера приложений. В случае DCOM в реестре Windows 32 клиентской машины необ- ходимо зарегистрировать библиотеку интерфейсов сервера приложений. Для этого может использоваться программа TREGSVR.EXE, исходный текст которой находится в каталоге DEMOS\ASTIVEX\TREGSVR каталога размещения Delphi. При использовании CORBA к проекту клиентского приложения нужно добавить модуль TLB с библиоте- кой интерфейсов сервера и сослаться на него в предложении uses. В случае сокетов и OLEnterprise раннее связывание невозможно. Однако такого рода серверы приложений используют так называемые дуальные интерфейсы (см. п.36.2.2), являющиеся прямыми потомками стандартного интерфейса IDispatch, что позволяет использовать простое (но менее удобное) позднее связывание (по умолча- нию дуальные интерфейсы имеют имя, содержащее имя сервера приложений, допол- ненное стандартной буквой I спереди и суффиксом Dispy. var OLEntrpriselnterfасе: IMyOLEntrpriseServerDisp; begin OLEntrpriselnterface := MyConnection.AppServer; OLEntrpriselnterface.MyMethod(X,Y); end; 38.2.7. Обновление и освежение данных Под обновлением здесь, как и раньше, понимается передача измененных данных на сервер приложений для внесения необходимых изменений в физические ТБД. Осве- жение - это повторное считывание уже полученных от сервера приложений данных. Обновление локальных данных реализуется вызовом метода ApplyUpdates клиент- ского НД. В качестве параметра вызова методу передается максимально допустимое количество ошибок, могущих возникнуть в процессе физического обновления данных. На сервер приложений этот метод передает протокол изменения локальных данных, который содержит свойство Delta клиентского НД. Например: ClientDataSetl.ApplyUpdates(0); После завершения процесса сервер возвращает клиенту количество действительно полученных ошибок обновления как результат функции ApplyUpdates и пакет не об- новленных по каким-либо причинам данных в свойстве Delta. Еще раз заметим, что
Общие вопросы создания трехзвенных приложений 527 возвращаются только те ошибки, которые не отсеяны брокером ограничений сервера приложений, т.е. «молчаливые» ошибки, возникшие в сервере базы данных. Если количество ошибок больше нуля, сервер приложений возбуждает для клиента событие type TReconcileAction = (raSkip, raAbort, raMerge, raCorrect, raCancel, raRefresh); TReconcileErrorEvent = procedure(DataSet: TClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction) of object; property OnReconcileError: TReconcileErrorEvent; Это событие создается для каждой ошибочной записи, которая является текущей в параметре вызова DataSet. Параметр UpdateKind содержит одно из значений, указан- ных для одноименного параметра в обработчике события OnUpdateError, а параметр Action дополняет параметр Response этого обработчика дополнительным значением raRefresh, которое требует от сервера приложений восстановить все поля ошибочной записи, вновь прочитав ее с сервера БД. Как и в обработчике OnUpdateError, в обра- ботчике OnReconcileError нельзя изменять курсор НД, но можно использовать значе- ния OldValue, NewValue и CurValue для диагностики ошибки и ее коррекции. Если для коррекции ошибки требуется вмешательство пользователя, обработчик может использовать стандартное диалоговое окно HandleReconcileError, определенное в модуле RecError (см. рис.38.10). Рис. 38.10. Окно коррекции ошибки. Этот модуль поставляется в исходном тексте в каталоге репозитория OBJREPOS каталога размещения Delphi. Скопируйте из этого каталога в ваш рабочий каталог файлы RECERROR. *, сделайте ссылку на модуль RecError в предложении uses и на- пишите такой обработчик события OnReconcileError для компонента TClientDataSet'. procedure TForml.ClientDataSetlReconcileError(DataSet: TClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction); begin Action := HandleReconcileError(DataSet, UpdateKind, E) end;
528 Глава 38 Поскольку модуль RecError доступен для изменения, вы сможете при желании его русифицировать и/или изменить так, чтобы он в максимальной степени отвечал нуж- дам вашей программы. Если вы включите его в ваш проект, не забудьте убрать его окно ReconcileErrorForm из списка автоматически создаваемых окон (опция Project | Option главного меню Delphi), т.к. функция HandleReconcileError создает это окно «на лету» (в ходе прогона программы). Поскольку пользователь работает с локальной копией данных, они могут устареть, если другой пользователь изменит их, поэтому для освежения данных необходимо время от времени вызывать метод Refresh. Этот метод приведет в возникновению ошибки, если локальные данные самого клиента были изменены, но не обновлены. Чтобы сохранить протокол изменений локальных данных, можно использовать ме- тод RefreshRecord, который обновляет только одну текущую запись и не меняет про- токол. Для исключения ошибки можно проверить статус локального НД. Например: if ClientDataSetl.UpdateStatus о usUnModified then raise Exception.Create('Перед освежением записи '+ 'следует сохранить сделанные в ней изменения'); ClientDataSetl.RefreshRecord; 38.2.8. Отложенная обработка и регулирование количества записей в пакете данных Как уже говорилось, в рамках трехзвенной архитектуры легко реализуется отло- женная обработка данных. Для этого используются методы procedure SaveToFile(const FileName: String = ''); procedure LoadFromFile(const FileName: String = ''); С помощью первого локальные данные сохраняются в файле, с помощью второго - читаются из него. Заметим, что если параметр обращения опущен, данные 'записыва- ются в файл или читаются из файла, имя которого содержит свойство FileName кли- ентского НД. Между записью данных и их считыванием может пройти определенное время, в течение которого клиентская программа может разорвать связь с сервером приложе- ний и работать с локальной копией данных, многократно сохраняя их в файле и счи- тывая их из него. После восстановления связи с сервером приложений клиент может обновить данные на сервере БД. Это и есть отложенная обработка данных. Количество записей в пакете данных, которые клиент получает от сервера за одно обращение к методу GetNextPacket, определяется свойством PacketRecords'. если зна- чение этого свойства равно (-1), считываются все данные из удаленного НД; если свойство содержит 0, считываются так называемые метаданные - информация о струк- туре таблицы, названиях полей, ограничениях и т.п.; наконец, любое положительное значение определяет максимальное количество записей в пакете. Так сказано в документации Delphi, но на практике это не совсем так. Предположим, мы установили в PacketRecords небольшое число, например, 5. Тогда, если с клиентским НД связаны визуальные компоненты (DBGrid, DBEdit, DBMemo и т.п.), при открытии НД считается столько записей, сколько их сможет принять таблица DBGrid, если она связана с НД а вот если ее нет, - столько, сколько содержит PacketRecords (или меньше, если количество записей в НД меньше PacketRecords). Более того, при каждом прокру- чивании DBGrid с помощью встроенной вертикальной полосы прокрутки будет считы-
Общие вопросы создания трехзвенных приложений 529 ваться не 5 записей, а больше, если освобождаемое в результате прокрутки количество строк DBGrid больше 5. Если таблицы нет или пользователь не прокручивает ее и вызы- вается метод GetNextPacket, будет прочитано не больше 5 записей. Во всех случаях свойство PacketRecords в сочетании с методом GetNextPacket по- зволяет существенно снизить сетевой трафик. 38.2.9. Работа с файлами данных Отложенная обработка позволяет использовать клиентское приложение для созда- ния локальных данных и работы с ними вообще без связи с сервером приложений. Фактически речь идет о локальных (однозвенных) СУБД, не использующих BDE или подобный механизм доступа к данным. Ясно, что такие приложения могут найти при- менение для разработки корпоративных сетей с небольшим количеством клиентов и не очень сложной моделью базы данных или простых специализированных локальных СУБД, не связанных сетью (например, бухгалтерских программ, программ складского учета, ведения личных архивов и т.п.). Сразу следует заметить, что формат используемых в такого рода приложениях файловых данных не совпадает ни с одним из популярных форматов, которым следу- ют локальные СУБД типа dBASE, Paradox, Accesses и т.п. Кроме того, файловые дан- ные не поддерживают утилиты Delphi SQL Explorer и Database Desktop, в них нельзя использовать псевдонимы БД и доступ к данным на основе 5£>£-запросов. Как и в любой другой однозвенной СУБД, в файловой системе будут проблемы разграничения транзакций при многопользовательском доступе к данным. Общая схема создания и использования клиентом файловых данных такова. 1. Создайте обычное клиентское приложений, но не размещайте в нем связных компонентов TXXXConnection (если вы модернизируете для этих целей сущест- вующее клиентское приложение, временно оборвите связь этих компонентов с удаленным сервером приложений). 2. Создайте один или несколько файлов с данными: • вы можете использовать для этого обычный прием копирования данных с сервера приложений, если ваша файловая модель данных должна повторять хотя бы часть уже существующей БД; в этом случае вам предварительно следует создать полноценное клиентское приложение со связными компо- нентами и сервером приложений; • используйте явно созданные объекты-поля и определения индексов так, как если бы вы работали с однозвенным приложением, после чего на их основе создайте набор данных (подробнее об этом см. ниже). Сохраните файловые данные в сетевом каталоге (каталогах). Поскольку дальше эти данные будут использоваться компонентами TClientDataSet, их сохранять следует только с помощью методов SaveToFile этих компонентов. 3. При старте клиентского приложения загружайте файловые данные методом LoadFromFile, работайте с ними как с обычными НД базы данных, после чего сохраняйте их методом SaveToFile. Заметим, что после успешного выполнения метода LoadFromFile клиентский НД автоматически открывается и для него не требуется обращения к Open. 4. При сохранении файловых данных вместе с ними сохраняется протокол их из- менений, что приводит к разбуханию файлов. Если вы не планируете обновлять
530 Глава 38 ваши файловые данные на сервере БД, полезно перед сохранением данных в файле вызвать метод MergeChangeLog, который очистит протокол изменений. 5. При установке программы иа машине клиента скопируйте в каталог WINDOWS\ system библиотеку dbclient . dll. Заметим, что если ваши таблицы построены как главная-подчиненная и подчинен- ная таблица используется как поле главной (см. ниже п.38.3), она читается из файла и записывается в него одновременно с главной. Если подчиненная таблица не является частью главной, она должна читаться и сохраняться отдельными командами. Для создания нового файлового НД следует сначала создать все необходимые поля НД (с помощью редактора полей клиентского НД) и в окне Инспектора Объектов установить нужные свойства объектов-полей. Для создания очередного поля используйте опцию New Field локального меню редактора полей или клавиши Ctrl+N. Если для НД необходимо создать индекс, щелкните по компоненту TClientDataSet и в окне Инспектора Объектов раскройте список свойства IndexDefs-, в строке Fields укажите список индексных полей (соседние поля в списке составного индекса отделяются точкой с запятой), в строке Name - имя индекса, а также при необходимости установите нужные флаги в свойство Options. После того как поля (и, возможно, индексы) созданы, щелкните по компоненту правой кнопкой мыши и выберите опцию Create Data Set, чтобы сохранить таблицу как файл данных. Поля и индексы можно создавать в ходе прогона программы. В следующем приме- ре создаются два поля файлового НД и индекс по полю Name-. procedure TForml.FormCreate(Sender: TObject); begin with ClientDataSetl do begin with FieldDefs.AddFieldDef do begin DataType := ftlnteger; Name := ' NN'; end; with FieldDefs.AddFieldDef do begin DataType := ftString; Size := 20; Name := 'Name'; end; with IndexDefs.AddlndexDef do begin Fields := 'Name'; Name := 'Strindex'; end; CreateDataSet; end; end; 38.3. ИСПОЛЬЗОВАНИЕ ВЛОЖЕННЫХ ТАБЛИЦ В НД ГЛАВНЫЙ-ДЕТАЛЬНЫЙ Создание НД главный-детальный в трехзвенной архитектуре ничем не отличается от аналогичного процесса для одно- или двухзвениой архитектуры, за исключением одной любопытной особенности. Дело в том, что для поддержки Oracle8 (см. гл.35) в
Общие вопросы создания трехзвенных приложений 531 компоненты TProvider и TDBGrid введена возможность обработки вложенных таблиц. Эту возможность можно использовать и без сервера Oracle8 (в одно- и двухзвенной архитектуре нет TProvider, поэтому для них эта возможность недоступна). Для создания и использования вложенной таблицы1 используют обычный прием связывания детальной и главной таблиц с помощью полей MasterSource и MasterFields детальной таблицы. Эта связь реализуется на сервере приложений, однако для обеих таблиц используется только один провайдер, связанный с главной таблицей (если вложенная таблица не используется, нужны два провайдера - для главной и детальной таблиц). Для правильной связи в главной и детальной таблицах должны быть созданы индексы по общему столбцу. В клиентской программе создаются поля-объекты, причем последний объект будет иметь имя детальной таблицы. После связывания с клиентским НД таблицы DBGrid и запуска приложения вы получите столбец, содержимое которого отмечается как DA- TASET. Щелчок по кнопке в этом столбце раскроет вложенную таблицу, которая будет содержать соответствующие детальные записи (рис.38.11)1 2. Рис. 38.11. Реализация связи главный-детальный в виде вложенной таблицы. Содержимое вложенной таблицы редактируется обычным образом и сохраняется в БД методом TClientDatSet.ApplayUpdates одновременно с главной таблицей. Если файл данных (см. п.38.2.9) создан копированием НД главный-детальный с вложенной таблицей, свойства вложенности детальной таблицы сохраняются и для него. Для создания связи главная-подчиненная традиционными средствами (рис.38.12) связь между таблицами реализуется в клиентском приложении с помощью свойств 1 Напомним, что вложенной является детальная таблица, сохраняемая как столбец главной таблицы в Oracle8. Поскольку сервер InterBase и все другие промышленные серверы БД не поддерживают столбцы- таблицы, в них детальная таблица сохраняется отдельно от главной. 2 Этот рисунок создан с помощью примера, любезно предоставленного Н.Салмановой (одним из авторов весьма полезной книги [5]). Связывались таблицы CLIENTS и HOLDINGS из демонстрационной БД IBLO- CAL. (поле связиЛССТЛВЯ).
532 Глава 38 MasterSource и MasterFields детального НД TClientDataSet (этих компонентов должно быть два - для главного и детального НД). Рис.38.12. Связь главный-детальный обычным способом. 38.4. СОЗДАНИЕ ПОВТОРНО ИСПОЛЬЗУЕМЫХ СЕРВЕРОВ ПРИЛОЖЕНИЙ В случае, когда несколько клиентов обслуживаются одним сервером приложений (режим Shared Instance технологии CORBA) или когда между повторными обраще- ниями к серверу в технологии MTS сервер может освобождать ресурсы (деактивизиро- ваться), интерфейс IProvider не может использоваться в чистом виде, т.к. сервер при- ложений должен «помнить» предыдущий вызов клиента. В практике программирова- ния повторно исполняемые фрагменты программы с разными исходными данными часто называют реентерабельными (повторно входимыми). Приблизительно такие же задачи, т.е. создание реентерабельных серверов приложений стоят перед программи- стами и в перечисленных случаях1. Реентерабельность серверов приложений реализуется за счет того, что информа- цию о предыдущем вызове сервера должен помнить и при необходимости передавать серверу сам клиент. С этой целью в большинстве случаев приходится расширять ин- терфейс сервера (см. п.38.1.8). Следующий метод модуля MTS получает для клиента пакет данных: function TMyMTSDataModule.GetCustomerRecords(MetaData: Boolean; out RecsOut: Integer): OleVariant; begin try if MetaData then Result := Providerl.GetRecords(0, RecsOut); 1 В документации Delphi такие серверы определяются словом stateless, которое приблизительно перево- дится как «не использующий информации о состоянии». Далее в книге мы будем использовать термин реентерабельный как грубый, ио относительно краткий синоним stateless.
Общие вопросы создания трехзвенных приложений 533 else Result := Provid’erl .GetRecords (-1, RecsOut); SetComplete; except SetAbort ; end; end; Клиент может использовать такой вызов этого метода: ClientDataSetl.Data := MyConnectionComponent.AppServer.GetCustomerRecords( False, RecsOut); 38.5. СОЗДАНИЕ КЛИЕНТСКОГО ПРИЛОЖЕНИЯ КАК КОМПОНЕНТА ACTIVEX Технология ActiveX разработана Microsoft специально для распространения работо- способных программ или компонентов по сети Internet. Поскольку процесс создания компонентов ActiveX в Delphi предельно упрощен, вы сможете без особого труда соз- дать полноценное клиентское приложение и распространять его по локальной сети или Internet с помощью обычных lFefe-браузеров, установленных на клиентских ма- шинах. В этом случае получается максимально облегченный клиент, т.к. на машине клиента нужен только ИЗД-браузер, способный работать с компонентами ActiveX (MS Internet Explorer версии 3.0 и более поздних). На машине, поставляющей клиентское приложение (это, вообще говоря, может быть любая сетевая машина, в том числе и сервер приложений), должен быть установлен и запущен ГРей-сервер, например, MS Internet Information Server, MS Personal Web Server, Netscape FastTrack Server или лю- бой другой, следующий стандартам ISAPI или NSAPI (используемая в Delphi техноло- гия WebBridge создает ИЗД-компоненты, поддерживающие оба стандарта). ГРе^-сервер не входит в комплект поставки Delphi и приобретается отдельно. Его можно также самостоятельно написать, используя компоненты страницы Internet. Для создания клиента в виде ActiveX выберите пиктограмму Active Form на страни- це ActiveX репозитория Delphi. На рис.38.13 показано диалоговое окно эксперта созда- ния Активной формы (эта форма всегда создается в виде DLL). В строке New ActiveX Name следует ввести имя Активной формы. При вводе имени автоматически меняется содержимое двух расположенных ниже строк: по умолчанию имя исполняемого модуля состоит из имени Активной формы с суффиксом Impl, а имя соответствующего проекта - с суффиксом Proj. Вы можете изменить эти умалчивае- мые значения нужным вам образом. В списке выбора строки Threading Model следует установить нужную модель потоков: • Single - для DLL выделяется один поток, поэтому все запросы клиентов выпол- няются последовательно; • Apartment - каждый экземпляр Активной формы обрабатывает один запрос кли- ента в отдельном потоке команд; экземпляры (точнее, связанные с ними дан- ные) сохраняются автоматически, но программист должен исключить возмож- ные конфликты, связанные с использованием потоками общей глобальной па- мяти; этот режим предпочтительно использовать как раз для lFefe-браузеров, поэтому он является умалчиваемым;
534 Глава 38 • Free - один экземпляр может обрабатывать несколько запросов в разных пото- ках команд; программист должен позаботиться о сохранности данных и исклю- чить возможные конфликты, связанные с общей памятью; • Both - выбирается Single или Free в зависимости от модели потоков вызываю- щей программы. Рис. 38.13. Окно создания Активной формы. С помощью переключателей в нижней части окна в DLL можно включить окно с лицензионным соглашением, номером версии и окно «О программе». Поскольку Активная форма представляет собой разновидность DLL, после нажатия ОК эксперт предложит создать соответствующий проект (рис.38.14). Рис.38.14. Сообщение эксперта: «Вы не можете добавить компонент ActiveX к текущему проекту, т.к. он не является библиотекой ActiveX. Щелкните по ОК, чтобы создать новую библиотеку». После щелчка по ОК появится обычное окно пустой формы, на которую вы долж- ны поместить связные компоненты, клиентские наборы данных и все необходимые визуальные компоненты так, как если бы вы делали обычное клиентское приложение. В заключение вам потребуется указать некоторые параметры для вашей Активной формы и создать ее. Параметры задаются с помощью опции Project | Web Deployment Options (рис.38.15). В процессе генерации Активной формы будут созданы два файла: HTML- файл и связанный с ним ОСУ-файл с компонентом ActiveX. В строке Target dir укажи-
Общие вопросы создания трехзвенных приложений 535 те каталог размещения (ЭСХ-файла с клиентским приложением, а в строке HTML dir - имя размещения итогового Я7М£-файла. На практике обычно первый и третий ката- логи одинаковы, т.к. не имеет смысла разделять HTML-файл и связанную с ним Ак- тивную форму. В строке Target URL следует указать путь размещения каталога для документов вашего FFefc-сервера в форме URL {Uniform Resource Locators - стандарт- ные идентификаторы ресурсов) Если, например, Активная форма помещается в ката- лог размещения FFefc-сервера (самый распространенный вариант), параметры будут иметь приблизительно такой вид: Target dir: C:\Inetpub\wwwroot Target URL: / HTML dir: C:\Inetpub\wwwroot (здесь C:\lnetpub\wwwroot - корневой каталог размещения FFefc-сервера). В более общем случае строка Target URL содержит URL, присвоенный вашему FFefc-cepeepy, например: Target URL: http://mymachine.myprovider.ru/ Рис. 38.15. Окно задания параметров Активной формы. Для создания компактного ОСА'-файла отметьте флажок в Use CAB file compression. Сжатие файла дает до 70% экономии дискового пространства и соответствующее снижение загрузки сети. Работа клиентского приложения поддерживается двумя DLL, которые так же следует внедрить в Активную форму. С этой целью отметьте Deploy additonal files и на странице, связанной с закладкой Additional Files, в окне Files asso-
536 Глава 38 dated with project (это окно будет недоступно, если вы не отметили Deploy additonal jiles) укажите полный маршрут доступа к файлам DBCLIENT.DLL и STDVCL32.DLL (обычно они располагаются в каталоге WINDOWSXSYSTEM). Для собственно создания Активной формы и /fZML-файла вызывается опция Proj- ect | Web deploy. Чтобы протестировать созданную Активную форму, запустите на клиентской ма- шине FFefc-браузер и укажите URL вашего Web-сервера. Если FFefc-сервер находится на машине клиента, а Активная форма расположена в корневом каталоге сервера, можно указать «зацикливающий» адрес http://127.0.0.1/MyActForm.htm (в отличие от указания Target URL в окне задания параметров Активной формы, URL в браузере должен ссылаться на имя Я7М£-файла). Появляющееся окно (рис.38.16) содержит заголовок и текст, созданные экспертом Delphi. При желании вы можете изменить эти параметры, отредактировав HTML-файл любым текстовым редактором. Рис. 38.16. Пример Активной формы с набором данных. Замечание. При экспериментах с Активной формой учтите, что популярный браузер MS Internet Explorer версии 3.0 кэширует OGY-файлы в каталоге WINDOWS/OCCACHE, поэтому перед очередным тестированием приложе- ния удалите ее предыдущую копию из этого каталога. Входящий в Windows 98 IE версии 4.0 не кэширует файлы.
Часть 4 НОВЫЕ ВОЗМОЖНОСТИ DELPHI 5
Глава 39 РАБОТА С ADO Delphi 5 поддерживает технологию ADO (ActiveX Data Objects - объекты данных, построенные как объекты ActiveX), которая усиленно развивается корпорацией Micro- soft. На основе этой технологии созданы соответствующие компоненты-наборы TADOTable, TADOQuery, TADOStoredProc, повторяющие в функциональном отноше- нии компоненты ТТаЫе, TQuery, TStoredProc, но не требующие развертывания и на- стройки на клиентской машине BDE. Основным достоинством ADO является ее естественная ориентация на создание «облег- ченного» клиента. В рамках этой технологии на машине разработчика БД устанавливаются базовые объекты MS ADO и соответствующие компоненты Delphi, обеспечивающие исполь- зование технологии ADO (эти установки осуществляются автоматически при развертывании Delphi). На машине сервера данных (это может быть файловый сервер в рамках файл/серверной технологии или машина с сервером данных - в технологии клиент/сервер) устанавливается так называемый провайдер данных - некоторая надстройка над специальной технологией OLE DB, «понимающая» запросы объектов ADO и «умеющая» переводить эти запросы в нужные действия с данными. Взаимодействие компонентов ADO и провайдера осуществляется на основе универсальной для Windows технологии ActiveX, причем провай- дер реализуется как COM-сервер, а/DO-компоненты - как СОМ-клиенты. Рис.39.1. Реализация технологии ADO в Delphi На машине сервера создается и размещается источник данных. В случае файл/серве- рных систем отдельные таблицы типа dBASE, FoxPro, Paradox и т.п. должны управляться соответствующим ODBC-драйвером, а в роли провайдера используется Microsoft OLE DB Provider for ODBC drivers. Если по каким-либо причинам не найден нужный драйвер файл/серверные таблицы можно перенести в формат MS Access 97. На их основе создается единый файл, содержащий все необходимые таблицы, индексы, хранимые процедуры и прочие элементы БД. Такой файл управляется машиной баз данных Microsoft Jet 4.0 Data- base Engine, а в роли провайдера используется Microsoft Jet 4.0 OLE DB Provider. Если используется промышленный сервер данных Oracle или MS SQL Server, дан- ные не нуждаются в какой-либо предварительной подготовке, а в роли провайдера используется соответственно Microsoft OLE DB Provider for Oracle или Microsoft OLE DB Provider for SQL Server. Нетрудно обнаружить и явный недостаток такой техноло- гии: ADO не может использоваться, если для соответствующей структуры данных (в частности, для БД многих популярных серверов - InterBase, Informix, DB2 и пр. не соз- дан нужный провайдер или ODBC драйвер).
540 Глава 39 На машине клиента располагаются связные компоненты TADOConnection и компо- ненты-наборы данных TADOTable, TADOQuery, TADOStoredProc, а также не показан- ные на рисунке компоненты-наборы TADODataSet и командные компоненты TADO- Command. Каждый из этих компонентов может связываться с провайдером данных либо с помощью связного компонента TADOConnection, либо минуя его и используя собственное свойство ConnectionString. Таким образом, TADOConnection играет роль концентратора соединений с источником данных компонентов-наборов и в этом смысле подобен компоненту TDatabase в традиционной архитектуре с BDE. Компоненты-наборы TADODataSet в функциональном плане повторяют свойства уже известных из предыдущих версий Delphi компонентов TClientDataSet технологии MIDAS. Командные компоненты TADOCommand предназначены для реализации за- просов на языке управления данными DDL (Data Definition Language), т.е. для реали- зации 5б£-запросов, которые не возвращают данные (запросы типа create, drop, update и т.п.). Специальный компонент RDSComection (не показан на рисунке) соз- дан для упрощения связи с MS Internet Explorer и предназначен для разработки интра- нет-приложений. Компоненты-наборы с помощью хорошо известных по предыдущим версиям Del- phi компонентов-источников TDataSource и визуализирующих компонентов TDBGrid, TDBMemo, TDBEdit и т.п. обеспечивают необходимый интерфейс с пользователем программы. Замечание. Ничто не дается бесплатно - эта старая истина во многом от- носится и к ADO. Скорость доступа к данным с помощью СОЛ/-средств (а технология ActiveX, являющаяся краеугольным камнем ADO, целиком базируется на СОМ) в общем случае оказывается заметно ниже традици- онного для Delphi механизма на основе BDE (для некоторых типичных случаев скорость уменьшается в десятки раз). 39.1. ТЕСТОВАЯ ПРОГРАММА Для иллюстрации основных приемов работы с ADO создадим простую программу для отображения реляционной связи таблиц Clients .dbf и Holdings .dbf из демон- страционной БД dbdemos, файлы которой обычно располагаются в каталоге C:\Program Files\Common Files\Borland SharedXData. 1. Создайте новое приложение Delphi. 2. Воспользуемся новыми возможностями Delphi (конструктором модуля дан- ных): щелкните по пиктограмме П или выберите File | New, затем выберите закладку New и дважды щелкните по пиктограмме Data Module. Подробнее о работе с конструктором модуля данных см.п.4.4. 3. На экране появится окно конструктора модуля данных, в которое следует по- местить компонент TAdoConnection, 2 компонента TAdoTable и 2 связных ком- понента TDataSource (рис.39.2). Заметим, что компоненты в левой части окна будут обведены красной рамкой до тех пор, пока вы не установите для них необходимые свойства. 5. Поскольку предполагается использовать файл/серверные таблицы типа dBASE (тип в данном случае определяется расширением dbf соответствующих фай- лов), в рамках ADO такие таблицы должны управляться драйвером ODBC. Этот
Работа с ADO 541 драйвер следует предварительно настроить на работу с нужным каталогом. Для этого щелкните по кнопке Пуск, выберите Настройка | Панель управления ODBC Data и щелкните по пиктограмме Somces.[32bit], Рис. 39.2. Модуль данных с нужными компонентами 5. В появившемся окне (рис.39.3,а) выберите в списке User Data Sources строку dBASEfiles) щелкните по кнопке Configure, затем в новом окне (рис.39.3,б) - по кнопке Select Directory и с помощью нового диалогового окна установите ката- лог размещения файлов, после чего закройте все окна кнопками ОК. При указа- нии каталога вам, возможно, придется сослаться на сетевой серверный диск. В этом случае щелкните по кнопке Сеть и запишите сетевое имя диска, например \\SRV\C а) б) Рис. 39.3. Настройка драйвера ODBC: а) - выбор драйвера; б) - выбор источника данных 1 В некоторых конфигурациях Delphi инсталлирует не все ODBC-драйверы. В этом случае в окне Us- erDSN вы можете не увидеть строки dBase files. Если это так, щелкните по кнопке A dd и в появившемся списке выберите Microsoft dBase Driver. В конечном счете будет открыто окно рис.39.3,б н вы сможете на- строить драйвер.
542 Глава 39 \ 6. Теперь следует настроить связь объектов ADO с соответствующим провайде- ром. Для этого дважды щелкните по компоненту AdoConnectionl или щелкните по нему один раз, выберите в окне Инспектора Объектов свойство Connection String и щелкните по кнопке в правой части строки этого свойства, чтобы вы- звать окно настройки связи (рис.39.4). 7. Щелкнете по кнопке Build и на закладке Provider нового окна (рис.39.5,а) выбе- рите строку Microsoft OLE DB Provider for ODBC Drivers, затем перейдите на закладку Connection и с помощью списка выбора Use data source пате устано- вите тип используемых данных: dBASE Files (рис.39.5,б). Рис.39.5. Настройка связи: а) - выбор провайдера; б) - выбор типа данных
Работа с ADO 543 8. Поскольку ранее мы уже настроили драйвер OBDC, можно щелкнуть по кнопке Test Connection, чтобы убедиться в нормальном функционировании связи. За- кройте окно кнопкой ОК и нажмите ОК еще раз - связь готова! Чтобы в даль- нейшем связь с БД устанавливалась без промежуточного диалога, в котором за- прашивается имя пользователя и пароль, поместите в значение LoginPrompt компонента AdoConnectionl значение False. 9. Теперь пора настроить оставшиеся компоненты. В строках Connection компо- нентов AdoTablel и AdoTable2 сошлитесь на компонент AdoConnectionl (этого не нужно делать, если вы поместили таблицы непосредственно на дерево ком- понентов модуля данных - в этом случае связи устанавливаются автоматиче- ски). В свойстве AdoTablel.TableName выберите таблицу clients, а в таком же свойстве AddoTabl2 - таблицу HOLDINGS. В свойствах DataSet компонентов DataSourcel и DataSource2 сошлитесь соответственно на AdoTablel и Ado- ТаЫе2. 10. Свяжем таблицы отношением главная-детальная: установите в свойство Mas- terSource таблицы AdoTable2 ссылку на DataSourcel и щелкните по кнопке в свойстве MasterFields, чтобы вызвать окно настройки (рис. 39.6). Рис.39.6. Окно настройки таблиц на связь главный-детальный Установите связь по общем полю ACCT NBR и закройте окно щелчком по кноп- ке ОК. 11. Щелкните по закладке Data Diagrams модуля данных и «стащите» в связанное с ней окно мышью таблицы AdoTablel и AdoTable2 - между ними тут же отобра- зится установленная реляционная связь (рис.39.7). 12. Сохраните на диске модуль данных (Unit2) под именем dmAdo, основную фор- му программы (Unit!) под именем fmAdo и сам проект - Ado.
544 Глава 39 Рис. 39.7. В модуле данных отображается реляционная связь главный-детальный 14. Перед тем как покинуть модуль данных, откройте таблицы AdoTablel и Ado- Tqble2. 15. Перейдите к основной форме finAdo и с помощью File | use Unit свяжите ее с модулем dtnAdo (установите значения True в их свойства Active). 16. Положите на форму две сетки TDBGrid и свяжите одну с DataSourcel, а другую - с DataSource2. Поскольку компоненты-таблицы открыты, сразу после уста- новления связей сетки наполнятся нужными данными (рис.39.8). ГЕЕ Пример реляционной связи таблиц ADO LAST_NAME FIRST.NAME ACCT.NBR | ADDRESS_1 ► Davis Jennifer 10234951100 Cranberry St. Jones Arthur 2094056J0 Hunnewell St Parker Debra 1209395 74 South St Sawyer Dave 3094095,101 Oakland St -11Z1 CITY , u Wellesley Los Altos J ^Atherton Los Altos ACCT.NBR. SYMBOL SHARES, - PUR_PRICE PUR_DATE ► 1023495 1023495 VG_ 44GT 2000 1000 __20 30 02.04.1993 20.01.1993 ~ 1023495 CIN 1500 38 09.09.1993 1023495 EMC 500 30 01.02.1990 Puc. 39.8. Основная форма программы
Работа с ADO 545 Программа готова. Вы можете перенести ее на любую сетевую машину с Windows 32, но без BDE, и убедиться в ее работоспособности. 39.2. УСТАНОВЛЕНИЕ СВЯЗИ С ADO Установление связи с ADO является ключевым моментом всей технологии. Как уже отмечалось, каждый компонент-набор-данных имеет два свойства, с помощью которых он может установить связь с ADO: Connection и ConnectionString. В первое помещается ссылка на специальный связной компонент TAdoConnection, играющий роль концентратора соединения с ADO, во вторую - собственно строка связи. Эти свойства взаимоисключающие, т.е. установка значения в одно из них ведет к очистке второго. 39.2.1. Структура строки связи Создаваемая тем или иным способом строка связи несет в себе множество (не ме- нее двух) параметров, разделенных друг от друга точкой с запятой. Собственно в тех- нологии ADO используются лишь четыре из них, остальные нужны для идентифика- ции пользователя при доступе к серверным БД и для настройки некоторых параметров О£>ВС-драйверов. 39.2.2. Формирование строки связи При нажатии кнопки в строке свойства ConnectionString компонента TAdoConnec- tion или компонентов-наборов появляется окно, показанное на рис.39.9. Рис.39.9. Диалоговое окно для формирования соединения с ADO У программиста есть две возможности: сослаться на специальный связной файл в первой строке или сформировать описание связи во второй. В первом случае можно использовать один и тот же файл сразу для нескольких соединений, поэтому измене- ние файла отразится на многочисленных связях, причем, возможно, не в одной про- грамме. Изменение содержимого во второй строке окна влияет только на соответст- вующий компонент-набор или только на те компоненты, которые будут ссылаться на данный связной компонент и только внутри одной единственной программы.
546 Глава 39 Выбор Use Data Link File делает доступным кнопку Browse. Выбор Use Connection String позволяет с помощью кнопки Build воспользоваться специальным диалоговым окном. После нажатия Browse создается окно, показанное на рис.39.10. Рис. 39.10. Окно выбора связного файла Как явствует из комментария, с помощью этого окна можно не только выбирать, но модифицировать и создавать новые связные файлы (в переводе этот комментарий зву- чит так: Чтобы создать новый связной файл, щелкните правой кнопкой мыши в области списка файлов и выберите из локального меню продолжение Соз- дать | Microsoft Data Link. Чтобы модифицировать связной файл, дважды щелкните по нему мышью. Последняя фраза не точна: чтобы отредактировать файл нужно щелкнуть по нему пра- вой кнопкой мыши и в локальном меню выбрать Свойства. Поскольку оба способа используют одинаковое диалоговое окно, мы вначале рас- смотрим диалоговый способ формирования связи. Диалоговый способ формирования связи Для диалогового способа формирования связи выберите переключатель Use Con- nection String и нажмите кнопку Build. На экране появится диалоговое окно с четырьмя закладками, изображенными на рис.39.11.
Работа с ADO 547 C Data Link Properties vedl Al Jest Connection I Справка $6 Data Link Properties Provider | Connecvon j Advanced '*1 | lhe ndwoAvn ргчичвх to We of r.T Uwe Er* V»e brt*“ Dare Srww Extended Properties Jet OLEDB:Compact Without Replica.. False Jet OLEDB:Create System Database Jet OLEDB:Database Locking Mode Jet OLEDBiDatabase Password Jet OLEDB:Dont Copy Locale on Co... Jet OLEDB:Encrypt Database Jet OLEDB:Engine Type Jet OLEDB:Global Bulk Transactions Jet OLEDB:Global Partial Bulk Ops Jet 0LEDB:New Database Password .lf»t П| FDA R*ni*hn P*th CADAUUdo.ds False False False 0 OK I Отмена I Справка I A"ow j jvng рат*^ * 2 [i X? Рис.39.11. Закладки диалогового окна настройки связи с провайдером Microsoft Jet 4.0 OLE DB Provider Закладка Provider используется для выбора механизма, который будет непосредст- венно взаимодействовать с данными, получая их от клиента и посылая их ему. Факти- чески, в списке показывается состав OLE DB провайдеров, установленных на вашей машине. Выбор провайдера является определяющим фактором. Для разных типов
548 Глава 39 данных должны использоваться только строго определенные провайдеры. Например, в файл/серверных БД должен использоваться Microsoft OLE DB Provider for ODBC driver, с предварительной настройкой соответствующего драйвера. Для работы с БД Access 97 выбирается провайдер Microsoft Jet 4.0 OLE DB Provider. Если используется сервер Oracle или MS SQL Server, БД работает совместно соответственно с провайде- ром Microsoft OLE DB Provider for Oracle или Microsoft OLE DB Provider for SQL Server и т.д. Некоторые типы данных (например, БД InterBase или Informix SQL Server) не имеют собственных провайдеров и поэтому (пока на будут созданы нужные про- вайдеры) не могут использоваться в технологии ADO. Поскольку большинство других связных параметров зависят от провайдера, содержимое трех других закладок также зависит от этого фактора. В этом разделе описывается содержимое закладок при вы- боре провайдера Microsoft Jet 4.0 OLE DB Provider. Закладка Connection определяет необходимые связные параметры для выбранного провайдера. В строке Select or enter a database пате требуется указать полный мар- шрут доступа к файлу БД (с указанием сетевого каталога). В качестве дополнительной информации на этой закладке указывается входное имя пользователя и пароль. Для простейшего варианта связи с Microsoft Jet 4.0 OLE DB Provider этих параметров вполне достаточно, поэтому с помощью кнопки Test Connection можно протестировать созданную связь. Для других провайдеров на закладке требуется задать ряд дополни- тельных параметров, например, имя используемой базы данных, входное имя и пароль для доступа к серверу БД и т.п. Закладка Advanced позволяет задать некоторые дополнительные свойства связи. Для большинства провайдеров на этой закладке доступны только переключатели Ac- cess permission, определяющие права доступа к данным, и строка Connect Timeout, с помощью которой можно определить максимальную паузу в обмене данными между провайдером и клиентом. Поскольку переключатели доступа - независимые, можно задать любую комбинацию режимов доступа: • Read - только чтение; • ReadWrite - чтение и запись; • Share Deny None - ничего нельзя использовать совместно несколькими клиентами; • Share Deny Read - нельзя совместно использовать данные, открытые в режиме чте- ния; • Share Deny Write - нельзя совместно использовать данные, открытые в режиме за- писи; • Share Exclusive - нельзя совместно использовать данные, открытые в режиме чте- ния и/или записи; • Write - только запись. На закладке АП приводятся все параметры связи - как заданные явно, так и назна- ченные по умолчанию. С помощью кнопки Edit Value значение любого параметра можно отредактировать. После нажатия ОК диалоговое окно формирования параметров связи будет закры- то и в нижней строке рис.39.9 появится соответствующий текст.
Работа с ADO 549 Формирование связного файла Как уже говорилось, окно рис.39.10 может создать и отредактировать связной файл: для редактирования нужно щелкнуть по файлу правой кнопкой мыши и в ло- кальном меню выбрать Свойства-, мы увидим диалоговое окно с уже знакомыми за- кладками (рис.39.12). Рис.39.12. Окно Свойства для связного файла Создать и отредактировать связной файл можно также с помощь Проводника Win- dows (в ^русифицированной версии - Windows Exlorer). Вызовите Проводник, выбе- рите папку, в которой будет размещаться связной файл, щелкните по содержимому папки правой кнопкой, в локальном меню выберите Создать и в дополнительном ме- ню - Microsoft Data Link (рис.39.13).
550 Глава 39 Обзор Data Links Г0ЕЗ Файл Цраркг Вид Переда Избранное Сервис Справа Назад п - Вверх | Вырезать Копировать Вставить Отменить | Удалить Адрес jQ C:\Program Files\Common Files\SYSTEM\ole dbXData Links Вее папки □ Д Program Files Й-Pl Accessories Й-Pl Beavis Ш-Pl Borland , Pl Chat i В Pl Common Files | Й Q Borland Sh I В-Pl Microsoft S |JfC3 Odbc l-Pl Services Fl-Pl System |-Q Ado -P1 Msadc Fl-Pl oledb Новый Microsoft Data Link У| Точе чный рисунок 1*1 Пзсшивка Microsoft Office' |*] Дйгиешж»мнп>1 Office '*Ь У) MierOVcA Access - базе цанньЛ Еда Я встроить вид пап» и Упорядочить о тачки { “ВИТЬ Встав» х Puc.39.13.Создание связного файла с помощью Проводника После того, как файл будет создан, просто дважды щелкните по нему: по умолча- нию связные файлы имеют расширение UDL, с которым связан диалог создания и ре- дактирования связи, показанный на рис.39.11 (вместо двойного щелчка моно вызвать в локальном меню Свойства). Замечание. Поскольку ADO активно использует технологию ActiveX, обмен текстовыми данными с такого рода объектами возможен только в формате WideString (по два байта на каждый символ) - именно в таком формате и будет создан связной файл. 39.3. ОСОБЕННОСТИ ИСПОЛЬЗОВАНИЯ КОМПОНЕНТОВ ADO В этой главе рассматриваются специфические особенности работы с компонентами ADO, расположенными на соответствующей странице палитры компонентов Delphi.
Работа с ADO 551 39.3.1. Базовые объекты ADO При описании особенностей технологии ADO мы часто будем употреблять терми- ны базовые объекты ADO и компоненты ADO. Базовые объекты произведены корпо- рацией Microsoft и вставляются в Delphi, если установлен соответствующий флажок вы- бора в диалоговом окне External Installation Options в процессе начальной установки Delphi (см. рис.39.14). External Installation Options Setup has determined that there are adds anal programs needed for Л of ths product’s features to be fJty functional You can let this installation run these retail program: at the completion of U ns install or you can run them directly from the CD at a laler txne. р Irislal IriwBase Client I* Install Microsoft Data Access Components 2.1 ,2 . f __CanceJ Рис.39.14. Выбор технологии ADO при развертывании Delphi На заметку. При удалении Delphi базовые объекты ADO не удаляются, поэтому при повторной установке Delphi окно рис.39.14 не содержит пе- реключателя Install Microsoft Data Access Components 2.1.2. Базовых объектов семь: Connection, Recordset, Command, Parameter, Field, Error и Property. Компоненты ADO в палитре компонентов Delphi представляют собой надстройки над базовыми объектами, экспонируя большинство их свойств, ме- тодов и событий в привычном для Delphi виде. Базовые объекты обычно сопровождаются коллекциями связанных с ними объек- тами. Например, с Connection может быть связан один или несколько объектов Er- ror, фиксирующий (-ие) ошибки установления связи. С Recordset - набор объектов Field, каждый из которых определяет единственное поле результирующего набора данных. С Command - один или несколько объектов Parameter, конкретизирующие выполнение метода Execute этого объекта, и коллекция объектов Error и т.д. В связи С этим говорят об основных (Connection, Recordset, Command) и вспомогательных (Error, Field, Parameter, Property) объектах ADO.
552 Глава 39 Connection Объект Connection предназначен для установления соединения с данными - это его главная задача. Кроме того, объект обеспечивает механизм транзакций. С объек- том связан набор объектов Error, в котором фиксируются все ошибки, связанные с работой Connection. Кроме того, на него может ссылаться произвольное количество объектов Command и Recordset. В этом случае Connection управляет транзакциями этих объектов. Recordset Объект Recordset представляет собой текущий набор данных. Он может быть по- лучен только после выполнения метода Execute какого-либо объекта Command. С объектом автоматически связывается набор объектов Field, в которых описываются все поля НД. Recordset способен хранить нужные записи, перемещаться по ним, до- бавлять, уничтожать и редактировать записи как в обычном режиме (при одновремен- ном изменении физических ТБД), так и в пакетном режиме (т.е. при кэшировании данных). При создании объекта автоматически создается и связанный с ним курсор, обеспечивающий просмотр, редактирование и изменение записей. Command С помощью объекта Command пользователь может выполнить над данными любую SQL-команду. С ним может быть связан набор объектов Parameter, в котором описы- ваются соответствующие параметры, необходимые для выполнения запроса. Харак- терной особенностью объекта является возможность асинхронного выполнения свя- занной с ним команды. При обнаружении ошибки с объектом связывается своя кол- лекция объектов Error. На заметку. Объекту Commands соответствуют целых четыре компонен- та Delphi - TADOCommand, TADOQuety TADOTable и TADOStoredProc. Сделано это, судя по всему, в целях унификации с компонентами страни- цы Data Access, хотя даже в этом случае возможности TADOCommand нельзя считать уникальными по сравнению с возможностями TADOQuery и наоборот. Parameter Объект Parameter определяет единственный параметр, который будет использо- ваться при выполнении метода Execute объекта Command - его тип, размер и использо- вание (входной, выходной, входной и выходной или только для чтения). При необхо- димости с Command можно связать коллекцию объектов Parameter для указания мно- жества параметров. Error Коллекция объектов Error хранит все ошибки, связанные с работой остальных объектов и, прежде всего, Connection, Command и Recordset.
Работа с ADO 553 Field Этот объект хранит всю необходимую информацию об одном поле НД. Поскольку обычно НД содержит несколько полей, с объектом Recordset связана коллекция объ- ектов Field. С любым полем Field можно связать произвольную коллекцию объек- тов Property, определяющих индивидуальные характеристики поля. Property Объект Property может быть связан с любым другим объектом ADO, кроме объ- ектов Connection и Error. Он может хранить как статические, так и динамические свойства. Статических (т.е. заранее заданных) свойств у объекта всего четыре: Name, Type, Value, Attributes. Остальные свойства - динамически и создаются в ходе выпол- нения программы. В объекте Property задаются некоторые индивидуальные характе- ристики связанного с ним объекта ADO. 39.3.2. Связной компонент TADOConnection Этот компонент осуществляет связь остальных компонентов с ADO. Для этих це- лей у него имеется строка ConnectionString. После того, как с помощью этой строки связь с данными установлена, на компонент могут ссылаться другие ADO- компоненты, разделяя установленную им связь. Однако компонент TADOConnection может выполнять гораздо более широкую роль, чем простая концентрация соединения. С помощью своих свойств и методов он может осуществлять тонкую настройку соединения, обеспечивать необходимый уро- вень изоляции транзакций, управлять транзакциями и т.д. Для установления связи нужно с помощью строки ConnectionString сформировать связные параметры и затем установить значение True в свойство Active или вызвать метод Open. Для разрыва связи выполняется метод Close компонента или в его свойст- во Active устанавливается значение False. Компонент содержит в свойстве ConnectionObject ссылку на базовый Л£>О-объект, с помощью которого и работает сам компонент. Это свойство открывает возможности детального управления связью, если, разумеется, программист хорошо знаком с тех- никой ADO. В свойствах CommandCount и DataSetCount содержится количество соответствую- щих объектов, которые обслуживаются данным компонентом. В сочетании со свойст- вами Commands и DataSets программист может получить доступ к любому интере- сующему его объекту. Например: var i: Integer; begin for i := 0 to (ADOConnectionl.DataSetCount) do ADOConnectionl.DataSets[i].Open; end; С помощью методов GetProcedureNames и GetTableNames можно получить список всех хранимых процедур и таблиц. Например: AdoConnectionl.Open; AdoConnectionl.GetTableNames(ListBoxl.Items)
554 Глава 39 Важной особенностью компонента является возможность управления с его помо- щью транзакциями. Для этого в состав компонента добавлены соответствующие мето- ды и события. С помощью метода BeginTrans стартует новая транзакция, методы CommitTrans и RollbackTrans подтверждают или отменяют ее действие. Разрешается произвольная глу- бина вложенности транзакций, т.е. после старта одной транзакции может немедленно стартовать следующая и т.д. Уровни разграничения транзакций (свойство IsolationLevel) несколько отличаются от аналогичных уровней BDE и в некоторых случаях могут не под- держиваться сервером БД. Транзакция, стартующая с помощью компонента TADOConec- tion, разделяется всеми другими связанными с ним компонентами. С помощью свойства InTransaction программа может определить, завершилась ли ранее начатая транзакция. Свойства Свойство Назначение type TXactAttribute = (xaCommitRetaining,xaAbortRetaining); TXactAttributes = eat of TXactAttrib-, ute; property Attributes: TXactAttributes; Показывает, будет ли объект запоминать под- тверждение и/или откат транзакций: xaCom- mitRetaining - запоминает подтверждение; xaAbortRetaining - запоминает откат. property Commandcount: Integer; Указывает количество командных компонен- тов, которые ссылаются на него. property Commands[Index: Integer]: TADOCommand; Открывает индексированный доступ к ко- мандным компонентам. | property CommandTimeout: Integer; Указывает (в секундах) предельно допустимое время выполнения команды. По умолчанию равно 30. property Connected: Boolean; Определяет, связан ли компонент с набором данных. property Connectionobject: —Connection; Содержит ссылку на базовый связной объект Connection технологии ADO, оболочкой для которого является компонент TADOConnec- tion. property Connectionstring: Wide- String; Содержит связную строку. property ConnectionTimeout: Integer; Указывает (в секундах) предельно допустимое время установления связи. По умолчанию равно 15. TConnectOption - (coConnectUnspeci- fied, coAsyncConnect); property ConnectOptions: TConnectOp- tion; Указывает, будет ли связь синхронной (coConnectUnspecified) или асинхронной (coAsyncConnect). Асинхронная связь может частично сгладить потери времени при мед- ленных серверах. type TCursorLocation - (clUseServer, clUseClient); property CursorLocation: TCursorLoca- tion; Определяет, будут ли курсоры создаваться клиентом (clUseClient) или сервером (clUse- Server). Обычно используются более гибкие клиентские курсоры. Серверные могут быть эффективнее при больших выборках. property DatasetCount: Integer; Указывает количество связанных с компонен- том наборов данных.
Работа с ADO 555 property DataSets[Index: Integer]: TCustomADODataSet; Открывает индексированный доступ к компо- нентам-наборам. property DefaultDatabase: WideString; Указывает умалчиваемую БД. property Errors: Errors; Содержит коллекцию ошибок, выявленных при работе компонента. property InTransaction: Boolean; Содержит True, если компонент поддерживает хотя бы одну еще незавершенную транзакцию. type TIsolationLevel = (ilUnspeci- fied, ilChaos, ilReadUncommitted, il- Browse, ilCursorStability, ilReadCom- mitted, ilRepeatableRead, ilSerializ- able, illsolated); property IsolationLevel: TIsolation- Level; Определяет уровень разграничения транзакций: UUnspecified - сервер использует другой уро- вень, чем тот, что был затребован; ilChaos - попытка установить более высокий уровень, чем при установлении связи; ilReadUncom- mitted, ilBrowse - видны неподтвержденные изменения; ilCursorStability, ilReadCommitted - изменения становятся видны только после их подтверждения; ilRepeatableRead - конкури- рующие транзакции оперируют собственными копиями данных; ilSerializable, illsolated - тран- закции полностью изолированы друг от друга. type TConnectMode = (cmUnknown, cmRead, cmWrite, cmReadWrite, cmShareDenyRead, cmShareDenyWrite, cmShareExclusive, cmShareDenyNone); property Mode: TConnectMode; Описывает набор возможностей для установ- ленного соединения: cmUnknown - связь уста- новлена с ошибками или не установлена во- все; cmRead - связь разрешает только чтение данных; cmWrite - связь разрешает только запись данных; cmReadWrite - связь разрешает чтение и запись данных; cmShareDenyRead - запрещаются другие связи с возможностью чтения; cmShareDenyWrite - запрещаются дру- гие связи с возможностью записи; cmShareExclusive, cmShareDenyNone - запре- щаются любые другие связи. property Properties: WideString; Определяет коллекцию свойств для прямого доступа к ADO. property Provider: WideString; Определяет провайдер данных. type TObjectState = (stClosed, stOpen, stConnecting, stExecuting, stFetching); property TObjectStates = set of TOb- jectState; Указывает текущее состояние связи: stClosed - связь закрыта и не установлена; stOpen - связь установлена и открыта; stConnecting - компо- нент пытается установить связь; stExecuting - в рамках связи выполняется команда; stFetching - в рамках связи наполняется набор данных. property Version: WideString; Указывает используемую версию ADO. Методы Метод Назначение function BeginTrans: Integer; Инициирует новую транзакцию в связанной БД. Возвращает уровень вложенности вновь начатой транзакции (1,2 и т.д.). procedure Cancel; Разрывает установленную ранее асинхронную связь.
556 Глава 39 procedure CloseDataSets(All: Boolean = True); Закрывает набор данных, с которым установ- | лена связь, но не разрывает связь с БД, Если АН имеет значение True, закрываются все НД, в противном случае не закрываются только те из них, для которых на клиентской стороне созданы курсоры. procedure CommitTrans; Подтверждает текущую транзакцию и закры- вает ее. Type TExecuteOption = (eoAsyncExecute, eoAsyncFetch, eoAsyncFetchNonBlocking, eoExe- cuteNoRecords); TExecuteOptions = set of TExe- cuteOption; function Execute(const CommandText: WideString; Executeoptions: TExe- cuteOptions = []): _RecordSet; over- load; procedure Execute(const CommandText: WideString; var RecordsAffected: In- teger; .Executeoptions: TExecuteOp- tions = [ebExecuteNoRecords]) ; over- load; Выполняет команду над связанным набором данных: CommandText - текст команды; Ехе- cuteOptions'. уточняющие опции; -RecordSet - возвращаемый набор данных; RecordsAffected - переменная, в которой возвращается количе- ство успешно выполненных команд. Уточ- няющие опции имеют следующий смысл: eoAsyncExecute - команда выполняется асин- хронно; eoAsyncFetch - после передачи коли- чества записей, достаточного для заполнения кэша, остальные записи передаются асин- хронно; eoAsyncFetchNonBlocking - команда . выполняется без блокирования потока; eoExecuteNoRecords - команда не должна воз- вращать записи; если записи все-таки возвра- щаются, они отвергаются. procedure GetProcedureNames(List: TStrings); В переменной List возвращает список всех зарегистрированных в БД хранимых процедур. procedure GetTableNames(List: TStrings; SystemTables: Boolean = False); В переменной List возвращает список всех ТБД. Если SystemTables имеет значение True, в список включаются также служебные табли- цы. procedure RollbackTrans; Отменяет текущую транзакцию. События Событие С чем связано property AfterConnect: TNotifyEvent; Возникает после установления связи. property AfterDisconnect: TNoti- fyEvent; Возникает после разрыва связи. property BeforeConnect: TNotifyEvent; Возникает перед установлением связи. property BeforeDisconnect: TNoti- fyEvent; Возникает перед разрывом связи. TEventStatus = (esOK, esErrorsOc- cured, esCantDeny, esCancel, esUn- wantedEvent); TBeginTransCompleteEvent = proce- dure (Connection: TADOConnection; TransactionLevel: Integer; const Er- ror: Error; var Eventstatus: TEvent- Status) of object; property OnBeginTransComplete: TBe- ginTransCompleteEvent read FOnBegin- TransComplete write FOnBeginTransCom- plete; Возникает перед завершением транзакции. TransactionLevel - содержит уровень завер- шенной транзакции; EventStatus - статус за- вершения: esOK - без ошибок; esErrorsOccured - были обнаружены ошибки; esCantDeny - текущая связь не может быть завершена; esCancel - текущая асинхронная связь была прервана; esUnwantedEvent - была ошибка.
Работа с ADO 557 TConnectErrorEvent = proce- dure (Connection: TADOConnection; Er- ror: Error; var Eventstatus: TEvent- Status) of object; property OnCommitTransComplete: TCon- nectErrorEvent read FOnCommitTransCom- plete write FOnCommitTransComplete; Возникает после подтверждения транзакции. property OnConnectComplete: TConnec- tErrorEvent read FOnConnectComplete write FOnConnectComplete; Возникает в момент установления связи. TDisconnectEvent - procedure (Connection: TADOConnection; var Event- Status: TEventStatus) of object; property OnDisconnect: TDisconnectEvent read FOnDisconnect write FOnDisconnect; Возникает при разрыве связи. TExecuteCompleteEvent = procedure (Connection: TADOConnection; Record- sAffected: Integer; const Error: Er- ror; var Eventstatus: TEventStatus; const Command: _Command; const Re- cordset: _Recordset) of object; property OnExecuteComplete: TExecute- CompleteEvent read FOnExecuteComplete write FOnExecuteComplete; Возникает после выполнения команды. TInfoMessageEvent = procedure (Connection: TADOConnection; const Error: Error; var Eventstatus: TE- ventStatus ) of object; property OnlnfoMessage: TInfoMes- sageEvent read FOnlnfoMessage write FOnlnfoMessacje; Возникает при получении связным компонен- том информационного сообщения от провай- дера. property OnRollbackTransComplete: TConnectErrorEvent read FOnRollback- TransComplete write FOnRollback- TransComplete; Возникает после отката текущей транзакции. TWillConnectEvent = procedure (Connection: TADOConnection; var Connec- tionstring, UserID, Password: WideString; var ConnectOptions: TConnectOption; var Eventstatus: TEventStatus) of object; property OnWillConnect: TWillConnec- tEvent read FOnWillConnect write FOn- WillConnect; Возникает перед установлением связи. TWillExecuteEvent = procedure (Connection: TADOConnection; var Com- mandText: WideString; var CursorType: TCursorType; var LockType: TADOLock- Type; var Executeoptions: TExecuteOp- tions; var Eventstatus: TEventStatus; const Command: —Command; const Re- cordset: —Recordset) of object; property OnWillExecute: TWillExe- cuteEvent read FOnWillExecute write FOnWi1lExecute; Возникает перед выполнением команды. 39.3.3. Компонент TADOCommand Этот компонент предназначен, в основном, для реализации б^Л-запроеов, не возвра- щающих никаких данных (подмножество Data Definition Language - язык определения
558 Глава 39 данных языка структурированных запросов SQL). К предложениям DDL относятся прак- тически все, которые не начинаются зарезервированным словом select. Замечание. Хотя конкретная реализация транслятора SQL зависит от вы- бранного провайдера, однако в целом компоненты ADO при реализации этого языка следуют промышленному стандарту SQL-92. Исполнение подобного рода запросов идет несколько иначе, чем запросов select. В BDE-ориентированных компонентах TQuery для реализации запросов select использу- ется метод Open (или свойство Active), в то время как DDL запросы выполняются методом ExecSQL. В ADO для этих целей выделен специальный компонент. Хотя, как мы увидим дальше, он способен при некоторых обстоятельствах возвращать наборы данных, а ком- понент TADOQuery имеет в своем составе метод ExecSQL, позволяющий ему выполнять DDL запросы. Иными словами, одни и те же запросы в рамках ADO можно выполнять с помощью двух разных компонентов - как TADOCommand, так и TADOQuery. Замечание. На самом деле - даже трех: рассмотренный выше связной компонент TAdoConnection также способен выполнять команду. Напри- мер: procedure TForml.ButtonlClick(Sender: TObject); begin AdoConnectionl.Connectionstring := 'Provider=Microsoft.Jet.OLEDB.4.0;'+ 'Data Source=C:\DATA\dbdemos.mdb'; AdoConnectionl.Connected := True; AdoDataSetl.RecordSet AdoConnectionl.Execute('SELECT * FROM CUSTOMER') end; Текст исполняемой команды хранится в свойстве CommandText компонента. Ком- понент способен за один раз исполнять одну и только одну команду. Особенностью TADOCommand является специализированный текстовый редактор, с помощью кото- рого можно сформировать команду. Этот редактор (рис.39.15) вызывается после щелчка по кнопке в строке свойства CommandText Инспектора Объектов. Все поле редактора поделено на три части. В левой верхней части отображается список таблиц БД, с которой связан компонент, в нижней левой - список полей для выделенной таблицы, всю остальную часть занимает собственно текстовый редактор. Справочные окна в левой части лишь облегчают набор текста, который, в основном, формируется вручную в правом поле. Например, чтобы набрать указанное на рисунке предложение, необходимо вручную ввести слово update, затем щелкнуть по строке customer и нажать кнопку Add Table to SQL, затем вновь вручную ввести SET, щелк- нуть по Company, нажать Add Field to SQL и т.д. Как уже говорилось, компонент TADOCommand способен возвращать записи. Для этого в него включены целых три реализации метода Execute, два из которых как раз и предназначены для создания наборов записей. Использование возвращаемого НД воз- можно с помощью компонента-посредника TADODataSet по следующей схеме: AdoDataSetl.RecordSet AdoCommandl.Execute; Для создания НД множество ExecuteOptions не должно содержать eoExecuteNo- Records.
Работа с ADO 559 Рие.39.15. Редактор команды компонента TADOCommand Свойства Свойство Назначение property Commandobject: _Command; Содержит ссылку на базовый командный объ- ект ADO. property CommandText: WideString; Содержит текст исполняемой команды. property CommandTimeout: Integer; Определяет предельное время выполнения коман- ды (в секундах). Умалчиваемое значение равно 30. type TCommandType - (cmdUnknown, cmdText, cmdTable, cmdStoredProc, cmdFile, cmdTableDirect); property CommandType: TCommandType; Определяет тип исполняемой команды: cmdUnknown - неизвестный тип; cmdText - команда представляет собой текст или имя исполняемой процедуры; cmdTable - команда есть имя таблицы; cmdStoredProc - команда есть имя хранимой процедуры; cmdFile - ко- манда есть имя файла, откуда берется набор данных; cmdTableDirect- команда возвращает все содержимое таблицы. property Connection: TADOConnection; Содержит ссылку на связной компонент. property ConnectionString: WideString; Содержит связную строку. Type TExecuteOption - (eoAsync- Execute, eoAsyncFetch, eoAsyncFetch- NonBlocking, eoExecuteNoRecords); TExecuteOptions = set of TExecuteOption; property Executeoptions: TExecuteOptions; Уточняет способ выполнения команды: eoAsyncExecute - выполняется асинхронно; eoAsyncFetch - выполняется асинхронно после наполнения кэша; eoAsyncFetchNonBlocking - выполнение происходит без блокирования потока; eoExecuteNoRecords - команда отвер- гает любые возвращаемые данные.
560 Глава 39 property ParamCheck: Boolean; Содержит True, если список параметров ко- манды должен обновляться при любом изме- нении текста команды. property Parameters: TParameters; Открывает доступ к параметрам команды. property Prepared: WordBool; Если содержит True, исполнение команды предварительно готовится. property Properties: Properties; Используется для непосредственного доступа к свойствам базового командного ADO- обьекта. type TObjectState = (stClosed, stOpen, stConnecting, stExecuting, stretching); TObjectStates = set of TObjectState; property States: TObjectStates; Указывает текущее состояние компонента: stClosed - компонент закрыт и не связан с дан- ными; stOpen - компонент открыт, но не вы- полняет команду; stConnecting компонент связан с набором данных; stExecuting - компо- нент выполняет команду; stFetching - компо- нент наполняет набор данных. Методы Метод Назначение procedure Assign(Source: TPersistent); override; Копирует основные свойства компонента Source в свойства текущего компонента. Ко- пируются; Connection; CommandText; CommandTimeout; CommandType; Prepared; Parameters. Procedure Cancel; Прекращает выполнение асинхронной команды. TExecuteOption = (eoAsyncExecute, eoAsyncFetch, eoAsyncFetchNonBlock- ing, eoExecuteNoRecords); TExecuteOptions = set of TExecuteOption; function Execute: _RecordSet; over- load; function Execute(const Parameters: OleVariant): _Recordset; overload; function Execute(var RecordsAffected: Integer; var Parameters: OleVariant; Executeoptions: TExecuteOptions = []): _RecordSet; overload; Выполняет команду. JRecordSet - возвращае- мый набор записей, если команда создает этот набор; Parameters - набор параметров, необхо- димых для выполнения команды; RecordsAffected - содержит количество воз- вращенных записей; ExecuteOptions - уточ- няющие параметры: eoAsyncExecute - команда выполняется асинхронно; eoAsyncFetch - после заполнения кэша оставшиеся записи форми- руются асинхронно; eoAsyncFetchNonBlocking - команда выполняется без блокирования по- тока; eoExecuteNoRecords - команда не должна возвращать записи. 39.3.4. Общие свойства компонентов-наборов Свойства, общие с BDE-компонентами В состав ЛЕО-компонентов входят 4 компонента-набора - TADODataSet, TADO- Table, TADOQuery и TADOStoredProc. Как и аналогичные BDE-ориентированные ком- поненты, они имеют общего родителя - абстрактный класс TDataSet, и, следовательно, большинство своих методов, свойств и событий они унаследовали от этого класса и разделяют их со своими BDE-аналогами (рис.39.16).
Работа с ADO 561 TComponent TDataSet TCustomADODataSet^^^^4 ТВ DE DataSet TD В DataSet АРО-наборы ВРЕ-наборы Рис.39.1б. Фрагмент иерархии наследования компонентов-наборов Например, для доступа к данным Л/ЭО-компоненты используют те же свойства, что и BDf-компоненты. Следующие операторы аналогичны по функциональности: Caption := AdoTablel.Fieldvalues['Company']; Caption := AdoTablel['Company']; Caption := AdoTablel.FieldByName('Company').AsString; Caption := AdoTablel.Fields[1].Value; Caption := AdoTablelCompany.AsString; (Предполагается, что поле ' Company' - второе по счету в массиве Fields и для него создан объект AdoTablelCompany). Разумеется, к моменту использования этих операторов компонент AdoTablel дол- жен быть связан через провайдер с физической ТБД и открыт. Модификация данных и навигация по ним также не отличаются от описанных ра- нее: AdoTablel.Edit; AdoTablelCompany.Value := 'Новая компания'; AdoTablel.Post; Или: AdoTablel.Open; while not AdoTablel.EOF do begin AdoTablel.Next end; Л/ЭО-наборы визуализируют данные также, как BDf-ориентированные наборы; че- рез компоненты-посредники TDataSource и компоненты страницы Data Controls гале- реи компонентов Delphi. Специфичные свойства Л/ЭО-наборы имеют непосредственного родителя TCustomAdoDataSet, от которого они унаследовали многие специфичные свойства. В этом разделе проводится общий
562 Глава 39 обзор особенностей ЛЛО-наборов, в последующих описываются наиболее важные свойства, методы и события класса TCustomADODataSet. Прежде всего, это свойства Connection и ConnectionString, с помощью которых Л1)0-набор может самостоятельно связаться с источником данных. С помощью свойства BlockReadSize программист может создать такой режим рабо- ты, когда навигация по данным не приводит к изменениям в связанных с набором ви- зуализирующих компонентах. Для этого в BlockReadSize следует поместить любое ненулевое значение. В этом случае свойство State автоматически примет значение dsBlockRead и навигация по НД будет проходить значительно быстрее. Чтобй отме- нить этот режим, достаточно поместить в BlockReadSize значение 0. В отличие от BDE, наборы ADO могут создавать курсоры двух типов - на стороне клиента или на стороне сервера (свойство CursorConnection). В сочетании со свойст- вом CursorType это дает программисту гибкий инструмент влияния на скоростные качества передачи данных и разграничение доступа к данным. С помощью свойства EnabledBCD программист может заменить вещественные данные форматом двоично-десятичных данных (BCD), Этот формат позволяет избе- жать ошибок округления вещественных чисел. Новым по сравнению с В£>£-ориентированными компонентами является также свойство LockType, позволяющее повысить скорость работы в многопользовательской среде за счет учета специфики БД. С помощью свойства Marshaloptions можно несколько снизить нагрузку на сеть при создании курсора на клиентской машине. Важным отличием является то, что ЛЛО-компоненты имеют свойство RecordSet, содержащее ссылку на одноименный базовый объект. Если программист хорошо зна- ком с технологией ADO, он может напрямую обратиться к этому свойству для более гибкого доступа к данным. Мы использовали это свойство в п.39.3.3, чтобы показать, каким образом компонент TADOCommand может передать набор данных: AdoDataSetl.RecordSet AdoCommandl.Execute; ЛЛО-компоненты способны работать с кэшированием данных, но делают это по- своему. Во-первых, в терминологии ADO вместо термина кэширование данных ис- пользуется понятие пакетной их обработки, после завершения которой все сделанные в пакете изменения либо подтверждаются, либо отвергаются. Для работы в пакетном режиме Л£>О-набор должен: 1. в свойстве CursorType содержать умалчиваемое значение stKeySet или значение stStatic; 2. в свойстве LockType содержать значение ItBatchOptimistic, 3. наконец, сами данные должны быть получены с помощью SgZ-sanpoca se- lect . Вот как, например, готовится компонент AdoDataSet к работе в пакетном режиме: with ADODataSetl do begin CursorLocation clUseServer; CursorType ctKeyset; LockType := ItBatchOptimistic; CommandType :- cmdText; CommandText := 'SELECT * FROM Employee'; Open; end;
Работа с ADO 563 Заметим, что поскольку компонент TADOTable не имеет свойств CommandType и Com- mandText, его перевод в режим пакетной обработки данных имеет свою специфику: AdoTablel.LockType ItBatchOptimistik; AdoTablel.Open; В пакетном режиме можно по этой же схеме выполнять кэшированные изменения в хранимых процедурах, которые возвращают набор записей: AdoStoredProcl.ProcedureName ;= 'MyBatchQuery'; AdoStoredProcl.LockType := ItBatchOptimistic; AdoStoredProcl.Open; После получения пакетного набора записей с помощью свойства Recordstatus можно проверить статус конкретной записи, а с помощью FilterGroup - отфильтровать их. Подтвердить изменения можно с помощью метода UpdateBatch-, прекратить режим пакетной обработки и отказаться от сделанных изменений - с помощью CancelBatch. При этом с помощью передаваемым этим методам параметров можно отменить или подтвердить изменения для всех или только для отфильтрованных записей. Есть определенная специфика в механизме сортировки записей ADO набора. Для сортировки в свойство Sort помещают список полей сортировки, разделенных запяты- ми. Каждое поле может дополнительно снабжаться признаками ascending (asc) или descending (desc) для указания соответственно нисходящего или восходящего по- рядка сортировки, например: ADOQueryl.Sort 'LastName ASC, DateDue DESC'; (если ни одно из этих слов не указано, реализуется нисходящая сортировка). Если в Sort помещена пустая строка, записи не сортируются и предъявляются в том виде, как они получены из БД. Если для НД создан клиентский курсор и нет соответствующих индексов для сортировки, на клиентской машине создается временный индекс, кото- рый уничтожается при изменении свойства Sort. Свойства класса TCustomADODataSet Свойство Назначение property BlockReadSize: Integer; Если содержит не ноль, разрывается связь между набором и связанными с ним визуали- зирующими компонентами. property CacheSize: Integer; Определяет размер (в единицах записей) внут- реннего буфера и, в то же время, максималь- ное количество записей, получаемых от серве- ра за один раз. property CanModify: Boolean; Указывает, будет ли набор обновляемым. Зна- чение в это свойство устанавливается автома- тически при открытии набора. property CommandText: WideString; Содержит текст команды. property CommandTimeout: Integer; Определяет максимально допустимое время исполнения команды в секундах. type TCommandType - (cmdUnknown, cmdText, cmdTable, cmdStoredProc, cmdFile, cmdTableDirect) ; property CommandType: TCommandType; Определяет тип исполняемой команды: cmdUnknown - неизвестный тип; cmdText - команда представляет собой текст или имя
564 Глава 39 исполняемой процедуры; cmdTable - команда есть имя таблицы; cmdStoredProc - команда есть имя хранимой процедуры; cmdFile - ко- манда есть имя файла, откуда берется набор данных; cmdTableDirect- команда возвращает все содержимое таблицы. property Connection: TADOConnection; Содержит ссылку на связной компонент. property Connectionstring: WideString; Содержит связную строку. type TCursorLocation = (clUseServer, clUseClient); property CursorLocation: TCursorLocation; Определяет способ создания курсора данных: clUseServer - курсор создается на сервере; clUseClient - курсор создается на клиентской машине. type TCursorType = (ctUnspecified, ctOpenForwardOnly, ctKeyset, ctDynamic, ctStatic); property CursorType: TCursorType; Определяет тип курсора: ctUnspecified - особен- ности курсора неизвестны; CtOpenForwardOnly - курсор может перемещаться только вперед, обеспечивая максимальную производительность просмотра данных; ctKeyset - индексный курсор (записи, измененные другим пользователем, не видны; удаленные другим пользователем - пере- стают быть доступными); ctDynamic - динамте- ский курсор (записи, измененные, удаленные или вставленные другим пользователем видны; курсор может перемещаться вперед и назад); ctStatic - статический курсор (работает со стати- ческой копией данных). property EnableBCD: Boolean; Если содержит True, данные представляются в двоично-десятичном формате, иначе - в фор- мате с плавающей запятой. Type TExecuteOption = (eoAsync- Execute, eoAsyncFetch, eoAsyncFetch- NonBlocking, eoExecuteNoRecords); TExecuteOptions = set of TExecuteOption; property Executeoptions: TExecuteOptions; Уточняет способ выполнения команды: eoAsyncExecute - выполняется асинхронно; eoAsyncFetch - выполняется асинхронно после наполнения кэша; eoAsyncFetchNonBlocking - выполнение происходит без блокирования потока; eoExecuteNoRecords - команда отвер- гает любые возвращаемые данные. property Filter: String; Задает фильтрующее выражение. property Filtered: Boolean; Разрешает или запрещает фильтрацию данных по условиям строки Filter. TFilterGroup =’(fgUnassigned, fgNone, fgPendingRecords, fgAffectedRecords, fgFetchedRecords, fgPredicate, fgCon- flictingRecords); property FilterGroup: TFilterGroup Фильтрует записи на основе анализа нх стату- са обновления: fgUnassigned, fgNone - запре- щает ^нльтрепто', fgPendingRecords - показы- вает только измененные записи, которые еще не подтверждены и не отвергнуты; fgAffected- Records - показывает только те записи, кото- рые участвовали в последнем подтверждении их изменений; fgFetchedRecords - показывает все записи в кэше; fgPredicate - показывает только удаленные записи; fgConflictingRecords - показывает записи, при подтверждении ко- торых возникли ошибки.
Работа с ADO 565 type TADOLockType = (ItUnspecified, ItReadOnly, ItPessimistic, ItOptimistic, ItBatchOptimistic); property LockType: TADOLockType; Определяет схему блокировки записей: ItUnspecified - специфика блокировки не зада- на; ItReadOnly - записи доступны только для чтения; ItPessimistic - запись блокируется в момент ее редактирования; ItOptimistic - за- пись блокируется только в момент подтвер- ждения сделанных в ней изменений; ItBatchOptimistic - используется при подтвер- ждении пакетного изменения записей. TMarshalOption = (moMarshalAll, mo- MarshalModifiedOnly); property Marshaloptions: TMarshalOption; Определяет специфику передачи данных на сервер, если курсор создан на стороне клиен- та: moMarshalAll - на сервер передаются все записи; moMarshalModifiedOnly - на сервер передаются только измененные записи. property MaxRecords: Integer; Определяет максимальное количество запи- сей, получаемых из набора. Умалчиваемое значение 0 снимает это ограничение. property ParamCheck: Boolean; Содержит True, если список параметров ко- манды должен обновляться при любом изме- нении текста команды. property Parameters: TParameters; Открывает доступ к параметрам команды. property Prepared: WordBool; Если содержит True, исполнение команды предварительно готовится. property Properties: Properties; Используется для непосредственного доступа к свойствам базового командного ADO- объекта. property RecNo: Integer; Указывает номер текущей записи в диапазоне от 1 до RecordCount. property Recordcount: Integer; Указывает текущее количество записей в на- боре. property RecordSet: _RecordSet; Содержит ссылку на базовый объект ADO. type TObjectState = (stClosed, stOpen, stConnecting, stExecuting, stFetching); TObjectStates = set of TObjectState; property RecordSetState: TObjectStates; Указывает текущее состояние набора данных: stClosed - компонент закрыт и не связан с дан- ными; stOpen - компонент открыт, но не вы- полняет команду; stConnecting компонент связан с набором данных; stExecuting - компо- нент выполняет команду; stFetching - компо- нент наполняет набор данных. type TRecordStatus = (rsOK, rsNew, rsModified, rsDeleted, rsUnmodified, rslnvalid, rsMultipleChanges, rsPend- ingChanges, rsCanceled, rsCantRe- lease, rsConcurrencyViolation, rsln- tegrityViolation, rsMaxChangesEx- ceeded, rsObjectOpen, rsOutOfMemory, rsPermissionDenied, rsSchemaViola- tion, rsDBDeleted); TRecordStatusSet = set of TRecordStatus; property Recordstatus: TRecordStatusSet; Определяет статус записи, участвующей в па- кетном режиме обновления: rsOK - успешно подтверждены все изменения; rsNew - встав- ленная запись; rsModified - запись была измене- на; rsDeleted - запись была уничтожена; rsUnmodified - запись не изменялась; rslnvalid - запись не сохранена из-за ошибки в закладке; rsMultipleChanges - запись не сохранеа из-за попытки нескольких ее изменений одновремен- но; rsPendingChanges - запись не сохранена из- за того, что ссылается на измененную запись; rsCanceled - запись не изменена по команде
566 Глава 39 Cancel', rsCantRelease - не сохранена из-за бло- кировки записи; rsConcurrencyViolation - не сохранена из-за нарушения оптимистичной схемы блокирования; rsIntegrityViolation - не сохранена из-за нарушения ссылочной целост- ности; rsMaxChangesExceeded - превышено максимально допустимое количество измене- ний; rsObjectOpen - не сохранена из-за несоот- ветствия открытого и сохраняемого объекта; rsOutOfMemory - не сохранена из-за нехватки памяти; rsPermissionDenied - не сохранена из-за недостаточных прав пользователя; rsSchemaViolation - не сохранена из-за ошибок в базовом Л£)О-объекте; rsDBDeleted - не сохра- нена, т.к. уже была успешно уничтожена. property Recordsize: Word; Содержит размер записи в байтах. property Sort: WideString; Определяет порядок сортировки записей. Методы класса TCustomADODataSet Любопытной особенностью ЛГХЭ-наборов являются инкапсулированные в них ме- тоды SaveToFile и LoadFromFile. В ADO эти методы используются как один из воз- можных механизмов обмена данными между разными компьютерами, а также для от- ложенной обработки данных. Перед вызовами методов /lDO-набор должен быть за- крыт. После успешного вызова LoadFromFile набор автоматически открывается в том состоянии, в котором он был сохранен методом SaveToFile. ADO наборы можно сортировать по закладкам (метод FilterOnBookmarks). Напри- мер: procedure TForml.ButtonlClick(Sender: TObject); var BM1, BM2: TBookmarkStr; begin with ADODataSetl do begin BM1 := Bookmark; MoveBy(3); BM2 := Bookmark; FilterOnBookmarks([BM1, BM2]); end; end; Для работы в многопользовательском режиме может оказаться полезным метод Clone, с помощью которого уже созданный НД дублируется в вызывающий. Напри- мер: AdoTablel.Open; //Создан исходный НД AdoTable2.Clone(AdoTablel, ItReadOnly); //Создан дубликат в НД //AdoTable2 Для ADO наборов определен метод Requery, с помощью которого программист может обновить НД. Его действие фактически эквивалентно последовательному ис- пользованию Close - Open. Особенностью является то обстоятельство, что влияющие на НД свойства, такие как CursorLocation, CursotType, LockTable и т.п. остаются неиз-
Работа с ADO 567 менными, в то время как в промежутке между использованием Close-Open эти свойст- ва можно изменить и тем самым повлиять на результирующий НД. Весьма необычным является метод Seek, осуществляющий поиск записи по задан- ному значению (значениям) ключевого поля (полей). В принципе, он реализует те же действия, что и метод Locate, но отличается от него несколькими важными особенно- стями. Во-первых, для Locate не имеет значения, индексировано ли поле (поля) по ко- торому (которым) ищутся записи. В то же время Seek ищет только по индексирован- ным полям, которые, к тому же, должны быть указаны в свойствах IndexNames или IndexFields. Далее, для Locate курсор не перемещается, если поиск неудачен, для Seek - перемешается всегда, в зависимости от указанного параметра SeekOption (если запись не найдена, курсор перемещается в начало НД, его конец, непосредственно перед ис- комой записью или сразу за ней). И, наконец, Seek не может вести поиска с игнориро- ванием возможной разницы в высоте букв или по частичному совпадению поисковых значений. К сожалению, многие провайдеры OLE DB не допускают установки значе- ний свойств IndexNames или IndexFields и, таким образом, не поддерживают Seek. С помощью метода Supports можно проверить различные характеристики набора и, в частности, может ли он поддерживать Seek: AdoTablel.Open; if Supports([coSeek]) then AdoTablel.Seek(90001,soAfterEQ) else if Supports([coLocate]) then AdoTablel.Locate('Number',90001, []); Метод ' • ' " i Назначение - function BookmarkValid(Bookmark: TBookmark): Boolean; override; Проверяет закладку Bookmark и возвращает True, если она связана с существующей запи- сью. TAffectRecords - (arCurrent, arFiltered, arAll, arAllChapters); procedure CancelBatch(AffectRecords: TAffectRecords - arAll); Отменяет режим пакетной обработки данных: arCurrent - отменяются изменения только в текущей записи; arFiltered - отменяются изме- нения во всех записях, соответствующих ус- ловию фильтрации FilterGroup: arAll - отме- няются все изменения во всех записях; arAllChapters - отменяются изменения на все уровнях вложенности (во всех секциях ADO). procedure CancelUpdates; Отменяет любые изменения в текущей записи. type TADOLockType - (ItUnspecified, ItReadOnly, ItPessimistic, ItOptimistic, ItBatchOptimistic); procedure Clone(Source: TCustomADODataSet; LockType: TADOLockType - ItUnspecified); Делает копию НД Source с заданной блоки- ровкой доступа LockType (описание типа TA- DOLockType см. на с.562). function CompareBookmarks(Bookmarkl, Bookmark2: TBookmark): Integer; override; Сравнивает две закладки и возвращает 0, -1 или +1, если первая равна, меньше или больше второй. function CreateBlobStream(Field: TField; Mode: TBlobStreamMode): TStream; override; Создает поток для чтения или записи поля BLOB: Field - поле; Mode - режим доступа к данным: bmRead - только чтение; bmWrie - только запись; bmReadWrite - чтение и запись.
568 Глава 39 TAffectRecords = (arCurrent, arFil- tered, arAll, arAHChapters) ; procedure DeleteRecords AffectRecords: TAffectRecords = arAll); Уничтожает запись или записи в текущем НД arCurrent текущая запись; arFiltered - отфильт- рованные записи; arAll - все записи; arAllChapters - все записи во всех секциях ADO. procedure FilterOnBookmarks (Bookmarks: array of const); Сортирует записи по связанным с ними за- кладкам, которые передаются в массиве Bookmarks. function IsSequenced: Boolean; override; Возвращает True, если БД поддерживает по- следовательную нумерацию записей в ТБД. В этом случае для навигации по НД можно ис- пользовать свойство RecNo. procedure LoadFromFile(const FileName: WideString); Загружает НД из файла. TLocateOption = (loCaselnsensitive, loPartialKey); TLocateOptions = set of TLocateOption; function Locate(const KeyFields: String; const KeyValues: variant; Options: TLocateOptions): Boolean; override; Ищет в НД нужную запись и делает ее теку- щей: KeyFields - список полей для поиска; KeyValues - значения поисковых полей; Op- tions - уточняющий признак: loCaselnsensitive - игнорировать возможную разницу в высоте букв; loPartialKey- искать по частичному сов- падению ключей. function Lookup(const KeyFields: String; const KeyValues: Variant; const ResultFields: String): Variant; override; Используется в подчиненных НД для поиска в полях KeyFields значений KeyValues. При ус- пехе возвращает в ResultFields найденные значения. function NextRecordset(var RecordsAffected: Integer): _Recordset; Для запросов, способных возвращать более одного НД производит доступ ко второму и последующим НД RecordAffects - количество записей во втором НД. procedure Requery(Options: TExecuteOptions = []); Освежает записи в НД. type TPersistFormat = (pfADTG, pfXML) ; procedure SaveToFile(const FileName: String = Format: TPersistFormat = pfADTG); Сохраняет текущий НД в файле с именем FileName', TSeekOption = (soFirstEQ, soLastEQ, soAfterEQ, soAfter, soBeforeEQ, soBe- fore); function Seek(const KeyValues: Variant; SeekOption: TSeekOption = soFirstEQ): Boolean; Ищет запись по значению (значениям) индекса (индексов): KeyValues - искомое значение инд- кусного поля (полей); SeekOption - дополни- тельные условия поиска: soFirstEQ - ищется первое значение от начала НД если не найдено - курсор устанавливается в конец НД; soLastEQ -;ищется последнее значение; если не найдено - курсор устанавливается в конец НД; soAfterEQ - если запись найдена, курсор указывает на нее, в противном случае - на запись со следующим значением ключа; soAfter - указывает на запись со следующим значением ключа независимо от исхода поиска; soBeforeEQ - если запись найде- на, курсор указывает на нее, в противном слу- чае - на запись с предыдущим значением ключа; soBefore - указывает на запись с предыдущим значением ключа независимо от исхода поиска.
Работа с ADO 569 type TCursorOption = (coHoldRecords, coMovePrevious, coAddNew, coDelete, coUpdate, coBookmark, coApproxPosi- tion, coUpdateBatch, coResync, coNo- tify, coFind , coSeek, coIndex); TCursorOptions = set of TCursorOption; function Supports(CursorOptions: TCursorOptions): Boolean; Указывает, будут ли поддерживаться курсор- ные операции, перечисленные в параметре CursorOperation: coHoldRecords - можно пе- ремещать курсор или добавлять записи без подтверждения сделанных изменений; coMovePrevious - курсор может смещаться к предыдущей записи; coAddNew - разрешается вставка записей; coDelete - разрешается уда- ление записей; coUpdate - разрешается изме- нение записей; coBookmark - можно создавать и использовать закладки; coApproxPosition - поддерживается свойство RecNo-, coUpdateBatch - возможно кэширование изме- нений; coResync - курсор можно изменить методом Resync базового объекта Resordset; coNotify - создаются сообщения (и события) при изменении состояния НД; coFind - может использоваться метод Locate; coSeek - может использоваться метод Seek, coIndex - можно изменять и устанавливать активный индекс. TAffectRecords = (arCurrent, arFil- tered, arAll, arAHChapters); procedure UpdateBatch(AffectRecords: TAffectRecords = arAll); Подтверждает все сделанные в пакетном режи- ме изменения, записывая их на диск. Параметр AffectRecords может иметь одно из следующих значений: arCurrent - подтверждаются измене- ния только в текущей записи; arFiltered - под- тверждаются изменения только в отфильтро- ванных записях; arAll - подтверждаются все изменения; arAllChapters - подтверждаются все изменения во всех секциях пакета. Type TUpdateStatus = (usUnmodified, usModified, uslnserted, usDeleted); function UpdateStatus: TUpdateStatus; override; Возвращает информацию о статусе обновле- ния текущей записи пакета: usUnmodified - изменений нет; usModified - запись модифици- рована; uslnserted - запись вставлена; usDeleted - запись удалена. События класса TCustomADODataSet Некоторые события используют следующие типы данных, определяющий статус и смысл события: type TEventStatus = (esOK, esErrorsOccured, esCantDeny, esCancel, esUnwantedEvent) ; Здесь: esOK - нет ошибок; esErrorsOccured - событие вызвало ошибки; esCantDeny - ожидаемую связь нельзя разорвать; esCancel - ожидаемую связь можно разорвать до ее активизации; esUnwantedEvent - блокирует связанные события. type TEventReason = (erAddNew, erDelete, erUpdate, erUndoUpdate, erUndoAddNew, erUndoDelete, erRequery, erResynch, erClose, erMove, erFirstChange, erMoveFirst, erMoveNext, erMovePrevious, erMoveLast); Здесь: erAddNew - добавлена запись; erDelete - уничтожена запись; erUpdate - за- пись изменена; erUndoUpdate - произошел откат изменений записи; erUndoAddNew -
570 Глава 39 произошел откат вставки записи; erUndoDelete - произошел откат стирания записи; erRequery - к НД был применен метод Requere\ erResynch - к НД был применен метод Resync, erClose - НД был закрыт; erMove - курсор сместился к другой записи; erFirstChange - произошло первое изменение НД; erMoveFirst - курсор сместился к первой записи; erMoveNext курсор сместился к следующей записи; erMovePrevious - курсор сместился к предыдущей записи; erMoveLast - курсор сместился к последней записи. Особенностью события OnEndOfRecordset является то, что оно предшествует со- бытиям BeforeScroll и AfterScroll. В обработчике события нельзя проверять конец НД с помощью функции EOF, т.к. сам НД может быть частично не наполнен, если наполне- ние его записями идет в асинхронном режиме. Обработчик события OnFetchProgressEvent обычно используется при длительном по времени обращении к БД в асинхронном режиме для индикации процесса. Напри- мер: procedure TForml.ADODataSetlFetchProgress(DataSet: TCustomADODataSet; Progress, MaxProgress: Integer; var Eventstatus: TEventStatus); begin Caption := 'Percent complete: ' + IntToStr(Trunc(Progress / MaxProgress * 100)) + '%'; Application.ProcessMessages; end; Событие С чем связано TEndOfRecordsetEvent “ procedure (DataSet: TCustomADODataSet; var MoreData: WordBool; var Eventstatus: TEventStatus) of object; property OnEndOfRecordset: TEndOfRecordsetEvent; Наступает, когда курсор перемещается в ко- нец набора. Переменная ModeData содержит True, если за последней записью есть еще за- писи, физически не полученные из БД. TRecordsetEvent “ procedure(DataSet: TCustomADODataSet; const Error: Er- ror; var Eventstatus: TEventStatus) of object; property OnFetchComplete: TRecordsetEvent; Возникает при полном заполнении набора записями в асинхронном режиме. Error - ссылка на базовый объект ADO. TFetchProgressEvent » procedure (DataSet: TCustomADODataSet; Prog- ress, MaxProgress: Integer; var Eventstatus: TEventStatus) of object; property OnFetchProgress: TFetchProgressEvent; Периодически возникает в процессе асин- хронного получения данных: MaxProgress указывает количество уже полученных запи- сей. TFieldChangeCompleteEvent = prooedur* (DataSet: TCustomADODataSet; const FieldCount: Integer; const Fields: OleVariant; const Error: Error; var Eventstatus: TeventStatus) of object; property OnFieldChangeComplete: TFieldChangeCompleteEvent; Возникает после завершения изменения поля текущей записи до передачи этого изменения в базовый объект ADO Field (Fileds - вариант- ный массив этих объектов; FieldCount - поряд ковый номер измененного поля в массиве Fields). TRecordsetErrorEvent = procedure (DataSet: TCustomADODataSet; const Reason: TEventReason; const Error: Error; var Eventstatus: TEventStatus) Возникает при смещении указателя к другой записи: DataSet - набор данных, возбудивший событие; Reason - смысл события; EventStatus - статус события.
Работа с ADO of object; property OnMoveComplete: TRecordsetErrorEvent TRecordChangeCompleteEvent = proce- dure (DataSet: TCustomADODataSet; const Reason: TEventReason; const Re- cordCount: Integer; const Error: Er- ror; var Eventstatus: TEventStatus) of object; property OnRecordChangeComplete: TRecordChangeCompleteEvent Возникает при изменении одной или несколь- ких записей НД: DataSet - набор данных, воз- будивший событие; Reason - смысл события; RecordCount - количество записей, в которых произошли изменения; Error - ссылка на ба- зовый объект ADO, EventStatus - статус собы- тия. TRecordsetErrorEvent = procedure (DataSet: TCustomADODataSet; const Reason: TEventReason; const Error: Error; var Eventstatus: TEventStatus) of object; property OnRecordsetChangeComplete: TRecordsetErrorEvent Возникает после изменения базового объекта ADO Recordset-. DataSet - набор данных, воз- будивший событие; Reason - смысл события; Error - ссылка на базовый объект ADO, Event- Status - статус события. TWillChangeFieldEvent = procedure (DataSet: TCustomADODataSet; const FieldCount: Integer; const Fields: OleVariant; var Eventstatus: TEvent- Status) of object; property OnWillChangeField: TWillChangeFieldEvent Возникает перед изменением поля: DataSet - набор данных, возбудивший событие; Field- Count - номер поля в массиве Fields-, Fields - массив полей; EventStatus - статус события. TWillChangeRecordEvent - procedure (DataSet: TCustomADODataSet; const Reason: TEventReason; const Record- Count: Integer; var Eventstatus: TE- ventStatus) of object; property OnWillChangeRecord: TWillChangeRecordEvent; Возникает перед изменением записи: DataSet - набор данных, возбудивший событие; Reason - смысл события; RecordCount - количество изменяемых записей; EventStatus - статус со- бытия. TRecordsetReasonEvent » procedure (DataSet: TCustomADODataSet; const Reason: TEventReason; var EventSta- tus: TEventStatus) of object; property OnWillChangeRecordset: TRe- cordsetReasonEvent; Возникает перед изменением базового набора Recordset: DataSet - набор данных, возбудив- ший событие; Reason - смысл события; Event- Status - статус события. TRecordsetReasonEvent - см. выше; property OnWillMove: TRecordsetReasonEvent Возникает перед смешением указателя теку- щей записи. 39.3.5. Компонент TADODataSet Этот компонент обеспечивает доступ к одной или нескольким ТБД с помо- щью запроса типа SELECT. Компонент рассчитан на возврат набора данных, по- этому его нельзя использовать для выполнения подмножества операторов DDL. (В компоненте есть свойство CommandText, однако в него можно поместить только оператор select. Для выполнения £)£)Л-предложений языка SQL можно использовать метод Execute компонента TADOCommand или метод ExecSQL ком- понента TADOQuery.) В отличие от TADOTable, компонент может обращаться не только к одной, но сра- зу к нескольким таблицам. На рис.39.17, например, показан результат такого запроса к двум таблицам, выполненного с помощью компонента ADODataSet.
572 Глава 39 гпг «Г Forml CustNo compare SaleDate ► Sight Diver 12.04.88 -J 2156 Davy Jones' Locker 17.04.88 1356 Tom Sawyer Diving Centre 20.04.88 1380 Blue Jack Aqua Center 06.11.Э4 i. A ''A 1384 VIP Divers Club 01.05.88 1510 Ocean Paradise 03.05.88 w ' I s «SI 1513 Fantastique Aquatica 11.05.88 Д| J ▼ | ft , ► 1 Рис.39.17. Связывание двух таблиц по полю CustNo Текст запроса: SELECT с.CustNo, с.company, o.SaleDate FROM customer c, orders о WHERE о.CustNo=c.CustNo Компонент TADODataSet - единственный компонент-набор, с помощью которого можно установить связь с удаленным источником данных TRDSConnaction (см. ниже п. 39.5.9) при создании трехзвенной архитектуры. Для этого у него определено свойство property RDSConnection: TRDSConnection; Если компонент связан с единственной таблицей, можно воспользоваться его ме- тодом GetlndexNames, чтобы получить список всех имен табличных индексов. Напри- мер: AdoDataSetl.GetlndexNames(Memol.Lines); Остальные свойства, методы и события компонент унаследовал от своих предков TCustomAdoDataSet и TDataSet. 39.5.6. Компонент TADOTable Этот компонент является прямым аналогом популярного BDE-компонента ТТаЫе. Также как ТТаЫе, он способен получать и обслуживать НД, состоящий из записей единственной физической таблицы БД, имя которой содержит его свойство Table- Name. Поскольку, как и все остальные компоненты-наборы, он является надстройкой к базовому объекту Command, он имеет свойство CommandText, которое, однако, недос- тупно программисту. Значение этого свойства у него формируется автоматически по имени связанной таблицы: Command.Commandstring := 'SELECT * FROM TableName'; где TableName - имя связанной таблицы.
Работа с ADO 573 Это, однако, не мешает компоненту работать в режиме кэшированных изменений (в режиме пакетной обработки): свойство связанного с ним базового объекта Сот- mandType у него всегда имеет значение cmdText, так что остается изменить лишь единственное свойство LockType (см. с.562): AdoTablel.LockType : = ItBatchOptimistic; AdoTablel.Open; (свойство CursorType у компонента автоматически имеет значение stKeySet). После этого компонент готов к режиму пакетной обработки. Компонент имеет такие специфические свойства: Свойство Назначение property IndexFieldNames: String; Содержит список индексов, которые необхо- димо использовать для доступа к данным. Соседние индексы в списке разделяются точ- кой с запятой. property MasterFields: String; Определяет поле связи при установлении свя- зи главный-детальный. property Mastersource: TDataSource; Определяет источник данных при установле- нии связи главный-детальный. property Readonly: Boolean; Содержит True, если данные из набора нельзя изменять. property TableDirect: Boolean read GetTableDirect write SetTableDirect default False; Содержит True, если компонент связан с фи- зической ТБД непосредственно (по имени). В противном случае компонент создает соответ- ствующее предложение SELECT. property TableName: WideString; Содержит имя связанной с компонентом ТБД. Остальные свойства, методы и события унаследованы компонентом от TCustomA- DODataSet и TDataSet. 39.5.7. Компонент TADOQuery В отличие от TADOCommand, этот компонент преимущественно предназначен для получения набора записей из одной или нескольких таблиц БД. На самом деле, факти- чески он целиком повторяет функциональность компонента TQuery, т.к. в него вклю- чен специфичный метод ExecSQL, с помощью которого компонент может выполнять предложения DDL языка SQL. Сам запрос формируется в многострочном свойстве SQL. Единственным отличием от ЯО£-аналога является свойство property RowsAffected: Integer; с помощью которого программист может узнать или указать максимальное количе- ство обновлений, которое сделано (или разрешается сделать) при выполнении за- проса.
574 Глава 39 Также как TQuery, TADOQuery имеет свойство DataSource, позволяющее передать параметры запроса от одного компонента другому (см. п.8.7.6). Остальные свойства, методы и события унаследованы компонентом от TCustomA- DODataSet и TDataSet. 39.5.8. Компонент TADOStoredProc Компонент TADOStoredProc предназначен для исполнения хранимой процедуры сервера БД. Для этого он имеет свойство ProcedureName и методы Open и ЕхсесРгос. Если процедура должна вернуть хотя бы одно значение, ее выполнение реализуется установкой значение True в свойство Active или обращением к методу Open. Для вы- полнения процедуры, состоящей из DDT-предложений, используется обращение к ЕхесРгос. * Как правило, хранимая процедура для своего выполнения требует одного или не- скольких параметров. В компоненте для передачи параметров хранимой процедуре используется свойство Parameters. С помощью свойства Prepared программист может потребовать предварительной подготовки перед исполнением процедуры. Подготов- ленные процедуры обычно выполняются значительно быстрее неподготовленных. В функциональном отношении компонент полностью идентичен BDE- ориентированному компоненту TStoredProc, но отличается от него более широкими возможностями, унаследованными им от TCustomADODataSet. 39.5.9. Компонент TRDSConnection и создание сервера приложений Этот компонент создан для поддержки многозвенной архитектуры (с одним или несколькими серверами приложений). Компонент служит для установления связи кли- ентской программы с сервером приложений. Для разработки сервера приложений в рамках универсальной технологии доступа к данным корпорации Microsoft в репозиторий Delphi 5 включен соответствующий экс- Active Server перт (закладка ActiveX, пиктограмма Object ). Его основная задача - подготовить «скелет» будущего сервера, создав заготовки для его интерфейсов и классов реализа- ции. Сздание сервера приложений Рассмотрим процесс создания сервера приложений с помощью этого эксперта (в терминологии, принятой в документации Microsoft, серверы приложений называются деловыми объектами). Для начала откройте новый проект. Его пустая форма будет появляться всякий раз, когда клиент обратится к серверу приложений. Теперь дважды щелкните по пикто- грамме эксперта - на экране появится окно, изображенное на рис.39.18. В строку CoClassName следует поместить имя класса, в котором будут реализовы- ваться необходимые интерфейсы (класс реализации интерфейса иногда называют со- классом).
Работа с ADO 575 Рис. 39.18. Окно эксперта создания удаленного сервера приложений С помощью списка Instancing можно выбрать один из следующих режимов созда- ния экземпляра сервера: • Internal - сервер создается как часть активной библиотеки DLL, если выбраны дру- гие значения, сервер представляет собой объект Автоматизации; • Single Instance - для каждого клиентского вызова создается один экземпляр сервера и ему выделяется свой процесс на серверной машине; • Multiple Instance - то же, что и Single Instance, но все экземпляры разделяют один процесс. Если выбран режим Internal, с помощью списка Threading Model можно выбрать модель потоков команд: • Single - для DLL выделяется один поток, поэтому все запросы клиентов выполня- ются последовательно; • Apartment - каждый экземпляр сервера обрабатывает один запрос клиента в от- дельном потоке команд; экземпляры (точнее, связанные с ними данные) сохраня- ются автоматически, но программист должен исключить возможные конфликты, связанные с использованием потоками общей глобальной памяти; • Free - один экземпляр сервера может обрабатывать несколько запросов в разных потоках команд; программист должен позаботиться о сохранности данных и ис- ключить возможные конфликты, связанные с общей памятью; • Both - сервер обслуживает клиентов как в режиме Apartment, так и в режиме Free.
576 Глава 39 Переключатели Active Server Туре используются для настройки lFefe-браузера, с помощью которого можно получить доступ к удаленным данным. Если выбран пере- ключатель Page-level event method (OnStartPage/OnEndPage), сервер будет создавать соответствующие события в момент начала создания и в конце формирования очеред- ной страницы. Этот переключатель должен быть активным, если вы собираетесь ис- пользовать Internet Explorer (IE) версий 3.5 и 4. Если вы не планируете доступ к дан- ным с помощью FKefe-браузера, используете версию IE 5.0, или сочетание IE 4.0 и MTS, рекомендуется активизировать переключатель Object Context. Флажок в строке Generate a template script test for this object потребует от эксперта создать тестовую страницу с расширением ASP в формате XML для просмотра данных с помощью FFefe-браузера. Назовите со-класс именем RDSAppServer, оставьте выбор Multiple Instance в строке Instancing., выберите Single в строке Threading Model и уберите флажок в строке Gen- erate a template script test for this object, после чего нажмите Enter (рис.39.19). Рис.39.19. Созданная экспертом библиотека типов Как видно из рисунка 39.19, эксперт создал библиотеку типов, в модуле которой нам следует описать интерфейсы и со-класс. Нажмите F12, чтобы вызвать связанный с библиотекой модуль Projectl_TLB. Теперь вы увидите, что эксперт создал еще один модуль Unit2 с заготовкой для реализации со-класса и с обращением к т.н. фабрике объектов, с помощью которой, собственно, и создается экземпляр удаленного сервера приложений. Сохраните проект на диске: модуль Unit2 под именем AppServer, модуль Until - под именем MainForm, а сам проект - под именем RServer.
Работа с ADO 577 Посмотрите внимательно текст модуля RServer TLB. Вы увидите в нем созданные экспертом заготовки для интерфейсов IRDSAppServer = interface (IDispatch) и IRDSAppServerDisp = dispinterface. В эти заготовки мы должны внести методы и свойства, с помощью которых удаленные данные становятся доступны клиентам. Пусть, например, мы хотим обеспечить доступ к данным из таблицы Employee де- монстрационной базы данных dbdemos.mdb, поставляемой вместе с Delphi 5. Прежде всего спуститесь по тексту модуля приблизительно на страницу вниз от его начала и вставьте в предложение uses ссылку на модуль ADODBJTLB (этот мо- дуль содержит описание типа _Recordset, без которого мы не сможем экспонировать таблицу): uses Windows, ActiveX, Classes, Graphics, OleServer, OleCtrls, StdVCL, ADOB_TLB; Еще ниже по тексту (приблизительно на 2 экранных страницы) эксперт заготовил заголовки двух интерфейсов, с помощью которых мы сможем получить доступ к дан- ным. Внесите в них такие изменения: // Interface: IRDSAppServer // Flags: (4416) Dual OleAutomation Dispatchable // GUID: {AEEAD5C1-CDA5-11D3-B4BD-00605205A033} /У *хЛх>е*1»’****>»-*********>»-1»'>»-*>»-******>»->г*>г*>г**х*х*х>»-х>»-х>»-х**>»-*****>»-**’Уу IRDSServer = interface(IDispatch) ['{AEEAD5C1-CDA5-11D3-B4BD-00605205A033}'] function Get_Employee: _Recordset; safecall; property Employee: _Recordset read Get_Employee; end; yy ><4i4»-li’*li’*li’***>i-*************ji'ii,***********************************yy // Displntf: IRDSAppServerDisp // Flags: (4416) Dual OleAutomation Dispatchable // GUID: (AEEAD5C1-CDA5-11D3-B4BD-00605205A033) // **************************************************************// IRDSAppServerDisp - dispinterface ['(AEEAD5C1-CDA5-11D3-B4BD-00605205A033)' ] property Employee: _Recordset readonly dispid 3; end; (в этом фрагменте намеренно сохранены стандартные комментарии, помещаемые экс- пертом в листинг; при повторении программы следует учесть, что соответствующие строки GUID будут содержать другие символы). Еще ниже по тексту модуля объявляются два класса реализации - TRDSServProper- ties и TRDSServ. Первый предназначен для экспонирования необходимых свойств и событий в окно Инспектора Объектов для работы с ними на этапе конструирования программы. Второй - основной со-класс, с помощью которого реализуется объект Proxy - полномочный представитель клиента на стороне сервера. Поскольку многие объявления в обоих интерфейсах совпадают, эксперт активно использует директивы условной компиляции (SIFDEF LIFE_SERVER_AT_DESIGN_TIME) и ($ENDIF)
578 Глава 39 для выделения тех частей, которые специфичны для интерфейса TRDSServProperties. Поскольку этот интерфейс носит вспомогательный характер, мы не будем каким-либо образом его изменять, но для интерфейса Proxy в защищенную секцию protected вставим объявление метода GetEmpl'. { $IFDEF LIVE_SERVER_AT_DESIGN_ TIME} TRDSServProperties= class; {$ENDIF} TRDSServ = class(TOleServer) private FIntf: IRDSServ; {$IFDEF LIVE_SERVER_AT_DESIGN_TIME} FProps: TRDSServProperties; function GetServerProperties: TRDSServProperties; {SENDIF} function GetDefaultlnterface: IRDSServ; protected function GetEmpl: Recordset; procedure InitServerData; overrider- public (в приводимый выше фрагмент намеренно включены директивы условной компиля- ции, чтобы вам было легче отыскать текст в листинге модуля; вставляемый метод подчеркнут). В разделе реализации модуля нужно расшифровать объявленный метод: function TRDSServ.GetEmpl: _Recordset; begin Result := Defaultlnterface.GetEmpl end; Неправда ли, такая строка очень похожа на рекурсивный вызов? На самом деле ни- какой рекурсии нет: Proxy обращается к методу GetEmpl умалчиваемого интерфейса сервера. Для реализации умалчиваемого интерфейса сервера предусмотрен класс TRDSServ, заблаговременно объявленный экспертом в дополнительном модуле AppServ. Перей- дите в этот модуль и измените его так: unit AppServ; interface uses ComObj, ActiveX, AspTlb, Serv_TLB, StdVcl, ADODB, ADODB TLB; type TRDSServ = class(TASPMTSObject, IRDSServ) function GetEmpl: Recordset; safecall; end; implementation
Работа с ADO 5 79 uses ComServ; { TRDSServ } function TRDSServ.GetEmpl: Recordset; var ConnStr: WideString; begin //Используем стандартный каталог для размещения файлов UDL: ConnStr := 'Eile name=' +DataLinkDir+ '\dbdemos.udl'; Result := CoRecordSet.Create; Result.CursorLocation := adUseClient; Result.Open('Employee', ConnStr, adOpenStatic, adLockBatchOptimistic, adCmdTable) end; initialization TAutoObjectFactory.Create(ComServer, TRDSServ, Class_RDSServ, ciMultilnstance, tmSingle); end. Во избежание недоразумений текст модуля приводится целиком. Вставленные фрагменты выделены подчеркнутым шрифтом. В модуле ADODB предусмотрена гло- бальная строковая переменная DataLinkDir, которая при развертывании Delphi полу- чает значение Program Files | Common Files | System | ole db | Data Links. В этом каталоге находится связной файл DBDEMOS.UDL, в котором указан маршрут доступа к демонстрационной БД DBDDEMOS.MDB. Теперь запустите вновь созданный сервер приложений, чтобы зарегистрировать его в реестре Windows. Перейдем к созданию клиента - именно в нем нам понадобится компонент TRDSConnection. Откройте повое приложение и поместите на пустую форму компо- нент TRDSConnection и компоненты TADODataSet, TDataSource и TDBGrid. Установи- те связи между ними: в свойство RDSConnection компонента ADODataSetl поместите ссылку на RDSConnectionl, в DataSet компонента DataSource - ссылку на ADOData- Setl и в DataSource компонента DBGridl - DataSourcel. Теперь необходимо согласо- вать свойства компонента ADODataSetl со свойствами удаленного сервера. Во- первых, щелкните по кнопке в свойстве ConnectionString, в появившемся окне (рис.39.4 или 39.9) щелкните по кнопке Use Data Link File, раскройте список и выбе- рите единственный файл DBDDEMOS. UDL. Затем в свойстве CommandType установи- те cmdTable, раскройте список свойства ComandText и выберите таблицу Employee. В заключение напишите такой обработчик события OnCreate главной формы кли- ента: procedure TForml.FormCreate(Sender: TObject); begin RDSConnectionl.ServerName := 'Serv.AppServ'; AdoDataSetl.Open end; После запуска программы вы увидите окно, Показанное на рис.39.20.
580 Глава 39 пег Forml EmpNo LastName ] FirstNar I ! ► э i Nelson Roberto 250 28.12.88 40000 4 Young Bruce 23Э 28.12.88 555001 5 Lambert Kim k22 06.02.89 25000 8 Johnson Leslie 410 05.04.89 25050 J 9 Foresr Phil 229 17.04.89 25050 11 V/eston K. J 34 17.01.90 33292.9375,. 12 Lee Terri 256 01.05.90 45332 Hall Stewart 227 04.06.90 34482.625, 15 Young Katherine 231 14.06.90 24400 20 Papadopoulos Chris 887 01.01.90 25050 241 Fisher Pete 888 12.09.90 23040, 20 Bennet Ann 5 01.02.91 34482.81 Рис.39.20. Окно работающего клиента (связь в сервером приложений) На заметку. Замечательной особенностью компонента TADODataSet яв- ляется его свойство RDSConnection, с помощью которого устанавливает- ся связь между связным компонентом TRDSConnection клиентским набо- ром данных. Поскольку этого свойства нет у других ЛДО-наборов, толь- ко TADODataSet ъммеп представлять удаленные НД на клиентской сто- роне в трехзвенных приложениях. Свойства TRDSConnection Центральным свойством компонента является ServerName, с помощью которого ус- танавливается доступ к нужному серверу приложений. По умолчанию содержит ссылку на фабрику объектов сервера, с помощью метода Create которой создается экземпляр сервера. Однако в наших экспериментах эта ссылка не работала и вместо нее мы исполь- зовали имя сервера как конкатенацию имени программы сервера и имени реализованно- го в нем со-класса. Например, RServer.RDSAppServer, Serv.AppServ и т.п. Ниже описываются специфичные свойства компонента. Свойство Назначение property AppServer; OleVariant; Содержит ссылку на связной интерфейс сер- вера приложений. property ComputerName: WideString; Идентифицирует местоположение сервера приложений. Если строка пуста, сервер распо- лагается на локальной машине. property Connected; Boolean; Содержит True, если установлена связь с сер- вером приложений. Property DataSpaceObject: DataSpace; Содержит ссылку иа базовый объект ADO Data Space. property InternetTimeout; Integer; Содержит максимальное время задержки (в миллисекундах) выполнения требования кли- ента.
Работа с ADO 581 property ServerName: WideString; Указывает имя сервера приложений. По умол- чанию содержит строку RDSServer.Data- Factory. Методы TRDSConnection Метод GetRecordSet вызывается автоматически, если с компонентом связан компо- нент TADODataSet, у которого в свойстве CommandType указано значение cmdTable, а в CommandText - имя нужной таблицы. Во всех остальных случаях метод GetRecordset следует вызывать непосредственно: ADODataSetl.CommandText := 'SELECT * FROM Employee'; ADODataSetl.RecordSet := RDSConnectionl.GetRecordset (ADODataSetl.CommandText, ''); Метод Назначение function GetRecordset(const CommandText: WideString; Connectionstring: WideString- 11): Recordset; Требует от удаленного сервера передать набор данных. procedure Registerclient(Client: TObject, Event:TConnectChangeEvent=nil); virtual; Регистрирует клиента, которому поставляются удаленные данные. procedure SendConnectEvent (Connecting: Boolean); Извещает всех зарегистрированных клиентов о том, что с сервером приложений установлена связь. procedure SetConnected(Value: Boolean); virtual; Изменяет значение свойства Connected. procedure UnRegisterClient(Client: TObject); virtual; Удаляет клиентское приложение из списка зарегистрированных. События TRDSConnection Событие С чем связано property AfterConnect: TNotifyEvent; Возникает после установления связи. property AfterDisconnect: TNotifyEvent; Возникает после разрыва связи. property BeforeConnect: TNotifyEvent; Возникает перед установлением связи. property BeforeDisconnect: TNot i fyEvent; Возникает перед разрывом связи. typo TLoginEvent - procedure (Sender:TObject; Username, Password: string) of object; property OnLogin: TLoginEvent; Возникает при регистрации входного имени пользователя, если свойство LoginPrompt име- ет значение True.
Глава 40 ТЕХНОЛОГИЯ INTERBASE EXPRESS Как и рассмотренная в предыдущей главе технология ADO, технология InterBase Express (IBX) рассчитана на создание «облегченного» клиента. С этой целью она предоставляет программисту способ непосредственного обращения к промышлен- ному серверу InterBase версии 5.5 без использования машины баз данных BDE или подобных средств доступа к данным. Delphi 5 не запрещает и традиционную архи- тектуру с BDE, которой, фактически, посвящена вся вторая часть книги. Однако, если вы собираетесь создавать клиент/серверную СУБД на основе InterBase 5.5, вряд ли вы откажетесь от более компактных и быстрых средств IBX, описываемых в этой главе. Замечание. Внимательное знакомство с технологией показало один ее существенный недостаток: любое SQL -предложение, передаваемое сер- веру, выполняется в рамках своей транзакции. Поскольку InterBase 5.5 автоматически закрывает транзакцию независимо от ее успеха, автома- тически закрываются и все связанные с этой транзакцией наборы данных. Их повторное открытие и позиционирование (см. пример на с.591) отни- мает много времени (у пользователя) и сил (у программиста). Возможно, версия 6 сервера InterBase будет лишена этого недостатка (см. методы RollbackRetaining и CommitRetaining на с.590). Кстати, простое редакти- рование таблиц с помощью сетки DBGrid или визуальных компонентов не приводит к завершению транзакции и не нуждается в переоткрытии таблицы (см. ниже простой пример). 40.1. ПРОСТОЙ ПРИМЕР Перед выполнением этого примера на вашей машине должен быть развернут ло- кальный сервер InterBase и этот сервер должен быть запущен. Характерной особенностью использования IBX является создание соединения с БД, которое достигается с помощью двух компонентов: TIBDatasase и TIBTransaction. Только после размещения на форме этих компонентов и их настройки, могут получить доступ к данным другие компоненты IBX. 1. Начните новый проект. 2. С помощью репозитория добавьте к проекту модуль данных (закладка New, пиктограмма Data Module). 3. Разместите в модуле данных компоненты TIBBatabase, TIBTransaction, два ком- понента TIBTable и два компонента TDataSource (рис.40.1). 4. Раскройте список свойства DatabaseName компонета IBDatabasel и с его помо- щью установите ссылку на демонстрационную БД EMPLOYEE. GDB (она размеща- ется в папке C:\Program FilesXCommon FilesXBorland SharedXData).
Технология InterBase Express 583 Рис.40.1. Модуль данных 5. Раскройте редактор свойства Params компонента IBDatabasel и введите регист- рационное имя и пароль: USER_NAME=SYSDBA PASSWORD=masterkey Замечание. Для версии 5.5 InterBase параметр USER_name пишется с символом подчеркивания (в предыдущих версиях этот символ не указы- вался). 6. Установите в свойство LoginPrompt компонента IBDatabasel значение True. 7. С помощью списка DefaultDatabase свяжите компонент IBTransactionl с ком- понентом IBDatabasel. 8. Настройте таблицу IBTablel на связь с IBDatabasel (свойство Database) и с IB- Transaction (свойство Transaction), после чего в списке TableName компонента выберите таблицу EMPLOYEE. Откройте таблицу (поместите True в свойство Ac- tive). При этом автоматически активизируются компоненты IBDatabasel и IB- Transactionl. 9. Аналогичным образом свяжите компонент IBTable2 с IBDatabasel, IBTransac- tionl, таблицей salary_hi story и тоже откройте таблицу. 10. Свяжите компонент DataSourcel с IBTablel, a DataSource? - с 1ВТаЫе2. 11. Таблицы employee (сотрудники) и salary_hi story (история продаж) связаны отношением главный-детальный по полю EMP_NO. Реализуем эту связь средст- вами модуля данных. Перетащите мышью таблицы IBTablel и IBTable? из лево- го поля с деревом компонентов на страницу с закладкой Data Diagram. Щелк- ните по кнопке главный-детальный , подведите указатель мыши в виде пе- речеркнутого круга к нижней стороне прямоугольника таблицы IBTablel, в мо- мент, когда указатель превратится в крестик, нажмите левую кнопку мыши и,
584~з^======= Глава 40 не отпуская ее, прочертите линию вниз, к прямоугольнику таблицы 1ВТаЫе2, до пересечения с его верхней стороной, после чего отпустите кнопку мыши. На эк- ране появится окно конструктора связей (рис.40.2). Рис.40.2. Окно конструктора связей главный-детальный 12. В списках Detail Fields и Master Fields щелкните по полям Emp_No, нажмите кнопку Add и закройте окно кнопкой ОК. Между прямоугольниками таблиц появится связь (рис.40.3). Рис.40.3. Диаграмма связи главный-детальный
Технология InterBase Express 585 13. Активизируйте главную форму программы (модуль Unit!) и положите на нее две сетки TDBGrid - одну для главной таблицы, другую - для детальной. 14. С помощью File | Use Unit главного меню Delphi свяжите модуль с моду- лем данных Unit2. 15. Поместите в свойство DataSource сетки DBGridl ссылку на DataSourcel модуля данных, а в такое же свойство DBGrid2 - на DataSource2 и запустите программу (рис.40.4). Рис. 40.4. Вид окна работающей программы ' 40.2. КОМПОНЕНТЫ ДЛЯ РЕАЛИЗАЦИИ IBX Все компоненты для реализации технологии IBX сосредоточены на панели Inter- Base галереи компонентов Delphi. Как уже говорилось, особенностью технологии является обязательное использование в ней компонентов TIBDatabase и TIBTransaction. Эти компоненты имеют общий родитель- ский класс TIBBase, с рассмотрения свойств которого мы и начинаем. 40.2.1. Класс TIBBase Этот класс инкапсулирует свойства, методы и события, общие для компонентов TIBDatabase и TIBTransaction. Свойства класса. Свойство Назначение property Database: TIBDatabase; Содержит ссылку на связанный компонент TIBDatabase. property DBHandle: PISC_DB_HANDLE; Содержит дескриптор базы данных.
586 Глава 40 property Owner: TObject; Содержит ссылку на собственника объекта. property Transaction: TIBTransaction; Содержит ссылку на связанный компонент TIBTransaction. property TRHandle: PISC_TR_HANDLE; Содержит дескриптор транзакции. Методы класса. Метод Назначение procedure CheckDatabase; Проверяет связь с БД и ее активность. procedure CheckTransaction; Поверяет связь с транзакцией и ее активность. События класса. Событие С чем связано property OnAfterDatabaseDisconnect: TNotifyEvent; Возникает после отсоединения от БД. property OnBeforeDatabaseDisconnect: TNotifyEvent; Возникает перед отсоединением от БД. property OnDatabaseFree: TNotifyEvent; Возникает после выгрузки объекта TIBData- base из памяти. property OnAfterTransactionEnd: TNotifyEvent; Возникает после окончания транзакции. Property OnBeforeTransactionEnd: TNotifyEvent; Возникает перед окончанием транзакции. property OnTransactionFree: TNotifyEvent; Возникает после выгрузки объекта TIBTrans- action из памяти. 40.2.2. Компонент TIBDatabase Создает соединение с БД InterBase. Свойства компонента. Свойство ' Назначение property DatabaseName: String; Указывает имя физической БД, с которой свя- зан компонент. property DBParamByDPB: [const Idx: Integer]; String; Позволяет проверить или установить заново какой-либо параметр БД. property DBSQLDialect: Integer; Содержит диалект языка SQL, который ис- пользуется БД. property DefaultTransaction: TIBTransaction; Содержит ссылку на умалчиваемую транзакцию property Handle: TISC_DB_Handle; Содержит дескриптор соединения. Использу- ется для прямого обращения к ЛРУ-функциям InterBase. property HandlelsShared: Boolean; Содержит True, если используется разделяе- мый дескриптор соединения. property IdleTimer: Integer; Указывает время в миллисекундах, по истече- нии которого автоматически разрывается со- единение с БД если за это время к ней не бы- ло обращения.
Технология InterBase Express 587 property IsReadOnly: Boolean; Содержит True, если БД работает только в режиме чтения (для версии InterBase 6). property Params: TStrimgs; Содержит список параметров, которые пере- даются серверу в момент соединения. property SQLDialect: Integer; Содержит диалект языка SQL, который ис- пользуется компонентом. property SQLObjectCount: Integer; Содержит количество объектов SQL, связан- ных с данным соединением. К объектам SQL относятся InterBase наборы данных, IBSQL и BLOB. property SQLObjects[Index: Integer]: TIBBase; Открывает индексированный доступ к SQL- объектам. type TTraceFlag - (tfQPrepare, tfQExecute, tfQFetch, tfError, tfStmt, tfConnect, tfTransact, tfBlob, tfService, tfMisc); TTraceFlags - set of TTraceFlag; property TraceFlags: TTraceFlags; Содержит флаги, определяющие различные состояния БД, которые должны прослежи- ваться в SQL-мониторе: tfQPrepare - готовится выполнение запро- са; tfQExecute - запрос выполняется; tfQFetch - идет наполнение набора данных; tfError - возникла ошибка при выполнении запроса; tfStmt - мониторинг всех предло- жений SQL', tfConnect - устанавливается связь с БД; tfTransact - мониторинг начала, подтверждения и отката транзакций; tfBlob - мониторинг операций над данными типа BLOB-, tjService - мониторинг сервисных операций; tfMisc - мониторинг любых дру- гих предложений, не перекрывающимися этими флагами. property Transactioncount: Integer; Указывает количество'транзакций, связанных в данный момент с компонентом. property Transactions [Index: Integer]: TIBTransaction; Открывает индексированный доступ к свя- занным с компонентом транзакциям. Свойство DatabaseName для локального сервера содержит имя файла БД. При свя- зи с удаленной БД по протоколу TCP/IP имя записывается в формате <имя_сервера>:<имя_файла_БД>; по протоколу NetBIOS - в формате \\<имя_сервера>\<имя_файла_БД>; по протоколу SPX - в формате <имя_сервера>@<имя_файла_БД> Компонент может управлять несколькими транзакциями одновременно. Свойство DefaultTransaction ссылается на транзакцию, которой компонент управляет в данный момент. Методы компонента. Метод Назначение function AddTransaction(TR: TIBTransaction): Integer; Устанавливает связь между компонентом и новой транзакцией.
588 Глава 40 procedure ApplyUpdates(const DataSets: array of TIBCustomDataSet); Подтверждает кэшированные изменения. procedure CheckActive; Проверяет активность компонента и возвра- щает ошибку, если связь не установлена. procedure CheckDatabaseName; Возбуждает ошибку, если свойство Databa- seName не заполнено. procedure Checkinactive; Возбуждает ошибку, если связь с БД установ- лена. procedure Close; Разрывает соединение с БД и закрывает все связанные с компонентом НД. Повторное от- крытие компонента не открывает заново НД. procedure CloseDataSets; Закрывает все связанные с компонентом набо- ры данных, но, в отличие от Close, не разры- вает связь с сервером и БД. procedure CreateDatabase; Создает новую БД, используя Params как пе- речень параметров новой БД. procedure DropDatabase; Уничтожает существующую БД. function FindTransaction (TR: TIBTransaction): Integer; Возвращает индекс указанной транзакции. procedure Forceclose; Закрывает БД, даже если в результате опера- ции возникла ошибка. procedure GetFieldNames(const TableName: String; List: TStrimgs); Возвращает список имен всех полей ука- занной таблицы. К моменту обращения к методу связь с БД должна быть установ- лена. procedure GetTableNames(List: TStrimgs; SystemTables: Boolean - False); Возвращает список всех ТБД. Если пара- метр SystemTables имеет значение True, список дополняется именами системных таблиц. function IndexOfDBConst(st: String): Integer; Возвращает индекс параметра по его имени (если возвращается -1, параметр с нужным именем не найден). procedure Open; Устанавливает связь с БД. procedure RemoveTransaction(Idx: Integer); Отсоединяет указанную транзакцию от ком- понента. procedure RemoveTransactions; Отсоединяет все транзакции от компонента. procedure SetHandle; Устанавливает дескриптор для БД. procedure TestConnected: Boolean; Проверяет связь с БД и возвращает True, если связь установлена. С помощью метода CreateDatabase можно создать новую базу данных. Для этого в свойство DatabaseName помещается месторасположение (компьютер, маршрут досту- па) и имя файла создаваемой БД, а в набор Params параметры БД так, как описано в п.17.1. Список параметров, например, может быть таким: USER "SYSDBA" PASSWORD "masterkey" PAGE_SIZE=4096 DEFAULT CHARACTER SET WIN1251 После выполнения метода будет создана полноценная БД с файлом по адресу, ука- занному в свойстве DatabaseName.
Технология InterBase Express 589 События компонента. Событие С чем связано property OnDialectDowngradeWarning: TNotifyEvent; Возникает в момент открытия БД, если ис- пользуемый в ней диалект SQL меньше ука- занного в свойстве SQLDialect. property OnldleTimer: TNotifyEvent; Возникает, если время «простоя» БД превысит указанное в свойстве IdleTimer. TDatabaseLoginEvent = procedure (Database: TIBDatabase; LoginParams: TStrimgs) of object; property OnLogin: TDatabaseLogin- Event; Возникает, если свойство LoginPrompt содер- жит True. В обработчике события регистраци- онное имя и пароль необходимо поместить во временный список LoginParams. 40.2.3. Компонент TIBTransaction Для установления рвязи с InterBase в технологии IBX хотя бы один компонент TIB- Transaction должен быть связан с компонентом TIBDatabase. В многозвенных прило- жениях каждый 5(Э/,-запрос исполняется в отдельной транзакции. Свойства компонента. Свойство Назначение property Active: Boolean; Содержит True, если компонент активен. property Databasecount: Integer; Содержит количество БД, обслуживаемых в данной транзакции. property Databases[Index: Integer]: TIBDatabase; Дает индексный доступ к БД, обслуживаемых транзакцией. type TTransactionAction = (taRollback, taCommit, taRollbackRe- taining, taCommitRetaining); property DefaultAction: TTransaction- Action; Указывает способ-завершения транзакции по истечении времени, заданного в свойстве IdleTimer. taRollback - выполнить откат тран- закции; taCommit - выполнить подтверждение транзакции: taRollbackRetaining - выполнить откат транзакции, но сохранить ее контекст (для InterBase v.6); taCommitRetaining - вы- полнить подтверждение транзакции, но сохра- нить ее контекст. property DefaultDatabase: TIBDatabase; Содержит текущую БД. property Handle: TISC_TR_HANDLE; Содержит дескриптор транзакции. property HandlelsShared: Boolean; Содержит True, если дескриптор транзакции разделяемый. property IdleTimer: Integer; Указывает, сколько времени в миллисекундах должно пройти перед автоматическим выпол- нением свойства DefaultAction. property InTransaction: Boolean; Указывает, находится ли БД в состоянии неза- вершенной транзакции. property Params: TStrimgs; Содержит набор параметров для компонента. property SQLObjectCount: Integer; Содержит количество обслуживаемых тран- закцией активных SgL-компонентов (наборов данных, IBSQL, BLOB^^^
590 Глава 40 property SQLObjects[Index: Integer]: TIBBase; Открывает индексный доступ к обслуживае- мым транзакцией SQL-компонентам. property TPB: PChar; Содержит ссылку на буфер параметров тран- закции (только в режиме чтения). Для записи параметров служит свойство Params. Property TPBLength: Short; Указывает длину буфера параметров компо- нента. Методы компонента. Метод Назначение function AddDatabase(db: TIBDatabase): Integer; Добавляет к компоненту новый компонент TIBDatabase. procedure CheckDatabasesInList; Проверяет список связанных компонентов TIBDatabase. Если список пуст, возникает исключение. procedure ChecklnTransaction; Проверяет активность транзакции и список обслуживаемые ею TIBDatabase. Если тран- закция неактивна или список пуст, возникает исключение. procedure CheckNotInTransaction; Проверяет активность транзакции и список обслуживаемые ею TIBDatabase. Если тран- закция активна или список не пуст, возникает исключение. procedure Commit; Подтверждает текущую транзакцию и завер- шает ее. procedure CommitRetaining; Аналогична Commit, но сохраняет контекст транзакции (для InterBase версии 6). function FindDatabase (db: TIBDatabase): Integer; Возвращает индекс связанного компонента TIBDatabase. procedure RemoveDatabase(Idx: Integer); Удаляет компонент TIBDatabase с индексом Idx из списка связанных. procedure RemoveDatabases; Удаляет все компоненты TIBDatabase из спи- ска связанных. procedure Rollback; Осуществляет откат текущей транзакции. procedure RollbackRetaining; Аналогична Rollback, но сохраняет контекст транзакции (для InterBase версии 6). Procedure StartTransaction; Начинает очередную транзакцию. В момент открытия компонента TIBTransation автоматически стартует связанная с ним транзакция. Только после этого можно открыть какие-то наборы данных. По- скольку транзакции не могут быть вложенными, такой фрагмент будет ошибочным (кстати, этот пример взят из фирменной документации!): procedure TForml.ApplyButtonClick(Sender: TObject); begin with CustomerQuery do begin Open; IBTransactionl.StartTransaction; //Ошибка! // Транзакция уже стартовала!
Технология InterBase Express 591 Tablel.Insert; Tablel.FieldByName('QUANTITY').Aslnteger := StrToInt(Editl.Text); Tablel.Post; IBTransactionl.Commit; //Конец транзакции end; end; Первым же исполняемым оператором открывается набор данных Customer Query. Если к этому моменту IBDatabasel и/или IBTransactionl не были открыты, они авто- матически открываются и, следовательно стартует транзакция (если они уже были открыты, транзакция уже стартовала). Второй исполняемый оператор пытается начать новую (вложенную) транзакцию, что является ошибкой. Вот правильное решение классической задачи перевода денег с дебиторского счета на кредиторский (рис. 40.5) Рис.40.5. Окно для примера использования транзакции procedure TForml.btnChangeClick(Sender: TObject); var Delta: Currency; BM1,BM2: TBookMark; begin try //Проверяем правильность записи суммы перевода Delta := StrToFloat(Editl.Text) ; except Editl.SetFocus ; Exit end; //Запоминаем положение курсоров таблиц Debit и Credit ВМ1 := tbDebit.GetBookmark; BM2 := tbCredit.GetBookmark; //Сумма перевода не может превышать сумму на счету дебитора: if tbDebitSumm.Value>Delta then begin try //Снимаем с дебиторского счета tbDebit.Edit; tbDebitSumm.Value := tbDebitSumm.Value-Delta; tbDebit.Post; //Начисляем на кредиторский счет
592 Глава 40 tbCredit.Edit; tbCreditSumm.Value := tbCreditSumm.Value+Delta; tbCredit.Post; //Ошибок нет - подтверждаем транзакцию IBTransactionl.Commit; except //Ошибка - откат транзакции ShowMessage('Операция не прошла!’); IBTransactionl.Rollback; end; //try {После подтверждения или отката транзакция завершается, а значит - закрываются таблицы. Вновь открываем их и устанавливаем их курсоры на прежние места. При открытии первой же таблицы авто- матически стартует новая транзакция} tbDebit.Open; tbCredit.Open; tbDebit.GotoBookmark(BM1); tbCredit.GotoBookmark(BM2); tbDebit.FreeBookmark(BM1); tbCredit.FreeBookmark(BM2); end //if tbDebitSumm.Value>Delta end; C компонентом связано единственное событие property OnldleTimer: TNotifyEvent; которое возникает после завершения транзакции по истечению времени, указанному в свойстве IdleTimer. 40.2.4. Компонент TIBTable Компонент повторяет в функциональном отношении ££)£-компонент ТТаЫе, по- этому его основные свойства, методы, события, а также приемы работы с ним см. в гл.7. Ниже описываются лишь специфичные для компонента свойства. Свойства компонента. Свойство Назначение property Bufferchunks: Integer; Указывает, по сколько записей будет наращи- ваться буфер таблицы по мере его наполнения. property UniDirectional: Boolean; Если содержит False, с таблицей будет создан двунаправленный курсор и навигация по ней разрешена как сверху вниз, так и снизу вверх. Если True, навигация по таблице разрешена только сверху вниз. property UpdateObject; Ссылается на объект, который будет реализо- вывать изменения в таблицах, открытых только для чтения. В отличие от ££>£-аналога, компонент может создавать однонаправленный курсор {ТТаЫе всегда создает двунаправленный курсор). Однонаправленные курсоры экономят ресурсы компьютера и реализуют более быструю навигацию по записям. Свойство UniDitectional объявлено в секции Publish, но не Publidhed, поэтому его нет в окне Ин-
Технология InterBase Express 593 Спекторе Объектов и программист не может изменить его на этапе конструирования про- граммы (но может - при прогоне программы). Если таблица с однонаправленным курсо- ром отображается в сетке TDBGrid, перемещение курсора вверх возможны только в пре- делах сетки. Перед изменением свойства UniDirectional компонент должен быть закрыт. В документации сказано, что компонент может отображать не только реальные ТБД, но и просмотры view. Это действительно так, только при ссылке на таблицу в свойстве TableName не стоит искать просмотры в раскрывающемся списке, а следует просто вручную записать имя просмотра в это свойство. Поскольку просмотры могут не быть обновляемыми (например, они могут содержать записи из нескольких таб- лиц), в компоненте появилось свойство UpdateObject, в которое чаще всего помещает- ся имя компонента TIBUpdateSQL, с помощью которого реализуются изменения ото- бражаемых данных, если просмотр «не живой». Компонент не имеет специфичных по сравнению с ТТаЫе методов и событий. 40.2.5. Компонент TIBQuery Компонент является функциональным аналогом компонента TQuery, описанному в гл.6. В этом разделе описываются только специфичные для компонента свойства. Метод Назначение property GenerateParamNames: Boolean; Если содержит True, компонент создает спи- сок параметров запроса (в TQuery этот список создается всегда). property StmtHandle: TISC_STMT_HANDLE; Дескриптор БД который можно использовать при непосредственном обращении к API- функциям InterBase. По мнению авторов, свойству GenerateParamNames трудно найти практическое объяснение, кроме как желание дать программисту средство сэкономить немного ре- сурсов компьютера. Кроме того, нам не удалось найти разницу в поведении компонен- та при изменении значения этого свойства. Например в таком фрагменте: procedure TForml.FormCreate(Sender: TObject); var k: Integer; begin IBQueryl.GenerateParamNames := False; for k := 0 to IBQueryl.Params.Count-1 do ListBoxl.Items.Add(IBQueryl.Params[k].Name); end; список ListBoxl всегда наполнялся именами параметров, независимо от значения свойства GenerateParamNames. Компонент не имеет специфичных методов и свойств. 40.2.6. Компонент TIBStoredProc Компонент полностью повторяет функциональность TStoredProc, поэтому все его свойства и приемы работы с ним не отличаются от тех, что описаны в гл.26.
594 Глава 40 Единственным отличием является наличие свойства property StmtHandle: TISC_STMT_HANDLE; с помощью которого можно обратиться непосредственно в API-функциям сервера. 40.2.7. Компонент TIBUpdateSQL Компонент полностью повторяет функциональность TUpdateSQL, поэтому все его свойства и приемы работы с ним не отличаются от тех, что описаны в гл.30. 40.2.8. Компонент TIBDataSet Компонент предназначен для просмотра и модификации набора записей, указанно- го в его свойстве SelectSQL. Особенностью компонента является то обстоятельство, что НД не будет обновляемым до тех пор, пока не будут определены модифицирую- щие его 5б/,-операторы в его строковых свойствах InsertSQL, DeleteSQL и ModifySQL. Если, например, в его свойстве InsertSQL будет записан правильный оператор insert, НД будет способен вставлять записи, но только так, как это определено в этом опера- торе. Точно так же, если в свойстве DeleteSQL определен оператор delete, НД будет способен уничтожить запись или записи, определенные в этом операторе и т.д. Дейст- вия пользователя являются своеобразным «спусковым крючком» и заставляют компо- нент автоматически выполнить нужный оператор, При этом если эти действия не сов- падают с теми, что определены дополнительными ^^/--операторами, они отвергаются. Если, например, в ModifySQL записан оператор UPDATE Credit SET Summ=l WHERE N_Count=2 то какие бы изменения в какие бы записи не вносил пользователь фактически будет выполнен 1 раз указанный выше оператор и запись (записи) из ТБД Credit со значени- ем поля N_Count=2 получит в поле Summ значение 1. Причем в сетке, отображающей НД, изменения будут видны так, как они сделаны пользователем, но фактически в ТБД изменения будут другими - оно станут видны после переоткрытия НД. Заметим, что если в свойство RefreshSQL поместить такой же оператор, как и в основное свойство SelectSQL, все изменения в реальных данных станут немедленно видны, поэтому в сетке пользователь увидит не свои изменения, а те, что сделаны фактически соответ- ствующими 52/,-операторами. Замечание. Поскольку изменения вносятся 52/,-операторами, НД по своему характеру может быть необновляемым, например, составленным из нескольких таблиц. Заметим также, что для внесения любых измене- ний в любой 52/,-оператор, НД должен быть предварительно закрыт. Свойства компонента. Свойство Назначение property Bufferchunks: Integer; Указывает, по сколько записей будет наращи- ваться буфер компонента по мере его напол- нения. property Database: TIBDatabase; t Содержит ссылку на связанный компонент TIBDatabase.
Технология InterBase Express 595 property DBHandle: PISC_DB_HANDLE; Содержит дескриптор набора данных. property DeleteSQL: TStrimgs; Содержит ^/.-предложение для уничтожения записей. Если свойство содержит пустую строку, в НД нельзя уничтожать записи. property InsertSQL: TStrimgs; Содержит .^^предложение для вставки запи- сей. Если свойство содержит пустую строку, в НД нельзя вставлять записи. property ModifySQL: TStrimgs; Содержит ^/.-предложение для изменения записей. Если свойство содержит пустую строку, в НД нельзя редактировать записи. property Params: TIBXSQLDA; Содержит параметры для параметрического запроса. property Prepared: Boolean; Если содержит True, осуществляется подго- товка перед исполнением запроса. property QDelete: TIBSQL; Содержит ссылку на объект, который инкап- сулирует предложение DELETE. property QInsert: TIBSQL; Содержит ссылку на объект, который инкап- сулирует предложение INSERT. property QModify: TIBSQL; Содержит ссылку на объект, который инкап- сулирует предложение UPDATE. property QRefresh: TIBSQL; Содержит ссылку на объект, который инкап- сулирует предложение REFRESHSQL. property QSelect: TIBSQL; Содержит ссылку на объект, который инкап- сулирует предложение SELECT. property RefreshSQL: TStrimgs; Содержит 5б£-предложение, позволяющее обновить НД. Обычно оно просто совпадает с предложением SelectSQL. property SelectSQL: TStrimgs; Содержит предложение SELECT, создающее НД type TIBSQLTypes = set of (SQLUnknown, SQLSelect, SQLInsert, SQLUpdate, SQLDelete, SQLDDL, SQLGet- Segment, SQLPutSegment, SQLExecProce- dure, SQLStartTransaction, SQLCommit, SQLRollback, SQLSelectForUpdate, SQLSetGenerator); property StatementType: TIBSQLTypes; Содержит тип ^^-предложения: SQLUnknown - тип неизвестен; SQLSelect - предложение SE- LECT; SQLInsert - предложение INSERT; SQLUpdate - предложение UPDATE; SQLDelete - предложение DELETE; SQLDDL - предложе- ние иа языке определения данных DDL; SQLGetSegment - читает часть открытого BLOB; SQLPutSegment - записывает часть BLOB; SQLExecProcedure - выполняет хранимую про- цедуру; SQLStartTransaction - стартует новую транзакцию; SQLCommit - подтверждает тран- закцию; SQLRollback - откатывает транзакцию; SQLSelectForUpdate - хранимая процедура представляет собой набор предложений для обновления данных; SQLSetGenerator - устанав- ливает новое значение для генератора. property Transaction: TIBTransaction; Содержит ссылку на связанный компонент TIBTransaction. property TRHandle: PISC_TR_HANDLE; Содержит дескриптор транзакции. property UpdateObject: TIBDataSetUpdateObject; Содержит ссылку на компонент TIBUpdateSQL, с помощью которого можно обновить Н Д только для чтения.
596 Глава 40 type TIBUpdateRecordTypes = set of (cusModified, cuslnserted, cusDe- leted, cusUnmodified, cusUninserted); property UpdateRecordTypes: TIBUp- dateRecordTypes; Указывает, какие записи в кэшированием на- боре видны при обновлении: cusModified - модифицированные записи; cuslnserted - вставленные записи; cusDeleted - удаленные записи; cusUnmodified - только неизмененные записи; cusUninserted - только ие вставленные записи. property UpdatesPending: Boolean; Указывает, содержит ли буфер обновления записи. Специфичные методы компонента. Метод Назначение procedure ApplyUpdates; Переписывает изменения кэшированных дан- ных в ТБД, но не завершает транзакцию. type TCachedUpdateStatus = (cusUnmodified, cusModified, cusln- serted, cusDeleted, cusUninserted); function CachedUpdateStatus: TCachedUpdateStatus; Возвращает статус кэшированных изменений: cusUnmodified - данные ие редактировались; cusModified - данные редактировались; cuslnserted - были вставлены записи; cusDeleted - были уничтожены записи; cusUninserted - записи не вставлялись. procedure CancelUpdates; Очищает буфер изменений. procedure FetchAll; Получает все записи от текущей до конца файла и сохраняет их локально. function GetFieldData(FieldNo: Inte- ger; Buffer: Pointer): Boolean; function GetFieldData(Field: TField; Buffer: Pointer) : Boolean; Читает в буфер Buffer данные из поля по его номеру FiledNo или из объекта-поля Field. function LocateNext(const KeyFields: Strimg; const KeyValues: Variant; Options: TLocateOptions): Boolean Позиционирует курсор на запись ниже теку- щей: KeyFields - имя ключевого поля; KeyValues - искомое значение; Options - до- полнительные условия поиска. procedure Prepare; Подготавливает компонент к выполнению запроса; procedure UnPrepare; Освобождает ресурсы, занятые при выполне- нии метода Prepare. В отличие от стандартного метода Locate, который, как и Lookup, доступен компо- ненту, метод LocateNext ищет запись от текущей к концу НД. Специфичные события компонента. Событие С чем связано property DatabaseDisconnected: TNotifyEvent; Возникает после разрыва соединения с БД. property DatabaseDisconnecting: TNotifyEvent; Возникает перед разрывом соединения с БД. Property DatabaseFree: TNotifyEvent; Возникает при разрушении связанного компо- нента TIBDatabase.
Технология InterBase Express 597 40.2.9. Компонент TIBSQL Компонент предназначен для выполнения ^/.-запросов с минимальными затрата- ми времени. Он не может быть связан с TDataSource и, следовательно, с визуализи- рующими компонентами. В отличие от других компонентов-наборов, TIBSQL про- изошел непосредственно от TComponent, поэтому все его свойства, методы и события «свои», хотя многие из них в функциональном отношении повторяют одноименные свойства, методы и события других компонентов-наборов. Для ускорения доступа к данным компонент использует специальные классы запи- сей и полей. Он создает однонаправленный курсор, так что перемещение по НД воз- модно только сверху вниз. Наконец, он лишен многочисленных событий AfterXXXX, BeforXXXX, а также OnCalcField, так что в НД нельзя создать вычисляемые поля или реализовать бизнес-правила на основе обработчиков этих событий. Специфичные свойства компонента. Свойство Назначение property Bof: Boolean; Содержит True, если курсор стоит перед пер- вой записью НД. property Database: TIBDatabase; Содержит ссылку на связанный компонент TIBDatabase. property DBHandle: PISC_DB_HANDLE; Содержит дескриптор БД. property Eof: Boolean; Содержит True, если курсор стоит после по- следней записи НД. property Fieldindex: [FieldName: String]: Integer; Возвращает индекс поля по его имени. property Fields[const Idx: Integer]: TIBXSQLVAR; Открывает индексный доступ к полям НД в формате TIBXSQLVAR (см. ниже примечание). property GenerateParamNames: Boolean; Если содержит True, компонент создает имена параметров для параметрического запроса. property GoToFirstRecordOnExecute: Boolean; Если содержит True, курсор сразу после от- крытия НД устанавливается иа его первую запись. property Handle: TISC_STMT_HANDLE; Содержит дескриптор запроса. property Open: Boolean; Содержит True, если НД открыт. property ParamCheck: Boolean; Если содержит True, параметры параметриче- ского запроса проверяются перед выполнение запроса. property Params: TIBXSQLDA; Содержит параметры запроса в формате TIBX- SQLDA. property Plan: String; Содержит план выполнения запроса после его подготовки. property Prepared: Boolean; Содержит True, если запрос был подготовлен методом Prepare. property Recordcount: Integer; Содержит количество записей в запросе. property RowsAffected: Integer; Возвращает количество записей, которые бы- ли изменены в результате выполнения запро- са. property SQL: TStrimgs; Содержит текст запроса.
598 Глава 40 property FSQLType, SQLType: TIBSQLTypes read Содержит тип запроса. Описание типа TIBSQLTypes см. в свойстве StatementType компонента TIBDataSet (с.595). property Transaction: TIBTransaction; Содержит ссылку иа связанный компонент TIBTransaction. property TRHandle: PISC_TR_HANDLE; Содержит дескриптор транзакции. property UniqueRelationName: String; Используйте это свойство, чтобы указать уни- кальное реляционное имя для запроса, касаю- щегося одной таблицы. Тип TIBXSQLVAR содержит практически такие же методы приведения типов ASXXXX, как и «обычный» тип TField, а его свойство Value имеет тип Variant. Таким образом, работа со значениями полей компонента TIBSQL практически не отличается от работы со значениями полей других компонентов-наборов. Это же замечание отно- сится к типу TIBXSQLDA и работе со значениями параметров. Указанные типы под- держивают специфику скоростной работы компонента. Методы компонента. Метод Назначение procedure Batchinput(Inputobject: TIBBatchlnput); Выполняет параметрический запрос для объ- екта InputObiect. procedure Batchoutput(Outputobject: TIBBatchOutput); Выполняет параметрический запрос для объ- екта OutputObject. procedure CheckClosed; Возбуждает исключение, если запрос не закрыт. procedure CheckOpen; Возбуждает исключение, если запрос закрыт. procedure CheckValidStatement; •» Возбуждает исключение, если SQL- предложеиие ошибочно. procedure Close; Закрывает запрос. function Current: TIBXSQLDA; Возвращает дескриптор текущей записи. procedure ExecQuery; Выполняет запрос. function FieldByName(FieldName: String): TIBXSQLVAR; Возвращает поле по его имени. procedure FreeHandle; Освобождает ресурсы InterBase, связанные с запросом. Function Next: TIBXSQLDA; Смещает курсор к следующей записи. Procedure Prepare; Готовит запрос к выполнению. Нетрудно обнаружить, что имеется единственный навигационный метод Next. Та- ким образом, последовательный просмотр записей НД возможен только от начала на- бора к его концу. Компонент имеет единственное свойство property OnSQLChanging: TNotifyEvent; которое возникает при изменении Sg£-3anpoca. 40.2.10. Компонент TIBDatabaselnfo Этот компонент предназначен для представления программе дополнительной слу- жебной информации о присоединенной базе данных. Компонент практически не имеет специфичных методов и событий.
Технология InterBase Express 599 Свойства компонента. Свойство Назначение , property Allocation: Longlntlnt; Возвращает количество распределенных в памяти страниц БД. property BackoutCount: TStrimgList; Возвращает количество удаленных версий страниц. property BaseLevel: Smalllnt; Возвращает 2-х байтный номер версии БД. property CurrentMemory: Longlntlnt; Возвращает объем памяти (в байтах), исполь- зуемый сервером. property Database: TIBDatabase; Содержит ссылку иа связанный компонент TIBDatabase. property DBFileName: String; Возвращает имя БД. property DBImplementationClass: Longlntlnt; Возвращает иомер реализуемого класса БД. property DBImplementationNo: Longln- tlnt; Возвращает номер реализации БД. property DBSiteName: String; Возвращает сайт-имя БД (обычно совпадает с именем компьютера, на котором она располо- жена). property DBSQLDialect: Longlntlnt; Возвращает используемый в БД диалект SQL. property DeleteCount: TStrimgList; Возвращает список БД, которые были унич- тожены с момента последнего присоединения к ней пользователя. property ExpungeCount: TStrimgList; Возвращает количество удаленных записей. property Fetches: Longlntlnt; Возвращает количество записей, прочитанных в локальный буфер. property ForcedWrites: Longlnt; Возвращает номер режима записи в БД: 0 - асинхронный режим; 1 - синхронный режим. property Insertcount: TStrimgList; Возвращает количество вставленных записей. property Marks: Longlnt; Возвращает количество записей в буфер кэша. property MaxMemory: Longlnt; Возвращает максимальный объем памяти в байтах, который использовался с момента первого присоединения к БД. property NoReserve: Longlnt; Указывает, резервировалась ли память для сохранения устаревших записей (0 - резерви- ровалась). property NumBuffers: Longlnt; Возвращает количество распределенных бу- феров кэша. property PageSize: Longlnt; Возвращает размер страницы в байтах. property Sweepinterval: Longlnt; Указывает, сколько транзакций должно ус- пешно завершиться до момента автоматиче- ской чистки страниц БД от ненужных версий записей. property UserNames: TStrimgList; Содержит список имен всех пользователей, присоединенныхкБ1Ь_
600 Глава 40 40.2.11. Компонент TIBSQLMonitor Компонент предназначен для создания отладочного инструментария, с помощью которого программист может осуществлять мониторинг всех ^/-предложений, кото- рыми обмениваются клиент и сервер. Для этого следует установить нужные флаги в свойство TraceFlags связанного компонента TIBDatabase (см. с.587). Компонент не имеет собственных свойств и методов. Единственное событие I TSQLEvent = procedure(EventText: String) of object; property OnSQL: TSQLEvent; возникает, если клиент передал серверу ^^/--предложение, тип которого выбран фла- гами TraceFlags. В этом случае обработчик события в строке EventText может прочи- тать соответствующее сообщение (рис.40.6). [Application: Project!] : [Commit (Hard commit)] Puc. 40.6. Пример отладочных сообщений компонента TIBSQLMonitor В [Application: Project! ] : [Execute] Select F.RDB$COMPUTED BLR. F.RDB$DEFAULT VALUE from RDBJRELATION FIELDS R, RDBJFIELDS F where R.RDB$RELATION NAME - 'COUNTRY' and R.RDBSFIELD SOURCE-F.RDB$FIELD NAME and R.RDBJFIELD NAME-'CURRENCY' [Application: Project! ] [Execute] select COUNTRY.". RDB$DB_KEY as IBXJNTERNALJ)BKEY from COUNTRY order by COUNTRY [Application: Project!] : [Fetch] select COUNTRY.", RDBtDB KEY as IB<INTERNALDBKEY from COUNTRY order by COUNTRY 40.2.11. Компонент TIBEvents Этот компонент полностью повторяет функциональность своего В/)Е-аналога TI- BEventAlert, описанного в гл. 31.
ПРИЛОЖЕНИЯ П1. РАБОТА С УТИЛИТОЙ BDE ADMINISTRATOR BDE Administrator предназначен для создания, изменения и удаления псевдонимов доступа к БД. С этой же целью может использоваться и рассматриваемая в приложе- нии П2 утилита SQL Explorer. ni l. СОЗДАНИЕ ПСЕВДОНИМА БД Параметры БД и ее местоположение определяются псевдонимом (именем) БД. Роль псевдонима состоит в том, что связанные с ним положение и параметры БД могут менять- ся без изменения уже созданных программ, в которых этот псевдоним используется. Для того, чтобы определить псевдоним, необходимо: 1. выбрать элемент меню Object | New, 2. выбрать в появившемся окне имя драйвера базы данных (STANDARD для БД, создан- ных с помощью Paradox, dBASE, Clipper и FoxPro, MSACCESS для Microsoft Access, ORACLE, INTRBASE, SYBASE, MSSQL, INFORMIX, DB2 соответственно для баз данных Oracle, InterBase, Sybase, MSSQL Server, Informix, DB2 и, если установлен, драйвер ODBC); 4. ввести имя псевдонима в левом окне; 4. определить необходимые параметры псевдонима в правом окне (рис.Ш.1); 5. щелкнуть по псевдониму правой кнопкой мыши и выбрать в локальном меню опцию Apply для подтверждения создания псевдонима. Рис.Ш.1. Параметры текущего псевдонима базы данных
602 Приложение П1 П1.1.1 Создание псевдонима STANDARD Этот тип псевдонима используется для доступа к локальным БД. Таблицы, индек- сы, мемо-поля таких БД хранятся отдельно друг от друга, каждый в своем файле. База данных в этом случае - группа файлов, собранных в одном каталоге на диске. Псевдо- ним БД указывает на такой каталог. При определении псевдонима используются следующие параметры: DEFAULT DRIVER - определяет тип СУБД; значениями этого параметра могут быть paradox, DBase, FOXPRO или asciidrv для довольно редкого случая использования в качестве таблиц БД колумнированных, т.е. разбитых на колонки, текстовых файлов; базы, созданные с помощью Clipper, обычно совместимы с форматами DBASE или FOXPRO; ENABLE BCD - если имеет значение True, указывает на необходимость для BDE пе- реводить целые и вещественные значения полей в значения BCD (Binary Coded Deci- mal - двоично-десятичные кодированные значения); BCD позволяет устранить ошибки округления, связанные с действиями над целыми числами; PATH - указывает полное имя каталога, в котором расположены файлы БД. П1.1.2. Создание псевдонима INTRBASE Псевдонимы типа INTRBASE используются для доступа к БД сервера InterBase. BATCH COUNT - число записей, изменяемых в рамках неявной транзакции. Перед изменением записей автоматически стартует транзакция на сервере. После измене- ния этого числа записей транзакция на сервере автоматически подтверждается. Ис- пользование batch COUNT дает возможность изменить число записей в пакете об- новлений для каждой подтвержденной транзакции, если размер подобного пакета на сервере недостаточно велик. По умолчанию - число записей, умещающихся в пакете длиной 32 Кбайт. BLOB SIZE - определяет максимальную длину (в килобайтах) BLOB-тля. Может меняться от 32 до 1000. BLOBS то CACHE - определяет, сколько записей с полями BLOB будут кэшировать- ся для клиента. По умолчанию 64. Может меняться от 64 до 65535. enable BCD - указывает на необходимость для BDE переводить целые и вещест- венные значения полей в значения BCD. ENABLE schema CACHE - при значении true позволяет хранить сведения о струк- туре удаленной БД на клиентском компьютере в каталоге, определяемом параметром schema CACHE DIR. Хранение структуры БД хорошо тем, что в этом случае BDE не нужно всякий раз считывать данную информацию из удаленной БД, что экономит время и ресурсы. Однако это возможно лишь для БД, чья структура (состав таблиц, столбцов, индексов и др.) не изменяется во времени. SCHEMA CACHE TIME - в случае хранения сведений о структуре БД параметр ука- зывает, с какой периодичностью BDE обновляет сведения о ней. Значения: • 0 - хранение схемы БД не используется; • -1 (по умолчанию) - после закрытия БД в приложении (т.е. после разрыва со- единения с БД); • число в диапазоне 1..2 147 483 647 - количество секунд между двумя обновле- ниями сведений о структуре БД.
Работа с утилитой BDE Administrator 603 LANGDRIVER - языковый драйвер, используемый для кодировки символьных полей и определяющий также порядок сортировки символьных значений. Для русскоязыч- ных приложений рекомендуется Pdox ANSI Cyrillic. Для этого драйвера без оши- бок работают символьные функции преобразования строчных/заглавных букв AnsiUpperCase nAnsiLowerCase. Pdox ANSI Cyrillic совместим с кодировкой сим- волов InterBase WIN1251 и порядком сортировки символов pxw_cyrl. Эти значения указываются в БД в атрибутах: default character set для баз данных, character set и collate для доменов и столбцов. MAX ROWS - указывает максимальное количество записей, которые драйвер пытается считать из удаленной БД при посылке каждого Sgl-aanpoca к серверу, с учетом запросов на чтение структуры БД и проверки соответствия значений столбцов накладываемым на них ограничениям. Используется для предотвращения безграничного расходования сис- темных ресурсов, например, при выдаче оператора SELECT для большой по объему таблицы БД. Малое значение MAX ROWS может привести к тому, что таблица БД не будет открыта, поскольку запрос чтения структуры таблицы может возвратить больше записей, чем указано в MAX ROWS. Однако MAX ROWS никак не воздействует на необ- новляемые запросы. По умолчанию принято (-1), что означает отсутствие лимита на количество считываемых строк. В случае, если количество возвращаемых строк больше установленного лимита, выдается ошибка dbierr_rowfetchlimit. OPEN MODE - устанавливает режим чтения (записи) данных из (в) БД (значение read/write, по умолчанию) или режим только для чтения (read only). SERVER NAME - для удаленных БД содержит имя сетевого сервера и маршрут доступа к БД. Вид указания этих параметров зависит от используемого сетевого протокола. Для протокола TCP/IP при расположении БД на сервере, который работает под управлением Windows 32, они задаются в формате ИмяСервера: Путь \ИмяБД. gdb, например: mdl:с:\ptoject\bd\mon.gdb При этом должны быть соответствующим образом настроены файлы HOSTS и SERVICES на компьютере, имеющем доступ к удаленной БД (более подробно см. п.15.6). SQLPASSTHRU MODE - важнейший в рамках технологии клиент/сервер режим, опре- деляющий, каким образом происходит взаимодействие BDE с сервером на уровне транзакций приложения клиента. Значение параметра определяет, может ли BDE пе- редавать серверу собственные команды для управления запросами, или нет. В послед- нем случае используется Passthrough SQL - операторы SQL, выполняемые при по- мощи компонента TQuery клиентского приложения. Также данный параметр опреде- ляет режимы использования неявного старта и подтверждения транзакций. Параметр SQLPASSTHRUMODE может принимать следующие значения: • shared autocommit - Passthrough SQL и команды BDE используют одно и то же соединение с удаленной БД, реализуемое в приложении через один компо- нент TDatabase', если в приложении транзакция явно не реализуется методом TDatabase.StartTransaction, происходит неявный старт транзакции и ее автома- тическое подтверждение; • shared NOAUTOCOMMIT - Passthrough SQL и команды BDE используют одно и то же соединение с удаленной БД, неявные транзакции стартуют аналогично режиму shared autocommit, однако их неявного подтверждения не происходит и это нужно делать явно; поведение Passthrough SQL в этом случае зависит от сервера.
604 Приложение П1 • NOT SHARED - Passthrough SQL и команды BDE не могут использовать одно и то же соединение с удаленной БД. Обновляемые запросы на SQL не поддержива- ются псевдонимами БД, для которых установлен этот режим. shared autocommit и shared noautocommit не поддерживают всех операторов Passthrough SQL. В этих режимах не следует управлять транзакциями при помощи Sei-оператора SET TRANSACTION. SQLQRYMODE - определяет режим выполнения запросов SQL. Возможные значения: • NULL - (по умолчанию) - для доступа как к локальным, так и серверным БД: за- просы вначале посылаются SgL-cepBepy, если тот не в состоянии выполнить за- прос, он выполняется локально; • server - только серверные БД: запрос посылается SQL-серверу, если он не может выполнить запрос, локального выполнения не происходит; • LOCAL - запрос всегда выполняется на клиентской машине, т.е. локально. Заметим, что запрос может быть отвергнут сервером БД, если: • имеет место гетерогенный запрос, т.е. запрос к различным БД; • запрос состоит более чем из одного SQL-оператора; • сервер БД не поддерживает синтаксис операторов, присутствующих в запросе. USER NAME - определяет имя, которое выдается в качестве подсказки имени поль- зователя при запросе имени и пароля в момент соединения с БД. П1.2. УСТАНОВКИ ПАРАМЕТРОВ ДРАЙВЕРА Параметры, присущие всем БД того или иного типа, устанавливаются для драйве- ров БД. Затем они применяются к псевдониму каждой БД того же типа. Некоторые параметры могут быть переопределены для конкретного псевдонима, а некоторые - нет. Установка параметров драйверов производится после выбора закладки Configuration и раскрытия дерева до ветви Drivers | Native | Имя драйвера (рис.П1.2). В ветви Drivers | ODBC производятся установки драйвера ODBC. Рис.П1.2. Установка параметров драйвера
Работа с утилитой BDE Administrator 605 П1.2.1. Paradox NET DIR - каталог, в котором расположен сетевой управляющий файл PDOXUSRS.NET. VERSION - внутренняя версия драйвера Paradox-, TYPE - тип сервера - SERVER (SgZ-сервер) или FILE (локальные или файл/серверные БД). LANGDRIVER - языковый драйвер, используемый для кодировки символьных полей и определяющий также порядок сортировки символьных значений. Для русскоязыч- ных приложений рекомендуется Pdox ANSI Cyrillic. Для этого драйвера без оши- бок работают символьные функции для перевода строчных букв в заглавные и обратно AnsiUpperCase и AnsiLowerCase. block size - определяет размер блока на диске при хранении записей таблиц Paradox. По умолчанию 2048. Размер блока кратен 1024 байтам. Возможные значения для уровней 5 и 7: 1024, 2048, 4096, 16384 или 32768. Для уровней 3 и 4 - 1024, 2048 или 4096. fill factor - процент заполнения блока на диске, когда BDE начнет выделение но- вого блока. Значение в диапазоне 1..100. По умолчанию 95. Малые размеры блока улуч- шают быстродействие индексов, но увеличивают их размер; большие размеры блока уменьшают размер индекса, но ухудшают быстродействие. LEVEL - тип временных таблиц Paradox (уровень версии): • 7 - 32-битные таблицы Paradox for Windows; • 5 - таблицы Paradox 5.0; • 4 - таблицы Paradox 4.0; • 3 - формат таблиц, совместимый с Paradox 3.5 и более ранними версиями. По умолчанию принят уровень 4. Уровни 4 и 5 позволяют использовать BLOB- поля, вторичные индексы, строгие ограничения ссылочной целостности. Уровень 7 следует использовать в случае применения поддержки улучшенной индексации таблиц. STRICTINTEGRTY - значение true запрещает изменять таблицы Paradox 4.0 и вы- ше, если изменения приводят к нарушению ссылочной целостности. П1.2.2. InterBase Ниже описываются только те параметры, которые не доступны в режиме опреде- ления псевдонима. Остальные параметры совпадают с описанными в п.Ш.1.2. version - неизменяемый параметр. Содержит версию драйвера InterBase. dll - неизменяемый параметр для внутреннего использования. Содержит имя биб- лиотеки для работы с 16-разрядными вызовами. DLL32 - неизменяемый параметр для внутреннего использования. Содержит имя библиотеки для работы с 32-разрядными вызовами. type - значение server указывает, что работа с БД происходит с использованием SgL-сервера. TRACE mode - числовое значение, определяющее способ трассировки информации в системный журнал.
606 Приложение III П1.3. СИСТЕМНЫЕ СТАРТОВЫЕ УСТАНОВКИ Выбрав закладку Configuration, раскройте ветвь дерева System | Init. Здесь могут быть установлены стартовые параметры запуска приложения, работающего с BDE. Кроме выбора языкового драйвера эти установки меняются редко и хотя бы на первых порах при работе с BDE рекомендуется не изменять установки, принятые по умолчанию. auto ODBC - true определяет импорт установленных драйверов ODBC. По умол- чанию FALSE. DATA repository - определяет имя активного словаря данных. DEFAULT DRIVER - указывает имя драйвера, установленного по умолчанию для ло- кальных БД (для псевдонимов, у которых в параметре type установлено значение file и при указании имен файлов не указаны расширения). LANGDRIVER - языковый драйвер по умолчанию. Для русскоязычных данных реко- мендуется Pdox ANSI Cyrillic. LOCAL share - определяет способность разделять доступ к локальным данным между активными приложениями, как использующими, так и не использующими BDE. Значение TRUE необходимо установить, если доступ к данным будет производиться в приложениях с использованием BDE и в обход BDE. По умолчанию FALSE. MAXBUFSIZE - максимальный размер буфера БД (в килобайтах). Значение должно быть кратным 128. По умолчанию 2048. minbufsize - минимальный размер буфера БД. maxfilehandles - максимальное число дескрипторов, используемых BDE. Значе- ние в диапазоне 5...4096. Большое значение увеличивает производительность, но тре- бует больших ресурсов Windows. По умолчанию 48. MEMSIZE - максимальный размер памяти, доступный BDE для использования. По умолчанию 16 Мбайт. sharedmemsize - максимальный размер памяти в килобайтах, которую BDE может использовать для разделения ресурсов. По умолчанию 2048 Кбайт. Разделяемыми ресурсами являются дескрипторы, драйверы, табличные объекты и т.п. Если их число велико, значение в параметре sharedmemsize следует увеличить. SHAREDMEMLOCATION - содержит адрес менеджера, управляющего разделяемой па- мятью. Менеджер загружается в память, начиная с указанного адреса. Если этот адрес конфликтует с другими приложениями, его следует изменить. Значения по умолчанию - Е000 (Windows 95), 7000 (Windows NT). SQLQRYMODE определяет режим выполнения запросов SQL. version - неизменяемый параметр, определяющий текущую версию BDE. П1.4. УСТАНОВКИ ФОРМАТОВ Ветвь System | Formats используется для установки форматов даты (Date), времени (Time) и числовых значений (Number). П1.4.1. Параметры формата даты SEPARATOR - задает символ для разделения дня, месяца, года в значениях даты, на- пример, символ «/» («31/12/96»). По умолчанию берется символ из системных устано- вок Windows.
Работа с утилитой BDE Administrator 607 MODE - задает порядок следования дня (D), месяца (М) и года (Y) в дате. Возмож- ные значения: • 0(MDY); • 1 (DMY); • 2 (YMD). По умолчанию берется порядок следования из системных установок Windows. FOURDIGITYEAR - определяет, показывать ли в значении года две (false) или че- тыре (true) цифры. yearbiased - указывает BDE, как преобразовывать значение года, введенное в ви- де двух цифр. Значение TRUE обязывает прибавлять к двузначному году 1900 (введено 96, получено 1996). При значении FALSE этого не происходит (введено 96, получено 0096). По умолчанию TRUE. LEADINGZEROM - указывает (true), что для одноразрядных значений месяца необ- ходимо прибавлять ведущий ноль (например, «16/4/96» преобразовать к виду «16/04/96»). По умолчанию FALSE. LEADINGZEROD - указывает (TRUE), что для одноразрядных значений дня необхо- димо прибавлять ведущий ноль (например, «6/12/96» преобразовать к виду «06/12/96»), По умолчанию FALSE. П1.4.2. Параметры формата времени twelvehour - определяет, показывается ли время в двенадцатичасовом формате (true) или в двадцатичетырехчасовом (false). Например, время «10:20 РМ» (12- часовой формат) в 24-часовом формате - «22:20». По умолчанию true. AMSTRING - устанавливает символы, определяющие время до полудня для 12- часового формата. По умолчанию «АМ». PMSTRING - устанавливает символы, определяющие время после полудня для 12- часового формата. По умолчанию «РМ». seconds - определяет, показываются ли секунды (true) или нет (false). По умол- чанию TRUE. milseconds - определяет, показываются ли миллисекунды (true) или нет (false). По умолчанию FALSE. П 1.4.3. Параметры числового формата Устанавливают порядок преобразования строковых значений в числовой формат. DECIMALSEPARATOR - устанавливает символ, отделяющий целую часть числа от дробной. По умолчанию берется значение, установленное в Windows. thousandseparator - устанавливает символ, служащий разделителем тысяч в це- лой части числа (например, 7,654,321.00). По умолчанию берется значение, установ- ленное в Windows. DECIMALDIGITS - указывает число разрядов в дробной части числа. По умолча- нию 2. L EADINGZERON - указывает, имеют ли числа в диапазоне 1..-1 ноль в целой части (true) или нет (false). Например, 0.22 и .22. По умолчанию true.
608 Приложение Ш П1.5. СОХРАНЕНИЕ КОНФИГУРАЦИИ Текущая конфигурация со значениями псевдонимов, драйверов и стартовых на- строек может быть сохранена в файле. Для этого нужно выбрать раздел меню Object | Save As Configuration и указать имя файла, после чего нажать кнопку ОК. По умолча- нию конфигурация записывается в файл Program Files\Borland\ Common Files\BDEMdapi32.cfg. Этот файл обновляется автоматически после выполнения под- тверждения для всех изменений {Object | Apply) или для каждого изменения {Apply в контекстном меню, вызываемой правой кнопкой мыши). Файл конфигурации, отлич- ный от принятого по умолчанию, может быть загружен с помощью опции Object | Open Configuration. Файлы конфигурации могут быть объединены. Это важно в том случае, когда не- обходимо установить на компьютер приложение или группу приложений и нужно добавить в файл конфигурации новые псевдонимы БД. Если вместе с дистрибутивом приложений поставляется и конфигурационный файл, псевдонимы из него могут быть добавлены в основную (или любую другую) конфигурацию. Для этого нужно выбрать опцию меню Object | Merge Configuration и указать конфигурационный файл, инфор- мация из которого будет добавляться, и затем ответить Yes на предупреждение о том, что результаты объединения не могут быть отменены операцией Undo..
С ЧЕМ СВЯЗАНО П2. РАБОТА С УТИЛИТОЙ SQL EXPLORER П2.1. НАЗНАЧЕНИЕ SQL EXPLORER SQL Explorer (рис.П2.1) представляет собой универсальный инструмент доступа к базам данных. С его помощью можно не только создавать псевдонимы БД, но и про- сматривать и, при необходимости, редактировать их содержимое? Доступ к SQL Ex- plorer открывает опция Database | Explorer главного меню Delphi. Рис.П2.1. Окно SQL Explorer с окном редактора BLOB. В окне SQL Explorer расположены главное меню, инструментальная панель и два рабочих поля - левое с закладками Database и Dictionary и правое с закладками Defini- tion, Data и Enter SQL. Главное меню и инструментальная панель служат для управления утилитой и ее настройки. Левое поле в режиме отображения Database позволяет просматривать уже создан- ные псевдонимы БД, создавать новые, редактировать и уничтожать их. Здесь же мож- но просмотреть структуру БД, связанной с конкретным псевдонимом: входящие в нее таблицы, домены, просмотры, хранимые процедуры и т.д. В режиме отображения Dictionary левое поле открывает доступ к словарям БД. Закладки правого поля меняются в зависимости от того, какой компонент выбран в левом поле. В режиме Definition оно отображает определение или параметры выбран- ного компонента (таблицы, поля, хранимой процедуры и т.п.). В режиме Data можно
610 Приложение П2 увидеть содержимое таблицы, выбранной в левом окне. Режим Enter SQL предназна- чен для ввода S^Z-onepaTopoB, их выполнения и отображения результатов выполне- ния. П2.2. ГЛАВНОЕ МЕНЮ П2.2.1. Опция Object Опции подменю Object связаны с командами, адресующимися высвеченному объ- екту в левом поле окна. Большинство из них доступны также в локальном меню ути- литы, появляющемся после щелчка правой кнопкой мыши. Open - открывает высвеченный объект. Close - закрывает объект. New - создает новый экземпляр объекта (псевдоним, таблицу, поле и т.п.). Delete - уничтожает объект. Rename - переименовывает объект. Apply - подтверждает все внесенные в объект изменения. Cancel - отменяет изменения, внесенные в объект. Save as - сохраняет объект под новым именем. Version Information - открывает окно с информацией о версии, дате, времени разра- ботки и размерах всех файлов BDE. Import То Dictionary - импортирует сведения об объекте в словарь BDE. Exit - завершает работу с SQL Explorer. П2.2.2. Опция Dictionary Опции подменю Dictionary обслуживают словари , т.е. хранилища информации о базах данных и существующих в них объектах (таблицах, ограничениях, ссылочных целостностях и т.п.). Select - открывает диалоговое окно Select a Dictionary для выбора одного из дос- тупных словарей. Выбранный словарь становится активным словарем, доступ к объек- там которого открывает левое поле в режиме Dictionary. Register - заносит текущий словарь в список доступных словарей окна Select a Dic- tionary. Unregister - удаляет регистрацию текущего словаря из списка Select a Dictionary. New - создает новый словарь. Delete - уничтожает текущий словарь. Import From Database - импортирует в текущий словарь сведения об объекте. Import From File - добавляет в текущий словарь сведения из файла, созданного оп- цией Export То File. Опция полезна для переноса словарей на машину другого разра- ботчика. Export То File - записывает в файл данные из текущего словаря. П2.2.3. Опция Edit Опции подменю Edit относятся, в основном, к выделенному тексту в режиме ввода и редактирования SgZ-onepaTopoB в правом поле (закладка Enter SQL).
Работа с утилитой SQL Explorer 611 Undo - отменяет последнюю команду или изменение текста. Cut - удаляет выделенный текст и помещает его в буфер Clipboard. Сору - копирует выделенный текст в буфер Clipboard. Paste - вставляет содержимое Clipboard в окно ввода/редактирования текста. Delete - удаляет выделенный текст. Select all - выделяет весь текст в окне ввода/редактирования. Text - вызывает текстовый редактор Delphi для ввода и редактирования SQL- операторов (опция доступна только если SQL Explorer был вызван с помощью опции Database | Explorer главного меню Delphi}. П2.2.4. Опция View Опции подменю View управляют видом окна утилиты. Toolbar - показывает/прячет инструментальную панель. Status Ваг - показывает/прячет полосу статуса. Blob Explorer - показывает/прячет окно отображения полей BLOB. System Data - если отмечена, утилита отображает такие системные данные, как таб- лицы, индексы, триггеры и т.д. Если вы не хотите видеть эти объекты, снимите отметку опции. Complete Text - если отмечена, позволяет увидеть не только текст определения объ- ектов (индексов, триггеров, ограничений), но и вспомогательный текст, который хранится в серверной БД. Этот текст нельзя редактировать (основной текст ре- дактировать можно). Change Text - если выбрана, отображает текст запросов, которые будут выполнены, если вы подтвердите сделанные вами изменения. Этот текст доступен только для чтения. Text Font - открывает диалоговое окно выбора шрифта, используемого для ввода запросов и отображения определений. Large Icons - если выбрана, объекты в правом окне снабжаются крупными знач- ками. Small Icons - если выбрана, объекты в правом окне снабжаются мелкими значками. List - если выбрана, объекты в правом окне показываются в виде списка. Detail - если выбрана, объекты в правом окне снабжаются дополнительной инфор- мацией. Auto Arrange - если выбрана, объекты в правом окне упорядочивают свое располо- жение. Refresh - обновляет для выбранного объекта информацию, считывая ее из БД. П2.2.5. Опция Options Опции подменю Options определяют некоторые параметры утилиты. Query - задает ограничители строк, предложений, комментариев в тексте запроса, а также определяет, будет ли возвращаемый НД изменяемый. Transaction Isolation - устанавливает уровень разграничения транзакций. Word Wrap - если отмечена, редактор ввода переносит текст запроса на новую строку, если очередное слово не умещается в рамке окна.
612 Приложение П2 Show Confirmations - определяет, будет ли появляться диалоговое окно для под- тверждения вносимых изменений. Show Warnings - указывает, будет ли появляться диалоговое окно с предупрежде- нием о возможной потере данных. Данные могут быть потеряны, например, при пере- мещении столбца на новое место, так как в этом случае запрос должен быть повторен с указанием нового порядка следования полей. Sync Pages - переносит схемную информацию из выбранной БД в выбранный сло- варь при переключении с закладки Database на закладку Dictionary. Перед пере- носом появится диалоговое окно для подтверждения операции. П2.3. РАБОТА С УТИЛИТОЙ П2.3.1. Управление псевдонимами С помощью SQL Explorer можно создавать новые псевдонимы БД, изменять их па- раметры и уничтожать подобно тому, как это описано в П1. В отличие от BDE Admin- istrator, утилита SQL Explorer не может изменить свойства всех однотипных псевдо- нимов или сделать общесистемные установки форматов. Для создания нового псевдонима установите режим отображения баз данных {Database) и высветите любую БД. Затем щелкните по левому полю правой кнопкой мыши и выберите New в локальном меню (можно также выбрать Object | New или нажать соответствующую кнопку на инструментальной панели). Остальные действия см. в п.Ш.2. Для уничтожения псевдонима сделайте его текущим (высвеченным) и выберите Object | Delete в главном или Delete в локальном меню. П2.3.2. Просмотр/редактирование таблиц Чтобы просмотреть содержимое таблицы и, при необходимости, добавить в нее новые записи, удалить ненужные, изменить значения полей, раскройте узел слева от названия нужной БД, затем раскройте узел Tables и щелкните по названию нужной таблицы. В правом поле на закладке Data вы увидите содержимое таблицы, а на инст- рументальной панели станут доступны кнопки навигатора БД. Чтобы просмотреть содержимое полей BLOB, вызовите окно исследователя BLOB {View \Blob Explorer в главном меню или соответствующая инструментальная кнопка), с помощью его локального меню сделайте окно неперекрываемым (опция A Iways On Тор) и настройте на нужный режим отображения поля BLOB-. Show As Default - отображать графику или текст в зависимости от настройки псев- донимов; Show As Graphic - показывать как изображение в формате ВМР\ Show As Text - показывать как текст. В последнем случае содержимое тето-иопя можно редактировать. П2.3.3. Создание и выполнение запросов Для создания и выполнения Л^Б-запроса выберите нужную БД в левом поле, а в правом - щелкните по закладке Enter SQL. Наберите текст запроса в окне редактора и
Работа с утилитой SQL Explorer 613 нажмите Ctrl+E или вызовите правой кнопкой мыши локальное меню и выберите в нем Execute, либо, наконец, щелкните по кнопке В нижней части появится окно с результатом выполнения запроса (рис.П2.2). Используйте кнопку для создания очередного запроса и кнопку jAi для просмотра и, возможно, повторного выполнения одного из предыдущих запросов. Рис.П2.2. Результат выполнения запроса. П3.3.4. Просмотр метаданных Чтобы увидеть метаданные БД, выберите нужный псевдоним в левом окне утилиты и нажмите кнопку «+» для раскрытия дерева метаданных. При этом для удаленных БД будут запрошены имя пользователя и пароль. В правом окне будут выведены характе- ристики псевдонима БД, а в левом - построено дерево метаданных (рис.П2.3). Дерево метаданных серверных БД включает в себя ветви: • Domains - домены; • Tables - таблицы; • Views - виртуальные таблицы (просмотры); • Procedures - хранимые процедуры; • Functions - функции, определенные пользователем; • Generators - генераторы; • Exceptions - исключения; • Blob Filters - BZOB-фильтры. В состав метаданных локальных БД входят только таблицы.
614 Приложение П2 Рис.П2.3. Дерево метаданных БД IB EMPLOY (слева) и характеристики псевдонима БД (справа). Для работы с определенным типом метаданных следует выбрать соответствующую ветвь дерева и раскрыть список, нажав кнопку «+» слева от названия ветви. Если в опциях элемента меню View отмечен флаг System Data, будут показываться и систем- ные данные, включаемые в каждую БД. Для хранимой процедуры (ветвь Procedures) показываются входные и выходные параметры. Для каждого параметра в правом окне приводится характеризующая его информация: порядок (Order), вид - входной или выходной (Kind), имя домена (Domain), тип данных (Туре), длина (Length) и число знаков в дробной части (Scale). Для просмотра определения компонента метаданных (домена, таблицы, процедуры и т.д.) или его текста следует выбрать соответствующее имя в дереве в левом окне, а в правом окне выбрать закладку Definition или Text. Для примера на рис.П2.4 показан текст SgZ-onepaTopa, с помощью которого была создана хранимая процедура ADD_EMP_PROJ.
Работа с утилитой SQL Explorer 615 Рис.П2.4. Текст SQL-onepamopa, создавшего хранимую процедуру ADD_EMP_PROJ
ПЗ. РАБОТА С УТИЛИТОЙ WISQL Утилита WISQL (InterBase Windows Interactive SQL) предназначена для обслужива- ния серверов InterBase. Она позволяет: • соединяться с БД на локальном или удаленном сервере; • выполнять любые запросы к БД и просматривать результаты их выполнения; • получать информацию о структуре БД. В отличие от других утилит, она использует собственный механизм доступа к БД, поэтому может работать на машинах без BDE и не использует псевдонимы BDE. П3.1. УСТАНОВКА НАБОРА СИМВОЛОВ ТЕКУЩЕЙ СЕССИИ Как известно, в БД могут храниться символьные данные, использующие различные наборы символов для указания той или иной национальной кодировки. Чтобы не воз- никло никаких проблем при обработке русскоязычных текстов, перед соединением с БД (созданной с указанием default character set WIN1251) необходимо выбрать опцию меню Session | Advanced Settings утилиты и установить в поле Character Set On кодировку WIN1251, после чего нажать кнопку ОК. П3.2. СОЗДАНИЕ БД И СОЕДИНЕНИЕ С НЕЙ Для создания БД необходимо щелкнуть по кнопке или выбрать опцию меню File | Create Database и затем в появившемся окне ввести необходимую информацию (рис. П3.1). Рис.П3.1. Создание БД
Работа с утилитой WISQL 617 Location Info - информация о расположении создаваемой БД. Local Server - данный компьютер; Remote Server - удаленный компьютер. В последнем случае необходимо указать имя сервера (поле Server) и сетевой протокол (поле NetWork Protocol). В слу- чае использования удаленного сервера: 1) его IP-адрес и имя должны быть описаны в файле HOSTS, например: 10.12.0.41 spv 2) протокол доступа к InterBase должен быть описан в файле SERVICE'. gds_db 3050/tcp Оба указанных файла находятся в каталоге WINDOWS. Database Name - определяет имя создаваемой БД и полный путь к ней (поле Database). Далее необходимо ввести имя пользователя (поле User Name) и пароль (поле Password). Напомним, что для локального сервера InterBase по умолчанию имя пользователя SYSDBA, а пароль masterkey (пароль чувствителен к высоте букв!). Default Option - параметры создаваемой БД. Именно в данном поле вводятся при- нимаемый по умолчанию набор символов (default character set) и другие пара- метры, которые указываются в SQL-операторе create database. Для соединения с БД необходимо щелкнуть по кнопке иди выбрать элемент меню File | Connect to Database и указать реквизиты базы данных. Диалоговое окно соединения с БД имеет такой же вид и смысл параметров, что и окно для создания БД. П3.4. ВЫПОЛНЕНИЕ SQL-ОПЕРАТОРОВ После соединения с БД к ней можно адресовать запросы с помощью операторов SQL. В верхнем окне WISQL вводится SQL-оператор и для выполнения нажимается кнопка s? № • . Листать введенные SQL-операторы можно при помощи кнопок Ы1 и ' “ . В нижнем окне после выполнения оператора дублируется текст самого оператора и, в слу- чае его правильности, показываются результаты выполнения запроса (рис.П3.2). Опера- торы могут завершаться точкой с запятой (при вводе операторов из WISQL это требова- ние не является обязательным). Могут выполняться все разрешенные синтаксисом InterBase SQL-операторы - от операторов определения метаданных (create table, create procedure, drop view, declare function и т.д.) до непосредственно запросов к таблицам БД для чтения (select) и изменения данных (insert, update, delete). П3.5. ПОДТВЕРЖДЕНИЕ И ОТКАТ ИЗМЕНЕНИЙ При старте WISQL стартует неявная транзакция. Поэтому все запросы к БД в рам- ках сессии WISQL не актуализируются до выдачи подтверждения. Подтверждение может выдаваться путем выполнения оператора COMMIT при вы- боре элемента меню File | Commit Work. В последнем случае запрашивается подтвер- ждение. Подтверждение запрашивается также при разрыве соединения с БД (кнопка или опция меню File | Disconnect from Database) и при выходе из WISQL (опция File | Exit).
618 Приложение ПЗ При разрыве соединения с БД или при выходе из WISQL отказаться от изменений, произведенных в рамках текущей транзакции, можно при помощи оператора ROLLBACK путем выбора элемента меню File | Rollback Work. После подтверждения или отката транзакции неявно стартует новая транзакция. Явно запустить транзакцию можно, выполнив оператор set transaction. Рис.П3.2. Запрос к БД. П3.6. ПРОСМОТР СТРУКТУРЫ КОМПОНЕНТОВ БД Просмотр структуры компонентов БД (метаданных) осуществляется с помощью элемента меню Metadata | View. Раскрыв в появившемся окне список View Information On, можно затем указать компонент БД, информацию о котором необходимо получить (рис.ПЗ.З). Рис. ПЗ.З. Выбор компонента БД, информацию о котором необходимо получить.
Работа с утилитой WISQL 619 Например, получим информацию о всех хранимых процедурах, объявленных в БД. Для этого в выпадающем списке выберем Procedure (поле View Information On), а поле Object Name оставим пустым и нажмем кнопку ОК. В результате получим список хранимых процедур, объявленных в БД (рис.П3.4). Рис.П3.4. Список хранимых процедур, объявленных в базе данных. Ввод в поле Object Name имени конкретного компонента БД (например, процедуры RASHOD TOVARA) приведет к выводу тела процедуры и списка параметров (рис.П3.5). Для получения информации о базе данных в целом, а также о конкретной таблице или просмотре используются опции подменю Metadata соответственно Extract Data- base, Extract Table и Extract View. П3.7. ВЫПОЛНЕНИЕ SCRIPT-ФАЙЛОВ Текст S(9£-3anpocoB может быть оформлен в виде файла и затем выполнен (элемент меню File | Run an ISQL Script). Преимущество такого подхода очевидно в тех случаях, когда необходимо периодически выполнять повторяющиеся последова- тельности операторов. Операторы создания БД, таблиц, процедур, триггеров и т.д. также могут выполняться из отдельного Scnpf-файла. Для случая создания в 5спрГ-файле хранимых процедур и триггеров необходимо применять оператор SET TERM НовыйРазделитель;
620 Приложение ПЗ Это связано с тем, что стандартным разделителем 50£-операторов является точка с запятой «;». В WISQL этот разделитель можно опускать, а вот в скрипте разделитель обязателен. Как известно, в теле хранимых процедур и триггеров операторы разде- ляются таким же разделителем. Например: CREATE PROCEDURE FIND_MAX_KOLVO (INJTOVAR VARCHAR(20)) RETURNS(MAX_KOLVO INTEGER) AS BEGIN SELECT MAX(KOLVO) FROM RASHOD WHERE TOVAR = : INJTOVAR INTO : MAX_KOLVO; SUSPEND; END; Рис.П3.5. Вывод текста хранимой процедуры RASHOD TOVARA Поэтому перед выполнением оператора create procedure или create trigger устанавливают новый разделитель, завершают им одно или несколько идущих подряд определений процедур и триггеров, а затем восстанавливают старый разделитель.
Работа с утилитой WISQL 621 Например: SET TERM ###; CREATE PROCEDURE FIND_MAX_KOLVO (IN_TOVAR VARCHAR(20)) RETURNS(MAX_KOLVO INTEGER) AS BEGIN SELECT MAX(KOLVO) FROM RASHOD WHERE TOVAR = : IN_TOVAR INTO : MAX_KOLVO; SUSPEND; END ### SET TERM ; ###
П4. ОПИСАНИЕ БИБЛИОТЕКИ IB UDF.DLL Ниже описаны функции, входящие в состав библиотеки ib_udf . dll. Эта библио- тека включается в комплект поставки InterBase и расположена в каталоге LIB его ката- лога размещения. Мы описываем меньшее число функций, нежели имеется в библио- теке, поскольку некоторые из них (например, LOWER, LTR1M и др.) не показали ус- тойчивых результатов при проведенном тестировании. ABS() - абсолютное значение Возвращает абсолютное значение числа, переданного в качестве параметра. Входные параметры: DOUBLE PRECISI ON Результат (по значению): DOUBLE PREC I SION DECLARE EXTERNAL FUNCTION abs DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_abs" MODULE_NAME "ib_udf"; ACOS() - арккосинус Возвращает (в радианах) арккосинус числа , переданного в качестве параметра, в интервале -1...+1. Если передаваемое значение выходит за данные границы, возвраща- ется 0. Входные параметры: DOUBLE PRECISION Результат (по значению): DOUBLE PREC ISI ON DECLARE EXTERNAL FUNCTION acos DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_acos" MODULE_NAME "ib_udf"; ASCII_CHAR() - символ по его коду Возвращает символ по его коду, переданному в качестве параметра (только для ла- тинских символов). Входные параметры: INTEGER Результат (по значению): CHAR(l) DECLARE EXTERNAL FUNCTION ascii_char INTEGER RETURNS CHAR(l) ENTRY_POINT "IB_UDF_ascii_char" MODULE_NAME "ib_udf"; ascii_val () - код символа Возвращает код символа, переданного в качестве параметра (только для латинских символов).
Описание библиотеки IB UDF.DLL 623 Входные параметры: CHAR (1) Результат (по значению): INTEGER DECLARE EXTERNAL FUNCTION ascii_val CHAR(l) RETURNS INTEGER BY VALUE ENTRY_POINT "IB_UDF_ascii_val" MODULE_NAME "ib_.udf"; ASIN () - арксинус Возвращает арксинус (в радианах) числа, переданного в качестве параметра. Пара- метр должен находиться в интервале Если параметр выходит за данные грани- цы, возвращается 0. Входные параметры: DOUBLE PRECISION Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION asin DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_asin" MODULE_NAME "ib_udf"; ATAN() - арктангенс Возвращает арктангенс числа. Входные параметры: DOUBLE PRECISION Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION atan DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_atan" MODULE_NAME "ib_udf"; ATAN2 () - арктангенс частного двух параметров Возвращает арктангенс выражения (параметр! / параметр2). Входные параметры: DOUBLE PRECISION, DOUBLE PRECISION Результат (по значению): DOUBLE PRECIS ION DECLARE EXTERNAL FUNCTION atan2 DOUBLE PRECISION, DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_atan2" MODULE_NAME "ib_udf"; BIN_AND() - логическое И Возвращает результат выражения (параметр! AND параметр2). Входные параметры: INTEGER, INTEGER Результат (по значению): INTEGER DECLARE EXTERNAL FUNCTION bin_and INTEGER, INTEGER
624 Приложение П4 RETURNS INTEGER BY VALUE ENTRY_POINT "IB_UDF_bin_and" MODULE_NAME "ib_udf" BIN_OR () - логическое ИЛИ Возвращает результат выражения (параметр! OR параметр2). Входные параметры: INTEGER, INTEGER Результат (по значению): INTEGER DECLARE EXTERNAL FUNCTION bin_or INTEGER, INTEGER RETURNS INTEGER BY VALUE ENTRY_POINT "IB_UDF_bin_or" MODULE_NAME "ib_udf" BIN_XOR () - логическое исключающее ИЛИ Возвращает результат выражения (параметр! XOR параметр2) Входные параметры: INTEGER, INTEGER Результат (по значению): INTEGER DECLARE EXTERNAL FUNCTION bin_xor INTEGER, INTEGER RETURNS INTEGER BY VALUE ENTRY_POINT "IB_UDF_bin_xor" MODULE_NAME "ib_udf" CEILING () - ближайшее большее целое Возвращает ближайшее целое, превосходящее параметр обращения. Входной параметр: DOUBLE PRECISION Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION ceiling DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_ceiling" MODULE_NAME "ib_udf" COS() - косинус Возвращает косинус числа, переданного в качестве параметра. Диапазон значений параметра - от -263 до +263. Если параметр выходит из этого диапазона, возвращается 0. Входной параметр: DOUB LE PRECISI ON Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION COS DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_COs" MODULE_NAME "ib_udf"
Описание библиотеки IB UDF.DLL 625 COSH () - гиперболический косинус Возвращает косинус числа, переданного в качестве параметра. Диапазон значений параметра -263..263. Если параметр выходит из диапазона, возвращается 0. Входной параметр: DOUBLE PRECI SION Результат (по значению): DOUBLE PREC I SION DECLARE EXTERNAL FUNCTION cosh DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_COSh" MODULE_NAME "ib_udf"; DIV() - деление Возвращает целую часть результата выражения (параметр! / параметр2) . Входные параметры: INTEGER, INTEGER Результат (по значению): DOUBLE PRECI SION DECLARE EXTERNAL FUNCTION div INTEGER, INTEGER RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_div" MODULE_NAME "ib_udf" FLOOR () - ближайшее меньшее целое Возвращает ближайшее целое, меньшее, чем значение параметра. Входной параметр: DOUBLE PREC I SION Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION floor DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_floor" MODULE_NAME "ib_udf" ln() - натуральный логарифм Возвращает натуральный логарифм от переданного параметра. Входной параметр: DOUBLE PREC I SION Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION In DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_ln" MODULE_NAME "ib_udf" LOG () -логарифм Y по основанию X Возвращает натуральный логарифм Y по основанию X, где Y - первый параметр, а X - второй. Например, LOG(IOO.IO) возвращает 2.
626 Приложение П4 Входные параметры: INTEGER, INTEGER Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION log DOUBLE PRECISION, DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_log" MODULE_NAME "ib_udf" LOGIO () - десятичный логарифм Возвращает десятичный логарифм переданного параметра. Входной параметр: DOUBLE PREC I SION Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION loglO DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_loglO" MODULE_NAME "ib_udf" PI () - число л = 3.14159 Возвращает число л. Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION pi RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_pi" MODULE_NAME "ib_udf" SIGN() - знак числа Возвращает -1,0, 1, если параметр соответственно меньше, равен или больше 0. Входной параметр: DOUBLE PREC ISI ON Результат (по значению): INTEGER DECLARE EXTERNAL FUNCTION SIGN DOUBLE PRECISION RETURNS INTEGER BY VALUE ENTRY_POINT "IB_UDF_sign" MODULE_NAME "ib_udf" SIN() - синус Возвращает синус числа, переданного в качестве параметра. Диапазон значений параметра -263..263. Если параметр выходит из диапазона, возвращается 0. Входной параметр: DOUBLE PREC I SION Результат (по значению): DOUBLE PRECISION DECLARE EXTERNAL FUNCTION sin DOUBLE PRECISION RETURNS DOUBLE PRECISION BY VALUE ENTRY_POINT "IB_UDF_sin" MODULE_NAME "ib_udf"
Литература 1. Архангельский А.Я. Программирование в Delphi 4 - М.: ЗАО «Издательство БИНОМ», 1999. - 768 с.: ил. 2. Дантеманн Джефф, Мишел Джим, Тейлор Дон. Программирование в среде Delphi: Пер. с англ./ Дантеманн Джефф, Мишел Джим, Тейлор Дон. - К.: НИПФ «ДиаСофт Лтд.», 1995. -608 с. 3. Дарахвелидзе П.Г., Марков Е.П. Delphi - среда визуального программирования: - СПб.: BHV - Санкт-Петербург, 1996. -352 с. 4. Дарахвелидзе П.Г., Марков Е.П. Delphi 4.-СПб.:БХВ - Санкт-Петербург, 1999.-816 с.,ил. 5. Елманова Н.З., Трепалин С.В. Delphi 4: технология COM. OLE, ActiveX, Авто- матизация MIDAS, Microsoft Transaction Server - M.: ДИАЛОГ-МИФИ, 1999. - 320 c. 6. Епанешников А., Епанешников В. Программирование в среде Delphi 2.0: Учебное пособие: В 4-х ч. Ч. 1. Описание среды - М.: ДИАЛОГ-МИФИ, 1997. -235 с. 7. Епанешников А., Епанешников В. Программирование в среде Delphi 2.0: Учебное пособие: В 4-х ч. Ч. 2. Язык Object Pascal 9.0 - М.: ДИАЛОГ-МИФИ, 1998. -319 с. 8. Епанешников А., Епанешников В. Программирование в среде Delphi: Учебное пособие: В 4-х ч. Ч. 3. Проектирование программ - М.: ДИАЛОГ-МИФИ, 1998. - 336 с. 9. Епанешников А., Епанешников В. Программирование в среде Delphi: Учебное по- собие: В 4-х ч. Ч. 4. Работа с базами данных. Организация справочной системы - М.: ДИАЛОГ-МИФИ, 1998. -400 с. 10. Калверт Ч. Delphi 2. Энциклопедия пользователя: Пер. с англ./Чарлз Калверт.-К.: Издательство «ДиаСофт Лтд.», 1998. -800 с. 11. Калверт Чарлз. Delphi 4. Энциклопедия пользователя: Пер. с англ./Чарлз Калверт,- К.: НИПФ «ДиаСофт Лтд.», 1996. -736 с.
628 Литература 12. Компания Advanced Information Systems и др. Oracle8. Энциклопедия пользовате- ля.: Пер. с англ./ Компания Advanced Information Systems и др.: К.: Издательство «ДиаСофт», 1998. -864 с. 13. Конопка Р. Создание оригинальных компонент в среде Delphi: Пер. с англ./Рэй Конопка.-К.: НИПФ «ДиаСофт Лтд.», 1996. -512 с. 14. Культин Н.Б. Программирование на Object Pascal в Delphi 5. - СПб.: БХВ - Санкт- Петербург, 1999. - 464 с., ил. 15. Лишнер Р. Секреты Delphi 2: Пер с англ. - Киев: НИФП «ДиаСофтЛтд», 1996. - 800 с. 16. Матчо Джон, Дэвид Р. Фолкнер. Delphi: Пер. с англ. - М.: БИНОМ, 1995. -464 с. 17. Мэтчо Дж. и др. Delphi 2. Руководство для профессионалов: Пер. с англ. - СПб.: BHV - Санкт-Петербург, 1997. -784 с. 18. Орлик С.В. Секреты Delphi на примерах: - М.: БИНОМ, 1996. -316 с. 19. Рубенкинг Н. Программирование в Delphi для «чайников». - Киев: Диалектика, 1996. -304 с. 20. Сван Т. Основы программирования в Delphi для Windows 95: Пер. с англ. - К.: «Диалектика», 1996. -480 с. 21. Сван Т. Delphi 4. Библия разработчика: Пер. с англ./Томас Сван.-К.: Издательство «ДиаСофтЛтд.», 1998. -512 с. 22. Справочник по Microsoft OLE DB 1.1/Пер. С англ,- Издательский отдел «Русская редакция» ТОО «Channel Trading Ltd.», 1997.-624 с.:ил. 23. Туротт Пол и др. Супербиблия Delphi 3: Пер. с англ./Пол Туррот и др. - К.: Изда- тельство «ДиаСофт», 1997. -848 с. 24. Фаронов В.В. Delphi 5. Учебный курс. -М.: «Нолидж», 1998. -464 с., ил. 25. Фаронов В.В., Шумаков В.П. Delphi 4. Руководство разработчика баз данных.- • М.:«Нолидж», 1999.-560 с., ил. 26. Федоров А.Г. Создание Windows-приложений в среде Delphi: -М.: ТОО фирма «КомпьютерПресс», 1995. -287 с., ил. 27. Хендерсен Кен. Delphi 3 и системы клиент-сервер. Руководство разработчика.: Пер. с англ. - К.: Диалектика, 1997. -786 с., ил. - Парал. тит. англ.
Некоторые важные классы 629 28. Хендерсон К. Руководство разработчика баз данных в Delphi 2.: Пер. с англ. - К.: «Диалектика», 1996. -544 с. 29. Чепмен Девис. Разработка Internet-приложений в Delphi 2: Пер. с англ./Девис Чеп- мен. - К.: Издательство «ДиаСофт», 1997. -640 с. 30. Шумаков П.В. Delphi 3 и разработка приложений баз данных. -М.: «Нолидж», 1998. -704 с., ил. 31. Энсор, Стивенсон. Огас1е8: рекомендации разработчикам: Пер. с англ. - К.: Изда- тельская группа BHV, 1998. - 128 с.
АЛФАВИТНЫЙ УКАЗАТЕЛЬ В M BDE, 66; 67 BDE Administrator, 66, 601 MIDAS, 75 s c SQL, 70 CLSID, 41 COM, 41 CORBA, 41; 42 SQL Builder, 169 SQL Explorer, 66,609 SQL Monitor, 66 SQLB. Cm. SQL Builder D V Database Desktop, 66 DataModule, 67 Datapump, 67 VisiBroker, 67 w G Windows Interactive SQL (WISQL), 300, GUID, 41 616 I InterBase Windows ISQL (WISQL), 306. См. также WISQL
Алфавитный указатель 631 А архитектура многозвенная. См. архитектура трехзвенная трехзвенная, 71,473 Б база данных, 23 клиент-серверная, 70, 297 локальная, 69 ' оптимизация работы с ней, 455 псевдоним ее, 46 файл-серверная, 69 БД. См. база данных библиотека типов, 479 бизнес-правила, 67,439,474 брокер ограничений, 511; 516 Г генератор, 299, 315,410 график заголовок его, 236 легенда его, 236 марки его, 240 оси его, 235 серия его, 234; 237 д данные двоично-десятичные, 562 диаграммы их, 80 домен, 326 драйвер ODBC, 540 драйвер языковый для таблиц Paradox, 50 3 закладка, 119 запись поиск ее, 119, 147 сортировка их, 117 текущая, 116 удаление ее, 146 запрос вложенный, 369 динамический, 186 многопоточность выполнения его, 462 оптимизация его, 458 параметрический, 181 план выполнения его, 459 статический, 175 И изменения каскадные, 29 индекс, 30, 343 оптимальная структура его, 458 полезность его, 458 работа с ним, 140 составной, 461 целесообразность использования его, 460 интерфейс, 41,479; 480 IDataBlock, 487 IDataBroker, 486 IDatalntercept, 487 IDispatch, 484 ITransport, 489 lUnknow, 41,482 делегирование его, 484 дуальный, 486, 526 идентификатор его, 41,479 расширение его, 520
632 Алфавитный указатель К каскадные воздействия, 312 класс TBDEDataSet, 74 TChartSeries, 243 TChartValueList, 245 TCheckConstraint, 136 TColumn, 74, 192 TCustomResolver, 495 TDataBlocklnterpreter, 488 TDataSet, 74 TDataSetField, 469 TDBDataSet, 74, 108,469 TDBGridColumns, 192 TField, 74, 86 TFieldDefs, 74 TFields, 468 TFieldType, 467 TIndexDefs, 74, 141 TObjectField, 467 TParaAttributes, 224 TParam, 175 TParams, 174 TReferenceField, 469 TSeriesMarks, 244 интерфейсный, 481 клиент, 23; 41, 298 облегченный, 73,473 тонкий. См. клиент облегченный ключ внешний, 30, 332 вторичный, 343 первичный, 24, 315, 331, 343 уникальный, 332 компонент ADO, 551 TADOCommand, 557 TADOConnection, 553 TADODataSet, 571 TADOQueiy, 573 TADOStoredProc, 574 TADOTable, 572 TBatchMove, 75 TBStoredProc, 594 TCkientDataSet, 498 TClientDataSet, 75 TCorbaConnection, 505 TCORBAConnection, 75 TCORBADataModule, 493 TDatabase, 75, 310, 347 TDataSetProvider, 75,494 TDataSource, 75 TDBChart, 77, 233 TDBCheckBox, 77, 213 TDBComboBox, 77, 216 TDBCtrlGrid, 77, 209 TDBEdit, 77,212 TDBGrid, 77, 191 TDBImage, 77 TDBListBox, 78,215 TDBLookupComboBox, 78, 217 TDBLookupListBox, 78, 218 TDBMemo, 78,218 TDBNavigator, 78 TDBRadioGroup, 78, 214 TDBRichEdit, 78, 222 TDBText, 78, 212 TDCOMConnection, 76, 505 TDecisionCube, 278; 280 TDecisionGraph, 79, 278 TDecisionGrid, 79,278; 283 TDecisionPivot, 79, 278; 290 TDecisionQuery, 77, 278; 279 TDecisionSource, 77, 278; 283 TField, 467 TIBDatabaselnfo, 598 TIBDataSet, 594 TIBEventAlerter, 431 TIBEvents, 600 TIBQuery, 593 TIBSQL, 597 TIBSQLMonitor, 600 TIBStoredProc, 593
Алфавитный указатель 633 TIBTable, 592 TIBTransaction, 589 TMTSDataModule, 491 TNestedTable, 75,470 TQRBand, 78, 247; 254 TQRChart, 78, 248 TQRChildBand, 78,247; 271 TQRCompositeReport, 78,270 TQRCSVFilter, 78 TQRDBImage, 78 TQRDBRichText, 78, 248 TQRDBText, 247 TQRExpr, 78, 247 TQRExprMemo, 248 TQRGroup, 78,247; 263 TQRHTMLFilter, 78 TQRImage, 78,248 TQRLabel, 78, 247 TQRMemo, 78,248 TQRPreview, 78, 248 TQRRichText, 78, 248 TQRShape, 78,248 TQRStringsBand, 247; 272 TQRSubDetail, 78, 247; 267 TQRSysData, 247; 262 TQRTextFilter, 79 TQuery, 75, 168,463 TQuickRep, 79, 248 TRDSConnection, 574 TRemoteDataModule, 490 TSession, 75, 227 TSimpleObjectBroker, 76 TSocetConnection, 76 TSocketConnection, 507 TStoredProc, 75 TTable, 75, 137,463 TUpdateSQL, 75 набор данных ADO, 560 связной, 475 кэш, 420 кэширование изменений, 420 в ADO, 562 Л локатор, 152 инкрементальный, 152 м метакуб, 274 модуль Customer Data, 80 модуль данных, 79 н набор данных, 108 закрытие его, 115 курсор его, 116 навигация по нему, 116 обновление его, 133 открытие его, 115 редактирование его, 116 фильтрация его, 123, 155, 179 О объект Command, 552 Connection, 552 Error, 552 Parameter, 552 Property, 553 Recordset, 552 автоматизации, 484 базовый ADO, 551 интерфейсный, 482 объект-поле, 92 ограничения целостности, 331 отчет, 247. См. также компонент TQuickRep главный-детальный, 267 композитный, 270 свойства его, 250 создание его, 256
634 Алфавитный указатель п поле, 31 BLOB, 106, 324 lookup. См. поле выбора автоинкрементное, 410 агрегированное, 359 агрегированные значения его, 189 вещественное, 104, 321 выбора, 98 вычисляемое, 97, 330, 358 даты и времени, 105, 322 логическое, 105 массив, 325 неделимость его, 31 обращение к значению его, 94 общее. См. поле связи ограничения на значения его, 135, 336 повторяющееся, 31 получение информации о нем, 126 проверка значения его, 95 редактор его, 93 связи, 28 совместимость значений его, 94, 325 строковое, 103, 323 фиксированно-десятичное, 321 целочисленное, 103, 321 право доступа к данным, 548 провайдер OLEDB, 539; 547 просмотр, 300, 385 процедура выбора, 388 действия, 388 хранимая, 388 Р редактор базы данных, 347 многомерного запроса, 279 многомерного куба, 280 реляционные связи формирование их в модуле данных, 81 С связь cADO, 545 многие-ко-многим, 26 один-к-одному, 26 один-ко-многим, 26 сервер, 23; 41 IntrBase, 299 Огас1е8,465 Web, 533 автоматизации, 485 баз данных, 70, 298 приложений, 72,473, 509, 510, 574 словарь данных, 451 со-класс, 574 событие, 431 уведомление о нем, 431 соединение таблиц внешнее, 374 внутреннее, 353 столбец. См. также поле СУБД, 23 заказные, 23 локальные, 23 промышленные, 23 распределенные, 23 специализированные, 23 специального назначения, 23 универсальные, 23
Алфавитный указатель 635 таблица. См. также соединение таблиц таблица, 23 вложенная, 466, 530 записи ее, 24 нормализация ее, 31,455 нормализованная, 37 подчиненная, 24 поля, 24 псевдоним ее, 355 реляционная, 24 родительская. См. таблица главная связи между ними, 24 создание ее, 330 столбец ее, 23 строка ее, 23 технология ActiveX, 533 ADO, 539 COM, 509 CORBA, 510; 524 DCOM, 509; 524 MTS, 510 OLEDB, 539 OLEnterprise, 510 транзакция, 39,412 изоляция ее, 414 откат ее, 39,413 подтверждение ее, 413 старт ее, 413; 417 управление ею в ADO, 554 триггер, 299,401 удаления каскадные, 29 Ф файл-сервер, 23 функции агрегатные, 189, 359 определяемые пользователем, 300, 434 X хранимая процедура, 299. См. также компонент TStoredProc ц целостность смысловая, 40 целостность БД нарушение ее, 29 ссылочная, 29
я Валерий Васильевич Фаронов Павел Владленович Шумаков Delphi 5 Руководство разработчика баз данных Верстка: Матусовская Е.В. Художник: Леонова Ю.В. ЛР № 064480 от 4 марта 1996 г. Подписано в печать 28.03.2000. Формат 70ХЮ0 1/16. Печать офсетная. Бумага газетная. Усл. печ. л. 40. Тираж 8000 экз. Заказ № 549. Издательство «Нолидж». Москва, Шлюзовая наб., д. 10. Тел.: (095) 325-29-59, 235-05-07. Тел. отдела распространения: (095) 954-30-44, 742-59-86. Отпечатано с готовых диапозитивов в ГПП «Печатный Двор» Министерства РФ по делам печати, телерадиовещания и средств массовых коммуникаций. 197110, Санкт-Петербург, Чкаловский пр., 15.