Опубликован: 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 >
Евгений Медведев
Евгений Медведев
Не могу вставить модуль данных
Анна Зеленина
Анна Зеленина
пытаюсь повторить упражнение в лекции 5
Сергей Власюк
Сергей Власюк
Украина
Игорь Крещенников
Игорь Крещенников
Россия, Новосибирск