Правильная ссылка на эту страницу
http://az-design.ru/Support/DataBase/DBTree2/2200.shtml

Расширение древовидной структуры

Архангельский Андрей

       Несмотря на то, что описанная структура выполняет возложенные на нее функции, но при визуализации в клиентской программе возникнут некоторые проблемы.
       1) Как определить наличие потомков у родителя?
       Весьма просто получить список всех корневых родителей:

Select * from People where Parent=0;

       Однако, при отображении этого списка на TreeView, нужно указать у какого родителя есть дети. Для того чтобы это определить нужно повторить вышеуказанный запрос для каждой строки набора, указав в условии "where Parent=" идентификатор текущей строки и посчитать количество строк, которые выдает этот запрос — если оно больше нуля, значит у этого родителя есть дети.

       (Есть достаточно много компонентов для Delphi, которые используют эту простую структуру и обещают отобразить дерево прямо из таблицы без дополнительных усилий со стороны разработчика. Например, в IBO4.6 есть компонент DBTreeView. Подробнее смотри пример Example00IBO)

       Проблема это возникает не только при отображении корневых родителей, но и при отображении любого узла. В этом случае, если узел имеет 500 потомков, то при его раскрытии необходимо выполнить 501 запрос, что существенно снижает производительность всей системы.
       Однако, если хранить количество детей в самой таблице, то при раскрытии узла потребуется только ОДИН запрос. Таблица теперь будет выглядеть следующи образом:

Create table People (
       PID        Integer not null primary key,
       Parent     Integer default 0,
       PCount     Integer default 0,
       Atribut1   Char(30),
       Atribut2   Char(100),
       Atribut3   Char(50));
Commit;
Alter table People add foreign key (Parent)
      references People on update cascade;
Commit;

       Изменять счетчик потомков можно с помощью набора тригеров:
       Триггер при вставке записи должен увеличить количество потомков у своего родителя

Create trigger Insert_People for People
active before insert position 0
as
Begin
   Update People p set p.PCount=p.PCount+1 where p.PID=new.Parent;
end

       Триггер по изменению проверяет, не меняется ли родитель элемента. Если да, то значит элемент перемещается от одного родителя к другому и нужно соответственно уменьшить PCount у старого и увеличить у нового родителя.

Create trigger Update_People for People
active before update position 0
as
Begin
  if (old.Parent<>new.Parent) then
     begin
       Update People p set p.PCount=p.PCount-1 where p.PID=old.Parent;
       Update People p set p.PCount=p.PCount+1 where p.PID=new.Parent;
     end
end

       При удалении нужно уменьшить количество потомков у родителя

Create trigger Delete_People for People
active before delete position 0
as
Begin
   Update People p set p.PCount=p.PCount-1 where p.PID=old.Parent;
end

       (Обратите внимание что во всех триггерах при обращении к таблице используется псевдоним p, а для полей в тригере используется уточнитель new или old. Это сделано для того, чтобы SQL-сервер не перепутал изменяемые поля в Update и поля таблицы в контексте триггера).
       Данная реализация отслеживания количества дочерних элементов приводит к тому, что одновременное добавление двух элементов к одному родителю приведет к блокировке (deadlock) обновления родительской записи. В много пользовательской среде такую ситуацию надо предусматривать — например, стараться делать добавление/удаление или перенос элементов в максимально коротких транзакциях read_committed, а при возникновении блокировки попытаться еще раз выполнить операцию без вывода сообщения об ошибке пользователю.
       Однако, большинство древовидных структур — это "справочники для чтения", просто потому что именно древовидная структура облегчает поиск элемента при отсутствии точной информации о его значении.
       2) Вторая проблема может считаться несущественной — сортировка "детей"
       Далеко не всегда сортировка элементов внутри ветки "Родитель-Потомки" выполняется по алфавиту того или иного атрибута. Выход достаточно простой — нужно ввести дополнительное поле, значение которого указывает на порядок сортировки всех потомков одного родителя. Тогда таблица приобретает следующий вид:

Create table People (
       PID         Integer not null primary key,
       Parent      Integer default 0,
       PCount      Integer default 0,
       POrder      Integer default 0,
       Atribut1    Char(30),
       Atribut2    Char(100),
       Atribut3    Char(50));
Commit;
Alter table People add foreign key (Parent)
      references People on update cascade;
Commit;

       Конкретное значение поля POrder имеет смысл только внутри одного узла и его можно генерировать при создании скрипта заполнения базы данных используя информацию из полей Atribut. Так, например, если перечисляются модели автомобилей ВАЗ, то в это поле можно записывать номера моделей — 2101, 2104, 2105, 2107, 2109, и т.д. Таким образом получиться предопределенный порядок записей.
       Можно также разрешить пользователю самому перестраивать порядок записей, тогда проще в это поле записывать последовательные значения. Однако возникает вопрос — что делать если две записи по каким-либо причинам имеют два одинаковых значения в поле POrder. Для самой таблицы и дерева никаких проблем не возникает. Однако, при попытке поменять положение двух записей с одинаковыми значениями POrder никаких изменений не произойдет. Возможное решение — при начале пересортировки определять и исправлять эту ситуацию. (см. "Изменение порядка узлов в дереве внутри одного узла").
       И, наконец, для того чтобы пример таблицы был более осмысленным, вместо атрибутов используем поля для описания структуры управления организацией (см. пример в файле DBCreate.doc):

Create table People (
       PID             AZInt32 not null primary key, /* Идентификатор   */
       Parent          AZInt32 default 0,            /* Родитель        */
       PCount          AZInt32 default 0,            /* Счетчик детей   */
       POrder          AZInt32 default 0,            /* порядок детей   */
       PSurName        AZLName not null,   /* фамилия, имя и отчество   */
       PBrDate         AZDate not null,    /* дата рождения             */
       PStatus         AZNames,            /* должность                 */
       PSalary         AZMoney,            /* зарплата                  */
Unique (PSurName, PBrDate));
Commit;
Insert into People(PID,Parent,PSurName,PBrDate)
            values(0,0,'','01/01/100');
Commit;
Alter table People add foreign key (Parent)
      references People on update cascade;
Commit;

       Несмотря на простоту таблицы, есть несколько подводных камней, которые необходимо отметить:
       — для описания всех полей используются домены. Это удобно, когда нужно переопределить какой-нибудь тип во всей БД. Кроме того, если использовать прямое обозначение типов данных, например, Integer, то для каждого упоминания этого типа данных будет создан свой домен типа RDB$123.
       — в IB7.5 в качестве комментариев приходится использовать классический вариант /*…*/, вместо более удобного в FireBird двойного тире.
       — в FireBird допускается в ограничении unique использовать поля со значениями Null. IB7.5 это не допускается — все поля входящие в уникальный ключ должны иметь ограничение not Null. Это создает проблемы, например, в этом примере. Невозможно уникально определить человека используя только Фамлию, Имя и Отчество. Необходимо использовать еще как минимум дату рождения. Но иногда дата рождения неизвестна и, в результате, невозможно ввести запись о человеке в таблицу. С другой стороны — использование минимально возможной даты приводит к путанице значений.

© 01.08.2009, Архангельский А.Г.

<<Пред. Оглавление
Начало раздела
След.>>




Дата последнего изменения:
Thursday, 21-Aug-2014 09:10:44 MSK


Постоянный адрес статьи:
http://az-design.ru/Support/DataBase/DBTree2/2200.shtml