Вы находитесь на странице: 1из 15

1

Лабораторная работа №22


Древовидная структура в базе данных

Тема: Древовидная структура в базе данных с применением


компонента TreeView.
Цель: Изучение свойств и методов компонента TreeView, в структуре
базы данных.
Оборудование: IBM – совместимые компьютеры.
Место проведения: Компьютерный класс.
Техника безопасности: См. инструкцию.

Теоретическая часть

Древовидные структуры не относятся напрямую к программированию


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

Типичный пример дерева - всем знакомое дерево каталогов. Примеров


таких структур множество - это могут быть отделы в каком-либо учреждении
или разделы библиотеки. Посмотрим на рисунок с фрагментом дерева
разделов библиотеки:

Рис. 22.1. Дерево разделов


2

Основная сложность хранения деревьев в таблице - это то, что мы не


знаем заранее, какова будет глубина вложенности разделов. Можно было бы
создать таблицу с 10 полями, например. Но если вложенных разделов будет
меньше, то таблица будет неэффективна - останется много пустых полей. А
если больше - ограничивать пользователя?

Самый простой способ сохранения структуры дерева и ее считывания


обратно - воспользоваться тем, что дерево - это список узлов, и имеет хорошо
знакомые нам методы:

//сохраняем в файл:
TreeView1.SaveToFile('myfile.txt');
//читаем из файла:
TreeView1.LoadFromFile('myfile.txt');

Однако этот способ имеет массу недостатков. Во-первых, в результате


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

Когда программист впервые сталкивается с необходимостью хранения


древовидных структур в базе данных, обычно он первым делом
подключается к Интернету и ищет какой-нибудь компонент, который бы
позволил это делать. Но не все нестандартные компоненты работают
качественно, да и зачем искать какой-то новый компонент, когда имеется
стандартный TreeView на вкладке Win32 Палитры компонентов?

Рецептов работы с деревьями в базах данных много, рассмотрим один из них,


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

Выполнение работы:

Подготовка проекта

1. Для реализации примера нам потребуется новая база данных.


Загрузите MS Access и создайте базу данных " TreeBD ", а в ней таблицу
" Razdels ". Вообще-то, в базе данных MS Access как таблицы, так и поля
могут иметь русские названия, однако мы будем использовать средства SQL,
который не всегда корректно обрабатывает русские идентификаторы. Кроме
того, данный способ можно использовать в любой СУБД, а далеко не все из
них так предупредительны, как MS Access, поэтому название таблицы и ее
полей выполним латиницей.

Таблица имеет три поля:

Таблица 22.1 . Поля таблицы "Разделы"

№ Имя поля Тип поля Дополнение

1 R_Num Счетчик Ключевое поле

2 R_ Parent Числовой Целое

3 R_Name Текстовый Длина 50 символов

Созданную базу данных сохраните в папке, где будем разрабатывать наш


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

Далее создадим в Delphi новый проект и простую форму:


4

Рис. 22.2 . Форма для работы с деревом

2. Назовите форму fMain, в свойстве Caption напишите "Реализация


сохранения дерева в БД", модуль формы сохраните как Main, а проект в
целом назовите, например, TreeToBD. Сделанная база данных TreeBD
должна быть в той же папке, что и проект.
3. Установите компонент TreeView (дерево) с вкладки Win32. Его
свойству Align присвойте alLeft, чтобы дерево заняло весь левый край. Затем
можете установить сплиттер - разделитель, ухватившись за который
пользователь сможет менять ширину дерева. Компонент Splitter находится
на вкладке Additional и его свойство Align по умолчанию равно alLeft –
разделитель "прилепится" к правому краю дерева.
4. Правее установите сетку DBGrid с вкладки Data Controls, и его
свойству Align присвойте alClient, чтобы сетка заняла все оставшееся место.
Ни главное меню, ни панель инструментов нам здесь не потребуются,
используем лишь два всплывающих PopupMenu - первый для дерева, второй
для сетки (выберите соответствующие PopupMenu в свойстве PopupMenu
этих компонентов).
5. С вкладки ADO нам потребуется компонент ADOConnection для
соединения с базой данных, таблица ADOTable и запрос ADOQuery для
вспомогательных нужд. С вкладки Data Access - компонент DataSource, для
связи сетки с таблицей. Подключите ADOConnection к базе данных и
откройте соединение. Таблицу подключите к ADOConnection (свойство
Connection), затем выберите в свойстве TableName нашу таблицу " Razdels ",
а свойство Name переименуйте в tRazdels - так будем обращаться к таблице.
Для удобства отображения названия полей откройте редактор полей таблицы
(дважды щелкнув по ней), добавьте все поля и у каждого поля измените
5

