Тема 11. Программирование работы для локальных баз данных

1.   Состояние набора данных, пересылка записи в базу данных

2.   Методы доступа к полям,  навигации и поиска записей

3.   Методы установки диапазона допустимых значений

4.   Методы создания и модификации таблиц

5.   Клиентские наборы данных

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

 

1.   Состояние набора данных, пересылка записи в базу данных

 

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

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

 

dsInactive

Набор данных закрыт, данные недоступны.

dsBrowse

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

dsEdit

Текущая запись может редактироваться.

dsInsert

Может вставляться новая запись.

dsSetKey

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

 

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

Close закрывает соединение с базой данных, устанавливая свойство Active набора данных в false. При этом State переводится в состояние dsInactive.

Open открывает соединение с базой данных, устанавливая свойство Active набора данных в true. При этом State переводится в состояние dsBrowse.

Edit переводит набор данных в состояние dsEdit.

Insert и InsertRecord вставляют новую пустую запись в набор данных и переводят State в состояние dslnsert.

EditKey, SetRange, SetRangeStart, SetRangeEnd и ApplyRange, связанные с поиском записи и с заданием допустимого диапазона изменения данных, переводят State в состояние dsSetKey.

При программировании работы с базой данных надо следить за тем, чтобы набор данных вовремя был переведен в соответствующее состояние. Например, если редактируете запись, не переведя предварительно набор данных в состояние dsEdit методом Edit, то будет сгенерировано исключение EDatabaseError с сообщением «Набор данных не в режиме Edit или Insert».

Пересылка записи в базу данных. Пока идет редактирование текущей записи, изменения осуществляются в буфере, а не в самой базе данных. Пересылка записи в базу данных производится только при выполнении метода Post. Метод может вызываться только тогда, когда набор данных находится в состоянии dsEdit или dslnsert. Этот метод можно вызывать явно, например, Table1->Post();, или вызываться неявно при любых перемещениях по набору данных, т.е. при перемещении курсора на другую запись, если набор данных находится в состоянии dsEdit или dslnsert. Отменить изменения, внесенные в запись, можно методом Cancel. Здесь, если предварительно не был вызван метод Post, запись возвращается к состоянию, предшествовавшему редактированию, и набор данных переводится в состояние dsBrowse.

Перед началом выполнения каждого из рассмотренных выше методов и после его выполнения генерируются соответствующие события набора данных Table. Например, перед выполнением метода Post возникает событие BeforePost, а после его окончания - событие AfterPost. Аналогичные события Beforelnsert и Afterlnsert сопровождают выполнения метода Insert и т.п. Подобные события и надо использовать для проверки данных и получения подтверждения на их изменение.

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

 

void    _fastcstll TFom1: :Table1BeforePost (TDataSet *DataSet)

{

 if (проверка введенных  данных)

{

if (Application->MessageBox("Хотите  занести текущую запись  в  базу данных?", "Подтвердите занесение  в  базу  данных", MB_YESNOCANCEL + MB_ICONQUESTION)!= IDYES)

{

Table1->Cancel ();

Abort () ;

}

}

else

{

Application->MessageBox("Ошибочные данные", "Ошибка", MB_ICONST0P);

Abort () ;

}

 }

К этому обработчику будет происходить обращение перед выполнением метода Post, как бы он не был вызван: явно или вследствие перемещения по базе данных, если текущая запись была изменена. В обработчике сначала производится проверка данных в записи. Если результат неудовлетворительный, то пользователю дается сообщение об ошибочности данных и выполняется функция Abort, прерывающая выполнение Post. Текущая запись остается в состоянии dsEdit, но ошибочные данные в ней не сбрасываются, что позволяет пользователю исправить их.

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

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

 

1.   Методы доступа к полям,  навигации и поиска записей

 

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

Доступ к полям. Поля отображаются объектами класса TField и производных от него классов TStringField, TSmallintField, TBooleanField и т.п. Эти объекты могут создаваться тремя способами:

