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

         

Способы обращения к полям набора данных


function FieldByName(const FieldName: string): TField;

Позволяет обращаться к полю, объявленному в структуре таблицы (TTable), или к полю, перечисленному в числе прочих в структуре набора данных, возвращаемого оператором SELECT (компонент TQuery). Например:

Table1.FieldByName('FIO').Value := 'Иванов И.И.';

Аналогичным целям служит свойство property FieIdValues[const FieldName: string]: Variant;

Оно позволяет обращаться к полю по его имени FieldName, например:

Table1.FieldValues['FIO'] := 'Иванов И.И.';

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

Table1['FIO'] := 'Иванов И.И.';

property Fields(Index: Integer]: TField;

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

Table1.Fields[2].Value := 'Иванов И.И.';

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

Table1FIO.Value := 'Иванов И.И.';



Блокировка таблиц в многопользовательском режиме


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

Запрет внесения изменений или просмотра другими пользователями НД, ассоциированного с данной таблицей, достигается с помощью метода procedure LockTable(LockType: TLockType); где параметр LockType определяет вид запрета:

ItReadLock -

запретить чтение;

ItWriteLock -

запретить запись

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

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

После того, как необходимость запрета пропадет, он должен быть отменен, что осуществляется методом procedure UnlockTable(LockType: TLockType); где параметр LockType указывает тип снимаемого запрета. Пример. Если таблица БД запрещена для записи в нее данных, в приложениях других пользователей исключение возбуждается при попытке перевести НД, ассоциированный с таблицей, в режим dsEdit (методом Edit), или удалить запись (метод Delete}. Будем анализировать успешность выполнения метода Edit в обработчике события возникновения ошибки при попытке перевести НД в режим редактирования Для этого можно написать такой обработчик события OnEditError.

procedure TForm1. Table1EditError(DataSet: TDataSet; E: EDatabaseError;

var Action: TDataAction);

begin

ShowMessage ('Таблица блокирована на запись другим пользователем') ;

Action := daAbort;

end;

ЗАМЕЧАНИЕ.

В архитектуре "клиент-сервер" не возникает потребности в реализации программных блокировок. Возможность одного пользователя вносить изменения в данные, уже корректируемые другим пользователем, определяется уровнем изоляции транзакций в приложении (свойство Translsolation компонента TDatabase) и уровнем изоляции транзакций сервера, а также механизмом блокировок сервера. Обычно блокировка накладывается на запись, измененную в рамках незавершенной транзакции. Однако, например, SQL-сервер Borland InterBase не блокирует чтение из записей, которые изменяются другим пользователем в рамках еще не завершенной транзакции В этом случае тот пользователь, чье приложение читает записи, видит записи в их последнем подтвержденном состоянии.

Синхронизация содержимого наборов данных в одном приложении


Как известно, в приложении может существовать несколько НД, ассоциированных с одной и той же таблицей БД. Например, это может быть компонент Table1, расположенный в модуле данных (Data Module) приложения, и компонент Table2, расположенный в форме и выполняющий там специфические функции, отличные от функций компонента Table 1. Тогда, если эти компоненты активны во время выполнения приложения, нужно обновлять содержимое одного набора данных в случае обновления другого.

Если, например, изменяется TDataModulel. Table 1, то для синхронизации изменения с содержимым TForm1 .Table2 следует написать такие обработчики событий:

procedure TDataModulel.TablelAfterDelete(DataSet: TDataSet);

begin

TFormI.Table2.Refresh;

end;

procedure TDataModulel.Table1AfterPost(DataSet: TDataSet);

begin

TFormI.Table2.Refresh; end;



Синхронизация содержимого наборов данных в разных приложениях


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

В клиентских приложениях, работающих в рамках архитектуры "клиент-сервер", подобная функциональность обеспечивается автоматически при уровне изоляции транзакций (Read Committed) и при условии использования компонента TTable. Компонент TQuery, выполнив запрос на чтение к удаленной БД, показывает записи НД в неизменном виде, независимо от того, изменялось ли после запроса содержимое таблицы БД. Для обновления информации в НД, реализуемом при помощи TQuery, приходится закрывать и повторно открывать компонент TQuery, т.е. повторно выполнять запрос к удаленной БД. При уровне изоляции транзакций Repeatable read пользователь в рамках транзакции видит данные только в том состоянии, в котором они находились на момент старта транзакции.

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

приложении. Отображение изменений производится лишь после выполнения данным пользователем метода Post или Delete. Если НД находится в состоянии dsBrowse, его обновление можно реализовать периодически, например, с помощью таймера:

procedure TFormI.TimerlTimer(Sender: TObject) ;

begin

IF Table1.State = dsBrowse THEN Table1.Refresh; end;

или, если нужно обновлять все НД приложения:

procedure TFormI.TimerlTimer(Sender: TObject);

var DSCnt : Integer;

i : Integer; begin

WITH Session.Databases [0] do begin

DSCnt := DataSetCount; // Получаем количество открытых НД

FOR i := О ТО DSCnt - 1 do // Обновляем каждый из них

IF DataSets[i].State = dsBrowse THEN

DataSets[i].Refresh;

END;//with

end;

Если нужно обновлять лишь некоторые из наборов данных, указатели на них можно поместить в список TStringsListl. Этот список можно создать и наполнить в момент создания формы (но не в момент ее активизации, т.к. форма может активизироваться много раз - после минимизации или после вызова другого приложения). При разрушении формы список удаляется:

var

DSList : TStringList;

procedure TFormI.FormCreate(Sender: TObject);

begin

DSList := TStringList.Create; // Создаем список

DSList.AddOb]ect(' '/Tablel); // Помешаем в него ссылку на Tablel

DSList. Add0b;ect (' \Table2); // и Table2

Timeri.Enabled := True; // Включаем таймер

end;

procedure TFormI.TimerlTimer(Sender: TObject) ;

var i : Integer;

begin

WITH DSList do begin

FOR i := 0 TO Count - 1 do

IF (Objects [i] as TDataSet).State = dsBrowse THEN (Objects[i] as TDataSet).Refresh;

END;//with

end;

procedure TFormI.FormDestroy(Sender: TObject);

begin

DSList.Free;

end;



Синхронизация содержимого наборов данных


Записи НД (компонент TTable, TQuery) размещаются в локальной копии на компьютере, на котором выполняется приложение Обновление локальной копии данных происходит в случае выполнения из данного приложения операции модификации НД (добавления, изменения или удаления записи, т е выполнения методов Post или Delete) Однако возникают случаи, когда содержимое локальной копии данных на конкретном компьютере (то есть, попросту говоря, содержимое НД) должно быть обновлено из физической таблицы БД (или синхронизировано с физической таблицей БД) Это достигается путем выполнения метода procedure Refresh;

При этом может встретиться два основных способа применения этого метода. Они рассматриваются ниже.



Обработка ошибок смены состоянии набора данных


В случае неудачи при выполнении методов Insert, Edit, Delete и Post обработку ошибки можно реализовать в соответствующих обработчиках событий OnEditError (ошибки при выполнении Insert и Edit}, OnDeleteError (ошибки при выполнении Delete} и OnPostError (ошибки при выполнении Post):

property OnEditError: TDataSetErrorEven;

property OnDeleteError: TDataSetErrorEven;

property OnPostError: TDataSetErrorEven;

где

TDataSetErrorEvent = procedure(DataSet: TDataSet; E: EDatabaseError;

var Action: TDataAction) of object;

TDataAction = (daFail, daAbort, daRetry);

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

DataSel -

указатель на компонент, в котором произошла ошибка;

Е -

ссылка на объект-исключение;

Action -

действие:

daFail -

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

daAbort -

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

daRetry -

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

Пример.

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

procedure TFormI.TablelEditError(DataSet: TDataSet; E:

EDatabaseError;

var Action: TDataAction);

begin

ShowMessage('Таблица Сотрудников заблокирована другим ' + 'пользователем') ;

Action := daAbort;

end;



Ограничения на значения полей


Набор данных имеет свойство property Constraints: TCheckConstraints; которое представляет собой коллекцию компонентов TCheckConstraints. Каждый такой компонент определяет ограничение, накладываемое на значение одного или более полей. Число ограничений, созданных для НД, определяется свойством коллекции Constraints property Count: Integer; Доступ к отдельному ограничению с индексом Index осуществляется при помощи свойства property Items|Index: Integer): TCheckConstraint;

При этом значение Index должно находиться в диапазоне 0..Count - 1. На рис. 7.40 показан список ограничений, определенных для НД, как он выглядит при обращении к свойству Constraints набора данных в инспекторе объектов.

Каждое ограничение имеет тип TCheckConstraint. Рассмотрим свойства этого компонента.

property CustomConstraint: string;

Содержит текст ограничения на значение поля (полей) в SQL-подобном синтаксисе, например:

Table1.Constraints. .Items[i].CustomConstraint := 'Razrjad > 7 and Razr]ad < 15';

property ErrorMessage string;

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

property FromDictionary: Boolean;

Указывает источник формирования ограничения - словарь данных (значение True) или непосредственно приложение (False).

property ImportedConstraint string;

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

Получение информации об индексах ТБД


Компонент TindexDefs содержит информацию обо всех индексах таблицы базы данных, объявленных в ней в текущий момент. У TTable имеется свойство property IndexDefs: TIndexDefs; которое содержит ссылку на объект класса TIndexDefs. Поэтому для каждого компонента TTable всегда можно получить информацию об индексах данной ТБД через свойства и методы TIndexDefs. Рассмотрим эти методы и свойства.

Свойства:

property Count: Integer; -

возвращает число индексов;

