Delphi 3 и создание приложений баз данных

         

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


В ветви Indices для каждой таблицы показываются индексы, построенные по ограничениям первичного и уникальных (если имеются) ключей, а также неуникальные индексы. Для каждого индекса показывается его имя и столбцы, входящие в его состав. Если выбрать закладку Text в правом окне, для каждого индекса показывается оператор CREATE INDEX, приведший к созданию индекса



Просмотр триггеров


Для каждой таблицы исходящая ветвь Triggers показывает список триггеров (если есть), определенных для таблицы. Для каждого триггера в правом окне (закладка Text) выводится текст триггера Выбрав закладку Definition, можно получить информацию о типе операции, активизирующей триггер (INSERT, UPDATE, DELETE), о времени вызова триггера относительно породившей его операции (AFTER, UPDATE) и о приоритете вызова среди других триггеров, определенных для данной операции и времени вызова (POSITION)



Использование утилиты Database Explorer


Утилита Database Explorer предназначена для:

просмотра метаданных (то есть определений данных) БД;

построения и выполнения запросов к БД;

ввода и корректировки данных;

создания словаря данных.

Утилита может применяться для просмотра метаданных как удаленных (серверных), так и локальных (Paradox, dBase) БД. В последнем случае состав типов метаданных существенно меньше, поскольку для локальных БД нельзя определить как триггеры, хранимые процедуры, просмотры, функции, определенные пользователем, и т.д.

В окне утилиты две закладки - просмотр метаданных БД (Databases} и работа со словарем данных (Dictionary). Работа со словарем данных близко пересекается с понятием бизнес-правил и потому описывается в разделе "Определение бизнес-правил" (подраздел "Использование словаря данных для определения атрибутов полей").



Откат изменений и целостность БД




Существует несколько способов внесения изменений в таблицы БД.

Пусть в НД (например, TTable), ассоциированном с какой-либо ТБД, выполнено удаление, добавление или корректировка записи.

Для локальных БД (Paradox, dBase и т.д.) характерен подход немедленного отображения изменений. Когда выполняется метод Post, изменения, внесенные в запись НД, немедленно физически запоминаются в ТБД, ассоциированной с этим НД То же верно и для метода Delete - после него запись немедленно физически удаляется из ТБД, ассоциированной с этим НД.

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

Необходимость отката изменений обусловливается еще и тем обстоятельством, что БД всегда должна находиться в целостном состоянии. Классическим примером перехода БД из одного целостного состояния в другое является бухгалтерская проводка, когда некоторая сумма S должна быть списана со счета К и зачислена на счет D. Только успешное выполнение этих двух операций гарантирует целостность информации в БД. Но целостность будет нарушена, если в результате сбоя сумма S будет списана со счета К, но не будет зачислена на счет D или, наоборот, зачислена на D, но не списана с К. Поэтому в случае ошибки списания/ зачисления суммы результата предыдущей операции зачисления/списания должны быть отменены.

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



Понятие транзакции


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

Таким образом, транзакция переводит БД из одного целостного состояния в другое

Пример.

Пусть нужно добавить запись в таблицу "Приход", прибавить количество прихода товара (из новой записи ТБД "Приход") в запись для данного товара в ТБД "Остаток товара на складе" и прибавить стоимость поступившего товара (из новой записи ТБД "Приход") в запись для данного товара и даты прихода в ТБД "Обороты по складу" (рис 32.1)

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

Управление транзакциями на уровне приложения, разработанного на Delphi, реализуются методами компонента TDataBase.

Начало транзакции инициируется методом

procedure StartTransaction;

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

procedure Commit;

Отказаться от физического запоминания сделанных изменений в БД ("откатить" изменения), можно, выполнив метод

procedure Rollback;

Выполнение методов Commit или Rollback завершает активную транзакцию, начатую методом Start Transaction. Метод Start Transaction нельзя выполнить, если для БД в текущий момент времени имеется активная транзакция, т.е. транзакция, незавершенная методами Commit или Rollback. В этом случае возбуждается исключение.

Проверить, имеются ли на текущий момент в БД активные незавершенные транзакции, можно при помощи свойства

property In Transaction: Boolean;

Это свойство возвращает True, если для БД имеется активная транзакция, и False, если не имеется.

Предыдущий пример изменения таблиц Prihod, Ostatok и Oborot в рамках одной транзакции реализуется следующим образом (рис 32.2)



Уровень изоляции транзакций Dirty Read


При уровне изоляции транзакций Dirty Read конкурирующие транзакции видят изменения, внесенные, но неподтвержденные текущей транзакцией. Если текущая транзакция откатит сделанные изменения, другие транзакции будут видеть недостоверные данные. Этот уровень изоляции может привести к серьезным ошибкам и применяется редко.

Именно этот уровень изоляции транзакций применяется при работе с локальными БД (Paradox, dBase), если при работе с таблицами таких БД производится запуск транзакций методом Start Transaction



Уровень изоляции транзакций Read Commited


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

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

Изменение данных.

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

Уровень изоляции транзакций Repeatable Read


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

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

Изменение данных.

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

Установка уровней изоляции транзакций в Delphi


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

property Translsolation: TTranslsolation;

возможные значения: tiDirtyRead, tiReadCommitted, tiRepeatableRead.

Разные серверы БД различным образом интерпретируют уровни изоляции транзакций, установленные в свойстве Translsolation. (Рис 32.3)



Уровни изоляции транзакций: приложение клиента


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

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

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

Уровень изоляции транзакции определяет:

• могут ли другие (конкурирующие) транзакции вносить изменения в данные, измененные текущей транзакцией;

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

Существуют следующие уровни изоляции транзакций - Dirty Read, Read Commited, Repeatable Read.



Свойство UpdateMode и обновление записей


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

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

Что произойдет в этом случае?

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

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

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

Свойство набора данных

property UpdateMode;

определяет, по каким полям ищется запись на сервере для обновления.

Возможные значения этого свойства:

• WhereAII (по умолчанию) - поиск записи, которая должна быть физически изменена, ведется по всем полям;

• WbereKeyOnly - поиск записи ведется только по значениям ключевых полей;

• WhereChanged - поиск ведется по ключевым полям и по тем из неключевых полей, которые были изменены при корректировке записи в приложении.

Наиболее жесткое условие поиска - WhereA II. Оно предотвращает запоминание изменений, внесенных в запись, если конкурирующая транзакция успела изменить хотя бы одно поле и подтвердить это изменение. Менее жесткое условие - WhereChanged. Оно предотвращает запоминание изменений, внесенных в запись, если конкурирующая транзакция успела изменить хотя бы одно ключевое поле или поле, которое было изменено и текущей транзакцией. Вероятность ошибок при этом выше, чем при WhereAll. Наименее жесткое и с наибольшей вероятностью ведущее к ошибке условие - WhereKeyOnly. Оно предотвращает запоминание изменений, если конкурирующая транзакция успела изменить хотя бы одно ключевое поле, которое пытается также изменить и текущая транзакция. Изменения других полей в расчет не принимаются.

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

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

Firma Familia Doljnost Oklad
Янтарь Ивонов Директор 1000

Пример.

