Программирование работы с базами. Лабораторная работа 4.


Тема 1: Программирование работы с базами
Тема 2: Создание отчетов

1.1. Состояние набора данных
Начнем рассмотрение вопросов программирования работы с базами данных с основного свойства State компонента типа TTable, определяющего состояние набора данных. Это свойство доступно только во время выполнения и только для чтения. Набор данных может находиться в одном из следующих состояний:
— dsInactive — набор данных закрыт, данные недоступны;
— dsBrowse — данные могут наблюдаться, но не изменяться. Это состояние по умолчанию после открытия набора данных;
— dsEdit — текущая запись может редактироваться;
— dsInsert — может вставляться новая запись;
— dsSetKey — доступен режим поиска записи и операция задания диапазона изменений SetRange. Может наблюдаться только ограниченное множество данных и не может проводиться редактирование или вставка новой записи.

Состояния могут устанавливаться в приложении во время выполнения, но не непосредственно, а с использованием различных методов.
Метод Close закрывает соединение с базой данных, устанавливая свойство Active набора данных в false. При этом State переводится в состояние dsInactive.
Метод Open открывает соединение с базой данных, устанавливая свойство Active набора данных в true. При этом State переводится в состояние dsBrowse.
Метод Edit переводит набор данных в состояние dsEdit.
Методы Insert и InsertRecord вставляют новую пустую запись в набор данных, выполняют еще ряд операций и переводят State в состояние dsInsert.
Методы EditKey, SetRange, SetRangeStart, SetRangeEnd и ApplyRange, связанные с поиском записи и с заданием допустимого диапазона изменения данных, переводят State в состояние dsSetKey.

1.2. Пересылка записи в базу данных
Пока идет редактирование текущей записи, изменения осуществляются в буфере, а не в самой базе данных. Пересылка записи в базу данных производится только при выполнении метода Post. Метод может вызываться только тогда, когда набор данных находиться в состоянии dsInsert или dsEdit. Этот метод может вызываться явно, например Table1->Post(). Но кроме того он вызывается неявно при любых перемещениях по набору данных, т.е. при перемещении курсора на новую текущую запись, если набор данных находится в состоянии dsEdit или dsInsert. Отменить изменения, внесенные в запись, можно методом Cancel. При выполнении этого метода, если предварительно не был вызван метод Post, запись возвращается к состоянию, предшествующему редактированию, и набор данных переводится состояние dsBrowse.
Перед началом выполнения каждого из рассмотренных выше методов и после его выполнения генерируется соответствующее событие набора данных Table. Например, перед выполнением метода Post возникает событие BeforePost, а после его окончания — событие AfterPost. Аналогичные события BeforeInsert и AfterInsert сопровождают выполнение метода Insert и т.п.
Один из множества возможных вариантов заключается в использовании события BeforePost. Обработчик этого события может иметь вид:

void_fastcall TForm1::Table1BeforePost(TDataSet *DataSet)
{
if (проверка введенных данных)
{
if (Application->MessageBox(«Хотите занести текущую запись в базу данных?»,
«Подтвердите занесение в базу данных»,
MB_YESNOCANCEL+MB_ICONQUESTION)!=IDYES)
{
DataSet->Cancel();
Abort();
}
}
else
{
Application->MessageBox(«Ошибочные данные»,»Ошибка»,MB_ICONSTOP);
Abort();
}
}

К этому обработчику будет происходить обращение перед выполнением метода Post, как бы он не был вызван: явно или вследствие перемещения по базе данных, если текущая запись была изменена. В обработчике сначала производится проверка данных в записи. Если она дает неудовлетворительный результат, то пользователю выдается сообщение об ошибочности данных и выполняется функция Abort(), прерывающая выполнение Post. Текущая запись остается в состоянии dsEdit, но ошибочные данные в ней не обрабатываются, что позволяет пользователю исправить их.
Если проверка данных в записи показала их правильность, то у пользователя запрашивается подтверждение изменений в базе данных. Если он ответит отрицательно, то для набора данных выполняется метод Cancel, а затем выполняется функция Abort. Cancel возвращает данные в текущей записи к состоянию, которое было до их редактирования, т.е. уничтожает результаты редактирования.
В операторе Cancel использована ссылка DataSet. Это параметр, передаваемый в обработчик события BeforePost и соответствующий набору данных, к которому применяется Post. Использование параметра DataSet позволяет написать обработчик в более общем виде и к такому обработчику можно обращаться при событиях в разных наборах данных. Для конкретизации вместо DataSet можно было использовать имя конкретного набора данных, например Table1.
Таким образом, приведенный выше обработчик BeforePost позволяет предотвратить непроизвольное или ошибочное изменение данных.