свойство DisplayLabel, соответственно, на "№", "Родитель" и "Название". Не


забудьте открыть таблицу.
6. Компонент DataSource подключите к tRazdels, а сетку – к
DataSource, в сетке должны отобразиться поля. Кроме этого, переименуйте
свойство Name запроса ADOQuery1 в Q1, ведь нам часто придется
обращаться к нему по имени. Запрос также подключите к ADOConnection, но
делать его активным не нужно.

На этом приготовления закончены.

Создание и сохранение в таблицу дерева разделов

Работа с деревьями состоит из двух этапов:

1. Сохранение дерева в таблицу.


2. Считывание дерева из таблицы.

В этом разделе лекции разберем первый этап. Щелкните дважды по


компоненту PopupMenu1, который "привязан" к дереву, и создайте в нем
следующие разделы:

 Создать главный раздел


 Добавить подраздел к выделенному
 Переименовать выделенный
 Удалить выделенный
 -
 Свернуть дерево
 Развернуть дерево

Все эти команды относятся к работе с разделами дерева. Прежде всего,


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

{Создать главный раздел}


procedure TfMain.N1Click(Sender: TObject);
var
s: String; //для получения имени раздела (подраздела)
NewRazd: TTreeNode; //для создания нового узла дерева
begin
//вначале очистим s
s:= '';
//Получим в s имя нового раздела:
if not InputQuery('Ввод имени раздела',
'Введите заголовок раздела:', s) then Exit;
//снимаем возможное выделение у дерева:
6

TreeView1.Selected:= nil;
//создаем главный раздел (ветвь):
NewRazd:= TreeView1.Items.Add(TreeView1.Selected, s);
//Сразу же сохраняем его в базу:
tRazdels.Append; //добавляем запись
tRazdels['R_Parent']:= 0; //не имеет родителя
//присваиваем значение созданного раздела:
tRazdels['R_Name']:= NewRazd.Text;
//сохраняем изменения в базе:
tRazdels.Post;
end;

Разберем код. Переменная NewRazd имеет тип TTreeNode, к которому


относятся все разделы и подразделы (узлы) дерева. В текстовую переменную
s с помощью функции InputQuery() мы получаем имя нового главного узла.
Функция имеет три строковых параметра:

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

Если переменная, передаваемая в качестве третьего параметра, пуста, то поле


ввода будет пустым. Если же в ней содержался текст - он будет выведен как
текст "по умолчанию". Функция возвращает True, если пользователь ввел
(или изменил) текст, и False в противном случае. В результате работы
функции для пользователя будет выведено простое окно с запросом:

Рис. 22.3 . Окно функции InputQuery()

Далее строкой

TreeView1.Selected:= nil;

снимаем выделение, если какой либо раздел был выделен, ведь в программе
создается главный раздел, не имеющий родителя. Свойство Selected
компонента TreeView указывает на выделенный узел и позволяет
производить с ним различные действия, например, получить текст узла:
7

TreeView1.Selected.Text;

А присваиваемое значение nil (ничто) снимает всякое выделение, если


таковое было. Далее создаем сам узел:

NewRazd:= TreeView1.Items.Add(TreeView1.Selected, s);

Разберем эту строку подробней. Переменная NewRazd - это новый узел