Пусть в ТБД, состоящей из 4 полей (столбцов): Firma, Familia, Doljnost и Oklad, две транзакции из разных приложений пытаются изменить запись
Firma Familia Doljuost Oklad
Янтарь Иванов Директор 2000

Пользователь А и В оба знают, что эти данные недостоверны. Во-первых, Ивонов не директор, а бухгалтер. А Иванов — действительно директор -получает не 1000, а 2000. 1000 получает Ивонов. Поэтому каждый из пользователей А и В решает изменить запись. Пусть пользователь А изменил фамилию на 'Иванов', а оклад на 2000 и подтвердил транзакцию. Запись была изменена так:

Несколько мгновений спустя пользователь В изменил должность на 'Бухгалтер' и также подтвердил транзакцию:

Firma Familia Doljnost Oklad
Янтарь Ивонов Бухгалтер 1000

Однако записи ['Янтарь'; 'Ивонов'; 'Бухгалтер'; 1000] физически не существует.



Явно и неявно стартуемые транзакции


Явная транзакция начинается и завершается методами - Start Transaction, Rollback/Commit компонента TDatabase. В рамках одного компонента TDatabase невозможно запустить две параллельные транзакции.

Неявная транзакция начинается методами Insert, Edit, Delete, Append и т.д. Она завершается после выполнения метода Post и откатывается в случае выполнения метода Cancel.

Неявная транзакция запускается и после посылки к серверу SQL-оператора (компонент TQuery), то есть после выполнения метода ExecSQL. Последний способ называется PassThroughSQL. Выдача автоматического подтверждения неявной транзакции определяется значением параметра SQLPASS THRU для псевдонима БД (утилита BDE Administrator). Этот параметр определяет:

• могут ли вызовы PassThroughSQL и стандартные вызовы BDE использовать одно и то же соединение с БД;

• видят ли друг друга транзакции, порожденные PassThroughSQL, и транзакции, порожденные вызовами BDE.

Значения параметра SQLPASSTHRU:

SHARED AUTOCOMMIT -

после выполнения ExecSQL, равно как и метода Post (компоненты TTable, TQuery), автоматически выдается Commit. При этом Post, Delete и PassThroughSQL могут использовать одно и то же соединение с БД (один и тот же компонент TDatabase).

• SHARED NO AUTOCOMMIT - неявная транзакция, порожденная PassThroughSQL, автоматически не завершается (Commit автоматически не выдается), и подтверждение транзакции необходимо выполнять программно. При этом BDE и PassThroughSQL могут использовать одно и то же соединение с БД (один и тот же компонент TDatabase).

• NO TSHA RED - транзакции каждого типа должны использовать отдельное соединение с БД каждая. Транзакции, начатые явно при помощи TDatabase StartTransaction, игнорируются. Подтверждение транзакции должно осуществляться явно посылкой COMMIT. До этих пор изменения, внесенные PassThroughSQL, не будут видны в компонентах TTable и TQuery, работающих с БД через другое соединение .

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

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



Управление транзакциями на SQL-сервере InterBase


InterBase управляет транзакциями при помощи SQL-операторов SET TRANSACTION (начать транзакцию), COMMIT (подтвердить транзакцию) и ROLLBACK, (откатить транзакцию).

Оператор SET TRANSACTION имеет формат

SET TRAXSACTION [READ WRITE |READ ONLY]

[WAIT | NO WAIT]

[[ISOLATION LEVEL] {SNAPSHOT [TABLE STABILITY]

| READ COMMITTED [[NO] RECORD_VERSION]}]

[RESERVING <список_таблиц> [FOR [SHARKD | PROTECTED][READ I WRITE]], [<список_таблиц>];



Использование кэшированных изменений


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

К преимуществам способа кэшированных изменений перед транзакционным способом можно отнести:

• минимизацию сетевого трафика при работе с удаленным сервером;

• отсутствие блокировок на изменяемые записи

Минимизация сетевого трафика при работе с удаленным сервером.

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

Отсутствие блокировок на изменяемые записи.

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

Активизация режима кэшированных изменений


Кэшированные изменения могут быть включены для наборов данных (компоненты TTable, TQuery, TStoredProc) путем установки в True свойства

property CachedUpdates: Boolean;

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

Отмена режима кэшированных изменений осуществляется путем установки данного свойства в False

Tovary. CachedUpdates := True;

...

Tovary. CachedUpdates := False;



Отмена кэшированных изменений


Сделанные изменения в НД (компоненты TTable, TQuery, TStoredProc) могут быть отменены методом

procedure CancelUpdates;



Подтверждение кэшированных изменений методом ApplyUpdates компонента TDatabase


Метод компонента TDatabase

procedure ApplyUpdates(const DataSets: array ofTDataSet);

применяется для подтверждения кэшированных изменений сразу в нескольких НД Список НД определяется параметром DataSets В случае указания нескольких НД их имена разделяются запятыми

Двухфазное запоминание в этом случае производится неявно' метод ApplyUpdates стартует транзакцию и пытается запомнить измененные записи в БД В случае неудачи, он откатывает транзакцию, но не изменяет статуса кэшированных изменений. Поэтому, если необходимо отменить кэшированные изменения, это необходимо сделать явно при помощи метода НД CancelUpdates. Управление ошибкой запоминания кэшированного изменения может осуществляться в обработчике события OnUpdateError. В случае успешного запоминания результатов кэшированных изменений в БД метод Apply Updates подтверждает транзакцию и подтверждает кэшированные изменения.

Пример.

Пусть требуется подтвердить кэшированные изменения НД Tovary и Rashod. В случае неудачи необходимо откатить кэшированные изменения:

TRY

DataBasel.ApplyUpdates([Tovary, Rashod]);

EXCEPT

ShowMessage('Изменения сохранить нельзя: они влекут нарушение ' + 'целостности базы данных') ;

Tovary.CancelUpdates ;

Rashod.CancelUpdates;

END;//try



Подтверждение кэшированных изменений методом ApplyUpdates набора данных


Метод компонентов TTable, TQuery, TStoredProc

procedure ApplyUpdates;

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

Метод компонентов TTable, TQuery, TStoredProc

procedure CommitUpdates;

применяется для обновления в БД кэшированных изменений, успешно подтвержденных методом ApplyUpdates.

Двухфазное подтверждение в случае использования методов ApplyUpdates и CommitUpdates необходимо реализовывать явно.

Пример.

Пусть требуется подтвердить кэшированные изменения НД Tovary и Rashod. В случае неудачи необходимо откатить кэшированные изменения:

Databasel.StartTransaction;

TRY

Tovary.ApplyUpdates ;

Rashod.ApplyUpdates;

Databasel.Commit;

Tovary.CommitUpdates;

Rashod.CommitUpdates ;

EXCEPT

Databasel.Rollback;

ShowMessage('Изменения могут привести к нарушению ' + 'целостности БД. Изменения отменены!' );

Tovary.CancelUpdates ;

Rashod.CancelUpdates ;

END;//try

В случае сбоя при выполнении Tovary.Apply Updates или Rashod.ApplyUpdates не произойдет подтверждения транзакции (Database]. Commit) и подтверждения кэшированных изменений для каждой из таблиц; значения измененных записей будут восстановлены в значения, имевшие место до изменений (Tovary.CancelUpdates, Tovary. CancelUpdates).

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