·      Автоматически генерироваться для каждого компонента набора данных (Table и др.)

·      Создаваться в процессе проектирования с помощью Редактора Полей

·      Создаваться программно в процессе выполнения приложения.

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

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

Программное создание объектов класса TField используется сравнительно редко и мы его рассматривать не будем.

Доступ к объектам полей возможен тремя способами:

·      По порядковому индексу объекта

·      По имени поля

·      По имени объекта

Доступ по порядковому индексу осуществляется через свойство TField* Fields[int i], где i - индекс объекта. Индексация начинается с 0. Например, Table1->Fields[0] - это первый объект поля таблицы Table1.

Доступ по имени поля осуществляется с помощью метода FieldByName("имя"). Например, Table1->FieldByName("Fam") - это объект, связанный с полем Fam.

Доступ по имени объекта возможен только к объектам, созданным с помощью Редактора Полей. По умолчанию C++Builder формирует имена объектов полей (Name) из имени таблицы и имени поля. Например, Table1Dep. Эти имена отображаются в Редакторе Полей. Обращение к объекту по имени не требует ссылки на таблицу. Можете просто написать Table1Dep объект.

Автоматически создаваемые объекты имени не имеют - их свойство Name пусто. Поэтому для них обращение по имени невозможно.

Среди рассмотренных способов доступа к полям наиболее быстрым является доступ по имени объекта. Его недостатком является жесткая кодировка поля, к которому производится обращение. Если надо, чтобы строка кода в разных ситуациях обращалась к разным полям, то надо использовать или доступ по индексу Fields[i], или по имени поля методом FieldByName(s).

Объекты класса TField и производные от них классы имеют множество свойств. Это свойства Readonly, DisplayLabel, CustomConstraint и многие другие. Сейчас рассмотрим, как добраться до главного свойства объекта - хранящегося в нем значения поля текущей записи.

Значение поля хранится в свойстве Value. Тип этого свойства - Variant, т.е. тип определяется типом поля. Например, Tabiel->FieldByName("Fam")->Value - это строка, a Table1->FieldByName("Year_b")->Value - это целое число.

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

 

EDep->Text  =  Table1->FieldByName ("Dep")->AsString;

EYear->Text  = Table1->FieldByName("Year_b")->AsString;

EPol->Text =  Table1->FieldByName("Pol")->AsString;

 

и в окна редактирования EDep, EYear и EPol будут занесены в текстовом виде значения в текущей записи полей Dep, Year_b и Pol, хотя поле Dep имеет тип строки, поле Year_b - целое значение, а поле Pol - булево. Если для поля Pol не заданы значения Display Values, то в окне редактирования EPol будут отображены значения «true» или «false». Если же заданы значения свойства DisplayValues, например «м;ж» или «мужск;женск», то отобразятся именно эти заданные значения.

То же свойство AsString работает и как обратное преобразование типов. Продолжая пример, можно после редактирования значений в полях EDep, EYear и EPol, внести эти значения в текущую запись, например, следующим кодом:

 

Table1->Edit();

Table1->FieldByName("Dep")->AsString = EDep->Text;

Table1->FieldByName("Year_b")->AsString =  EYear->Text;

Table1->FieldByName("Pol")->AsString  =  EPol->Text;

Table1->Post();

 

Для полей Year_b и Pol текст будет преобразован соответственно в целое и булево значение. При этом не обязательно, чтобы в окне EPol указать полностью обозначение пола сотрудника. Достаточно написать только первую букву: «t» или «f», если отображаемые значения true и false, и «м» или «ж>>, если отображаемые значения «мужск» и «женск».

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

Помимо AsString имеются еще аналогичные свойства AsInteger, AsFloat, AsBoolean, AsDateTime. Свойство AsInteger осуществляет преобразования между типом данного поля и целым 32-битным числом, свойство AsFloat делает то же самое для действительных чисел с плавающей запятой, свойство AsBoolean - для булевых значений, свойство AsDateTime - для значений дат и времени в формате TDateTime.

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

 