дерева. Каждый узел - объект, обладающий своими свойствами и методами.
Все узлы хранятся в списке - свойстве Items дерева TreeView, а метод Add()
этого свойства позволяет добавить новый узел. У метода два параметра -
выделенный узел (у нас он равен nil ) и строка текста, которая будет
присвоена новому узлу. Таким образом, в дереве появляется новый главный
узел.

Затем сохраняем его в базу данных, предварительно добавив в таблицу


новую запись:

tRazdels.Append; //добавляем запись


tRazdels['R_Parent']:= 0; //не имеет родителя
//присваиваем значение созданного раздела:
tRazdels['R_Name']:= NewRazd.Text;
//сохраняем изменения в базе:
tRazdels.Post;

Методы, Append или Insert автоматически переводят таблицу в режим


редактирования.

Обратите внимание на то, что мы сохраняем ноль в поле "R_ Parent ", так как
это - главный раздел, не имеющий родителя. Свойство Text нового узла
NewRazd содержит название нового узла, которое присваиваем полю
"R_Name".

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


выделенному":

{Добавить подраздел к выделенному разделу(подразделу)}


procedure TfMain.N2Click(Sender: TObject);
var
s: String; //для получения имени раздела (подраздела)
z: String; //для формирования заголовка окна
NewRazd: TTreeNode; //для создания нового узла дерева
begin
//Проверим - есть ли выделенный раздел?
//Если нет - выходим:
if TreeView1.Selected = nil then Exit;
8

//вначале очистим s
s:= '';
//сформируем заголовок окна запроса:
z:= 'Раздел " + TreeView1.Selected.Text +
'";
//Получим в s имя нового раздела:
if not InputQuery(PChar(z), 'Введите заголовок подраздела:',s) then Exit;

//создаем подраздел:
NewRazd:= TreeView1.Items.AddChild(TreeView1.Selected, s);

//перед сохранением подраздела в базу, прежде получим