Database1.StartTransaction;

TRY

Tovary.ApplyUpdates ;

Rashod.ApplyUpdates;

Databasel.Commit;

Tovary.CommitUpdates;

Rashod.CommitUpdates ;

EXCEPT

Databasel.Rollback;

ShowMessage('Изменения могут привести к нарушению ' + 'целостности БД. Изменения отменены!' );

END;//try



Подтверждение кэшированных изменений


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



Видимость измененных записей


Свойство набора данных

property UpdateRecordTypes: TUpdateRecordTypes;

TUpdateRecordTypes = set of (rtModified, rtlnserted, rtDeleted, rtUnModified);

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

rtModified -

измененные записи

rtlnserted -

добавленные записи

rtDeleted -

удаленные записи

rtUnmodified- неизменявшиеся записи

Пример.

Показать в НД только удаленные записи:

RashodTable.UpdateRecordTypes := [rtDeleted];

Пример.

Показать в НД только неизменявшиеся, добавленные и измененные записи (удаленные не показывать):

RashodTable.UpdateRecordTypes := [rtUnmodified,rtModified, rtlnserted];

ЗАМЕЧАНИЕ.

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

Использование обработчика OnUpdateError


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

• нарушение значения поля связи в родительской или дочерней таблицах;

• нарушение уникальности индекса, построенного по первичному или уникальному ключу таблицы БД;

• попытка ввести в поле связи дочерней таблицы значение, отсутствующее в поле связи родительской таблицы;

• нарушение требования ввода в одно из полей обязательного значения;

• нарушение ограничений, накладываемых на значение поля.

Реагировать на ошибки, возникающие при подтверждении кэшированных изменений, можно в обработчике события OnUpdateError:

procedure TForm1.TovaryUpdateError(DataSet: TDataSet;

E: DatabaseError; UpdateKind: TUpdateKind;

var UpdateAction: TUpdateAction);

begin

...

end;

где параметры имеют следующее назначение:

DataSet: TDataSet -

набор данных, для которого при попытке подтверждения кэшированных изменений произошла ошибка;

UpdateKind: TUpdateKind -

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

ukModify -

запись была изменена (скорректировано значение хотя бы одного поля);

uklnsert -

запись была добавлена;

ukDelete -

запись была удалена.

var UpdateAction: TUpdateAction -

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

E: DatabaseError -

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

Использование параметра UpdateKind


Параметр UpdateKind позволяет в случае ошибки предпринять какие-либо действия, зависящие от способа изменения записи.

Пример.

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

procedure TForm1.TovaryUpdateError(DataSet: TDataSet;

E: EDatabaseError; UpdateKind: TUpdateKind;

var UpdateAction: TUpdateAction);

begin

IF UpdateKind = ukDelete THEN Tovary.RevertRecord;

end;



Использование параметра UpdateAction


Изменяемый параметр UpdateAction определяет действие, которое следует произвести в отношении ошибочной записи. Действие указывается перед выходом из обработчика:

procedure TForm1.TovaryUpdateError(DataSet: TDataSet;

E: EDatabaseError; UpdateKind: TUpdateKind;

var UpdateAction: TUpdateAction);

begin

// какие-либо операторы, если нужны

UpdateAction := uaSkip;

end;

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

Значение Что влечет
uaFail Отменить изменения для записи, выдать сообщение об ошибке.
uaAbort Отменить изменения для записи, сообщение об ошибке не выдавать. Применяется также в случаях, когда генерируется сообщение об ошибке, отличное от выдаваемого системой.
uaSkip Не отменять изменений для записи, но запись физически в таблицу БД не переносить (что, собственно, и невозможно без изменения значений записи, повлекших ошибку).
uaRetry Повторить попытку подтверждения изменений для записи. Предполагается, что на момент повторения попытки значения полей данной записи скорректированы программным способом. В противном случае произойдет зацикливание.

Можно заметить, что при применении способа обработки ошибочной записи uaSkip в физической ТБД будут подтверждены изменения не всех записей. а только тех из изменений, для которых не произошла ошибка при подтверждении кэшированных изменений.

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

Пример.

Пусть имеется таблица "Товары" и дочерняя таблица "Расход товара". Пусть эти таблицы соединены по полю связи "Товар"(рис. 33.1).

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

Database1.StartTransaction;

TRY

Tovary.ApplyUpdates;

Databasel.Commit;

Tovary.CommitUpdates;

EXCEPT

Databasel.Rollback;

END;//try

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

Напишем обработчик On UpdateError для НД "Товар":

procedure TForm1.TovaryUpdateError(DataSet: TDataSet;

E: EDatabaseError; UpdateKind: TUpdateKind;

var UpdateAction: TUpdateAction) ;

begin

CASE RadioGroup1.Itemlndex OF

0 : UpdateAction := uaFail;

1 : UpdateAction := uaAbort;

2 : UpdateAction := uaSkip;

3 : UpdateAction := uaRetry;

END;//case

end;

Компонент RadioGroupl содержит возможность выбора одного из значений изменяемого параметра UpdateAction.

Изменяя значение параметра UpdateAction, посмотрим, изменение каких записей будет подтверждено при возникновении ошибки при выполнении метода Tovary ApplyUpdates',

• UpdateAction = uaFail.

Как показано на рис.33.2.а) и б), в режиме uaFail результаты кэшированных изменений полностью отменены:



Использование параметра Е


Используя параметр Е, можно определить причину возникновения ошибки и в соответствии с этой причиной предпринять какие-либо действия.

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

Параметр Е: свойство Message

Тип исключения, на которое ссылается Е, - EDatabaseError. Код ошибки в этом типе отсутствует, но имеется свойство

property Message: string;

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

Пример.

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

procedure TForm1.TovaryUpdateError(DataSet: TDataSet;

Е: EDatabaseError; UpdateKind: TUpdateKind;

var UpdateAction: TUpdateAction);

var S : String;

PartCat, PartCode : Word;

begin



Использование компонента TUpdateSQL


Компонент TUpdateSQL используется для подтверждения кэшированных изменений в НД, связанном с компонентом TQuery. Такой НД является результатом выполнения SQL-оператора SELECT и может быть доступным для обновления или доступным только для чтения. В последнем случае свойство НД

property CanModify: Boolean;

(реальная возможность модификации НД) возвращает False, даже если свойство компонента TQuery

property RequestLive: Boolean;

(разрешение модификации НД) установлено в True на этапе разработки приложения или программно во время выполнения.

Компонент TUpdateSQL позволяет вносить изменения в НД, доступные только для чтения. Этот компонент как бы объединяет в себе три компонента TQuery, содержащих SQL-операторы INSERT, UPDATE, DELETE для соответственно добавления, изменения или удаления записи.



Компонент TUpdateSQL: этап разработки


1. Разместите в форме компонент TUpdateSQL.

2. Выберите компонент TQuery, кэшированные изменения в котором должен подтверждать компонент TUpdateSQL. Свойство TQuery. CachedUpdates должно быть установлено в True.

3. Укажите в свойстве TQuery. UpdateObject имя компонента TUpdateSQL.