First

Перемещение к первой записи

Last

Перемещение к последней записи

Next

Перемещение к следующей записи

Prior

Перемещение к предыдущей записи

MoveTo(int i)

Перемещение к концу (при i > 0) или к началу (при i < 0) на i записей

 

При перемещениях можно совершить ошибку, выйдя за пределы имеющихся записей. Например, если курсор находится на первой записи и выполняется метод Prior, то будет выход за начало таблицы, а если на последней записи и выполняете метод Next, то будет выход после последней записи. Чтобы контролировать начало и конец таблицы, существуют два свойства: Eof (end-of-file) - конец данных, и Bof (beginning of file) - начало данных. Эти свойства становятся равными true, если делается попытка переместить курсор соответственно за пределы последней или первой записи, а также после выполнения методов соответственно Last и First.

Рассмотрим пример. Пусть в приложении имеется выпадающий список с именем CBdep, который нужно заполнить данными, содержащимися в полях Dep всех записей таблицы, соединенной с компонентом Table1. Это можно сделать следующим кодом:

 

CBdep->С1еаг () ;

Table1->First() ;

while (!Table1->Eof)

{

CBdep->Items->Add(Table1Dep->AsString) ;

Table1->Next () ;

}

 CBdep->ItemIndex  =  0;

Table1->First () ;

 

Первый оператор очищает список CBdep. Второй - устанавливает курсор таблицы на первую запись. Далее в цикле по всем записям, пока не достигнута последняя, (проверяется выражением Table1->Eof) для каждой записи в список заносится значение поля Dep, после чего методом Next курсор перемещается к следующей записи. После окончания цикла индекс списка и курсор таблицы переводятся соответственно на первую строку и запись.

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

Методика SetKey. Для ее применения таблица предварительно должна быть индексирована по тому полю, по которому должен будет проводиться поиск. Затем таблица устанавливается в состояние поиска dsSetKey методом SetKey. В состоянии dsSetKey набор данных воспринимает последующий оператор присваивания значения полю не как присваивание, а как задание ключа поиска. Поэтому после установки состояния dsSetKey оператором присваивания устанавливается требуемое значение ключа поиска по интересующему полю. В заключение методом GotoKey курсор переводится на запись, в которой значение указанного поля равно ключу. Если таких записей несколько, то курсор переводится на первую из них. Если соответствующая запись не находится, то метод GotoKey возвращает false. Для полей типа строк лучше использовать метод GotoNearest. Этот метод перемещает курсор на первую запись, значение поля в которой максимально близко к ключу. Метод GotoNearest можно применять и к цифровым полям. В этом случае он переместит курсор на первую запись, значение поля в которой больше или равно заданному значению ключа.

Например, нужно найти первую запись, в которой год рождения (поле Year_b) равен заданному пользователем в окне редактирования Eyear:

 

Table1->IndexFieldNames  =  "Year_b";

Table1->SetKey();

Table1->FieldByName("Year_b")->AsString  =  EYear->Text;

if (!Table1->GotoKey())   ShowMessage("Запись  не  найдена");

 

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

Если нужно найти в таблице сотрудника по его фамилии, заданной пользователем в окне редактирование EFam, можно выполнить код:

 

Table1->IndexFieldNames = "Fam";

Table1->SetKey();

Table1->FieldByName("Fam")->AsString  = EFam->Text;

Table1->GotoNearest();

Даже если точно такой фамилии не найдется, курсор перейдет на наиболее похожую.

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

bool _fastcall FindKey(const System::TVarRec * KeyValues, const int KeyValSize);

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

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

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

OPENARRAY(TVarRec,(список  ключей))

Макрос OPENARRAY может воспринимать список, включающий до 19 элементов, разделенных запятыми.

Рассмотрим примеры. Нужно выполнить тот же поиск по фамилии, что и приведенный выше. В этом случае код может иметь вид:

 

Table1->IndexFieldNames = "Fam";

Table1->FindNearest(&TVarRec(EFam->Text) , 0) ;

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

 