property Items[Index: Integer|: TIndexDef; -

коллекция объектов типа TIndexDef, каждый из которых содержит информацию о конкретном индексе. Index должен принадлежать диапазону [0..Count-1].

Экземпляр типа TIndexDef имеет следующие свойства (информацию о методах объекта данного класса можно получить в системе помощи Delphi):

property Name: string; -

возвращает имя индекса;

Пример.

Записать в ListBox1 имена всех индексов ТБД, ассоциированной с Table 1:

ListBoxl.Clear;

Table1.IndexDefs.Update;

FOR i := 0 TO Tablel.IndexDefs.Count - 1 do ListBoxl.Items.Add(Tablel.IndexDefs[i].Name) ;

Перед считыванием значения свойства Name необходимо выполнить метод Tablel IndexDefs.Update (см. ниже) для обновления информации обо всех имеющихся индексах.

Отметим, что для Paradox-таблиц имя первичного индекса не выдается; вместо этого выдается пустая строка, поскольку первичный индекс для Paradox-таблиц не имеет имени (рис. 8.1):

Пустую строку имени первичного индекса можно заменять, например, словом 'Primary':

var i : Integer;

Ima : String;

begin

ListBox1:Clear;

Table1.IndexDefs.Update;

FOR i := 0 TO Table1.IndexDefs.Count - 1 do begin

Ima := Table1.IndexDefs[i].Name;

IF Ima = '' THEN Ima := 'Primary';

ListBoxl.Items.Add(Ima) ;

END; //FOR

end;

property Fields: string, -

возвращает список полей, по которым построен данный индекс Поля в строке разделены точкой с запятой Именно в таком виде строку можно указывать в свойстве IndexFie!dNames (компонент TTable)

property Options: TIndexOptions -

возвращает характеристики индекса в виде множества TIndexOptions = set of (ixPrimary ixUnique, ixDescending, ixNonMaintatned, ixCaselnsensitne). Таким образом, возвращаемое значение может состоять максимум из 6 элементов

Пример

Показать в Edit1 Text список полей индекса, чье имя является текущим в ListBox1 (результат на рис 82)

// обработчик события выбора элемента в ListBoxl:

procedure TForm1.ListBox1Click(Sender: TObject);

var Teklndex : Integer;

begin

Teklndex := ListBox1.Itemlndex; { индекс текущего элемента в ListBoxl}

Edit1.Text := Tablel.IndexDefs[Teklndex].Fields;

Label2.Caption := 'Поля индекса ' + ListBoxl.Items[Teklndex] + ':' ;

end;

Методы

procedure Add(const Name, Fields: string; Options: TIndexOptions);

Метод Add создает новый объект TIndexDef и помещает его в коллекцию TIndexDefs Items На момент создания должны быть определены такие параметры нового объекта TIndexDef, как Name, Fields, Options Их назначение совпадает с назначением аналогичных свойств компонента TindexDef . Этот метод в основном используется при создании новых таблиц

procedure Update;

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

procedure Clear;

очищает элементы коллекции TIndexDefs Items

function Index0f(const Name: string): Integer;

возвращает из коллекции TIndexDefs Items индекс элемента, у которого свойство Name совпадает с параметром Name данного метода

function FindIndexForFields(const Fields: string): TIndexDef;

Отыскивает индекс по списку его полей, которые содержатся в строке Fields Возвращает указатель на объект TIndexDefв коллекции TIndexDefs Items, в котором содержимое свойства Fields совпадает с параметром Fields индекса

Метод GetIndexNames

procedure GetIndexNames(List: Tstrings) - возвращает в параметре List список имен индексов Заметим, что для Paradox-таблиц имя главного индекса не выдается

Пример.

Выдать список индексов ТБД, ассоциированной с Table1 (рис.8.3):

ListBox2.Clear;

Tablel.GetIndexNames(ListBox2.Items) ;

Свойства IndexFieldCount и IndexFields

Свойство IndexFieldCount: Integer; - возвращает число полей в текущем индексе НД. Номер первого поля 0.

Свойство IndexFields[Index: Integer]: TField; содержит набор полей текущего индекса; обращение IndexFields [Index] возвращает информацию о поле, определенном в текущем индексе под номером Index (нумерация полей начинается с 0). Для n определенных полей Index лежит в диапазоне 0.. (n-1). Например, чтобы записать в ListBox1 имена всех полей текущего индекса, можно использовать такой фрагмент программы:

var i : Integer;

ListBox1.Clear;

For i:= 0 TO Tablel.IndexFieldCount - 1 do ListBoxl.Items.Add(Tablel.IndexFields[I].FieldName) ;

Поскольку при обращении к Index Fields [Index] возвращается указатель на объект типа TField, для данного поля текущего индекса можно пользоваться всеми свойствами и методами типа TField.



Установка текущего индекса ТТаЫе


То, какой индекс является текущим для данного НД (компонент TTable), в ряде случаев имеет важное значение.

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

Во-вторых, многие методы и свойства TTable работают напрямую с текущим индексом. Это, например, метод SetRange для фильтрации записей в TTable и связанные с ним; методы для поиска записи, удовлетворяющей условию - FindKey, FindNearest и другие.

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

1) Путем занесения имени индекса в свойство property IndexName: string; например:

Tablel.IndexName := 'INDEX_BY_FIO';

Пример.

Для описанного выше примера заполнения ListBox именами доступных индексов сделаем для Table текущим индекс, имя которого является текущим в ListBoxl:

Tablel.IndexName := ListBoxl.Items[ListBox1.Itemlndex];

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

2) Путем занесения списка индексных полей в свойство property IndexFieldNames: string;

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

Table1.IndexFieldNames := 'FIO; Doljnost';

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



Добавление нового индекса


Добавление нового индекса происходит в режиме исключительного доступа к ТБД (свойство Exclusive = True) и осуществляется методом procedure Addlndex(const Name, Fields: string; Options: TIndexOptions); где параметр Name определяет имя индекса, а параметр Fields - список индексных полей. В случае нескольких полей их имена должны разделяться точкой с запятой. Должны указываться только поля, объявленные в структуре ТБД. В противном случае будет возбуждена исключительная ситуация и создание индекса будет блокировано. Параметр Options является множеством, которое содержит значения, определяющие свойства индекса:

TIndexOptions = set of (ixPrimary, ixUnique, ixDescending,ixExpression, ixCaseInsensitive) ;

ixPrimary -

определяет первичный индекс;

ixUnique -

определяет уникальный индекс;

ixDescending -

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

ixCaseInsensitive -

определяет индекс, нечувствительный к высоте букв. Так, например, если для индекса установлен этот режим, значения "КАРТОФЕЛЬ", "Картофель" и "картофель" будут сочтены идентичными.

Например,

определить новый индекс с именем WWW, построенный по полям 'NN; DatePrih', нечувствительный к высоте букв:

Table1.Close;

Table1.Exclusive := True;

Table1.Open;