//номер его родителя:
Q1.SQL.Clear;
Q1.SQL.Add('select * from Razdels where R_Name="’+
NewRazd.Parent.Text+’"’);
Q1.Open;

//Теперь сохраняем его в базу:


tRazdels.Append; //добавляем запись
//присваиваем № родителя:
tRazdels['R_Parent']:= Q1['R_Num'];
//присваиваем название узла:
tRazdels['R_Name']:= NewRazd.Text;
//сохраняем изменения в базе:
tRazdels.Post;
end;

Код этой процедуры очень похож на код предыдущей, но есть и отличия.


Прежде всего, проверяем - а имеется ли выделенный раздел? Ведь фокус
ввода мог быть и на сетке DBGrid, когда пользователь щелкнул правой
кнопкой по дереву, и выбрал эту команду. В этом случае, если не делать
проверки, мы получим ошибку, пытаясь добавить дочерний узел к пустоте.

Далее, ввели строковую переменную z, чтобы сформировать запрос. Ведь


пользователю будет удобней, если в окне InputQuery() он сразу увидит, к
какому именно разделу он добавляет подраздел.

Затем, при добавлении дочернего узла вместо метода Add() используем


метод AddChild().

Ну и, наконец, при сохранении узла в таблицу записываем не только


созданный узел, но и номер его родителя, получив его с помощью запроса
9

Q1.SQL.Add('select * from Razdels where


R_Name="'+NewRazd.Parent.Text+'"');

Запрос формирует набор данных с единственной строкой - записью родителя


добавляемого элемента. Поле Q1['R_Num'], как вы понимаете, хранит номер
этого родителя в запросе.

Код процедуры переименования выделенного раздела выглядит так:

{Переименовать выделенный раздел (подраздел)}


procedure TfMain.N3Click(Sender: TObject);
var
s: String; //для получения имени раздела (подраздела)
z: String; //для формирования заголовка окна
begin
//Проверим - есть ли выделенный раздел?
//Если нет - выходим:
if TreeView1.Selected = nil then Exit;
//получаем текущий текст:
s:= TreeView1.Selected.Text;
//формируем заголовок:
z:= 'Редактирование "' + s + '"';
//если не изменили, выходим:
if not InputQuery(PChar(z), 'Введите новый заголовок:', s) then Exit;
//находим эту запись в таблице, учитывая, что ее по каким то
//причинам может и не быть:
if not tRazdels.Locate('R_Name', TreeView1.Selected.Text, [])
then begin
ShowMessage('Ошибка! Указанный раздел не существует в таблице.');
Exit;
end; //if
//если до сих пор не вышли из процедуры, значит запись найдена,
//и является текущей. изменяем ее:
tRazdels.Edit;
tRazdels['R_Name']:= s;
tRazdels.Post;
//теперь меняем текст выделенного узла:
TreeView1.Selected.Text := s;
end;

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


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

Удаляется выделенный узел при выполнении следующего кода:

{Удалить выделенный раздел (подраздел)}


procedure TfMain.N4Click(Sender: TObject);
var
s: String; //для строки запроса
begin
//Проверим - есть ли выделенный раздел?
//Если нет - выходим:
if TreeView1.Selected = nil then Exit;
//иначе формируем строку запроса:
s:= 'Удалить "' +
TreeView1.Selected.Text + '"?';
//запросим подтверждение у пользователя:
if Application.MessageBox(PChar(s), 'Внимание!',
MB_YESNOCANCEL+MB_ICONQUESTION) <> IDYES then Exit;
//если не вышли - пользователь желает удалить раздел.
//найдем и удалим его вначале из таблицы:
if tRazdels.Locate('R_Name', TreeView1.Selected.Text, []) then
tRazdels.Delete;
//теперь удаляем раздел из дерева:
TreeView1.Items.Delete(TreeView1.Selected);
end;

Сгенерируем процедуры для сворачивания и разворачивания дерева.

{свернуть дерево}
TreeView1.FullCollapse;

{развернуть дерево}
TreeView1.FullExpand;

Итак, метод FullCollapse дерева TreeView сворачивает его узлы, а метод


FullExpand разворачивает.

Теперь сохраните проект и скомпилируйте его. Попробуйте заполнить дерево


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

Чтение древовидной структуры из таблицы

Прежде всего, создадим пункты для второго всплывающего меню, которое


"привязано" к сетке DBGrid. Пункты будут такими:

 Очистить дерево
11

 -
 Заполнить дерево

Для очищения дерева нам требуется просто очистить его свойство Items,
делается это одной строкой:

TreeView1.Items.Clear;

Займемся заполнением дерева. Прежде разберемся с алгоритмом. Вначале


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

Создайте обработчик команды "Заполнить дерево". Код обработчика будет


таким:

{Заполнить дерево}
procedure TfMain.N10Click(Sender: TObject);
begin
//если таблица пуста, сразу выходим:
if tRazdels.IsEmpty then Exit;
//если в старом дереве есть узлы, очистим их:
TreeView1.Items.Clear;
//вначале запросим все главные узлы:
Q1.SQL.Clear;
Q1.SQL.Add('select * from Razdels where R_Parent=0');
Q1.Open;
if Q1.IsEmpty then Exit; //если НД пуст, выходим.
//теперь занесем их в дерево:
while not Q1.Eof do begin
TreeView1.Selected := nil;
TreeView1.Items.Add(TreeView1.Selected,
Q1.FieldByName('R_Name').AsString);
Q1.Next;
end; //while

//делаем запрос, выводящий пару: Родительский узел - Дочерний узел


//и поочередно прописываем их в дерево процедурой TreeViewAddChild:
Q1.SQL.Clear;
Q1.SQL.Append('select r.R_Name, d.R_Name '+
'from Razdels r, Razdels d '+
'where r.R_Num=d.R_Parent');
12

Q1.Open;
if Q1.IsEmpty then Exit; //если нет вложенных узлов, выходим
Q1.First;
while not Q1.Eof do begin
TreeViewAddChild(Q1.Fields[0].AsString, Q1.Fields[1].AsString);
Q1.Next;
end; //while

//раскрываем дерево:
TreeView1.FullExpand;
end;

Разберем этот код. В самом начале мы проверяем - не пуста ли таблица, и


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

Далее мы создаем запрос:

//вначале запросим все главные узлы:


Q1.SQL.Clear;
Q1.SQL.Add('select * from Razdels where R_Parent=0');
Q1.Open;
if Q1.IsEmpty then Exit; //если НД пуст, выходим.

Здесь после выполнения метода Open мы получаем все разделы, не имеющие


родителя. Иначе говоря, главные ветви дерева. Потом мы проверяем - а есть
ли главные узлы в таблице? Ведь таблица может быть пуста или испорчена,
и тогда дальнейшее выполнение программы не имеет смысла.

Если таблица не пуста и главные разделы в ней есть, то мы обходим


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

while not Q1.Eof do begin


TreeView1.Selected := nil;
TreeView1.Items.Add(TreeView1.Selected,
Q1.FieldByName('R_Name').AsString);
Q1.Next;
end; //while

В результате, в наше дерево пропишутся все главные разделы. После этого


нам нужно будет сделать еще один запрос, который выведет все записи,
13

имеющие родителя, в виде "Раздел - подраздел". Запрос формируется


следующим образом:

Q1.SQL.Clear;
Q1.SQL.Append('select r.R_Name, d.R_Name '+
'from Razdels r, Razdels d '+
'where r.R_Num=d.R_Parent');
Q1.Open;
if Q1.IsEmpty then Exit; //если нет вложенных узлов, выходим

Обратите внимание, в запросе мы используем две копии одной и той же


таблицы! Подробнее о псевдонимах таблиц в запросах смотрите "Краткий
курс языка запросов SQL" . В результате этого запроса мы получим
примерно такой набор данных:

Рис. 10.4 . Полученный набор данных

Далее мы обрабатываем полученный НД от первой до последней записи:

Q1.First;
while not Q1.Eof do begin
TreeViewAddChild(Q1.Fields[0].AsString, Q1.Fields[1].AsString);
Q1.Next;
end; //while

Здесь мы использовали обращение к полю не по имени, а по индексу, то


есть, Q1.Fields[0] - это первое поле. Как видно из рисунка, дважды обращаясь
в запросе к одному и тому же полю, мы получим разные названия этих полей
(R_Name и R_Name1). Поэтому обращаться к полю по его имени не
получится. В цикле мы двигаемся от первой записи к последней, вызывая
процедуру TreeViewAddChild, которой у нас еще нет. И в конце процедуры
мы распахиваем все узлы полученного дерева.
14

Теперь сделаем процедуру, которой будем передавать все полученные


подразделы. В начале модуля, в разделе private, объявите следующую
процедуру:

private
{ Private declarations }
procedure TreeViewAddChild(rod, doch: String);

Здесь, в параметре rod мы будем передавать название родительского раздела,


а в doch - название подраздела. Не убирая курсор с названия процедуры,
нажмите <Ctrl + Shift + C>. Эта комбинация клавиш автоматически
генерирует тело объявленной процедуры. Код процедуры следующий:

procedure TfMain.TreeViewAddChild(rod, doch: String);


var i : Integer; //счетчик
begin
//ищем родительский узел в дереве и выделяем его:
for i := 0 to TreeView1.Items.Count-1 do begin
//если родитель найден, выделяем его и прерываем цикл:
if TreeView1.Items[i].Text = rod then begin
TreeView1.Items[i].Selected := True;
Break;
end; //if
end; //for
//теперь родитель имеет выделение и мы можем добавить к нему
//наш узел:
TreeView1.Items.AddChild(TreeView1.Selected, doch);
end;

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

TreeView1.Items.AddChild(TreeView1.Selected, doch);

Вот и все. Сохраните проект, скомпилируйте и попробуйте программу в


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

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


любой СУБД для самых разных целей.
15

Студент должен знать: Основные положения теории баз данных,


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

Оценить