Table1->IndexFieldNames = "Dep;Fam";

Table1->FindNearest(OPENARRAY(TVarRec,(EDep->Text,EFam->Text)));

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

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

Table1->FindNearest(&TVarRec(EFam->Text) ,0) ;

Метод Locate - наиболее универсальный метод, поскольку, он применим к любым наборам данных. Метод объявлен следующим образом:

 

bool  _fastcall Locate(const System::AnsiString KeyFields, const System::Variant SKeyValues, TLocateOptions Options);

В качестве первого параметра KeyFields передается строка, содержащая список ключевых полей. В качестве второго параметра передается KeyValues - массив ключевых значений. А третий параметр Options является множеством опций, элементами которого могут быть loCaseInsensitive - нечувствительность поиска к регистру, в котором введены символы, и loPartialKey - допустимость частичного совпадения. Метод возвращает false, если искомая запись не найдена.

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

 

TLocateOptions SearchOptions;

SearchOptions < loPartialKey  < loCaselnsensitive;

Table1->Locate("Fam", EFam->Text, SearchOptions);

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

Приведенный код можно сократить до двух операторов:

 

TLocateOptions SearchOptions;

Table1->Locate("Fam", EFam->Text, SearchOptions<loPartialKey<loCaseInsensitive);

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

 

TLocateOptions  SearchOptions;

Variant locvalues[] = {EDep->Text, EFam->Text};

Tabiel->Locate("Dep; Fam", VarArrayOf(locvalues,1),

SearchOptions<loPartialKey<loCaseInsensitive);

Метод Lookup. Этот метод определен следующим образом:

 

System::Variant _fastcall Lookup(const KeyFields, const SKeyValues, const ResultFields);

Первые два параметра аналогичны методу Locate. Третий параметр - строка, перечисляющая поля, значения которых возвращаются в виде массива Variant. Если не найдено соответствующей записи, функция возвращает false.

Например, если нужно найти запись, относящуюся к сотруднику, фамилия которого указана в окне EFam, и вывести в окно EDep название отдела, в котором он работает, то эти операции можно осуществить следующим образом:

EDep->Text = Table1->Lookup("Fam",EFam->Text, "Dep");

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

 

3 Методы установки диапазона допустимых значений

 

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

Например, операторы

 

Table1->IndexFieldNames  = "Fam";

Table1->SetRangeStart();

Table1->FieldByName ("Fam")->AsString  = "A";

Table1->SetRangeEnd();

Table1->FieldByName ("Fam")->AsString  =  "Г";

Table1->ApplyRange();

приведут к тому, что доступными будут только записи сотрудников, фамилии которых начинаются с букв «А», «Б», «В».

На результаты работы методов SetRangeStart и SetRangeEnd для числовых полей влияет свойство набора данных KeyExclusive. Оно определяет, будут ли считаться сами заданные границы входящими в диапазон (при KeyExclusive = false, это значение принято по умолчанию), или не входящими (при KeyExclusive =true). Иначе говоря, при KeyExclusive = false используются операции отношения >=, <=, при KeyExclusive = true - >, <. Например, если нужно отобрать записи, в которых год рождения сотрудников лежит в определенных пределах, то при коде

 

Table1->IndexFieldNames = "Year_b" ;

Table1->SetRangeStart() ;

Table1->FieldByName("Year_b")->AsInteger = 1950;

Table1->SetRangeEnd() ;

Table1->FieldByName("Year-b")->AsInteger = I960;

Table1->ApplyRange() ;

 

будут отобраны записи, в которых год рождения лежит в пределах 1950-1960, включая годы 1950 и 1960. А при коде

 

Table1->lndexFieldNames = "Year_b";

Table1->SetRangeStart(); Table1->FieldByName("Year_b")->AsInteger  =  1950;

Table1->KeyExclusive  =  true;

Table1->SetRangeEnd(); Table1->FieldByName("Year_b")->AsInteger  = 1960;