Table1.Addlndex('WWW, 'NN; DatePrih' , [ixCaseInsensitive]);

Table1.Close;

Table1.Exclusive := False;

Table1.Open;



Удаление существующего индекса


Удаление существующего индекса происходит в режиме исключительного доступа к ТБД (свойство Exclusive = True) и осуществляется методом procedure Deletelndex(const Name: string); где параметр Name определяет имя удаляемого индекса. При попытке удаления несуществующего индекса возбуждается исключительная ситуация и удаление блокируется.

Пример.

Удалить индекс с именем WWW:

Table1.Deletelndex('WWW) ;



Установка приоритетного доступа при многопользовательском режиме


Свойство Exclusive дает пользователю исключительный доступ к НД (значение True). Это означает, что никто иной не только не может вносить изменения в НД, но вообще не имеет доступа к НД. Установить исключительный доступ можно, лишь когда ни один пользователь не имеет доступа к НД и тот не открыт.

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

ЗАМЕЧАНИЕ.

Delphi тоже считается в данном случае пользователем. Поэтому, если в программном коде делается попытка получения прав исключительного доступа к ТБД Table1.Exclusive := True; и программа запущена из Delphi, попытка получения исключительных прав будет блокирована, поскольку на Вашей машине имеется два пользователя, осуществляющих доступ к этой ТБД: выполняющееся приложение и Delphi.

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



Очистка записей ТБД


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

Пример.

Очистить от записей ТБД, ассоциированную с НД Tablel:

Table1.Close;

Table1.Exclusive := True;

Table1.EmptyTable;

Table1.Exclusive := False;

Table1.Open;

Еще раз напомню, что таблица не будет очищена, если этот фрагмент выполняется из среды Delphi.



Уничтожение таблицы


procedure DeleteTable; физически удаляет ТБД. Метод применим только к закрытым НД.



Создание новой таблицы


procedure CreateTable; создает новую пустую таблицу. Перед созданием таблицы для данного компонента TTable нужно указать:

• имя БД - в свойстве DatabaseName;

• имя таблицы - в свойстве TableName;

• тип таблицы - в свойстве ТаblеТуре. Возможно указание следующих типов:

ttASCII

: многоколончатый текстовый файл, используемый для чтения как ТБД;

ttDBase

: таблица dBASE;

ttParadox

: таблица Paradox;

описания полей - в свойстве FieldDefs;

описания индексов - в свойстве IndexDefs.

Для добавления описаний полей в свойство FieldDefs оно сначала очищается, а затем для каждого поля информация в FieldDefs заносится методом

procedure Add(const Name: string; DataType: TFieldType; Size: Word; Required: Boolean);

где: параметр Name определяет имя поля; параметр DataType определяет тип поля:

TFieldType = (ftUnknown, ftString, ftSmallint, ftlnteger, ftWord, ftBoolean, ftFloat, ftCurrency, ftBCD, ftDate, ftTime, ftDateTime, ftBytes, ftVarBytes, ftAutoInc, ftBlob, ftMemo, ftGraphic, ftFmtMemo, ftParadoxOle, ftDBaseOle, ftTypedBinary) ;

параметр Size определяет размер поля (если его указание необходимо) или 0 (если тип поля подразумевает его размер, например ftlnteger);

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

Для добавления описаний индексов в свойство IndexDefs оно сначала очищается, а затем для каждого поля информация в IndexDefs заносится методом procedure Add(const Name, Fields: string; Options: TIndexOptions); где параметр Name определяет название индекса; параметр Fields определяет список индексных полей. В случае нескольких полей их имена разделяются точкой с запятой. Могут быть указаны только те поля, описания которых перед этим добавлены в свойство FieldDefs; параметр Options определяет свойства индекса (см. описание метода Addlndex).

Пример.

Создать новую ТБД с именем 'NewT' в БД 'book1', состоящую из двух полей - символьного поля FIO и целочисленного Oklad, с одним первичным индексом по полю FIO:

WITH Table1 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

Заметим, что ТБД может быть создана также и при помощи SQL-оператора CREA ТЕ TABLE. Для "персональных" СУБД типа Paradox и dBase это альтернативный методу CreateTable способ; для "промышленных" СУБД это единственный способ динамического создания ТБД из работающего приложения, реализованного с помощью Delphi.



Обзор методов


Для поиска записей в НД в компоненте TTable применяются следующие методы:

function FindKey([список значений]): Boolean -

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

procedure FindNearest([список значений]) -

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

Помимо этого, у TTable имеются методы, унаследованные от родительского класса TDBDataSet:

function Locate([список полей], [список значений]) : Boolean -

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

function Lookup([список поисковых полей], [список значений], [список результирующих полей]) : Variant; -

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

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



Установка значений для поиска


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

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



Точный поиск


Для точного поиска (поиска на точное соответствие) применяется метод function FindKey( [список значений]): Boolean;

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

Пример. Пусть имеется НД с полями GrNum (номер группы), NN (номенклатурный номер товара), Tovar (наименование товара):

Предположим, что поисковое значение GrNum вводится в Edit1, a NN - в Edit2 (рис.8.4). Тогда обработчик нажатия клавиши поиска FindButton может выглядеть так (для простоты в обработчике не контролируется правильность ввода в компонентах Edit1, Edit2):

procedure TForm1.FindButtonClick(Sender: T0b]ect);

var GrTmp, NNTmp : Longint;

begin

GrTmp := StrToInt(Edit1.Text);

NNTmp := StrToInt(Edit2.Text);

// поиск записи

IF not Table1.FindKey([GrTmp,NNTmp]) then ShowMessage ('Нет товара с такой группой и номером!');

end;

Пусть в Edit1 введено "12" и в Edit2 - "4". Тогда указатель переместится на запись, у которой значение GrNum равно 12 и значение поля NN равно 4 (рис. 8.5):

Пусть в Editi введено "12" и в Edit2 введено "О". Тогда указатель не переместится с текущей записи и будет выдано сообщение 'Нет товара с такой группой и номером!'.

Для неточного поиска в Delphi имеется группа методов SetKey, EditKey, GotoKey, которые должны выполняться вместе и которые по функциональности аналогичны методу FindKey. Использование их является менее удобным. Сначала нужно перевести НД в состояние dsSetKey (методом SetKey или, если он уже применялся для данного индекса, EditKey), затем присвоить поисковые значения полям и выполнить метод GoToKey. После этого НД переходит в состояние dsBrowse. Результат выполнения аналогичен результату, возвращаемому методом FindKey. Например, код

IF not Table1.FindKey([GrTmp,NNTmp]) then ShowMessage('Нет товара с такой группой и номером!');

эквивалентен более громоздкому коду с применением GotoKey.

Table1.SetKey;

Table1GrNum.Value := GrTmp;

Table1NN.Value := NNTmp;

IF not Table1.GoToKey then

ShowMessage('Нет товара с такой группой и номером! ');



Неточный поиск


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

procedure FindNearest( [список параметров]);

При поиске на неточное соответствие предпринимается попытка отыскать в НД запись, у которой индексные поля соответствуют значениям, указанным в списке значений. Если такая запись найдена, указатель текущей записи в НД перемещается на нее или на следующую за ней запись, в зависимости от значения свойства property KeyExclusive: Boolean. Если KeyExclusive = False (пo умолчанию), указатель текущей записи перемещается на нее. Если KeyExclusive = True, указатель текущей записи перемещается на следующую запись.

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

В приводимых ниже примерах подразумевается KeyExclusive = False.

Предположим, что поисковое значение GrNum вводится в Editi, а NN - в Edit2. Тогда обработчик нажатия клавиши поиска FindButton может выглядеть так:

procedure TForm1.FindButtonClick(Sender: TObject) ;

var GrTmp, NNTmp : Longint;

begin

GrTmp := StrToInt(Edit1.Text);

NNTmp := StrToInt(Edit2.Text);

//Поиск записи

Table1.FindNearest([GrTmp,NNTmp]) ;

end;

Пусть в Edit1 введено "12" и в Edit2 введено "4". Тогда указатель переместится на запись, у которой поле GrNum содержит 12 и поле NN содержит 4 (рис. 8.6):

Пусть в Edit 1 введено " 12" и в Edit2 введено "О". Тогда указатель переместится на запись с большим значением индекса. Наращение индекса ведется по внутреннему полю (если оно есть) и только затем - по полю более высокого приоритета. В нашем случае внутреннее поле в индексе - NN. В данном случае в НД имеется запись с тем же номером группы и большим номенклатурным номером (рис. 8.7):

Однако если в Editi введено "50" и в Edit2 введено "О" (или что-либо другое, для данного состояния НД это неважно), записи с той же группой (50) и большим значением номенклатурного номера нет. Поэтому указатель записи перемещается на запись с большим номером группы (рис. 8.8).

Заметим, что теоретически возможно в условиях поиска опускать значение внешнего поля индекса (в нашем случае поля GrNum). Например, можно задать в Edit1 "0" и в Edit2 - "1". Однако, вопреки ожиданиям, курсор НД не встанет на первую запись с номенклатурным номером 1 для первой попавшейся группы, а встанет на первую запись с группой, превышающей 0, т.е. на логически первую запись в НД при сортировке по полям GrNum, NN (рис. 8.9).

Существует более громоздкая альтернатива методу FindNearest -выполнение группы методов Set Key, EditKey, GoToNearest и заполнение полей поисковыми значениями (подробнее см. в конце описания метода FindKey). Например, выполнение метода

Table1.FindNearest([GrTmp,NNTmp]) ; может быть заменено эквивалентным по последствиям кодом

Table1.SetKey;

Table1GrNum.Value := GrTmp;

Table1NN.Value := NNTmp;

Table1.GotoNearest;

ЗАМЕЧАНИЕ.

Если нужно осуществить поиск по индексу, отличному от текущего, необходимо:

1. сохранить список текущих индексных полей в строковой переменной;

2. заменить список текущих индексных полей НД на необходимый;

3. осуществить поиск;

4. восстановить список текущих индексных полей НД из строковой переменной.

Пример.

Пусть текущая сортировка в НД осуществляется по имени товара (т.е. в текущий момент Tablel. IndexFieldNames = 'Tovar') и текущей является вторая логическая запись (рис. 8.10).

Тогда после выполнения обработчика нажатия кнопки FindButton, если в Editi введено "100" и в Edit2 введено "О":

procedure TForm1.FindButtonClick(Sender: TObject) ;

var GrTmp, NNTmp : Longint;

OldIndexFieldNames : Strings;

begin

OldIndexFieldNames := Tablel.IndexFieldNames;

Tablel .IndexFieldNames := 'GrNunuNN';

{...}

Table1.FindNearest([GrTmp,NNTmp]) ;

Table1.IndexFieldNames := OldIndexFieldNames;

end;

указатель записи встанет на искомую запись (рис. 8.11).

Перед выполнением последней строки обработчика

Table1.IndexFieldNames := OldIndexFieldNames;

НД будет иметь вид, показанный на рис. 8.12.

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



Инкрементальный локатор


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

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

Пусть необходимо реализовать инкрементальный локатор для уточняющего поиска записи по названию товара. Пусть описанный выше НД отсортирован по индексному полю 'Tovar'. Пусть текущая запись в нем - логически первая. Тогда он имеет вид, показанный на рис. 8.13.

Ввод значения для поиска осуществляется в компонент Edit3. Напишем обработчик для события OnChange, возникающего при любом изменении значения в Edit3:

procedure TForm1.Edit3Change(Sender: T0b;ect) ;

begin

Table1.FindNearest([Edit3.Text]) ;

end;

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

Введем в Edit3 символ "К" (Edit3.Text = 'К');

Тогда Tablel.FindNearest([Edit3.Text]) ;

есть на самом

деле Table1.FindNearest(['К']) ;

в результате курсор переместится на 1-ю запись, имеющую в поле Tovar значение, большее строки 'К' (рис. 8.14).

Введем в Edit3 следующий символ, "о" (Edit3.Text = 'Ко').

В результате курсор переместится на 1-ю запись, имеющую в поле Tovar значение, большее строки 'Ко' (рис. 8.15).

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

производится поиск по этому номеру. Для этой цели используется такой обработчик изменения значения в компоненте Editi:

procedure TForm1.Edit1Change(Sender: TObject) ;

var GrTmp : Longint;

begin

GrTmp := StrToInt(Edit1.Text);

//поиск записи

Table1.FindNearest([GrTmp]) ;

end;

Будем вводить в Edit1 значение "100". Тогда после ввода "1" текущей останется запись с номером группы, равным 1 (рис. 8.17).

после ввода "100" текущей станет запись с номером группы, равным 100, т.е. удовлетворяющая условию поиска (рис. 8.19):



Поиск по части текущего индекса


Если это необходимо, можно осуществлять поиск по частичному множеству индексных полей. Тогда поиск будет производиться не по всем полям данного индекса, а по их части. Для этого необходимо с помощью свойства property KeyFieldCount: Integer; указать, сколько начальных полей индекса будут использоваться при поиске Установка значения свойства KeyFieldCount актуальна только в случае использования методов GoToKey и GoToNearest. Как осуществить поиск по частичному множеству индексных полей для методов Find и FmdNearest, см ниже.

Пример.

Пусть при текущем индексе по полям 'Doljnost; FIO' необходимо осуществить поиск по должности, и по 'должности; ФИО'. Условия поиска будем вводить в Editi (должность) и Edit2 (ФИО) Результаты работы приводимого ниже кода показаны на рис 8.20 а и 8 20 б.

procedure TForm1.OneFieldFindButtonClick(Sender: T0bject);

begin

// поиск по 1 полю индекса

WITH Table1 do begin

SetKey;

KeyFieldCount := 1;

Table1Doljnost.Value := Editl.Text;

GoToNearest;

END; // with

end;

procedure TForm1. FullKeyFmfButtonClick (Sender : TObject);

begin

// поиск по 2 полям индекса

WITH Table1 do begin

SetKey;

KeyFieldCount := 2; II на всякий случай, восстановим

Table1Doljnost.Value := Edit1.Text;

Table1FIO.Value := Edit2.Text;

GoToNearest;

END; // with

end;

При этом есть ограничения- подмножество полей индекса должно быть в полном индексе непрерывным, т.е при индексе из 4 полей можно осуществить поиск по 1-му, 2-му, 3-му полям одновременно и нельзя по 1-му, 4-му, 6-му полям.

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

// поиск по одному полю из двух

WITH Tablel do

FindNearest ( [Editl.Text] ) ;

// поиск по двум полям из двух:

WITH Tablel do

FindNearest( [Editl.Text, Edit2.Text] ) ;



Обзор методов


Помимо описываемых ниже методов, присущих только TTable, наборы данных имеют также общие свойства, методы и события для фильтрации записей - Filter, Filtered, OnFilter-Record, FindFirst, FindLast, FindNext, FmdPrior Они описаны в разделе "Общие принципы работы с наборами данных"

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

procedure SetRangeStart; -

устанавливает нижнюю границу фильтра;

procedure EditRangeEnd;-

устанавливает верхнюю границу фильтра;

procedure ApplyRange;

- осуществляет фильтрацию записей в TTable; условия фильтрации определяются методами SetRangeStart и SetRangeEnd,

procedure SetRange(const StartValues, EndValues: array of const);-

имеет тот же эффект, что и последовательное выполнение методов SetRangeStart, SetRangeEnd и ApplyRange. В качестве параметра используются массивы констант, каждый из которых содержит значения ключевых полей.

Заметим, что фильтрация методами ApplyRange/SetRange должна проводиться по ключевым полям. По умолчанию берется текущий индекс, определяемый свойством TTable.IndexNamewiH TTable.IndexFieldNames. В случае, если значения этих свойств не установлены, по умолчанию используется главный индекс ТБД. Поэтому, если нужно использовать индекс, отличный от главного, необходимо явно переустановить значение свойства TTable.IndexName (имя текущего индекса) или TTable.IndexFieldNames (список полей текущего индекса).



Использование SetRange


Метод procedure SetRange(const StartValues, EndValues: array ofconst); показывает в НД только те записи, индексные поля которых лежат в диапазоне [StartValues.. EndValues].

Пример.

Пусть В НД Tablel показываются все записи из ТБД "TOV.DB" (Товары). Включим в структуру записи НД Tablel 2 поля : GrNum (Номер группы, Smallint) и Tovar (Наименование товара, String).

Пусть текущий индекс построен по полю 'GrNum'.

Тогда, для фильтрации записей в НД таким образом, чтобы показывались записи только с определенным номером группы, располагаем в форме компоненты Edit1 (для ввода номера группы) и CheckBox1. Если CheckBox1 отмечен (CheckBoxl.Checked = True), то производится фильтрация по номеру группы, введенному в Edit1; если CheckBoxl не отмечен или с него снята отметка (CheckBox1 -Checked = False), в НД показываются все записи из ТБД "TOV.DB" (рис.8.21).

Напишем обработчик события CheckBox1.OnClick, возникающего при отметке CheckBoxl. Заметим, что для простоты правильность ввода в Editi не проверяется.

procedure TForm1.CheckBoxIClick(Sender: TObject);

var GrNumTmp : Integer;

begin

IF CheckBoxl.Checked THEN

begin

GrNumTmp := StrToInt(Edit1.Text);

{————фильтрация записей в НД————}

WITH Tablel do begin

CancelRange;

SetRange([GrNumTmp],[GrNumTmp]) ;

END; {with}

end {then}

ELSE

{—————отмена фильтрации ——————}

Tablel.CancelRange;

end;

Неотфильтрованный НД показан на рис.8.21.

В отфильтрованном НД показываются только те записи, индексное поле текущего индекса у которых (т.е. в нашем случае поле GrNum) имеет значение, лежащее в заданном диапазоне. В данном случае диапазон определяется переменной GrNumTmp. Поэтому для GrNumTmp =3 будут показаны записи, принадлежащие к одной группе 3 (рис.8.22).

Если бы мы хотели, чтобы в НД фильтровались записи из нескольких групп, то нам следовало бы добавить в форму второй компонент Edit2, в котором вводился бы номер конечной группы, в то время как в Edit1 вводился бы номер начальной группы. Далее необходимо модифицировать обработчик события CheckBoxl.OnClick, изменив SetRange на SetRange([GrNumTmpl],[GrNumTmp2]);

procedure TForm1.CheckBoxIClick(Sender: T0b;ect) ;

var GrNumTrnpl,GrNumTmp2 : Integer;

begin

IF CheckBoxl.Checked THEN

begin

GrNumTmp1 := StrToInt(Edit1.Text);

GrNumTmp2 := StrToInt(Edit2.Text) ;

{————фильтрация записей в НД-———}

WITH Table1 do begin

CancelRange;

SetRange([GrNumTrnpl],[GrNumTmp2]) ;

END; {with}

end {then}

ELSE

{

———отмена фильтрации ——————}

Table1.CancelRange;

end;

Результаты фильтрации записей с номерами группы от 2 до 4 показаны на рис.8.23.



Методы SetRangeStart, SetRangeEnd, ApplyRange


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

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

procedure TForm1.CheckBoxIClick(Sender: TObject);

var GrNumTrnp1, GrNumTmp2 : Integer;

begin

IF CheckBox1.Checked THEN

begin

WITH Tablel do begin

CancelRange;

SetRangeStart ;

Table1. FieldByName ('GrNum') . Aslnteger: = GrNumTnpl ;

SetRangeEnd;

Tablel. FieldByName ('GrNum') .Aslnteger := GrMumTmp2 ;

ApplyRange;

END; {with}

end {then}

ELSE

Tablel.CancelRange;

end;



Метод CancelRange


Метод procedure CancelRange; служит для отмены предыдущих условий фильтрации. Если предыдущую фильтрацию не отменить, возможно, следующие фильтрации принесут не такой результат, которого Вы ожидаете.

Изменим пример, приводившийся выше. Сначала удалим из него вызов метода CancelRange. Пусть изначально НД сортируется по наименованию товара (поле 'Tovar'). Разместим в форме группу зависимых переключателей RadioGroup1, позволяющую переключать текущие индексы Tablel:

procedure TFormI.RadioGroup1Click(Sender: TObject);

begin

WITH RadioGroupl do begin

CASE Itemlndex OF

0 : Table1.IndexFieldNames := 'Tovar'; //текущий индекс по полю 'Tovar'

1 : Tablel.IndexFieldNames := 'GrNum'; //текущий индекс по полю 'GrNum'

END;//case

END;//with

end;

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

procedure TForm1.Button1Click(Sender: TObject);

var GrNumTrnp1,GrNumTmp2 : Integer;

begin

{проверка правильности Edit1.Text и Edit2.Text фильтрация по начальному и конечному номеру группы}

WITH Tablel do begin

//отмечаем строку текущего выбора в RadioGroupl:

RadioGroupl.Itemlndex := 1;

{смена текущего индекса}

IndexFieldNames := 'GrNum';

SetRange ( [GrNumTrnp1] , [GrNumTmp2] ) ;

END; {with}

end;

Как видно, для фильтрации по полю GrNum необходимо сменить текущий индекс таким образом, чтобы GrNum было индексным полем. Начинаем работу с неотфильтрованным НД (рис.8.24):

Фильтруем НД по номеру группы, например, только по 3 группе (рис.8.25).

Через некоторое время возвращаемся к сортировке по товару. Для этого отмечаем соответствующий переключатель в группе RadioGroup1. При этом видим, что показываются записи всех групп (рис.8.26):

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

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

Однако, если попробовать сделать фильтрацию по группе с номером 2, фильтрация будет осуществлена (рис.8.28).

Увиденное можно объяснить следующим образом. Первоначально имеет место фильтрация по индексу GrNum в диапазоне номеров групп [3..3]. После этого мы делаем текущим индекс, построенный по полю Tovar. Фильтрация по группам теперь невозможна, поскольку поле GrNum не входит в новый текущий индекс. Когда мы вновь делаем текущим индекс, построенный по полю GrNum, при этом не меняя диапазона групп [3..3], фильтрация не выполняется, поскольку она не отменена и диапазоны фильтрации не изменились. Когда же мы задаем новые условия фильтрации в диапазоне групп [2..2], НД фильтруется, т.к. изменился диапазон.

Обойти эту особенность можно, отменяя перед новой фильтрацией (по какому бы то ни было индексу) результаты предыдущей фильтрации методом НД CancelRange:

procedure TForm1.ButtonlClick(Sender: T0bject) ;

var GrNumTrnp1,GrNumTmp2 : Integer;

begin

{

проверка правильности Edit1.Text и Edit2.Text}

{фильтрация по начальному и конечному номеру группы}

WITH Tablel do begin

CancelRange;

//отмечаем строку выбора текущего выбора в RadioGroupl:

RadioGroup1.Itemlndex := 1;

{смена текущего индекса}

IndexFieldNames := 'GrNum';

SetRange([GrNumTrnpl],[GrNumTmp2]) ;

END; {with}

end;



Методы EditRangeStart, EditRangeEnd


procedure EditRangeStart;

procedure EditRangeEnd;

Эти методы предназначены для смены условий фильтрации, установленных ранее с использованием методов соответственно SetRangeStart и SetRangeEnd. Напомним, что сама фильтрация в этом случае выполняется методом Apply Range. Преимущества их использования ясны не всегда. Например, можно было бы предположить, что для рассмотренной в п.8.4.4 ситуации эти методы способны заменить использование CancelRange :

procedure TFormI.ButtonlClick(Sender: TObject);

var GrNumTrnpl, GrNumTmp2 : Integer;

const Num : Integer = 0;

begin

{проверка правильности Edit.Text}

INC(Num) ;

WITH Table1 do begin

IF Num = 1 THEN

begin

SetRangeStart;

FieldByName('GrNum').As Integer:= GrNumTrnp1;

SetRangeEnd;

FieldByName('GrNum').Aslnteger := GrNumTmp2;

ApplyRange;

end

ELSE

begin

EditRangeStart;

FieldByName('GrNum').Aslnteger:= GrNumTrnp1;

EditRangeEnd;

FieldByName('GrNum').Aslnteger := GrNumTmp2;

ApplyRange;

end;

END; {with}

end;

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

procedure TForm1.ButtonlClick(Sender: TObject);

var GrNumTrnp1,GrNumTmp2 : Integers;

begin

{проверка правильности Edit1.Text и Edit2.Text}

{фильтрация по начальному и конечному номеру группы}

WITH Tablel do begin

SetRangeStart;

FieldByName('GrNum') .Aslnteger:= GrNumTrnp1;

SetRangeEnd;

FieldByName('GrNum').Aslnteger := GrNumTmp2;

ApplyRange;

END; {with}

end;



Свойство KeyExclusive


Свойство property KeyExclusive: Boolean; применяется для фильтрации записей в TTable с использованием методов SetRangeStart, SetRangeEnd u EditRangeStart, EditRangeEnd.

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

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

WITH Tablel do begin

CancelRange;

SetRangeStart;

KeyExclusive := True;

FieldByName('GrNum').Aslnteger:= GrNumTrnp1;

SetRangeEnd;

FieldByName('GrNum').Aslnteger := GrNumTmp2;

ApplyRange;

END; {with}



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


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

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

• при использовании SetRangeStart, SetRangeEnd и т.д. значение каждого поля должно устанавливаться явно.

Пример.

Пусть рассмотренный выше НД отсортирован по индексу, состоящему из полей 'GrNum;Tovar'. Реализуем фильтрацию записей по заданному значению поля GrNum и любому значению поля Tovar (рис.8.29).

Для простоты проверку правильности ввода номера группы не производим. Обработчик выбора CheckBoxl ("Фильтровать") выглядит так:

procedure TFormI.CheckBoxIClick(Sender: TObject);

var GrNumTmp : Integer;

TovarTmp : String;

begin

IF CheckBoxl.Checked THEN

begin

GrNumTmp := StrToInt(Editi.Text);

TovarTmp := Edit2.Text;

{

——————фильтрация записей в НД————}

WITH Tablel do begin

CancelRange;

SetRange([GrNumTmp,TovarTmp],[GrNumTmp,'яя']) ;

END; {with}

end {then}

ELSE

{

————————отмена фильтрации ———}

Tablel.CancelRange;

end;

Реализацию выборки обеспечивает метод

SetRange([GrNumTmp,TovarTmp],[GrNumTmp,'яя']);

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

Пусть введена группа и не введено наименование товара. В этом случае в отфильтрованный НД попадут все товары данной группы, т.е. записи , у которых определено наименование товара (рис.8.30):

Пусть введен номер группы и наименовании товара - 'Макароны'. В этом случае в отфильтрованный НД попадут товары данной группы, у которых наименование больше или равно "Макароны" (рис.8.31):



Фильтрация по частичному соответствию


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

Пример.

Пусть рассматривавшаяся выше ТБД отсортирована п( наименованию товара 'Tovar' (рис.8.32):

Обработчик отметки CheckBox1 ("Фильтровать"):

procedure TForm1.CheckBoxIClick(Sender: TObject);

begin

IF CheckBoxl.Checked THEN

begin

{————фильтрация записей в НД————}

WITH Tablel do begin

CancelRange;

SetRange([Editl.Text],['яя']);

END; {with}

end {then}

ELSE

{———отмена фильтрации ——————}

Tablel.CancelRange;

end;

В этом случае в НД будут показаны все записи с названием товара, равны или большим указанного в Edit1 (рис.8.33):

Сменим вызов метода SetRange на следующий:

SetRange([Edit1.Text],[Edit1.Text + 'яя']);

Тогда в результате фильтрации в отфильтрованный НД попадут только записи, начинающиеся с введенного в Editi фрагмента названия товара, т.е. с буквы М (рис.8.34):



Фильтрация по части составного индекса


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

В частности, для предыдущего примера можно указать в качестве текущего индекс 'Tovar;GrNum'. Тогда применение метода

SetRange([Edit1.Text],[Edit1.Text + 'яя']);

означает, что, поскольку в квадратных скобках в качестве начального и конечного условия фильтрации указаны не два значения поля, а одно, фильтрацию) следует проводить на предмет соответствия ведущего поля индекса (в нашем случае 'Tovar') заданному поисковому значению (в нашем случае начальное значение - Edit1.Text; конечное значение - Edit1.Text + 'яя').



Ограничения возможностей фильтрации при использовании методов SetRange/SetRangeStart и др.


Заметим, что рассмотренный механизм фильтрации позволяет отфильтровывать только те записи, у которых значения ключевых полей больше или равны нижней границе и меньше или равны верхней границе фильтрации. Иными словами, затруднительно задать сложное условие типа "все записи, у которых поле А < 100 и7и поле Z содержит вхождение строкм “поиск” ".

При возникновении подобных проблем вместо компонента TTable стоит использовать компонент TQuery и запросы на SQL.



Совмещение курсоров двух НД


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

Для этой цели применяется метод procedure Table1 .GoToCurrent(Table2:TTable); устанавливающий курсор НД Table 1 на ту же запись, на которой находится курсор НД Table2.

ЗАМЕЧАНИЕ.

Метод GoToCurrent переводит НД Table1 в режим dsBrowse с запоминанием изменений (метод Post). В случае невозможности перевода в режим dsBrowse возбуждается исключение и курсор НД Table2 остается неизменным (т.е. совмещение курсоров не происходит).

Создание отдельной формы


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

1. Заводим новую форму (дочернюю), в ней - TTable, TDataSource и группу компонентов TDBEdit (TDBComboBox, TDBLookup и т.д.), связанных между собой стандартным образом. Естественно, что компоненты TTable в родительской и дочерней формах должны указывать на одну таблицу базы данных. Желательно, чтобы и индексные файлы в них были одинаковы (свойство Table IndexName) Внешний вид этих форм показан соответственно на рис 8.35.а и 8.35 б.

И в родительской и в дочерней форме компонент TTable, связанный с ТБД "Сотрудники кафедры", имеет имя Table1. Поэтому в тех случаях, когда из дочерней формы нужно обратиться к Table1 в родительской форме, следует приписывать ей в качестве префикса название родительской формы, Prnt. Table7. В то же время, при обращении к Table1, принадлежащему дочерней форме, из самой дочерней формы можно указывать префикс {Chld.Table7), а можно и не указывать (Table1), например: Table1.GoToCurrent(PrntForm.Table1);

Здесь Table1.GoCurrent означает "выполнить метод GoToCnrrent, принадлежащий компоненту Table1 дочерней формы". Чтобы исключить разночтения, будем далее в программном коде явно указывать название формы при обращении к Table1

ChldForm.Table1.GoToCurrent(PrntForm.Table1);

2. В модуле unit, содержащем родительскую форму, разместим тестовую переменную SotrState, проинициализировав ее пустым значением:

const

SotrState : String = '';

Эта переменная пригодится нам в дальнейшем для передачи в дочернюю форму кода требуемой операции 'Insert' или 'Edit'. Можно, конечно, было переводить родительскую TTable в режимы dslnsert или dsEdit в родительской форме, однако применение такого подхода работает при совмещении указателей на компонент TDataSource (см. ниже раздел "Переназначение DataSource во время выполнения"), но не подходит при использовании в дочерней форме метода GoToCurrent: этот метод автоматически переводит родительский НД в режим dsBrowse

3. В родительской форме добавляем 2 кнопки для вызова дочерней формы одну (InsertButton) на добавление записи, другую (EditButton) - на корректировку При этом присваиваем переменной SotrState соответственно значение 'Insert' или 'Edit'.

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

4 В процедуре дочерней формы, вызываемой при активизации формы, для случая добавлении записи переводим Table дочерней формы в состояние dsInsert, для случая корректировки - совмещаем курсоры Table родительской и дочерней форм.

ДочерняяТаЬ1е.GoToCurrent(PoдитeльcкaяTable) и затем переводим дочернюю форму в состояние dsEdit.

5 В дочерней форме добавляем клавиши "Запомнить" (PostButton) и "Отменить" (CancelButton) Кнопке "Отменить" приписываем модальный результат mrCancel (установив свойство ModalResult = mrCancel). Это важно, поскольку дочерняя форма будет вызываться из родительской как модальная Кнопке "Запомнить" свойство mrOk не назначаем (т е. ее свойство ModalResult = mrNone). В случае, если выполнение метода Post прошло успешно (и исключение не было возбуждено), явно устанавливаем результат работы родительской формы в mrOk: Chid.ModalResult := mrOk;

Это нужно для того, чтобы в случае неуспешности выполнения метода Post мы могли вернуть фокус управления на поля дочерней формы для того, чтобы пользователь попробовал скорректировать значения и повторить попытку выполнения метода Post для запоминания изменений, внесенных в запись НД в дочерней форме. Если бы кнопка "Отменить" автоматически выставляла модальный результат mrOk при своем нажатии, после завершения обработчика нажатия этой кнопки происходил бы выход из дочерней формы, несмотря на то, что содержащийся в обработчике текст в случае неудачи Post предписывает не выходить из дочерней формы, а передать фокус управления ее полям.

6. Как сказано выше, в обработчике нажатия клавиши "Запомнить" выполняем метод Post, а в обработчике нажатия клавиши "Отменить" - метод Cancel, по отношению к НД Table1 дочерней формы. Поскольку Table1 как дочерней, так и родительской формы, во-первых, связаны с одной и той же ТБД "Сотрудники кафедры", а во-вторых, курсоры этих НД совмещены (что актуально для режима dsEdit), выполнение Chid.Table1.Post равносильно выполнению Prnt.Table1 .Post. В случае возбуждения исключения EDBEngineError для простоты полагаем, что произошло дублирование ключевого поля (если в ТБД есть уникальные ключевые поля), хотя, впрочем, могут быть и другие причины.

Заметим, что для PrntForm.Table1 нужно вызывать метод Refresh, который обновляет содержимое НД в родительской форме.

Родительская форма:

// нажата кнопка "Вставить"

procedure TPrntForm.InsertButtonClick(Sender: TObject);

begin

SotrState := 'Insert';

ChldForm.ShowModal;

SotrState := '';

end;

// нажата кнопка "Изменить"

procedure TPrntForm.EditButtonClick (Sender: TObject) ;

begin

SotrState := 'Edit';

ChldForm.ShowModal;

SotrState := '';

end;

// нажата кнопка "Удалить"

procedure TPrntForm.DeleteButtonClick(Sender: TObject);

begin

IF MessageDIg('Подтвердите удаление записи',

mtlnformation, [mbYes, mbNo], 0) = mrYes THEN

Prnt.Table1.Delete;

end;

Дочерняя форма: при активизации дочерней формы переводим НД в режимы Insert или Edit. В последнем случае совмещаем курсоры

procedure TChldForm.FormActivate(Sender: TObject);

begin

IF SotrState = 'Edit' THEN

begin

ChldForm.Table1.GoToCurrent(PrntForm.Table1);

ChldForm.Table1.Edit;

end

ELSE

ChldForm.Table1.Insert;

end;

// обработчик нажатия клавиши "Запомнить"

procedure TChldForm.PostButtonClick(Sender: TObject);

begin

TRY

ChldForm.Table1.Post;

PrntForm.Table1.Refresh;

IF SotrState = 'Insert' THEN

PrntForm.Table1.GoToCurrent(ChldForm.Table1);

// при успешном выполнении Post выходим из модальной формы:

ChldForm.ModalResult := mrOk;

EXCEPT

on EDBEngineError do begin

ShowMessage('Дублирование ключевого поля!');

DBEditI.SetFocus; // возвращаемся на 1 поле DBEdit

end; {on}

ELSE

begin

ShowMessage('Ошибка иного типа при Post');

DBEditI.SetFocus;

end;

END; {try}

end;

// нажата кнопка "Отменить" (ModalResult = mrCancel)

Procedure TChldForm.CancelButtonClick (Sender: TObject);

begin ChldForm.Table1.Cancel;

end;

// если при выходе из дочерней формы набор данных - не в состоянии dsBrowse (что может быть при нажатии кнопки 'х') вверху слева, НД принудителъно переводится в режим dsBrowse

procedure TChldForm.FormDeactivate(Sender: TObject);

begin

IF ChldForm.Table1.State <> dsBrowse THEN

ChldForm.Table1.Cancel;

end;

КОММЕНТАРИЙ.

Может оказаться, что на момент выхода из формы методы Posг или Cancel к НД не выполнены (например, когда выход осуществляется также по нажатию иных кнопок, например "Выход"). Поэтому в родительской форме, после выхода из дочерней формы, будет нелишним проверить, находится ли НД в состоянии dsBrowse и если нет, принудительно перевести его в это состояния методом Cancel, отменяющим все сделанные изменения в записи. Естественно, что можно предусмотреть и иную реакцию на попытку выхода при НД, находящемся в режимах dslnsert или dsEdit.

Представление обеих форм в момент внесения изменений в ТБД из дочерней формы показано на рис.8.36.



Переназначение TDataSource во время выполнения


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

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

1. В родительской форме расположить компоненты TDataSource (назовем его DataSourcel), TTable, TDBGrid (или иные визуальные компоненты для

работы с БД) и связать указанные компоненты между собой стандартным способом.

2. Во всех дочерних таблицах расположить компоненты TDataSource (назовем его DataSource2), TDBEdit (или иные визуальные компоненты для работы с БД) и связать их с DataSource2. При этом свойство DataSource2.DataSet не должно указывать ни на какой НД, т.е. должно оставаться пустым в инспекторе объектов во время проектирования, а во время выполнения в обработчике события создания дочерней формы полезно поместить предложение DataSource2.DataSet := nil;

Часто бывает полезным для присваивания имен полей расположить в дочерней форме еще и компонент TTable, связав его стандартным образом с DataSource2 и визуальными компонентами для работы с НД. Однако после этого компонент Table нужно из дочерней формы удалить и очистить свойство DataSource2. DataSet описанным выше образом.

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

DataSource2.DataSet := ИМяРодительскойФормы.ИмяКомпонентаТаЬ1е;

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

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

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

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

DataSource2.DataSet := nil;



Создание отдельной формы


Этот пример аналогичен приведенному в разделе "Создание отдельной формы для изменения записи ПД. Форма добавления 1корректировки записи с использованием механизма совмещения курсоров ".

Отличия в реализации следующие. Во-первых, НД переводится в состояние Insert или Edit в родительской форме. Во-вторых, в дочерней форме доступ к НД осуществляется переадресацией DataSource во время выполнения.

Родительская форма:

// нажата кнопка "Вставить"

procedure TPrntForm.InsertButtonClick(Sender: TObject);

begin

PrntForm.Table1.Insert;

ChldForm.ShowModal;

end;

// нажата кнопка "Изменить"

procedure TPrntForm.EditButtonClick(Sender: TObject);

begin

PrntForm.Table1.Edit;

ChldForm.ShowModal;

end;

// нажата кнопка "Удалить"

procedure TPrntForm.DeleteButtonClick(Sender: TObject);

begin

IF MessageDIg('Подтвердите удаление записи', mtlnformation, [mbYes, mbNo], 0) = mrYes THEN PrntForm.Table1.Delete;

end;

Дочерняя форма:

// обработчик активизации дочерней формы

procedure TChldForm.FormActivate(Sender: TObject);

begin

// при активизации дочерней формы ее DataSource указывает на НД из родительской формы

ChldForm.DataSourcel.DataSet := PrntForm.Table1;

end;

// обработчик нажатия клавиши "Запомнить"

procedure TChldForm.PostButtonClick(Sender: TObject);

begin

TRY

PrntForm.Table1.Post;

// при успешном выполнении Post выходим из модальной формы:

ChldForm.ModalResult := mrOk;

EXCEPT

on EDBEngineError do begin

ShowMessage('Дублирование ключевого поля!');

DBEditI.SetFocus; // возвращаемся на 1 поле DBEdit

end; {on}

ELSE

begin

ShowMessage('Ошибка иного типа при Post');

DBEditI.SetFocus;

end;

END; {try}

end;

// нажата кнопка "Отменить" (ModalResulfc = mrCancel)

procedure TChldForm.CancelButtonClick(Sender: TObject);

begin

PrntForm.Table1.Cancel;

end; ,

// обработчик события деактивизации дочерней формы DataSource дочерней формы указывает в "пустоту";

// принудительный перевод НД в состояние Browse, если на момент выхода из дочерней формы НД находится

// в ином состоянии

procedure TChldForm.FormDeactivate(Sender: TObject);

begin

ChldForm.DataSourcel.DataSet := nil;

IF PrntForm.Table1.State <> dsBrowse THEN

PrntForm.Table1.Cancel;

end;

// обработчик события создания формы

procedure TChldForm.FormCreate(Sender: TObject);

begin

// DataSource дочерней формы изначально указывает в "пустоту"

ChldForm.DataSourcel.DataSet := nil;

end;



Использование контейнера TDataModule


Компонент типа TDataModule представляет собой контейнер, в который вмещаются компоненты TTable, TQuery, DataSource и т.д. Создать экземпляр dataModule можно, выбрав в среде Delphi пункт меню File|NewDataModule. Компонент TDataModule похож на форму (TForm) в том отношении, что служит контейнером для иных компонентов. Однако, в отличие от TForm, в dataModule можно помещать только невизуальные компоненты для работы базами данных. Связь компонентов TDataSource, TTable и TQuery, расположенных в DataModule, производится так же, как если бы они были расположены в одной форме.

TDataModule нужно сохранить под каким-либо именем и добавить имя модуля unit, в котором описан TDataModule, в текст модулей unit всех иных рм приложения, которые будут использовать НД и TDataSource, положенные в этом TDataModule. Это производится в главном меню среды delphi, в элементе меню File|Use Unit.

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

TDataSource из TDataModule. При этом имя является составным: сначала идет имя компонента TDataModule и затем через точку - имя компонента TDataSource, например, DataModulel.DataSource1.

Перепишем предыдущий пример для использования механизма TDataModule. Для этого добавим в приложение компонент DataModule1 и разместим в нем компоненты Table1 (связанный с ТБД "Сотрудники кафедры") и DataSourcel (связанный с DataModulel.Table1).

Тогда в родительской форме установим свойство DBGrid1.DataSource = DataModulel .DataSourcel; в дочерней форме установим свойства DataSource всех компонентов, работающих с отдельными полями ТБД "Сотрудники кафедры" (в данном случае это компоненты TDBEdit) равными DataModulel .DataSourcel. При этом не будем забывать о заполнении свойства DataField этиx компонентов, иначе компоненты TDBEdit не будут связаны с полями. Пример компонента TDataModule с размещенными в нем компонентами TTable и TDataSource показан на рис. 8.37.

Виды родительской и дочерней форм во время разработки приложения показаны соответственно на рис. 8.38 а) и б).