4. Выберите мышью (сделайте текущим) компонент TUpdateSQL и дважды щелкните на нем мышью. На экране появится редактор свойств TUpdateSQL (рис. 33.7).

5. В списке Table Name выберите таблицу БД, в которой необходимо подтверждать кэшированные изменения.

6. В окне А'еу Fields выберите поля, входящие в индекс, который следует использовать при обновлении записи. Для локальных СУБД по указанным полям должен быть построен реально существующий индекс;

для удаленных СУБД существование такого индекса желательно, но не обязательно.

7. Если нужно выделить поля, входящие в первичный ключ таблицы, нажмите кнопку Primary Key Fields.

8. В окне Update Fields выделите поля, значения в которых следует обновлять;

9. Нажмите кнопку Generate SQL, чтобы сгенерировать SQL-операторы для добавления, изменения и удаления записей.

10. Перейдите на страницу SQL и просмотрите три сгенерированных оператора - INSERT, UPDATE, DELETE (рис. 33.8).

11. Измените сгенерированные операторы, если это необходимо.

Сгенерированные SQL-операторы содержатся в свойствах компонента TUpdateSQL

property InsertSQL: TStrings;

property ModifySQL: TStrings;

property DeIeteSQL: TStrings;



Компонент TUpdateSQL: выполнение SQL-операторов


Существует два способа выполнения SQL-операторов, определенных в компоненте TUpdateSQL.

Автоматическое выполнение SQL-операторов компонента TUpdateSQL

При использовании одного компонента TUpdateSQL для занесения подтвержденных изменений в ТБД следует использовать автоматический способ. Он заключается в том, что применение метода TDatabase. Apply Updates для каждой подтвержденной записи приводит к автоматическому выбору и выполнению соответствующего SQL-оператора из компонента TUpdateSQL, ассоциированного с одним из изменяемых НД.

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

Использование обработчика события OnUpdateRecord для вызова SQL-операторов компонента TUpdateSQL

Если с НД связано несколько компонентов TUpdateSQL, вызов SQL-оператора из нужного компонента TUpdateSQL производят в обработчике события OnUpdateRecord. Параметры обработчика OnUpdateRecord идентичны параметрам рассмотренного выше обработчика события On UpdateError за исключением отсутствующего параметра Е:

procedure TFormI.QuerylUpdateRecord(DataSet: TDataSet;

UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);

begin

end;

Для установки значений параметров изменяемой записи используют метод SetParams компонента TUpdateSQL:

procedure SetParams(UpdateKind: TUpdateKind);

UpdateKind -

указывает тип действия:

ukModify -

запись была изменена (скорректировано значение хотя бы одного поля);

uklnsert -

запись была добавлена;

ukDelete -

запись была удалена.

Метод SetParams замещает параметры в SQL-операторе компонента TUpdateSQL значениями соответствующих полей ТБД. Какой SQL-оператор выполнять, определяет значение UpdateKind.

Для выполнения соответствующего SQL-оператора используют метод ExecSQL компонента TUpdateSQL:

procedure ExecSQL(UpdateKind: TUpdateKind);

где UpdateKind указывает тип действия.

Существует также метод Apply, который объединяет в себе функциональность методов SetParams и ExecSQL:

procedure Apply(UpdateKind: TUpdateKind);

Метод Apply в соответствии со значением параметра UpdateKind определяет требуемый SQL-оператор, изменяет параметры этого оператора и затем выполняет его.

Пример.

Обработчик события OnUpdateRecord, содержащий вызов соответствующих SQL-операторов компонента TUpdateSQL:

procedure TFormI.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;

ЗАМЕЧАНИЕ.

Значение uaApplied изменяемого параметра UpdateAction обработчика On UpdateRecord подтверждает изменения в данной записи Этот обработчик может иметь более широкое применение, однако поскольку возможности этого обработчика не всегда вписываются в транзакционный способ изменений в БД, в настоящем материале они не рассматриваются; более подробно с этими возможностями можно ознакомиться в Borland Delphi Database Application Developer's Guide

Понятие события


В SQL-языке InterBase определен оператор

POST_EVENT "Имя события";

После его выполнения наступает событие с данными именем события. Сервер БД уведомляет о наступлении события все активные приложения, зарегистрировавшие свой интерес к данному событию посредством выполнения оператора EVENT INIT. Эти операторы мы изучать не будем по той простой причине, что в клиентских приложениях, написанных на Delphi, они неявно выполняются компонентом TIBEventAlerter.

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

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

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



Приложения Delphi и компонент TIBEventAlerter


Компонент TIBEventAlerter расположен в палитре компонентов на странице Samples. Тот факт, что он расположен не на страницах Data Contras или Data Access, можно объяснить специализацией компонента на работу с Borland InterBase.

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

property Database: TDatabase; -

содержит имя компонента TDatabase, управляющего соединением с БД, работающей под управлением Borland InterBase.

property Events: TStrings; -

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

property Registered: Boolean; -

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

procedure RegisterEvents; -

выполняет регистрацию приложения;

procedure UnregisterEvents; -

отключает регистрацию приложения как приемника сообщений. Данный метод не может применяться в обработчике события OnEventAlert. Перед выполнением метода в обработчике OnEventAlert полезно установить изменяемый параметр CancelAlerts в False. procedure OnEventAlert: TEventAlert; - обработчик события TEventAlert;

TEventAlert = procedure( Sender: TObject; EventName: String;

EventCount: longint; var CancelAlerts: Boolean);

Назначение параметров:

• EventName - содержит имя события, посланного сервером приложению клиента;

EventCount -

содержит количество событий (типа, определенного параметром EventName), имевших место на сервере с момента последней передачи клиентскому приложению уведомления о наступлении событий;

• CancelAlerts - изменяемый параметр; значение True (по умолчанию) сообщает серверу о том, что клиентское приложение продолжает интересоваться событиями из списка, указанного в свойстве Events; False сообщает о том, что интерес приложения к уведомлению о событиях иссяк.



Использование компонента


Пусть в БД, расположенной на сервере, для таблицы БД 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 имя компонента TDatabase, управляющего соединением с удаленной БД. Установим свойство этого компонента Registered в True. В списке свойства Events определим события, при уведомлении о наступлении которых с сервера, в клиентском приложении должны предприниматься какие-либо действия (рис. 34.1).

Определим обработчик события OnEventAlert. Пусть нам необходимо, чтобы через каждые десять изменений происходило обновление набора данных RashodQuery (компонент TQuery), ассоциированного с таблицей RASHOD. Это важно, поскольку клиентское приложение "не видит" изменений в БД. сделанных другими приложениями.

procedure TFormI.IBEventAlerterlEventAlert(Sender: TObject;

EventName: string; EventCount: Longint; var CancelAlerts:

Boolean) ;

const GettingEventsCnt : Integer = 0;

STEP = 10;

Period : Integer = STEP;

begin

GettingEventsCnt := GettingEventsCnt + EventCount;

IF GettingEventsCnt > Period THEN begin

RashodQuery.Close;

RashodQuery.Open;

Period := GettingEventsCnt + STEP;

END;//if



Обмен сообщениями между приложениями


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

Определим в удаленной БД следующие процедуры:

CREATE PROCEDURE B_SEND_INIT

AS