Table1->ApplyRange();

записи, соответствующие 1950 году, не войдут в число отобранных.

Имеется и более простой способ установки диапазона - метод SetRange. Он определен следующим образом:

 

void _fastcall SetRange(StartValues, StartValues_Size, EndValues, EndValues_Size);

Открытые массивы StartValues и EndValues должны содержать соответственно нижние и верхние значения диапазонов полей, являющихся ключевыми. Таким образом, метод SetRange заменяет последовательное обращение к методам SetRangeStart, SetRangeEnd и ApplyRange. Например, приведенные ранее операторы, задающие диапазон фамилий в отобранных записях, могут быть заменены следующими:

 

Table1->IndexFieldNames = "Fam";

Table1->SetRange(STVarRec("A") , 0, STVarReC("Г") , 0) ;

Если нужно выбрать тех сотрудников, фамилии которых начинаются на «А»-«В» и  которые работают в цехе 1, то приведенные выше операторы можно изменить следующим образом:

 

Table1->IndexFieldNames = "Dep;Fam";

Table1->SetRange(OPENARRAY(TVarRec,("Цех 1","А")), OPENARRAY(TVarRec,("Цех 1","Г")));

 

4.    Методы создания и модификации таблиц

 

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

CreateTable

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

DeleteTable

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

RenameTable(s)

Метод переименовывает существующую таблицу, присваивая ей новое имя, содержащееся в s. Одновременно переименовываются все сопутствующие таблице файлы.

Deletelndex(s)

Удаляет вторичный индекс с именем s из таблицы.

 

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

 

Table1->Active  =  False; // Компонент Table1 делается неактивным

Table1->TableName = "Dep.db";           // Указывается имя  таблицы

//  Таблица создается, если  в  базе  данных  нет  такой  таблицы

if(! Table1->Exists)