Как видно из рисунков, в формах PrntForm и ChldForm напрочь отсутствуют компоненты TTable и TDataSource, связанные с ТБД "Сотрудники кафедры".

Приведем обработчики событий для обеих форм:

Родительская форма

// нажата кнопка "Вставить"

procedure TPrntForm.InsertButtonClick(Sender: TObject);

begin

DataModulel.Table1.Insert;

ChldForm.ShowModal ;

end;

// нажата кнопка "Изменить"

procedure TPrntForm.EditButtonClick(Sender: TObject);

begin

DataModulel.Table1.Edit;

ChldForm.ShowModal ;

end;

// нажата кнопка "Удалить"

procedure TPrntForm.DeleteButtonClick(Sender: TObject);

begin

IF MessageDIg('Подтвердите удаление записи',

mtInformation, [mbYes, mbNo], 0) = mrYes THEN DataModulel.Table1.Delete;

end;

Дочерняя форма

// обработчик нажатия клавиши "Запомнить"

procedure TChldForm.PostButtonClick(Sender: TObject);

begin

TRY

DataModulel.Table1.Post;

// при успешном выполнении Post выходим из модальной формы:

ChldForm.ModalResult := mrOk;

EXCEPT

on EDBEngineError do begin

ShowMessage('Дублирование ключевого поля!');

