Практическое занятие № 23
Тема: "Обработка данных в отношении многие ко многим"
Цель работы: получить практические навыки по созданию приложений для работы с таблицами, связанными отношением многие ко многим.
Ход работы
Выполните задание в СУБД Access
Пусть имеется БД "Catalog" для хранения информации о книгах, читателях и выдачах книг.
- Books - таблица книг;
- Readers - таблица читателей;
- Uchet - таблица учета выдачи.

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

Указания: для формы задайте свойства: BorderStyle=bsSingle, BorderIcons=bsMaximized=false, Caption=Учет выдачи книг, Position=poDesktopCenter.
Для подключения БД нанесите на форму компонент ADOConnectuion (ADO) и с помощью свойства ConnectionString, подключите БД.
Отображение списка книг
Для отображения данных будем использовать компонент ADOTable. Нанесите на форму компонент ADOTable и задайте свойства: Connection=ADOConnection1, TableName=books. Для сортировки списка книг по названию задайте свойство IndexFieldNames=nazv_book. Двойным щелчком раскройте компонент, добавьте все поля таблицы, поля "id_book" и "label" сделайте невидимым, остальные поля переименуйте в сетке задайте подходящую ширину.
В событии OnCreate формы введите код подключения к таблице:
procedure TForm1.FormCreate(Sender: TObject);
begin
//подключаем таблицу books
ADOTable1.Active:=true;
end;
При удалении формы нужно разрывать связь с БД. Для этого в событии OnDestroy формы введите код:
procedure TForm1.FormCreate(Sender: TObject);
begin
//закрываем соединение с БД
ADOConnection1.Connected=false;
end;
Нанесите на форму компонент DataSource (DataAccess) и задайте свойство: DataSet=ADOTable1.
Нанесите на форму компонент DBGrid (DataControls) и задайте свойство: DataSource=DataSource1.Чтобы при выходе из сетки в ней оставалось выделение, задайте свойство Options - dgAlwaysShowSelection=true.
Отображение списка читателей
Для отображения данных будем использовать компонент ADOTable. Нанесите на форму компонент ADOTable и задайте свойства: Connection=ADOConnection1, TableName=readers. Для сортировки списка читателей по фамилии, имени и отчеству задайте свойство IndexFieldNames=fam;imya;otch. Двойным щелчком раскройте компонент, добавьте все поля таблицы, поле "id_reader" сделайте невидимым, остальные поля переименуйте в сетке и задайте подходящую ширину.
В событии OnCreate формы добавьте код подключения к таблице:
//подключаем главную readers
ADOTable2.Active:=true;
Нанесите на форму компонент DataSource (DataAccess) и задайте свойство: DataSet=ADOTable2.
Нанесите на форму компонент DBGrid (DataControls) и задайте свойство: DataSource=DataSource2.Чтобы при выходе из сетки в ней оставалось выделение, задайте свойство Options - dgAlwaysShowSelection=true.
Отображение списка учета выдачи книг
Список учета выдачи книг строится на основании запроса по таблицам "books" и "uchet". Нанесите на форму компонент ADOQuery и задайте свойства: Connection=ADOConnection1, SQL=введите текст запроса вида:
select id_uchet, id_book, uchet.id_reader, data_out, fam, imya, otch, data_in
from readers inner join uchet
on readers.id_reader=uchet.id_reader
where id_book=:id_book
order by data_out Desc
Обратите внимание, что поле id_reader в запросе указано с именем таблицы: uchet.id_reader. Это связано с тем, что данное поле имеется в обоих таблицах, участвующих в запросе: readers и uchet. Таким образом, мы указываем поле какой именно таблицы нас интересует.
В запросе используется параметр ":id_book", для которого обязательно задайте тип данных. Для этого в свойстве Parameters компонента ADOQuery1 выберите данный параметр и задайте свойства: DataType=Integer, Value - Type = Integer.
Двойным щелчком раскройте компонент ADOQuery, добавьте все поля запроса. Поля "id_uchet", "id_book", "id_reader" сделайте невидимым, остальные поля переименуйте в сетке.
Нанесите на форму компонент DataSource (DataAccess) и задайте свойство: DataSet=ADOQuery1.
Нанесите на форму компонент DBGrid (DataControls) и задайте свойство: DataSource=DataSource3.
В третьей сетке не предполагается редактирование данных на прямую. Поэтому для сетки задайте свойство Options - dgRowSelect = true (в сетке будут выделяться строки целиком. Данный режим автоматически блокирует сетку для ввода).
Организация корректного обновления данных в сетке с оценками
Если пользователь переходит по списку книг, то в третьей сетке с оценками должны отображаться записи о фактах ее выдачи.
В событии AfterScroll компонента ADOTable1 введите код:
procedure TForm1.ADOTable1AfterScroll(DataSet: TDataSet);
begin
//если списка книг нет, то код не выполняется
if ADOTable1.RecordCount=0 then Exit;
//отключаем третью таблицу
ADOQuery1.Active:=false;
//передаем в запрос в качестве входящего параметра код книги
ADOQuery1.Parameters.ParamByName('id_book').Value:=
ADOTable1.FieldByName('id_book').Value ;
//подключаемся к таблице для отображения данных
ADOQuery1.Active:=true;
//после обновления набора данных в запросе нужно указать,
//что главная таблица в нашем запросе - это uchet
ADOQuery1.Properties['Unique Table'].Value:='uchet';
end;
Настройка параметров сеток
Заблокируем редактирование сеток с помощью стандартных клавиш (Tab, Insert, Ctrl+Delete).
Для отключения добавления новой записи с помощью клавиши Tab задайте для всех сеток свойство Options - dgTabs = false.
Для отключения в сетках реакции на нажатия указанных клавиш для первой сетки в событии OnKeyDown введите код:
procedure TForm1.DBGrid1KeyDown(Sender: TObject; var Key: Word;Shift: TShiftState);
begin
//если нажата клавиша Insert
if (Key=VK_Insert)
//или нажаты клавишы Ctrl+Delete
or (ssCtrl in Shift) and (Key=VK_Delete) then
//нажатие игнорируется
abort;
end;
Для присваивание этого же кода остальным сеткам в их событии OnKeyDown выберите заголовок данного обработчика.
По умолчанию сетки нельзя пролистывать с помощью колеса мыши. Для решения данной проблемы нанесите на форму компонент ApplicationEnents (Additional) и в событии OnMessage введите код:
procedure TForm1.ApplicationEvents1Message(var Msg: tagMSG;var Handled: Boolean);
var
i: SmallInt;
begin
if Msg.message = WM_MOUSEWHEEL then
begin
Msg.message := WM_KEYDOWN;
Msg.lParam := 0;
i := HiWord(Msg.wParam);
if i > 0 then
Msg.wParam := VK_UP
else
Msg.wParam := VK_DOWN;
Handled := False;
end;
end;
Реализуйте редактирование данных по учету выдачи книг читателям. Пользователь может выдать книгу читателю, сделать отметку о возврате книги, удалить запись о выданной книге.
Все операции реализуются с помощью кнопок.
Учет выдачи книг
Для записи факта выдачи книги необходимо выбрать книгу, выбрать читателя и щелкнуть на кнопке "Выдать книгу". После этого в третью таблицу вносятся данные:
"id_uchet", "id_book", "id_reader", "data_out" (дата выдачи книги)
Кроме записи о выдаче в таблицу "books" в поле "label" ставиться признак, что книга выдана (цифра 1).
Нанесите на форму компонент Button и задайте свойство Caption = "Выдать книгу". Для кнопки введите код:
procedure TForm1.Button1Click(Sender: TObject);
begin
//если значение поля "label" = 1 (то есть книга уже выдана)
if ADOTable1.FieldByName('label').asinteger=1 then
begin
//выдаем сообщение об ошибке
Application.MessageBox('Книга уже выдана',
'Операция отменена',
mb_OK+mb_iconstop);
//завершаем работу кода
Exit;
end;
//добавляем новую запись в сетку регистрации выдачи книги
ADOQuery1.Append;
//записываем код книги
ADOQuery1.fieldbyname('id_book').Value:=ADOtable1.FieldByName('id_book').Value;
//записываем код читателя
ADOQuery1.fieldbyname('id_reader').Value:=ADOtable2.FieldByName('id_reader').Value;
//записываем дату выдачи (текущая)
ADOQuery1.fieldbyname('data_out').Value:=FormatDateTime('dd/MM/YYYY',Date);
//сохраняем запись в БД
ADOQuery1.Post;
//далее в таблице books нужно поставить отметку, что книга выдана
//переводим запись о книге в режим редактирования
ADOTable1.Edit;
//в поле label ставим отметку (цифра 1)
ADOTable1.FieldByName('label').Value:=1;
//сохраняем запись о книге
ADOTable1.Post;
end;
Обновление данных о текущей записи
По умолчанию после добавления новой записи о выдаче в сетке не отображаются поля из связанной таблицы readers.
Проблему можно решить обновлением запроса с помощью метода ADOQuery1.Requery(). Однако такой метод является плохим способом, так как обновляет весь набор данных, что может потребовать некоторого времени на операцию, и ставит курсор в начало набора, что вынуждает разработчика вначале запоминать текущую позицию в наборе, а после обновления набора эту позицию восстанавливать.
Существует способ, который обновляет только текущую запись, не требуя перезагрузки всего набора и не теряя текущей позиции. Данный код пишут сразу после задания главной таблицы в запросе на основе нескольких таблиц.
Подключите модуль ADOInt.
Для таблицы ADOTable1 в событии AfterScroll добавьте команды вида:
//задаем дополнительные параметры перед обновлением
//задаем «главную» таблицу в наборе данных
ADOQuery1.Properties['Unique Table'].Value:='uchet';
ADOQuery1.Properties['Update Resync'].Value:= adResyncAll;
//к тексту запроса добавляем условие в виде:
//ключевое поле «главной таблицы» = ?
ADOQuery1.Properties['Resync Command'].Value:= AdoQuery1.Sql.Text+' where id_uchet=?';
//далее выполняем обновление записи
ADOQuery1.UpdateCursorPos;
ADOQuery1.Recordset.Resync(adAffectCurrent, adResyncAllValues);
ADOQuery1.Resync([]);
Теперь в сетке учета выдачи книг будет обновляться только текущая запись.
Учет возврата книг
Для записи факта возврата книги необходимо выбрать запись о факте выдачи книги и и щелкнуть на кнопке "Вернуть книгу". После этого в третьей таблице заполняется поле "data_in" для записи даты возврата книги.
Кроме записи о выдаче в таблицу "books" в поле "label" ставиться признак, что книгу вернули (0).
Нанесите на форму компонент Button и задайте свойство Caption = "Вернуть книгу". Для кнопки введите код:
procedure TForm1.Button2Click(Sender: TObject);
//переменная для сохранения позиции курсора в сетке
var pos:integer;
begin
//если книга уже возвращена (поле data_in заполнено)
if ADOQuery1.FieldByName('data_in').AsString<>'' then
begin
//выдаем сообщение об ошибке
Application.MessageBox('Книга указанным читателем уже возвращена',
'Операция отменена',
mb_OK+mb_iconstop);
//завершаем работу кода
Exit;
end;
//запоминаем позицию курсора в сетке с данными о выдаче
pos:=ADOQuery1.FieldByName('id_uchet').AsInteger;
//переводим запись о книге в режим редактирования
ADOQuery1.Edit;
//записываем дату возврата (текущая)
ADOQuery1.FieldByName('data_in').Value:=FormatDateTime('dd/MM/YYYY',date);
//сохраняем запись в БД
ADOQuery1.Post;
//устанавливаем курсор на сохраненное значение
ADOQuery1.Locate('id_uchet',pos,[]);
//далее в таблице books нужно поставить отметку, что книга возвращена
//переводим запись о книге в режим редактирования
ADOTable1.Edit;
//в поле label ставим отметку (цифра 0)
ADOTable1.FieldByName('label').Value:=0;
//сохраняем запись о книге
ADOTable1.Post;
end;
Удаление записи о выдаче книги
Нанесите на форму компонент Button и задайте свойство Caption = "Удалить". Для кнопки введите код:
procedure TForm1.Button3Click(Sender: TObject);
begin
//если данных о выдаче книги нет, то код не выполняется
if ADOQuery1.RecordCount=0 then Exit;
//если поле "data_in" пустое,
//т.е. книга еще не возвращена
if ADOQuery1.FieldByName('data_in').AsString='' then
begin
//выдаем сообщение об ошибке
Application.MessageBox('Нельзя удалить запись о невозвращенной книге',
'Отмена операции',
MB_OK+MB_IconStop);
//завершаем работу кода
Exit;
end;
//выдаем
запрос на удаление
if
Application.MessageBox('Удалить данные о выдаче?',
'Подтвердите операцию',
mb_yesno+mb_iconquestion+mb_defbutton2)=idYes then
//удаляем запись
ADOQuery1.Delete;
end;
Для удобства пользователя будем выполнять условное форматирование данных в сетке: выданные книги помечаются другим цветом. После возврата книги цветовая метка снимается.
Указания: в событии OnDrawColumnCell первой сетки введите код:
procedure
TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn;
State: TGridDrawState);
begin
//если книга не выдана (label=0)
if ADOTable1.FieldByName('label').AsInteger=0 then
//задаем цвет фона по умолчанию
DBGrid1.Canvas.Brush.Color:=clWindow
//если книга выдана (label<>0)
else
//задаем другой цвет фона (светло-красный)
DBGrid1.Canvas.Brush.Color:=$009A96D4;
//если ячейка является активной в сетке
if gdSelected in State then
//задаем для нее другой цвет
DBGrid1.Canvas.Brush.Color:=clMenuHighLight;
//закрашиваем сетку заданными цветами
DBGrid1.Canvas.FillRect(rect);
//после закраски выводим текст в сетке
DBGrid1.Canvas.TextOut(rect.left+2, rect.Top+2, Column.Field.Text);
end;
Для ускорения ввода данных реализуем интерактивный поиск книги или читателя по шифру. Для такого поиска на форме имеются два поля. По мере ввода цифр шифра в полое происходит автоматический переход курсора в сетке на найденную запись. В полях реализуем технологии PlaceHolder полей HTML: если поле пустое, то в нем отображается текст подсказка.
Указания: нанесите на форму компонент Edit (Standart) и задайте свойства: Text = "Введите шифр книги...", Font - Color = clGray.
По умолчанию в поле отображается текст подсказка. Если при входе в поле там находится данный текст, то считается, что поле пустое. В событии OnEnter поля введите код:
procedure TForm1.Edit1Enter(Sender: TObject);
begin
//если текст в поле равен начальному (данные не введены)
if Edit1.Text='Введите шифр книги...' then
//поле очищается
Edit1.Text:='';
//цвет текста становится черным
Edit1.Font.Color:=clBlack;
end;
При выходе из поля, если поле осталось пустым, то в поле отображается текст подсказка и текст отображается серым цветом. В событии OnExit поля введите код:
procedure TForm1.Edit1Exit(Sender: TObject);
begin
//если поле пустое
if Edit1.Text= '' then
begin
//в поле отображаем текст по умолчанию
Edit1.Text:='Введите шифр книги...';
//текст в поле делаем серым
Edit1.Font.Color:=clGray;
end;
end;
При вводе текста в поле, указатель в сетке автоматически переходит на найденную запись. В событии OnChange поля введите код:
procedure TForm1.Edit1Change(Sender: TObject);
begin
//в поле "shifr" таблицы "books" ищем текст из поля Edit1
ADOTable1.Locate('shifr',Edit1.Text,[loPartialKey]);
end;
Аналогично реализуем поиск читателя по шифру.
Нанесите на форму компонент Edit (Standart) и задайте свойства: Text = "Введите шифр читателя...", Font - Color = clGray.
По умолчанию в поле отображается текст подсказка. Если при входе в поле там находится данный текст, то считается, что поле пустое. В событии OnEnter поля введите код:
procedure TForm1.Edit2Enter(Sender: TObject);
begin
//если текст в поле равен начальному (данные не введены)
if Edit2.Text='Введите шифр читателя...' then
//поле очищается
Edit2.Text:='';
//цвет текста становится черным
Edit2.Font.Color:=clBlack;
end;
При выходе из поля, если поле осталось пустым, то в поле отображается текст подсказка и текст отображается серым цветом. В событии OnExit поля введите код:
procedure TForm1.Edit2Exit(Sender: TObject);
begin
//если поле пустое
if Edit2.Text= '' then
begin
//в поле отображаем текст по умолчанию
Edit2.Text:='Введите шифр читателя...';
//текст в поле делаем серым
Edit2.Font.Color:=clGray;
end;
end;
При вводе текста в поле, указатель в сетке автоматически переходит на найденную запись. В событии OnChange поля введите код:
procedure TForm1.Edit2Change(Sender: TObject);
begin
//в поле "shifr" таблицы "readers" ищем текст из поля Edit2
ADOTable2.Locate('shifr',Edit2.Text,[loPartialKey]);
end;
Выполните задание в СУБД SQL Server
Пусть имеется БД "Catalog" для хранения информации о книгах, читателях и выдачах книг.
- Books - таблица книг;
- Readers - таблица читателей;
- Uchet - таблица учета выдачи.

Скачайте готовую БД и разместите ее на сервере SQL Server.
Указания: распакуйте архив с файлами БД. Запустите утилиту "Среда SQL Server Management Studio". В левой части щелкните правой кнопкой на узле "Базы данных" и выберите команду "Присоединить". В новом окне в помощью кнопки "Добавить" укажите файд БД и щелкните ОК.
Скопируйте папку с проектом и внесите в него изменения для работы с БД SQL Server.
Указания: скопируйте папку с проектом. Откройте копию проекта и внесите ряд изменений.
Для подключения БД SQL Server у компонента ADOConnectuion (ADO) откройте свойство ConnectionString. В новом окне щелкните на кнопке "Build", затем выберите драйвер "Microsoft OLE DB Provider for SQL Server" и щелкните "Далее". Укажите имя сервера в формате "имя_компьютера, 1433", Пользователь = sa, Пароль = 111, База данных = catalog.
После выполненных изменений ваш проект будет работать с БД в формате SQL Server.