{

// Указывается  тип  таблицы

Table->TableТуре = ttParadox;        // Начало  описания полей таблицы

Table1->FieldDefs->Clear();    // Создается указатель на  объект описания поля

TFieldDef *pNewDef = Table1->FieldDefs->AddFieldDef(); // Описание первого поля

pNewDef->Name = "Dep"; pNewDef->DataType = ftstring; pNewDef->Size = 20;

pNewDef->Required = True; // Описание второго поля

pNewDef = Table1->FieldDefs->AddFieldDef() ;

pNewDef->Name = "Proisv"; pNewDef->DataType = ftBoolean;

// Описание индекса

Table1->IndexDefs->Clear() ; // Индекс  без имени - первичный ключ таблицы

Table1->IndexDefs->Add(" ", "Dep",TIndexOptions() <ixPrimary < ixUnique);

// Создание  таблицы методом CreateTable

Table1->CreateTable();

Table1->Open(); // Вставка  первой записи

Table1->Insert() ;

Table1->FieldByName("Dep")->AsString =  "Бухгалтерия";

Table1->FieldByName("Proisv")->AsBoolean  =  false;

Table1->Post();

 

5. Клиентские наборы данных

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

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

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

Клиентские наборы данных обладают еще одной полезной особенностью. С помощью клиентских наборов данных наиболее просто осуществлять взаимную трансляцию таблиц баз данных, созданных в разных СУБД и работающих на разных платформах.

 

5.2. Наборы данных, основанные на файлах

 

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

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

Можно также осуществлять чтение данных из файла методом LoadFromFile:

void _fastcall LoadFromFile(const  AnsiString FileName  =  "");

Если параметр FileName - пустая строка или вообще не указан, то чтение производится из файла, заданного свойством FileName. Таким образом, оператор

ClientDataSetl->LoadFromFile(""); обеспечит чтение данных из того же файла, из которого производится чтение при открытии набора. Отличие в том, что это чтение можно произвести в любой момент, а не только в момент открытия набора. А введя в приложение диалог открытия файла можно с помощью LoadFromFile организовать чтение из любого выбранного пользователем файла таблицы.

Метод SaveToFile позволяет в любой момент сохранить данные в файле. Он объявлен следующим образом:

void _fastcall SaveToFile(const AnsiString FileName = "", TDataPacketFormat Format = dfBinary);

Параметр FileName аналогичен рассмотренному для метода LoadFromFile. A параметр Format указывает формат файла и может принимать одно из следующих значений:

 

dfBinary

двоичный формат

dfXML

формат XML с расширенным набором символов escape-последовательности

dfXMLUTF8

формат XML с расширенным набором символов, использующим UTF8

 

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

int  MySavePoint;

MySavePoint = ClientDataSetl->SavePoint;

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

ClientDataSetl->SavePoint  = MySavePoint;

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

MySavePoint  =  ClientDataSetl->SavePoint;

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

Значение свойства IndexFieldName может задаваться как во время проектирования, так и во время выполнения в виде строки, перечисляющей поля, разделяющие «;». Например, значение «Dep; Fam; Nam; Par» соответствует упорядочиванию по названию отдела, а внутри каждого отдела - по алфавитной последовательности фамилий, имен и отчеств. Но иногда, требуется сформировать индекс, на который можно было бы ссылаться по имени. Это имя задается в свойстве IndexName. Но прежде, чем ссылаться на имя индекса, его надо определить. Это делается с помощью свойства IndexDefs. Щелчок на кнопке с многоточием около этого свойства в Инспекторе Объектов вызывает окно формирования коллекций объектов. Нажав в нем кнопку New, можно создать новый объект индекса и в Инспекторе Объектов увидеть его свойства. Свойство Name определяет имя индекса, на которое в дальнейшем можно ссылаться. В свойстве Fields записывается строка перечисления полей.

После заполнения свойств Name и Fields индекса, можно выйти из редактора индексов и посмотреть свойство IndexName компонента клиентского набора. Около этого свойства в Инспекторе Объектов появится выпадающий список, из которого можно выбрать имя введенного индекса.

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

 

5. 3. Совокупные характеристики

 

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

Задается поле, содержащее совокупную характеристику, в Редакторе Полей, вызываемом двойным щелчком на компоненте ClientDataSet. При щелчке правой кнопкой в этом окне и выбрав в контекстном меню раздел New, откроется окно задания нового поля. Это окно отличается от аналогичного окна компонента Table двумя дополнительными радиокнопками: InternalCalc и Aggregate. Первая из них определяет вычисляемое Поле, которое, не вычисляется динамически при чтении данных, и сохраняется в таблице в виде отдельного поля. А кнопка Aggregate позволяет определить поле совокупной характеристики. Для этого поля достаточно задать имя Name, а тип поля автоматически установится равным Aggregate.

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

Sum  Сумма значений числового поля или арифметического выражения.

Avg  Среднее значение числового поля, или поля даты и времени, или арифметического выражения,

Count Число непустых значений указанного поля или выражения. Функция Соunt(*) возвращает общее число записей, независимо от значений полей. 

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

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

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

Max(Year_b) -  Min(Year_b) вычисляет диапазон значений поля Year_b. Выражение

Sum(Year_b) /  Count(Year_b)

вычисляет среднее значение поля Year_b, то же, что вычислит и более простое выражение

Avg (Year_b) А выражение

Avg (2010 - Year_b)

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

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

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

Ссылка на индекс из поля совокупной характеристики осуществляется следующим образом. В свойстве поля IndexName надо задать индекс, на который опирается вычисление суммарной характеристики. А в свойстве GroupingLevel надо задать число первых полей в индексе, совпадение которых выделяет группу записей, по которой вычисляется характеристика. Пусть, например, задан текущий индекс вида «Dep;Year_b», индексирующий набор данных по подразделениям, в которых работают сотрудники, а внутри каждого подразделения - по году рождения. Тогда, если сослаться на этот индекс в поле совокупной характеристики выражением Count(*) и задать GroupingLevel = 1, то поле покажет число сотрудников подразделения, соответствующего текущей записи. А если задать GroupingLevel = 2, то поле покажет число сотрудников, у которых значения полей подразделения и года рождения совпадают с этими полями в текущей записи, т.е. число ровесников указанного текущей записью сотрудника в данном подразделении.

Значение GroupingLevel = 0 соответствует подсчету характеристик по всем записям набора данных, независимо от текущего индекса.

 

Портфельные наборы данных

 

Если клиентский набор данных должен представлять собой «портфельную» копию какого-то другого набора данных, для связи с этим другим набором в приложение должен быть введен компонент-провайдер, обеспечивающий связь с этим набором данных. Провайдер может размещаться в том же приложении, в котором находится набор данных, или может быть частью удаленного сервера. В качестве провайдера может выступать компонент DataSetProvider со страницы Data Access. В этом случае ссылка на этот провайдер должна содержаться в свойстве ProviderName.

Рассмотрим некоторые свойства компонента DataSetProvider. Основным свойством является DataSet - набор данных, являющийся сервером для данного провайдера. В качестве него могут выступать любые наборы данных: Table, Query, клиентские наборы данных и т.п. Провайдер упаковывает послание от этого набора данных в пакет, который может воспринимать клиентский набор данных или брокер XML. Через провайдер осуществляется и обратная передача данных от клиента серверному набору. Передача осуществляется с помощью специального объекта, на который указывает свойство только для чтения и только времени выполнения Resolver. Этот объект автоматически создается в момент выполнения любого метода, передающего информацию на сервер. До этого момента Resolver = nil. Объект осуществляет передачу данных и выявляет записи, обновление которых вызывает ошибки.

Свойство ResolveToDataSet определяет, как передаются на сервер изменения данных, сделанные в клиентском наборе. При ResolveToDataSet = true данные передаются в набор, указанный свойством DataSet. Последующая передача их в базу данных в этом случае должна осуществляться набором данных DataSet. При ResolveToDataSet = false данные передаются непосредственно на сервер, связанный с набором DataSet. Это более эффективно, так как не требует лишних операций со стороны набора DataSet. К тому же, это единственный способ обновить базу данных, если набор DataSet работает только для чтения, например, является однонаправленным набором данных.

Если клиентский набор данных открывается методом Open и файла, задаваемого свойством FileName, нет или это свойство не задано, то клиентский набор воздействует через провайдер на набор-источник, открывает его, если он был закрыт, и считывает через провайдер данные источника.

Данные содержатся в свойстве только для чтения Data типа OleVariant. У клиентского набора данных имеется аналогичное свойство Data, но его значение можно задавать во время выполнения. Таким образом, если в какой-то момент надо передать данные из серверного набора с помощью провайдера DataSet-Providerl в клиентский набор ClientDataSetl, надо выполнить оператор

ClientDataSetl->Data  = DataSetProviderl->Data;

Так осуществляется передача данных от набора-источника набору-клиенту. Обратная передача от клиента источнику осуществляется методом ApplyUpdates клиентского набора данных:

virtual int      _fastcall  ApplyUpdates(int MaxErrors);

Параметр MaxErrors определяет максимальное число ошибок передачи, при превышении которого передача прекращается. Если задать MaxErrors = -1, количество ошибок не ограничено. Метод возвращает число ошибок, произошедших при передаче.

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

 

virtual void _fastcall CloneCursor(TCustomClientDataSet* Source, bool Reset,  bool KeepSettings = false);

 

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

Если и Reset, и KeepSettings равны false, то, помимо самих данных, будут согласованы следующие свойства и события: Filter, Filtered, FilterOptions, OnFilterRecord, IndexName, MasterSource, MasterFields, Readonly, RemoteServer, ProviderName. Таким образом, в наборах будут согласованы не только данные, но и форма их отображения. Если Reset = true, то указанные выше свойства и события в приемнике очищаются. Если Reset = false, a KeepSettings = true, то указанные свойства и события приемника остаются неизменными. В этом случае надо быть уверенным, что они совместимы с копируемыми данными.

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