DBEdit1.SetFocus; // возвращаемся на 1 поле DBEdit

end; {on}

ELSE

begin

ShowMessage('Ошибка иного типа при Post');

DBEdit1.SetFocus;

end;

END; {try}

end;

// нажата кнопка "Отменить" (ModalResulfc = mrCancel)

procedure TChldForm.CancelButtonClick(Sender: TObject);

begin

DataModulel.Table1.Cancels;

end;



Множественный взгляд на НД


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

Чтобы работа с одной ТБД из разных наборов или форм происходила над одной и той же физической записью ТБД, можно применять следующие механизмы: совмещать курсоры разных НД методом GotoCurrent; переназначать свойство DataSet компонента TDataSource во время выполнения; использовать в разных формах НД и TDataSource, расположенные в одном и том же контейнере TDataModule.



Общие сведения


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

НД, возвращаемый TQuery, может быть составлен из записей нескольких таблиц (над которыми выполнена операция объединения. Join);

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

Результирующий НД компонента TQuery формируется путем выполнения запроса к БД на языке SQL (Structured Query Languague, язык структурированных запросов). Такой запрос использует SQL-оператор SELECT. Текст любого запроса хранится в свойстве SQL компонента TQuery.

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

Статический запрос

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

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

Формируемые запросы -

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