BEGIN

POST_EVENT "B_LOADED";

END

CREATE PROCEDURE A_SEND_INIT

AS

BEGIN

POST_EVENT "A_LOADED";

END

В форме приложения В разместим компоненты DatabaseB, IBEventAlerterB, StoredProcB (рис. 34.2).

Компонент DatabaseB управляет соединением приложения В с удаленной БД, общей для приложений А и В. Компонент StoredProcB предназначен для вызова хранимой процедуры B_SEND_INIT после нажатия кнопки "Послать сообщение к А":

procedure TFormB.SendButtonClick(Sender: TObject);

begin

DataBaseB.StartTransaction;

StoredProcB.ExecProc;

DataBaseB.Commit;

end;

ЗАМЕЧАНИЕ.

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

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

Компонент IBEventAlerterB отслеживает получение от сервера БД сообщения о наступлении события "A_LOADED". В этом случае просто визуализируется полученное сообщение в компоненте ListBoxB:

procedure TFormB.IBEventAlerterBEventAlert(Sender: TObject;

EventName: string; EventCount: Longint; var CancelAlerts:

Boolean) ;

begin

ListBoxB.Items.Add(EventName) ;

end;

В форме приложения А (рис. 34.3) размещены компоненты DatabaseA, IBEvenlAlerterA, StoredProcA.

Компонент DatabaseA управляет соединением приложения А с удаленной БД, общей для приложений А и В. Компонент StoredProcA предназначен для вызова хранимой процедуры A_SEND_INIT. Этот вызов происходит после получения компонентом IBEventAlerter А уведомления о наступлении события "B_LOADED":

procedure TFormA.IBEventAlerterAEventAlert(Sender: TObject;

EventName: string; EventCount: Longint; var CancelAlerts:

Boolean);

begin

ListBoxA.Items.Add(EventName) ;

DataBaseA.StartTransaction;

StoredProcA.ExecProc;

DataBaseA.Commit;

end;



Обмен инициализирующими сообщениями между приложениями


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

В этом случае приложение В должно претерпеть некоторые изменения. Отсылка сообщения к приложению А и ожидание поступления от него подтверждающего сообщения вынесена в отдельную форму WailingForm. В ней размещены компоненты DatabaseB, StoredProcB, Timer 1 и IBEventAlerterB (рис. 34.4).

Диалоговое окно "Ожидание ответа от приложения А"

Рис. 34.4. Форма WailingForm

Компонент DatabaseB управляет соединением приложения В с удаленной БД, общей для приложений А и В. Компонент StoredProcB предназначен для вызова хранимой процедуры B_SEND_INIT. Транзакция вызова хранимой процедуры осуществляется каждые 5 секунд компонентом Timer! (тип TTimer) в обработчике события On Timer:

procedure TWaitingForm.TimerlTimer(Sender: TObject);

begin

DatabaseB.StartTransaction;

StoredProcB.ExecProc;

Databases.Commit;

end;

Повторяющийся вызов хранимой процедуры необходим, чтобы приложение А, если оно загружается позднее приложения В, наверняка получило от сервера уведомление о наступлении события "B_LOADED". Как только компонент IBEvenlAlerierB получит от сервера обратное инициализирующее сообщение, посланное приложением А, компонент Timer} дезактивизируется и повторяющийся вызов хранимой процедуры B_SEND_INIT будет прекращен, программа сообщает серверу о том, что событие "A_LOADED" ее больше не интересует, а форма WailingForm закрывается:

procedure TWaitingForm.IBEventAlerterBEventAlert(Sender: TObject;

EventName: string; EventCount: Longint; var CancelAlerts: Boolean);

begin

Timer1.Enabled := False;

FormB.ListBoxB.Items.Add(EventName) ;

CancelAlerts := True;

WaitingForm.ModalResult := mrOk;

end;

Перед вызовом из главной формы приложения В форма WaitingForm динамически создается, а после окончания работы - уничтожается:

procedure TFormB.FormActivate(Sender: TObject);

begin

WaitingForm := TWaitingForm.Create(Self);

WaitingForm.ShowModal;

WaitingForm.Free;

end;

В форме WaitingForm отменены иконки выхода, минимизации и максимизации (свойство Border-Icons), чтобы предотвратить выход из формы до получения инициализирующего сообщения от приложения А.



Понятие функции, определяемой пользователем


Состав встроенных функций InterBase весьма небогат - в него входят функции вычисления агрегированных значений (MIN, MAX, SUM, A VG), функция преобразования букв UPPER и функция приведения типа CAST.

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

Специально для таких целей в InterBase существует аппарат функции, определяемых пользователем (User Defined Functions, UDF).

Разработка UDF может осуществляться на любом алгоритмическом языке, позволяющем создавать DLL (dynamic link library, динамически загружаемые библиотеки). Каждая UDF оформляется в виде функции, входящей в состав DLL. Таким образом, одна динамически загружаемая библиотека состоит минимум из одной функции.

После того как DLL разработана, она либо перемещается в подкаталог BIN каталога размещения InterBase, либо располагается в ином каталоге, путь к которому известен в операционной системе.

Каждая функция определяется оператором DECLARE EXTERNAL FUNCTION. Этот оператор устанавливает связь между функцией из DLL и ее описанием в БД. После этого функция может использоваться в SQL-операторах наряду со стандартными функциями InterBase.

Преимущества UDF:

• динамически загружаемая библиотека и определенные в ней функции могут использоваться более чем одной БД и более чем одним приложением; таким образом осуществляется повторное использование однажды написанного кода;

• пользователь может реализовать в UDF достаточно сложные алгоритмы.



Общие положения


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

• заголовок Library имя, где имя - имя создаваемой DLL;

• в разделе реализации модуля нужно разместить один или несколько блоков exports, в которых через запятую перечисляются имена экспортируемых функций; каждая функция должна описываться выше блока по тексту модуля;

• каждая экспортируемая функция объявляется с использованием директив export и cdecl(последняя указывает компилятору, что функция использует соглашения для передачи параметров, принятые в C/C++).

Пример

определения функции:

function SomeName(parami : Integer) : Integer; cdecl; export;

begin

end;

exports

SomeName;



Совместимость типов параметров


При описании параметров в БД (оператор DECLARE EXTERNAL FUNCTION) и параметров функций в DLL следует помнить о совместимости типов Object Pascal и InterBase:

Тип InterBase Тип Object Pascal

INTEGER Integer

DOUBLE PRECISION Double

CSTRING PChar

DATE IBDateTime = 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_dll";

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

InterBase Object Pascal

INTEGER integer

DOUBLE PRECISION double

DATE IBDateTime

Тип PChar всегда передается по ссылке.

Будем использовать только параметры, передаваемые по значению, поскольку вызовы UDF предполагается осуществлять в SQL-операторах типа SELECT, INSERT, UPDATE, DELETE, а эти операторы не будут изменять содержимое параметров.



Особенности использования в UDF параметров типа PChar


Параметры типа PChar используются для совместимости с форматом представления строк C/C++, однако в Object Pascal со строками, передаваемыми как PChar, в теле функции лучше работать как с длинной строкой Pascal (String), воспользовавшись преобразованием из типа PChar в String и обратно.