1.3. Доступ к полям
Поля отображаются объектами класса TField и производных от него классов TStringField, TSmallintField, TBooleanField и т.п. Эти объекты могут создаваться тремя способами:
— Автоматически генерироваться для каждого компонента набора данных (Table и др.);
— Создаваться в процессе проектирования с помощью Редактора Полей;
— Создаваться программно в процессе выполнения приложения.

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

Доступ по порядковому индексу осуществляется через свойство TField* Fields[int i], где i — индекс объекта. Индексация начинается с 0. Например, Table1->Fields[0] — это первый объект поля таблицы Table1.
Доступ по имени поля осуществляется с помощью метода FiledsByName(«имя»). Например, Table1->FieldByName(«Fam») — это объект, связанный с полем Fam.
Доступ по имени объекта возможен только к объектам, созданным с помощью Редактора Полей. По умолчанию, С++ Builder формирует имена объектов полей (Name) из имени таблицы и имени поля. Например, Table1Dep. Эти имена видны при работе с Редактором Полей. Эти имена можно заменить на любые другие. Обращение к объекту по имени не требует ссылки на таблицу. Можно просто написать Table1Dep и это будет необходимый объект.
Автоматически создаваемые объекты имени не имеют – их свойство Name пусто, поэтому для них обращение по имени не возможно.
Среди рассмотренных способов доступа к полям наиболее быстрым, конечно, является доступ по имени объекта. Его недостатком является жесткая кодировка поля, к которому производится обращение. Если надо, чтобы строка кода в разных ситуациях обращалась к разным полям, то надо использовать или доступ по индексу Fields[i] (тогда индекс i можно сделать переменным), или по имени поля методом FieldByName(s) (s можно сделать переменной).
Рассмотрим, как добраться до главного свойства объекта – хранящегося в нем значения поля текущей записи.
Значение поля хранится в свойстве Value. Тип этого свойства — Variant, т.е. тип определяется типом поля. Например, Table1->FieldByName(«Fam»)->Value — это строка, а Table1->FieldByName(«Year_b»)->Value — это целое число.
Имеется также ряд свойств, переводящих один тип в другой. Например, свойство AsString переводит тип любого поля в строку при чтении значения поля, и осуществляет обратный перевод строки в тип поля при записи значения поля. Например, можно записать:

EDep->Text = Table1->FieldByName(«DEp») ->AsString;
EYear->Text = Table1->FieldByName(«Year_b»)->AsString;
ESex->Text = Table1->FieldByName(«Sex»)->AsString;

и в окна редактирования EDep, EYear, ESex будут занесены в текстовом виде значения в текущей записи полей Dep, Year_b, Sex, хотя поле Dep имеет тип строки, поле Year_b — целое значение, а поле Sex — булевое. Если для поля Sex задавалось значение DisplayValues, то в окне редактирования ESex будут отображены значения “true” или “false”. Если же в DisplayValues написаны «м;ж» или «мужск;женск», то отобразятся именно эти значения.
То же свойство AsString работает и как обратное преобразование типов. Продолжить пример можно после редактирования значений в полях редактирования EDep, EYear, Esex:

Table1->Edit ();
Table1->FieldByName(“Dep”)->AsString = EDep->Text;
Table1->FieldByName(“Year_b”)->AsString = EYear->Text;
Table1->FieldByName(“Sex”)->AsString = ESex->Text;
Table1->Post ();