SQL-операторы (SELECT, INSERT, UPDATE, DELETE), применяемые в компоненте TQuery, достаточно подробно описаны в части книги, посвященной использованию SQL-сервера InterBase и архитектуры "клиент-сервер".



Соединение компонента TQuery с базой данных


Для случаев работы с локальными и удаленными БД имеются некоторые различия в способе соединения компонента TQuery с базой данных, для которой и будет выполняться SQL-оператор из свойства SQL этого компонента Свойствос property Database: TDatabase; возвращает указатель на компонент TDatabase, выполняющий соединение данного TQuery с базой данных. Если компонент TDatabase явно в приложении не создан (что характерно при работе с локальными БД), на период сеанса автоматически создается временный компонент TDatabase.

Свойство property DatabaseName: TFileName; позволяет указать, с какой БД будет работать компонент TQuery.

При работе с локальными БД в свойстве DatabaseName указывается:

• псевдоним БД, ранее определенный при помощи утилиты BDE Administrator,

• переопределенный псевдоним из свойства DatabaseName явно определенного компонента TDatabase, если он используется;

• путь на диске к конкретному каталогу (если свойство DatabaseName хранит пустое значение, подразумевается, что указан текущий каталог). Если свойство DatabaseName хранит пустое значение (подразумевается путь • текущий каталог) или явно указан путь на диске к вполне конкретному каталогу, БД будут искаться именно там (для случая работы с локальными СУБД).

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

• если свойство DatabaseName хранит пустое значение (подразумевается путь к текущему каталогу)

SELECT *

FROM "RASHOD.DB"

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

SELECT *

FROM RASHOD

• если свойство DaгabaseName хранит пустое значение (подразумевается путь к конкретному каталогу), этот каталог можно указать в составе имени файла ТБД:

SELECT *

FROM "С:\BOOK\LOC_SKLD\RASHOD.DB"

где "C:\BOOK\LOC_SKLD" указывает конкретный каталог, в котором следует искать файл "RASHOD.DB";

если свойство DaгabaseName хранит псевдоним БД, переустановленный псевдоним БД или путь к конкретному каталогу

SELECT *

FROM RASHOD

При работе с удаленными БД

(архитектура "клиент-сервер") в свойстве DatabaseNawe указывается:

• псевдоним БД, ранее определенный при помощи BDE Administrator;

• переопределенный псевдоним из свойства DatabaseName явно определенного компонента TDatabase, если он используется.



Соединение компонента TQuery и визуальных компонентов для работы с данными


Соединение компонента TQuery с визуальными компонентами происходит через промежуточный компонент TDataSource. Для этого в компоненте TDataSource в свойстве DataSet необходимо указать имя компонента TQuery. В визуальных компонентах (TDBGrid, TDBEdit и т.д.) в свойстве DaгaSource указывается имя компонента TDataSource и, если необходимо, в свойстве DataField выбирается имя интересующего поля.



Выполнение статических запросов


Для формирования статического запроса необходимо:

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

2. В появившемся окне текстового редактора набрать текст SQL-запроса (рис. 9.1);

3. Установить свойство Active компонента TQuery в True, если НД должен быть открыт в момент начала работы приложения или оставить свойство Active в состоянии False, если открытие НД будет производиться в программе в некоторый момент работы приложения.

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

Формирование текста SQL-оператора SELECT может осуществляться не вручную, а при помощи встроенного в Delphi средства Visual Query Builder, действующего по принципу QBE (Query By Example, запрос по образцу). Для его запуска нужно сделать компонент TQuery текущим, нажать правую кнопку мыши и выбрать режим Query Builder. Заметим, что в TQuery на момент запуска Visual Query Builder должно быть установлено значение свойства DaгabaseName.

Visual Query Builder запрашивает имена таблиц, которые будут участвовать в запросе (рис. 9.2). Выбрав нужную таблицу, нажмите кнопку Add и так до тех пор, пока не будут выбраны все нужные таблицы. После этого нажмите кнопку Cancel.

Далее в появившемся окне из верхней его части берется название соответствующего поля и "перетаскивается" в нижнюю часть; для каждого такого поля создается столбец условий . По информации в ячейках таких столбцов и строится текст SQL-запроса. Например, для того, чтобы реализовать внутреннее соединение таблиц RASHOD и ТО VARY по условию TOVARY.TOVAR = RASHOD.TOVAR, следует ввести TOVARY.TOVAR в графе Criteria для столбца, соответствующего таблице RASHOD (рис.9.3) или RASHOD.TOVAR для столбца, соответствующего таблице ТОVARY.

