Опубликован: 07.05.2010 | Уровень: специалист | Доступ: платный
Лекция 10:

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

< Лекция 9 || Лекция 10: 12 || Лекция 11 >

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

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

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

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

< Лекция 9 || Лекция 10: 12 || Лекция 11 >
Евгений Медведев
Евгений Медведев

В лекции №2 вставляю модуль данных. При попытке заменить name на  fDM выдает ошибку: "The project already contains a form or module named fDM!". Что делать? 

Анна Зеленина
Анна Зеленина

При вводе типов успешно сохраняется только 1я строчка. При попытке ввести второй тип вылезает сообщение об ошибке "project mymenu.exe raised exception class EOleException with message 'Microsoft Драйвер ODBC Paradox В операции должен использоваться обновляемый запрос'. 

Денис Попов
Денис Попов
Россия, Оренбург, Оренбургский государственный университет, 2015
Рустам Кадыров
Рустам Кадыров
Россия, Тирлян, Тирлянская школа №5, 2003