Пример.

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

function TrimChar(InString : PChar) : PChar; cdecl; export;

begin

Result := PChar(Trim(AnsiString(InString)));

end;



Особенности использования в UDF параметров типа даты и времени


Значения InterBase типа DATE в Object Pascal интерпретируются как запись, состоящая из двух полей - целочисленного знакового и беззнакового.

IBDateTime = record

Days : Integer;

Msec : Cardinal;

end;

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



Объявление UDF в БД InterBase


Для объявления функции, определенной пользователем, в БД InterBase, необходимо выполнить оператор

DECLARE EXTERNAL FUNCTION ИмяФункции

[<Тип данных> | CSTRING (число) [, <Тип данных> | CSTRING (число) ...]]

RETURNS {< Тип данных > [BY VALUE] | CSTRING (число)}

ENTRY_POINT "<Имя функции в DLL>"

MODULE_NAME "< Имя DLL >";

ИмяФункции -

имя функции, под которой функция будет известна в БД. Это имя может отличаться от имени UDF в DLL. После имени функции следует список типов входных параметров функции. Это либо тип данных, разрешенный в InterBase, либо CSTRING (число для строковых значений. Число определяет размер строкового значения в символах. Если число меньше действительного размера строки, строка при передаче в UDF усекается.

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

После ENTR Y_POINTa кавычках указывается имя функции в DLL, а после слов MODULE_NAME - имя модуля DLL (без расширения).

Удалить из БД объявление функции, определенной пользователем, можно при помощи оператора

DROP EXTERNAL FUNCTION ИмяФункции;



Пример создания DLL с несколькими UDF и объявления их в БД


Создадим DLL с именем 'UDF_DLL', в состав которой входят три функции:

library udf_dll;

uses

SysUtils,

Classes;

type

TIBDateTime = record

Days,

MSecIO : Cardinal;

end;

PInteger = ^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.MSecIO / (MSecsPerDay * 10);

DecodeDate(DT,Gd,Ms,Dn) ;

Result := Integer(Dn);

end;

//функция возвращает (по ссылке) номер месяца передаваемой в качестве параметра даты

function Mes(var InDate : TIBDateTime) : PInteger; cdecl; export;

var DT : TDateTime;

Gd,Ms,Dn : Word;

R : Integer;

begin

DT := InDate.Days - 15018 + InDate.MSecIO / (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_dll";

DECLARE EXTERNAL FUNCTION MES DATE

RETURNS INTEGER

ENTRY_POINT "Mes"

MODULE_NAME "udf_dll";

Примеры использования объявленных в БД функций пользователя в операторе SELECT:

SELECT TRIMCHAR (POKUP) || ' ' II GOROD

FROM POKUPATELI

SELECT *

FROM RASHOD

WHERE DEN(DAT_RASH) > 10;



Размещение бизнес-правил


Бизнес-правила

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

Таким образом, бизнес-правила определяют условия поддержания БД в целостном состоянии.

Идеология архитектуры "клиент-сервер" требует переноса максимально возможного числа БП на сервер. К преимуществам такого подхода относятся:

• гарантия целостности БД, поскольку БП сосредоточены в едином месте (в базе данных);

• автоматическое применение БП, определенных на сервере БД, для любых приложений;

• отсутствие различных реализации БП в разнотипных клиентских приложениях, работающих с БД;

• быстрое срабатывание БП, поскольку они реализуются на сервере и, следовательно, нет необходимости посылать данные клиенту, увеличивая при этом сетевой трафик;

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

К недостаткам хранения бизнес-правил на сервере можно отнести:

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

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

На практике в клиентских приложениях реализуют лишь такие бизнес-правила, которые тяжело или невозможно реализовать с применением средств сервера. Все остальные БП переносятся на сервер.



Ограничения значения столбца записи


Ограничение первичного ключа

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

CREATE TABLE SAL_HIST (QUORTER INTEGER NOT NULL, PRIMARY KEY (QUORTER));

Ограничение уникального ключа

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

CREATE TABLE VLADLIM (... NAZVVLAD VARCHAR(50) NOT NULL, UNIQUE (NAZVVLAD) );

Ограничение ссылочной целостности

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

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 POL_TYPE AS CHAR(3) CHECK(VALUE IN ("Муж","Жен")) ;

Ограничение диапазона возможных значений

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

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( ... CHECK (STOLBEZ CONTAINING "USD") ; ... );

Требования присутствия ведущих символов

Устанавливает, что значение столбца должно начинаться с определенной группы символов. В приводимом ниже примере значение поля STOLBEZ должно начинаться с символов 'USD'.

CREATE TABLE TBL( ... CHECK (STOLBEZ STARTING WITH "USD") ; ... );

Требование отношения со значением в другой таблице

Устанавливает, что значение столбца находится в некотором отношении (=, >, < и т.д.) со значением, получаемым путем выполнения запроса к другой таблице. В приводимом ниже примере значение в столбце 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) ) ) ;

Требование существования хотя бы одной записи в другой таблице

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

В приводимом ниже примере обязательно существование хотя бы одной записи в таблице 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,



Запрет добавления записей в просмотре


Для просмотра VIEW может быть включен режим WITH CHECK OPTION, предотвращающий добавление записей, не удовлетворяющих условию WHERE оператора SELECT данного просмотра Например, для приведенного ниже обзора будет отвергаться попытка добавления записи со значением поля KOLVO, меньшим 1000

CREATE VIEW RASH_1000_CHECK

AS

SELECT * FROM RASHOD WHERE KOLVO > 1000 WITH CHECK OPTION;



Использование триггеров для поддержания ссылочной целостности


Для поддержания ссылочной целостности на сервере могут использоваться триггеры. Они автоматически запускаются при выполнении любого изменения таблицы БД (добавление новой записи, корректировка или удаление существующей). Время запуска - до или после события - определяется в заголовке триггера. Большим преимуществом триггеров является возможность обращения (при изменении записи) к старому (OLD) и новому (NEW) значению столбца.

Триггеры могут использоваться для:

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

• внесения уникального значения в столбец, по которому построен уникальный или первичный ключ;

внесения изменений в семантически связанные таблицы;

• ведения журнала изменений БД.

Пример.

Приводимые ниже триггеры выполняют каскадные обновления в дочерней таблице RASHOD после изменения значения столбца связи в записи в родительской таблице TOVARY:

CREATE TRIGGER BU_TOVARY FOR TOVARY

ACTIVE

BEFORE UPDATE

AS

BEGIN