Сформированный запрос может быть просмотрен (кнопка с изображением очков) и выполнен (кнопка с изображением зеленого треугольника). Кнопка с изображением галочки приводит к закрытию Visual Query Builder и выходу в приложение. При этом текст сформированного запроса помещается в свойство SQL данного компонента TQuery (рис. 9.4).

Visual Query Builder обычно используется для создания черновых вариантов оператора SELECT. Впоследствии текст запроса трансформируется разработчиком к нужному виду.

Для тех, кто только начинает осваивать язык SQL, Visual Query Builder может послужить средством обучения синтаксису оператора SELECT.



Методы открытия и закрытия компонента TQuery


Компонент TQuery может возвращать НД (если компонент использует оператор SELECT, то есть осуществляет выборку из одной или более таблиц БД) и выполнять действие над одной или более таблицей БД (SQL-операторы INSERT, UPDATE, DELETE).

В случае использования оператора SELECT после открытия компонента TQuery возвращается НД, в котором указатель текущей записи всегда установлен на первую запись (если она имеется). Такой компонент TQuery следует открывать:

• установкой свойства Active в значение True, или

• выполнением метода

procedure Open;

Например,

RashodQuery.Active := True;

TovaryQuery.Open;

В случае использования операторов INSERT, UPDATE, DELETE набор данных не возвращается. Такой компонент TQuery следует открывать, выполняя метод

procedure ExecSQL;

Например,

InsertQuery.ExecSQL;

Метод ExecSQL посылает серверу для выполнения SQL-оператор из свойства SQL данного компонента TQuery.

Закрытие компонента TQuery осуществляется методом procedure Close; или установкой в False свойства Active, например:

RashodQuery.Active := False;

TovaryQuery.Open;

При этом следует помнить, что для компонента TQuery, не возвращающего набор данных, выполнение метода Close не имеет последствий, поскольку с данным компонентом не связан открытый НД. Для динамических запросов, особенно для отсылаемых к удаленной БД, полезно использовать методы, осуществляющие "связывание" параметров с их фактическими значениями (Prepare) и отменяюще такое "связывание" (UnPrepare) Более подробно о них будет рассказано далее в подразделах, посвященных выполнению динамических запросов.



Изменяемые TQuery


Записи НД, возвращаемые компонентом TQuery, могут изменяться, подобно тому, как это происходит в компоненте TTable. Записи можно редактировать в компоненте TDBGrid, связанном с TQuery (метод автоматического перевода НД в состояния dsInsert, dsEdit, автоматического выполнения методов Insert, Edit, Delete, Post и Cancel). Также может применяться метод программного формирования значения полей записи, ввода таких значений с использованием компонента TDBEdit, TDBCheckBox и других. В этом случае методы Insert, Edit, Delete, Post и Cancel вызываются в программе явно .

Возможность изменения НД, возвращаемого после выполнения оператора SELECT, определяется свойством property CanModify: Boolean; Значение этого свойства устанавливается автоматически, исходя из определенных факторов. Если в процессе выполнения свойство CanModify установлено в True, набор данных доступен для изменения. При значении False записи НД не могут быть добавлены, изменены или удалены.

Свойство CanModify всегда устанавливается в False, если в False установлено свойство RequestLive компонента TQuery: property RequestLive: Boolean; Значение данного свойства может быть установлено как во время проектирования, так и во время разработки приложения. По умолчанию всегда устанавливается False (НД доступен только для чтения). Однако установка данного свойства в значение True (НД может быть изменен) вовсе не означает, что НД действительно будет позволено изменяться и что свойство CanModify будет установлено в True. Это произойдет только в том случае, если синтаксис оператора SELECT при выполнении запроса будет признан "верным".

Синтаксис оператора SELECT будет признан "неверным", если:

• НД формируется более чем из одной ТБД;

• присутствует предложение принудительной сортировки результирующего набора данных ORDER BY;

• значения хотя бы одного столбца результирующего НД сформировано с использованием агрегатных функций (SUM, COUNT, AVG, MIN, MAX);

• при доступе к СУБД Sybase в таблице отсутствует уникальный индекс

В том случае, если свойство RequestLive установлено в True, а синтаксис оператора SELECT признан "неверным":

• возвращается НД, доступный только для чтения - при доступе к таблицам локальных СУБД (Paradox, dBase);

• выдается ошибка - при доступе к серверным СУБД (InterBase, Oracle) и т.д.

Если изменения, внесенные в НД методами Post, Delete, не отображаются в НД, его содержимое можно обновить методом procedure Refresh; Однако в этом случае оператор SELECT должен быть выполнен для таблицы локальной СУБД и эта таблица должна иметь уникальный индекс. Для НД, возвращенных в результате выполнения запроса к удаленной СУБД, выполнение метода Refresh не влечет за собой никаких последствий.

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

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

property Constrained: Boolean;

Например, для НД, полученного по запросу

SELECT *

FROM RASHOD

WHERE KOLVO > 1000

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

В том случае, если НД доступен только для чтения, его записи могут быть изменены при помощи SQL-операторов INSERT, UPDATE, DELETE. При этом изменения в НД не отображаются Для отображения изменений НД следует переоткрыть, выполнив метод Close и повторно Open. Повторное открытие НД устанавливает указатель текущей записи на первую строку, что удобно далеко не всегда. В этом случае можно

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

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

procedure TForm1.LocatorButtonClick(Sender: TObject);

begin

RashodQuery.Locate('N_RASH',Editl.Text, [loPartialKey]);

end;

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

Например,

пусть в набор данных (компонент RashodQuery) добавляются записи при помощи SQL-оператора INSERT (компонент InsertQuery). Однозначно идентифицирует запись в НД RashodQuery поле 'М'. Тогда восстановление указателя текущей записи после повторного открытия НД RashodQuery может быть реализован так:

TmpN_Rash := FieldByName('М').Aslnteger;

InsertQuery.ExecSQL;

RashodQuery.Close;

RashodQuery.Open;

RashodQuery.Locate('М',TmpN_Rash, []);

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



Понятие динамического запроса


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

Например, пусть в процессе выполнения приложения может быть выдан запрос 'выдать все записи из таблицы 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")

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

SELECT *

FROM RASHOD

WHERE (TOVAR = :TOVAR) AND

(DAT_RASH = :DAT_RASH)

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

SELECT *

FROM RASHOD

WHERE (TOVAR = :PARAMETR1) AND

(DAT_RASH = : ZNACHENIE)



Формирование динамического запроса


Для формирования динамического запроса необходимо:

1. выбрать для существующего компонента TQuery в инспекторе объектов свойство SQL и нажать кнопку текстового редактора;

2. в появившемся окне текстового редактора набрать текст SQL-запроса с параметрами;

3. выбрать в инспекторе объектов свойство Params и нажать кнопку в строке данного свойства; в появившемся окне будут показаны имена всех параметров, введенных в текст динамического SQL-оператора на шаге 2; список параметров отслеживается автоматически всякий раз при изменении содержимого свойства SQL;

4. каждому параметру из списка необходимо поставить в соответствие определенный тип и, если нужно, стартовое значение в поле Value. Переключатель Null Value позволяет указать в качестве стартового значения NULL (рис. 9.5). Стартовые значения присваивать необязательно, однако каждому параметру необходимо поставить в соответствие определенный тип данных, иначе попытка открытия компонента TQuery приведет к возбуждению исключения;

5.

компонент TQuery можно сделать активным (установить свойство Active = True) на стадии разработки приложения только в том случае, если каждому из параметров присвоено стартовое значение.

Компонент TQuery, содержащий динамический запрос, если его свойство Active установлено на этапе разработки в значение True, открывается при

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

Если компонент TQuery, содержащий динамический запрос, не открыт в момент создания формы, его можно открыть в некоторый момент времени, программно установив значения параметров и выполнив метод Open или установив свойство Active в True.

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



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


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

Параметры компонента TQuery доступны через его свойство property Params[Index: Word]:TParams;

Это свойство является набором параметров, где каждый параметр определяется индексом в диапазоне (0...ParamCount-l, где ParamCount есть число параметров, которое можно получить с помощью свойства ParamCount компонента TQuery: property ParamCount: Word;

Обратиться к конкретному параметру можно:

1) указав индекс параметра в свойстве Params компонента TQuery, например, Params[0]. Порядок следования параметров аналогичен показываемому в окне редактора параметров (активизирующегося после нажатия кнопки в строке свойства Params инспектора объектов);

2) через метод компонента Tquery function ParamByName(const Value: string): TParam;

где Value определяет имя параметра.

Для установки значения конкретного параметра используется одно из свойств компонента TParam A \NNN (AsString, Aslnteger и т.д.) или более общее свойство property Value: Variant;

Например,

RashodQuery.Params[0].AsDate := StrToDate(Edit1.Text) ;

RashodQuery.ParamByName('DAT_RASH').Value := TmpDat_Rash;

Пример.

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

SELECT *

FROM RASHOD

WHERE (TOVAR = :TOVAR) AND

(DAT_RASH = :DAT_RASH)

будем вводить текущие значения параметров в компоненты типа TEdit с именами Dat_Rash_Edit и Tovar_Edit. Открытие НД (компонент TQuery с именем RashodQuery) будем производить после нажатия кнопки GoButton (рис. 9.6).

Обработчик события OnClick кнопки GoButton:

procedure TForm1.GoButtonClick(Sender: TObject);

var TmpDat_Rash : TDateTime;

begin

TRY

TmpDat_Rash := StrToDate(Dat_Rash_Edit.Text);

EXCEPT

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

Dat_Rash_Edit.SetFocus ;

Exit;

END;//try

WITH RashodQuery do begin

Close;

ParamByName('DAT_RASH').Value := TmpDat_Rash;

ParamByName('TOVAR').Value := Tovar_Edit.Text; Open;

END;//with

end;

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

ЗАМЕЧАНИЕ 1.

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

TRY

RashodQuery.Open;

EXCEPT

END;//try

ЗАМЕЧАНИЕ 2.

Параметры более чувствительны к значениям даты и времени, чем поля (компонент TField). Известно, что переменные типа TDateTime хранят дату и время, и, если при преобразовании AsDateTime время или дата отсутствуют, они устанавливаются в значении поля, равными нулю:

SomeField.AsDateTime := StrToDate(Edit1.Text);

Подобное преобразование для параметра может стать источником ошибки на этапе выполнения. Поэтому для отдельных значений даты параметрам в инспекторе объектов может быть поставлен в соответствие тип Date или Time, а не только тип DateTime. В первом случае преобразовывать дату следует, используя свойство компонента TParam AsDate, а во втором - используя свойство AsTime компонента TParam:

SomeQuery.ParamByName('DateParam').AsDate := ...

SomeQuery.ParamByName('TimeParam').AsTime := ...



Методы Prepare и Unprepare


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

Для того чтобы "подготовить" запрос к многократному использованию, следует хотя бы один раз выполнить метод Prepare компонента TQuery: procedure Prepare; Выполнение этого метода для динамических запросов с неизменным синтаксисом лучше делать в обработчике события формы OnCreate. В дальнейшем можно многократно присваивать параметрам различные значения и выполнять запрос методами Open и ExecSQL.

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