Для полей Year_b и Sex текст будет преобразован соответственно в целое булево значение. При этом необязательно в окне ESex писать полностью обозначения пола сотрудников. Достаточно написать только первую букву.
Помимо AsString имеются еще аналогичные свойства AsInteger, AsFloat, AsBoolean, AsDateTime. Свойство AsInteger осуществляет преобразование между типом данного поля и целым 32-битным числом, свойство AsFloat делает то же самое для действительных чисел с плавающей точкой, свойство AsBoolean — для булевых значений, свойство AsDateTime — для значений дат и времени в принятом в C++ Builder формате TDateTime.
В поле DataType Можно узнать тип требуемого поля по принимаемым значениям: ftUnknown (неизвестное), ftAutoInc (автоматически нарастающее), ftString (строка) и т.д.

1.4. Методы навигации
Наборы данных имеют ряд методов, позволяющих осуществлять навигацию — перемещение по таблице:
— First — перемещение к первой записи;
— Last — перемещение к последней записи;
— Next — перемещение к следующей записи;
— Prior — перемещение к предыдущей записи;
— MoveTo(int i) — перемещение к концу (при i>0) или к началу (при i<0) на i записей.

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

CBdep-> Clear();
Table1->First();
while (!Table1->Eof)
{
CBdep->Items->Add(Table1Dep->AsString);
Table1->Next();
}
CBEDep->ItemIndex=0;
Table1->First();

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

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

Table1->IndexFieldNames=»Fam»;
Table1->SetKey();
Table1->FieldByName(«Fam»)->AsString=EFam-> Text;
Table1->GotoNearest();

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

bool_fastcall FindKey(const System::TVarRec*KeyValues,const int KeyValues_Size);

Параметр KeyValues представляет собой открытый массив: разделяемый запятыми список значений полей, по которым индексирован набор данных, в той последовательности, в которой они входят в индекс. Параметр KeyValues_Size определяет индекс последнего поля в массиве, участвующего в поиске. Поскольку индексы начинаются с 0, то KeyValues_Size на единицу меньше количества полей, участвующих в списке. Вместо FindKey для полей строкового типа можно использовать аналогичный метод FindNearest, обеспечивающий переход к наиболее совпадающей строке, если полного совпадения не получено.
Для методов FindKey и FindNearest удобно применять макрос OPENNARRAY, создающий временный открытый массив и определяющий его размер:

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, а второй задает ключи поиска для этих полей.

3) Метод Locate объявляется следующим образом:

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

В качестве первого параметра KeyFields передается строка, содержащая список ключевых полей. В качестве второго параметра передается KeyValues — массив ключевых значений. Третий параметр Options является множеством опций, элементами которого могут быть IoCaseInsensitive — нечувствительность поиска к регистру, в котором введены символы, и IoPartitialKey — допустимость частичного совпадения. Метод возвращает false, если искомая запись не найдена. В простейшем случае применение Locate отличается от рассмотренных ранее методов только отсутствием необходимости индексировать набор данных определенным образом. Например, поиск по фамилии может осуществляться операторами:

TLocateOptions SearchOptions;
SearchOptions<<loPartialKey<<loCaseInsensitive;
Table1->Locate(«Fam», EFam->Text,Searchoptions);

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

TLocateOptions SearchOptions;
Variant locvalues[]={EDep->Text,EFam->Text};
Table1->Locate(«Dep;Fam»,VarArrayOf(locvalues,1),
SearchOptions<<loPartialKey<<loCaseIsensitive);

4) Последний метод поиска — Lookup. Этот метод определен следующим образом:

System::Variant _fastcall Lookup (const System::AnsiString KeyFields,
const System::Variant &KeyValues,
const System::AnsiString ResultFields);

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

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

Table1->IndexFieldName=»Fam»;
Table1->SetRangeStart();
Table1->FieldByName(«Fam»)->AsString=»A»;
Table1->SetRangeEnd();
Table1->FieldByName(«Fam»)->AsString=»Г»;
Table1->ApplyRange();

приведут к тому, что доступными будут только записи сотрудников, фамилии которых начинаются с «А», «Б», «В».
На результаты работы методов SetRangeStart и SetRangeEnd для числовых полей влияет свойство набора данных KeyExclusive. Оно определяет, будут ли считаться сами заданные границы входящими в диапазон (при KeyExclusive=false) или не входящими (при KeyExclusive=true).
Имеется и более простой способ установки диапазона — метод SetRange. Он определен следующим образом:

void __fastcall SetRange (const System::TVarRec * StartValues,
const int StartValues_Size,
const System::TVarRec * EndValues, const int EndValues_Size);

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