IF (OLD.TOVAR 0 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

Пример.

Приводимый ниже триггер при добавлении записи в таблицу RASHOD присваивает столбцу N_RASH уникальное значение, для чего используется генератор RASHOD_N_RASH.

CREATE TRIGGER BI_RASHOD_GEN FOR RASHOD ACTIVE

BEFORE INSERT

BEGIN

NEW.N_RASH = GEN_ID(RASHOD_N_RASH,1) ;

END

Пример.

Приводимый ниже триггер при добавлении новой записи в таблицу RASHOD (расход товара) прибавляет значение поля KOLVO (количество) вновь введенной записи к полю KOLVO в таблице STAT_TOVARY для записи с той же датой (DAT_RASH) и названием товара (TOVAR). Если в таблице STAT_TOVARY такая запись отсутствует, она создается.

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 (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

Пример.

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

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



Реализация бизнес-правил в компонентах типа "набор данных"


Компоненты типа "набор данных" позволяют реализовывать бизнес-правила в следующих обработчиках событий:

OnNewRecord -

происходит при добавлении новой записи сразу после перехода НД в состояние dslnsert из состояния dsBrowse, но перед выдачей полей новой записи пользователю для ввода значений; используется для присваивания полям значений по умолчанию;

BeforeOpen, BeforeClose, BeforeCancel, BeforeEdit, Beforelnsert -

происходят до выполнения соответствующего метода;

After Post, AfterDelete, AfterOpen, AfterClose, AfterCancel, AfterEdit, Afterlnsert •

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

OnCalcFields -

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

OnDeleteError -

наступает при ошибке удаления записи;

OnUpdateError -

происходит при ошибке редактирования записи;

OnPostError -

происходит при возникновения ошибки в ходе выполнения метода Post.

Определение алгоритма вычисления значений вычисляемых полей

Обработчик события OnCalcFields применяется для определения алгоритма расчета значения вычисляемых полей. Например,

procedure SomeTableCalcFields(DataSet: TDataSet);

begin

SomeTableVychPole.Value := SomeTablePolel.Value / SomeTablePolel.Value;

end;

Присваивание значений полей по умолчанию

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

procedure SomeTableNewRecord(DataSet: TDataSet);

begin

WITH Some do begin

FieldByName('polel').AsInteger := ...;

FieldByName('poleN').AsInteger := ...;

END;//with

end;

Автоматическое присваивание значения полям связи

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

Например,

при добавлении новой записи в таблицу ChildTable устанавливается значение поля связи, равное значению поля 'cod_parent' текущей записи родительской таблицы ParentTable:

procedure ChildTableNewRecord(DataSet: TDataSet);

begin

ChildTable.FieldByName('cod_parenf).AsInteger :=

ParentTable.FieldByName('cod_parenf).AsInteger;

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.Clear;

SQL.ADD('SELECT MAX(COD_UNIQUE)') ;

SQL.ADD('FROM RASHOD');

Open;

Max_cod_unique := WorkQuery.Fields[0].Aslnteger + 1;

Close;

END;//with

ChildTable.FieldByName('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

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

procedure RashodTableNewRecord(DataSet: TDataSet);

begin

StoredProc1.ExecProc;

ChildTable.FieldByName('cod_unique').Aslnteger := StoredProc1.ParamByName('UV).Aslnteger;

end;

Внесение изменений в связанную таблицу БД

Обычно изменения в связанные таблицы вносятся в обработчиках событий AfterPost, AfterDelete.

Например,

уменьшить значение поля 'Kolvo' таблицы StatTable на значение поля 'Kolvo' из удаленной записи таблицы RashodTable:

procedure RashodTableAfterDelete(DataSet: TDataSet);

begin

WITH StatTable do begin

Edit;

FieldByName('Kolvo').Value := FieldByName('Kolvo').Value - DeletedRashodValue;

Post;

END;//with

end;

Внесение признака изменения таблицы в глобальную переменную

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

procedure TDM.SomeTableAfterDelete(DataSet: TDataSet);

begin

IzmSomeTable := True;

end;



Свойство Constrained (компонент TQuery)


Предотвратить ввод записей, не удовлетворяющих условиям, перечисленным в предложении WHERE оператора SELECT, можно путем установки в True значения свойства

property Constrained: Boolean;

Например, для НД, возвращенного оператором

SELECT * FROM RASHOD WHERE KOLVO > 1000

при Constrained, содержащим True, будут блокироваться попытки запоминания записей со значением поля KOLVO, меньшим 1000.



Свойство Constraints


Свойство набора данных

property Constraints: TCheckConstraints;

является коллекцией ограничений на значения столбцов (полей) НД. Указание ограничения производится в SQL-подобном синтаксисе (рис. 36.1). В случае, если значение поля не удовлетворяет наложенным на него ограничениям, при попытке запоминания записи возбуждается исключение.



Реализация бизнес-правил в компоненте TField


Анализ правильности введенного в поле значения

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

Пример.

Значение поля Company не должно содержать символ '@':

procedure TForm1.Table1CompanyValidate(Sender: TField);

begin

IF POS('@',Table1Company.AsString) > 0 THEN raise Exception.Create('Неверное значение');

end;

Пример.

Значение поля PUR_PRICE не должно превышать 100:

procedure TForm1.Table1PUR_PRICESetText(Sender: TField;

const Text: String);

var Tmp : Real;

begin

Tmp := StrToFloat(Text) ;

IF Tmp > 100 THEN ShowMessage('Ошибочное значение')

ELSE Table1PUR_PRICE.Value := Tmp;

end;

или

procedure TForm1.Table1PUR_PRICEChange(Sender: TField);

begin

IF Table1PUR_PRICE.Value > 100 THEN raise Exception.Create('Ошибочное значение');

end;

Форматирование содержимого поля при показе и редактировании

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

Пример.

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

procedure TForm1.Table1CompanyGetText(Sender: TField; var Text: OpenString;

DisplayText: Boolean);

begin

IF DisplayText THEN Text := AnsiUpperCase(Table1Company.AsString)

ELSE Text := ' " ' + Table1Company.AsString + ' " ';

end;

Свойства CustomConstraint и ConstraintErrorMessage

Свойство поля

property CustomConstraint: string;

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

property ConstraintErrorMessage: string;



Реализация бизнес-правил в иных компонентах


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

Проверка правильности значения

Приводимый ниже пример проверяет значение в Edit 1.Text на предмет соответствия формату даты. В дальнейшем это значение, приведенное к формату даты, планируется записать в поле типа даты таблицы БД. Проверка производится в обработчике события OnButtonClick (компонент TButton):

procedure TToObrForm.GoButtonClick(Sender: TObject);

var Tmp: TDateTime;

begin

TRY

Tmp := StrToDate(Editl.Text);

EXCEPT

ShowMessage('Неверная дата') ;

Editl.SetFocus;

Exit;

END;//try

end;

Запрет подтверждения изменений в БД

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

procedure TToObrForm.GoButtonClick(Sender: TObject);

var Tmp: TDateTime;

begin

TRY

Tmp := StrToDate(Editl.Text);

EXCEPT

ShowMessage('Неверная дата');

Edit1.SetFocus;

PostButton.Enabled := False;

Exit;

END;//try

end;

Отмена выхода из формы до осуществления каких-либо действий

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

procedure TForm1.DBEditlChange(Sender: TObject);

begin

ExitButton.Enabled := False;

end;

procedure TForm1.Table1AfterPost(DataSet: TDataSet) ;

begin

ExitButton.Enabled := True;

end;

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

Выполнение действий при переходе на другую запись

Событие OnDataChange компонента TDataSource происходит при изменении текущей записи в НД (в режиме dsBrowse} или при изменении какого-либо поля записи (режим dsEdit).

В приводимом примере подразумевается, что для текущей записи в ParentTable по запросу (нажатие кнопки SelectChildRecordsButton) в НД ChildTable фильтруются дочерние записи.

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

procedure TForm1.ParentDataSourceDataChange(Sender: TObject;

Field: TField) ;

begin

IF ParentTable.State <> dsBrowse THEN Exit;

ChildDBGrid.Enadbled := True;

ChildDBGrid.Color := cISilver;

SelectChildRecordsButton.Visible := True;

end;

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



Реализация бизнес-правил в приложении клиента


Для реализации бизнес-правил в приложении клиента могут использоваться различные компоненты. В первую очередь это компоненты типа "набор данных" (TTable, TQuery, TStoredProc) и компоненты TField. Интерфейсные компоненты (например, TEdit и TButton), могут использоваться для контроля за вводом пользователя и могут блокировать закрытие формы, если он ввел неправильные значения.



Использование словаря данных для определения атрибутов полей


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

Словарь данных позволяет:

сформировать поименованный набор атрибутов поля без привязки его к конкретному полю (столбцу) конкретной таблицы БД;

по мере необходимости ставить поименованный набор атрибутов в соответствие конкретным полям (столбцам) конкретных ТБД.

В состав атрибутов, которые можно определить в поименованном наборе атрибутов, входят: выравнивание (Alignment), заголовок поля при показе (Display Label), ширина показа (Display Width}, только для чтения (Read Only), требующее обязательного ввода значения (Required}, видимое (Visible), маска при редактировании (Edit Mask), маска при показе (Display Mask) и другие.

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

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

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

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

Рассмотрим процесс назначения атрибутов полю и затем импорт их в приложение на следующем примере.

Пусть существует таблица БД 'zpa_knig.db' (записная книжка), в состав которой входят поля NN (порядковый номер, автоинкрементное поле), FIO (фамилия, имя, отчество, символьное поле), Tel (телефон, символьное поле), Prim (примечание, символьное поле).

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

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

1 Выберем в главном меню Delphi элемент Database \ Explore

2 В появившемся окне утилиты SQL Explorer выберем закладку Dictionary

3 Импортируем в словарь базу данных, содержащую таблицу 'zpa_knig db' (псевдоним БД book) Для этого в меню SQL Explorer выберем элемент Dictionary | Import from Database и затем в появившемся окне запроса выберем с помощью выпадающего меню псевдоним book, после чего нажмем кнопку Ok

4 Создадим новый набор атрибутов. Для этого установим мышь на элемент дерева AttributeSets, нажмем правую кнопку мыши и в появившемся меню выберем элемент New

5 В правом окне для вновь созданного набора атрибутов введем имя

Tel_attribute_set

В левом окне установим значения атрибутов Display

Label, Edit Mask

и Display Mask (рис 36 2)

6. Подтвердим внесенные для набора атрибутов Tel_attribute_set изменения,

вызвав правой кнопкой мыши меню и выбрав элемент Apply

7 Раскроем ветвь для БД book, раскроем ветвь Tables, выберем zap_knig, раскроем ее ветвь Fields и выберем поле Те/ (правое окно) В левом окне для данного поля установим значение A ttribute set в Te/_attr;bute_scf при помощи выпадающего списка (рис 36 3)

8 Подтвердим внесение изменений в набор атрибутов для поля Tel, для чего вызовем правой кнопкой мыши всплывающее меню и выберем элемент Apply

9 В приложении Delphi разместим в форме компонент TTable, связанный с ТБД zap_kmg, а также компоненты TDataSource и TDBGrid, связанные между собой стандартным образом Как видно из рис 36 4, характеристики поля Tel пока не отражают набора атрибутов, установленных этому полю в словаре данных

10 Используя редактор полей, явно определим для Table1 компоненты TField Как видно из рис 36.5, свойства поля Tel (компонент Table1Tel, тип TStrmgField) отражают атрибуты, установленные этому полю в словаре данных.

11. В режиме выполнения приложения поле Tel использует маску ввода и маску показа, установленные в словаре данных (рис 36.6).

Рис 36.6 В режиме выполнения приложения поле Tel использует маску вводи

и меню показа, установленные в словаре данных

Нормализация таблиц: теория и практика


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

ФИО

Должность

Иванов И.И.

директор

Петров П.П.

инженер

Яковлев Я.Я.

инженер

Сотрудники

Оклады

Должность

Оклад

директор

1000

бухгалтер

600

инженер

650

С точки зрения теории, нормализация таблиц приводит к наиболее ясному отображению сущностей из предметной области. Устраняя избыточность данных, она тем самым существенно экономит дисковое пространство. Например, если удалить таблицу "Оклады" и ввести поле "Оклад" в таблицу "Сотрудники", будет налицо явно нерациональное расходование дискового пространства, что всегда критично при больших объемах данных. Например, если в таблице "Сотрудники" присутствуют сведения о 1000 инженерах, одно и то же значение оклада инженера (значение 650) будет дублироваться 999 раз.

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

Кроме того, на практике высокая степень нормализации таблиц приводит к большому количеству таблиц, в результате чего структура информации в БД не воспринимается разработчиком целостно. Невозможность целостного представления структуры данных в БД является одним из "человеческих факторов", способных внести серьезные ошибки в структуру БД уже на стадии разработки БД, что впоследствии может иметь самые серьезные - и всегда негативные - последствия. Например, БД, состоящая из 50-100 таблиц, каждая из которых имеет минимум 2-3 связи с другими ТБД, уже выходит за рамки человеческого понимания, не говоря о БД в 500 и более таблиц.

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



Частичная зависимость структуры данных от методов доступа к ним


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

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

S1 + S2 +S3 - S4

где

S1 - начальный лимит (ТБД "Бюджетные лимиты подразделения");

S2 - сумма всех корректировок лимита по данному подразделению (таблица "Корректировки лимита");

S3 - сумма всех записей сверх лимита по данному подразделению (таблица "Суммы сверх лимита");

S4 - сумма всех предыдущих расходов подразделения по данному лимиту (таблица "Расходы лимитированных средств").

Пусть это соответственно значения 10000000 + 1000000 + 400000 - 6000000 = 5400000(рублей).

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

Чтобы блокировать текущий лимит данного подразделения, добавим в таблицу "Бюджетные лимиты подразделения" поле STATUS CHAR(l), и каждую транзакцию на изменение таблиц "Бюджетные лимиты подразделения", "Корректировки лимита", "Суммы сверх лимита", "Расходы лимитированных средств" в приложении будем начинать с изменения этого статуса:

Database1.StartTransaction;

TQuery1.Params[0] := Code;

// Code = код записи в ТБД "Бюджетные лимиты подразделения"

TRY

TQueryl.ExecSQL;

{Текст запроса вида

UPDATE ... SET STATUS = 'A' WHERE CodeZap = :Par1...}

{Вычисление текущего остатка данного бюджетного лимита по данному подразделению}

{Выполнение действий по изменению записей в дочерних таблицах}

EXCEPT

...

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

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

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

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



Физические характеристики БД


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

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

Решением указанных проблем является периодическое создание резервной копии и восстановление из нее БД. При этом:

• собирается "мусор", т.е. версии записей, которые далее не будут востребованы;

• устраняются "дыры" на страницах БД, образовавшиеся после удаления записей;

• каждая таблица размещается в непрерывном блоке страниц.

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

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