Свойство компонента Tquery property Prepared: Boolean; возвращает True, если НД был "подготовлен" методом Prepare.

Метод procedure UnPrepare; позволяет освободить ресурсы, выделенные для "подготовленного" запроса. Этот метод неявно вызывается всякий раз, когда изменяется текст SQL-оператора запроса, что также ведет к немедленному закрытию НД.



Указание значения NULL для параметров


Значение NULL показывает отсутствие значения в поле или присутствие неопределенного значения. Для числовых полей значение 0 и пустая строка " для строковых полей - это вполне определенные значения, отличные от NULL (значение неопределенное).

Часто при добавлении или корректировки записей (SQL-операторы INSERT, UPDATE) необходимо указать значение NULL для какого-либо параметра. Для этого следует выполнить метод Clear компонента ТРаrат:

procedure Clear;

Например,

пусть для корректировки записи в таблице 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)

Пусть, в частности, значение параметра :new_POKUP (название покупателя) вводится пользователем в компонент TEdit с именем Pokup_Edit. Пусть на значение поля POKUP в таблице RASHOD наложено ограничение, согласно которому значение поля должно либо быть равным NULL, либо совпадать с одним из значений поля POKUP в таблице POKUPATELI.

Если пользователь ввел в поле ввода компонента Pokup_Edit пустую строку ("), присваивание

UpdateQuery.ParamByName('new_POKUP').Value := Pokup_Edit.Text;

и последующее выполнение метода UpdateQuery.ExecSQL приведет к ошибке, поскольку в таблице POKUPATELI нет записи со значением " в поле POKUP. Поэтому в случае, если в Pokup_Edit введена пустая строка, параметру :new_POKUP присваивается значение NULL:

IF Pokup_Edit.Text = '' THEN

ParamByName('new_POKUP').Clear

ELSE

ParamByName('new_POKUP').Value := Pokup_Edit.Text;



Передача параметров через свойство DataSource


Для передачи параметров может служить свойство DataSource компонента TQuery: property DataSource: TDataSource; В случае использования этого свойства явные присваивания значений параметрам динамического запроса не производятся, то есть не кодируются операторы присваивания типа

Query1.ParamByName('ИмяПараметра').Value := Значение;

Как же поступает приложение, когда нужно открыть набор данных (выполнить метод Open) и когда у динамического запроса есть параметры, а значения им не присвоены?

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

Пример. Пусть в свойстве SQL компонента RashodQuery содержится динамический запрос

SELECT *

FROM RASHOD

WHERE TOVAR = :TOVAR

ORDER BY DAT_RASH, KOLVO

Компонент RashodQuery открыт при старте приложения и значение параметру :TOVAR перед этим не назначалось. Свойство RashodQuery.DataSet = DS_TovarQuery. DS_TovarQuery - компонент типа TDataSource; он связан с набором данных TovaryQuery, у которого есть поле Tovar. Поэтому в качестве значения параметра :TOVAR берется текущее значение поля Tovar из НД TovaryQuery, как если бы это было назначено при помощи оператора

RashodQuery.ParamByName('Tovar').Value :=

TovaryQuery.FieldByName('Tovar').Value;

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

Вид окна программы, содержащей оба названных компонента, приводится на рис.9.7.



Формируемые запросы


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

Свойство SQL компонента TQuery имеет тип TStrings: property SQL: TStrings; и потому содержимое свойства SQL может формироваться программно методами Add (добавить элемент). Delete (удалить элемент), Clear (очистить список) и прочими.

Пример.

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

SELECT *

FROM RASHOD

WHERE POKUP = :POKUP

то есть запрос "выдать все записи из таблицы расхода товаров RASHOD, у которых имя покупателя POKUP совпадает со значением, указанным в параметре :POKUP". Пусть значение в параметр :POKUP берется из поля POKUP текущей на данный момент записи в таблице POKUPATELI (компонент PokupQuery):

WITH RashodQuery do begin Close;

ParamByName('POKUP').Value :=PokupQuery.FieldByName('POKUP').Value;

Open;

END;//with

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

WITH RashodQuery do begin

Close; SQL.Add ('SELECT *') ;

SQL.Add('FROM RASHOD')

SQL.Add('WHERE POKUP = ' + PokupQuery.FieldByName('POKUP').AsString) ;

Open;

END;//with

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

Пример.

Пусть для оператора UPDATE, выполняющего корректировку записи в таблице RASHOD, значения полей DAT_RASH, KOLVO, TOVAR, POKUP могут вводиться компонентами TEdit с именами Dat_Rash_Edit, Kolvo_Edit, Tovar_Edit, Pokup_Edit. Требуется сформировать такой SQL-оператор, чтобы в нем указывались в качестве обновляемых только поля, для которых введено значение в соответствующих компонентах. Вид формы показан на рис. 9.8. Для формирования оператора UPDATE используется такой обработчик нажатия экранной кнопки "Изменить":

procedure TForm!.UpdateButtonClick(Sender: TObject) ;

var TmpN_Rash : Integer;

Cnt : Integer; //счетчик непустых TEdit

Separator : String[1]; //запятая или пустая строка

begin

{ проверку правильности соответствия значения в Dat Rash Edit.Text формату даты и значения в Koivo_Edit.Text формату целого числа для простоты не производим }

Cnt := 0;

IF Dat_Rash_Edit.Text 0 " THEN INC(Cnt);

IF Kolvo_Edit.Text 0 " THEN INC(Cnt);

IF Tovar_Edit.Text 0 " THEN INC(Cnt);

IF Pokup_Edit.Text 0 " THEN INC(Cnt);

IF Cnt = 0 THEN begin

ShowMessage('He введено ни одно новое значение');

Dat_Rash_Edit.SetFocus;

Exit;

END;//if

//формирование текста оператора UPDATE

WITH ListBoxl.Items do begin

Clear;

Add ('UPDATE RASHOD') ;

Add('SET') ;

IF Dat_Rash_Edit.Text <> " THEN begin

DEC(Cnt) ;

IF Cnt > 0 THEN

Separator := ','

ELSE Separator := " ;

Add(' DAT_RASH = \" + Dat_Rash_Edit.Text + '"' -Separator);

END;//if

IF Kolvo_Edit.Text 0 "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 + '"');

TmpN_Rash := RashodQuery.FieldByName('N_RASH').As Integer;

Add('WHERE N_RASH = ' + IntToSTr(TmpN_Rash)) ;

END;//with

//выполнение сформированного SQL-оператора

WITH UpdateQuery do begin

SQL.Clear;

SQL := ListBoxl.Items;

TRY

ExecSQL;

EXCEPT

on EDBEngineError do begin

ShowMessage('Ошибка БД. Проверьте уникальность номера ' + 'расхода!');

Exit;

end;//do

ELSE

SHowMessage('Неклассифицированная ошибка при ExecSQL') ;

END;//try

RashodQuery.Close;

RashodQuery.Open;

END;//with



Динамический редактор SQL


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

Пусть SQL-оператор вводится в некоторое поле ввода (лучше типа TMemo). После ввода пользователь нажимает экранную кнопку "Выполнить", и только что введенный им запрос выполняется для одной или более ТБД. Вид окна программы, реализующей динамический редактор SQL-операторов, показан на рис.9.9. Результат выполнения запроса выводится в отдельной форме после нажатия в основной форме кнопки "Выполнить SQL-запрос" (рис. 9.10).

Техника реализации такого режима работы проста: текст запроса вводится в компонент Memo1 и перед выполнением переписывается в свойство SQL компонента TQuery (Form2. Query I):

procedure TForm1.GoQueryButtonClick(Sender: TObject) ;

begin

Form2.Query1.Close;

Form2.Query1.SQL.Clear;

Form2.Query1.SQL.Add(Memol.Text) ;

Form2.Query1.Open;

Form2.Show; end;



Использование TQuery для получения агрегированных значений


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

Пусть, например, необходимо помещать уникальное значение в поле N_RASH (номер записи расхода со склада) в таблицу RASHOD. SQL-операторы INSERT, UPDATE не работают с автоинкрементными полями (локальные СУБД). Поэтому при занесении новой записи в таблицу RASHOD нужно определить уникальное значение поля N_RASH, для чего можно сделать запрос к таблице RASHOD:

WITH WorkQuery do begin

Close;

Clear;

SQL.Add('SELECT COUNT(*), MAX(N_RASH) AS M') ;

SQL.Add('FROM RASHOD') ;

Open;

END;//with

Пусть добавление новой записи реализуется в компоненте 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').AsInteger :=

WorkQuery.FieldByName('M').AsInteger + 1;

ExecSQL;

END; //with

ПОЯСНЕНИЕ.

После выполнения запроса в компоненте WorkQuery выдается результирующий НД, содержащий одну строку. Указатель текущей записи во вновь открытом НД всегда устанавливается на первую запись. Поэтому, даже если таблица RASHOD пуста, в поле М будет значение NULL, преобразуемое затем свойством AsInteger в 0.

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

SELECT MAX(N_RASH) AS M FROM RASHOD

Однако, применяя вместо этого оператор

SELECT COUNT(*), MAX(N_RASH) AS M FROM RASHOD

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



Использование компонента TQuery для локальных и удаленных БД


Использование компонента TQuery происходит аналогично для случая выполнения запросов к таблицам локальных СУБД (Paradox, dBase и т.д.) и для случая выполнения запросов к удаленным СУБД (InterBase, Oracle, Informix, Sybase, MS SQL Server). Имеются, правда, ограничения для случая запросов к таблицам локальных СУБД. Эти ограничения состоят в усеченных возможностях использования синтаксиса SQL-операторов.

Характеристики работы компонента TQuery для случая локальных и удаленных СУБД позволяют рекомендовать:

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

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

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

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

ЗАМЕЧАНИЕ.

В примерах, приводимых к излагаемому материалу, в частности, по созданию приложений в архитектуре "клиент-сервер", встречается доступ к удаленным таблицам при помощи компонента TTable, но в основном из-за того, что реально "удаленная" БД работает под управлением локальной версии SQL-сервера Borland InterBase, поставляемого вместе с Delphi Client/Server Suite, а также из-за того, что объем данных в учебных таблицах не превышает 10 записей.

Исследовать процесс соединения с сервером и реальные процессы доступа к БД, которые порождают те или иные SQL-операторы, можно, запустив приложение на выполнение под средой Delphi и вызвав SQL Monitor (пункт главного меню Database \ SQL Monitor или запустив его как отдельное приложение). В качестве примера приведем окно SQL Monitor, показывающее (рис. 9.11 и 9.12) операции над БД из приложения для выполнения оператора SELECT, реализующего внутреннее соединение таблиц RASHOD (8 записей) и TOVARY (3 записи):

SELECT R.DAT_RASH, R.TOVAR, R.KOLVO, Т.ZENA FROM RASHOD R, TOVARY T WHERE R.TOVAR = T.TOVAR

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

property UniDirectional: Boolean;

Это позволит отключить для НД механизм двунаправленных курсоров в BDE, что для больших объемов записей способно привести к экономии времени и ресурсов.