СДЕЛАЙТЕ СВОИ УРОКИ ЕЩЁ ЭФФЕКТИВНЕЕ, А ЖИЗНЬ СВОБОДНЕЕ

Благодаря готовым учебным материалам для работы в классе и дистанционно

Скидки до 50 % на комплекты
только до

Готовые ключевые этапы урока всегда будут у вас под рукой

Организационный момент

Проверка знаний

Объяснение материала

Закрепление изученного

Итоги урока

20.03.2020 ГРУППА 651. Лекция - Отчеты. Quick Report

Категория: Информатика

Нажмите, чтобы узнать подробности

Просмотр содержимого документа
«20.03.2020 ГРУППА 651. Лекция - Отчеты. Quick Report»

Лекция 10. Сохранение древовидных структур в базе данных

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

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


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

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

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

//сохраняем в файл:

TreeView1.SaveToFile('myfile.txt');

//читаем из файла:

TreeView1.LoadFromFile('myfile.txt');

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

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

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

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

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

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

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

Имя поля

Тип поля

Дополнение

1

R_Num

Счетчик

Ключевое поле

2

R_ Parent

Числовой

Целое

3

R_Name

Текстовый

Длина 50 символов

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

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


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

Как всегда, назовите форму fMain, в свойстве Caption напишите "Реализация сохранения дерева в БД", модуль формы сохраните как Main, а проект в целом назовите, например, TreeToBD. Сделанная база данных TreeBD должна быть в той же папке, что и проект.

Далее установите компонент TreeView (дерево) с вкладки Win32. Его свойству Align присвойте alLeft, чтобы дерево заняло весь левый край. Затем можете установить сплиттер - разделитель, ухватившись за который пользователь сможет менять ширину дерева. Компонент Splitterнаходится на вкладке Additional и его свойство Align по умолчанию равно alLeft - разделитель "прилепится" к правому краю дерева.

Правее установите сетку DBGrid с вкладки Data Controls, и его свойству Align присвойте alClient, чтобы сетка заняла все оставшееся место. Ни главное меню, ни панель инструментов нам здесь не потребуются, используем лишь два всплывающих PopupMenu - первый для дерева, второй для сетки (выберите соответствующие PopupMenu в свойстве PopupMenu этих компонентов).

Далее с вкладки ADO нам потребуется компонент ADOConnection для соединения с базой данных, таблица ADOTable и запрос ADOQuery для вспомогательных нужд. С вкладки Data Access - компонент DataSource, для связи сетки с таблицей. Подключите ADOConnection к базе данных и откройте соединение ( "ADO. Связь с таблицей MS Access" ). Таблицу подключите к ADOConnection (свойство Connection ), затем выберите в свойстве TableName нашу таблицу " Razdels ", а свойство Name переименуйте в tRazdels - так будем обращаться к таблице. Для удобства отображения названия полей откройте редактор полей таблицы (дважды щелкнув по ней), добавьте все поля и у каждого поля измените свойство DisplayLabel, соответственно, на "№", "Родитель" и "Название". Не забудьте открыть таблицу.

Компонент 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;

//снимаем возможное выделение у дерева:

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 в противном случае. В результате работы функции для пользователя будет выведено простое окно с запросом:


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

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

TreeView1.Selected:= nil;

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

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 автоматически переводят таблицу в режим редактирования, поэтому вызывать метод Edit излишне?

Обратите внимание на то, что мы сохраняем ноль в поле "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;

//вначале очистим 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().

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

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;

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

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

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

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. Пункты будут такими:

  • Очистить дерево

  • -

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

Для очищения дерева нам требуется просто очистить его свойство 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');

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

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

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, которой у нас еще нет. И в конце процедуры мы распахиваем все узлы полученного дерева.

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

private

{ Private declarations }

procedure TreeViewAddChild(rod, doch: String);

Здесь, в параметре rod мы будем передавать название родительского раздела, а в doch - название подраздела. Не убирая курсор с названия процедуры, нажмите 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);

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

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




Скачать

Рекомендуем курсы ПК и ППК для учителей

Вебинар для учителей

Свидетельство об участии БЕСПЛАТНО!