Table1->IndexFieldNames=»Fam»;
Table1->SetRange(&TVarRec(«A»),0,&TVarRec(«Г»),0);

1.7. Методы модификации таблиц

Помимо рассмотренных методов модификации записей Table имеет также ряд методов, позволяющих модифицировать таблицы и индексы:
— CreateTable — метод создает новую таблицу, исходя из установок компонента Table, содержащихся в свойствах Fields или FieldDefs. Если таблица с именем, указанным в свойстве TableName уже имеется, она будет переписана. Применение этого метода позволяет, например, взять структуру существующей таблицы, как-то изменить ее, затем изменить свойство TableName на имя новой таблицы и создать эту таблицу;
— DeleteTable — метод уничтожает существующую таблицу, которая задана свойствами DatabaseName и TableName. Таблицу необходимо предварительно закрыть;
— RenameTable(s) — метод переименовывает существующую таблицу, присваивая ей новое имя, содержащееся в s. Одновременно переименовываются все сопутствующие таблице файлы;
— DeleteIndex(s) — удаляется вторичный индекс с именем s из таблицы.

2. Создание отчетов

Компоненты, относящиеся к системе подготовки и печати отчетов, находятся на закладке QReport. QuickReport использует генератор отчетов, состоящий из множества полос. Полоса (band) — это область отчета или раздел, содержащий некоторый текст, изображения, графики, диаграммы и т.п. Если полоса и размещенные на ней компоненты связаны с базой данных, то содержание этой полосы печатается столько раз, сколько соответствующих записей имеется в источнике данных. Таким образом, достаточно расположить компоненты, связанные с данными, на полосе, а печатаемые значения и их количество будут автоматически управляться базой данных. Как в компонентах, связанных с данными, так и между полосами можно задавать связи между головной и вспомогательной таблицами. Основным компонентом, на котором размещаются все полосы отчета, является QuickRep. Компонент QuickRep имеет ряд свойств, основным из которых является свойство DataSet, определяющее набор данных. Этим набором может являться компонент типа TTable, TQuery и т.п.
Свойство компонента OuickRep — Bands — имеет ряд подсвойств:
— HasTitle — имеется полоса заголовка отчета, которая печатается один раз в начале отчета;
— HasDetail — имеется полоса детализации, которая печатается столько раз, сколько записей в ней передается;
— HasPageHeader — имеется верхний колонтитул (заголовок) на каждой странице отчета;
— HasPageFooter — имеется нижний колонтитул на каждой странице отчета;
— HasColumnHeader — имеется заголовок печатаемой таблицы.

Палитра компонент страницы QuickReport имеет следующие компоненты полос:
QRBand (полоса) — универсальный компонент, который может использоваться в качестве полосы любого типа
QRSubDetail (детали) — используется в качестве полосы детализации

QRGroup (группировка) — используется для группировки данных и полос

QRChildBand (дочерняя) — используется для создания дочерних полос, которые могут содержать другие компоненты QuickReport и полосы

QRStringBand (полоса текста) — используется для компоновки в отчет дополнительных текстов
Наиболее универсальным является компонент QRBand. Эта полоса может использоваться вместо компонентов некоторых полос других типов. Но все-таки использовать специализированные полосы удобнее.

3. План выполнения работы
1. Создайте описанные варианты приложения, откомпилируйте и посмотрите в работе.
2. Выполните индивидуальное задание.

4. Индивидуальные задания
1. Осуществите пересылку записи в базу данных с окном подтверждения;
2. Осуществить методы навигации по таблице с использованием кнопок;
3. Осуществить поиск записи по ключевому полю, и по одному из других полей;
4. Установить диапазон допустимых значений по одному из полей (не ключевому);
5. Для разработанной таблицы организовать отчет.

5. Вопросы по лабораторной работе
1. Как переслать записи в базу данных с окном подтверждения?
2. Как осуществить методы навигации по таблице с использованием кнопок?
3. Как осуществить доступ к полям?
4. Как осуществить поиск записи по ключевому полю, и по одному из других полей?
5. Как установить диапазон допустимых значений по одному из полей (не ключевому)?
6. Как для разработанной таблицы организовать отчет?