ВыходВход

Меню сайта

Категории статей
Теория [1]
Древовидные структуры данных. Методология, описание
Использование в SQL DB [0]
Решения используемые в SQL DB
Использование в Perl [7]
Применение технологии Nested Sets в Perl программировании
Использование в PHP [2]
Применение технологии Nested Sets в PHP программировании

Меню пользователя

Поиск по статьям

Друзья сайта

Модуль Perl - 1. Создание, определение, удаление
» Каталог статей » Использование в Perl
Модуль Perl - 1. Создание, определение, удаление

Модуль Perl - 1. Создание, определение, удаление

В предыдущей статье мы рассмотрели теорию управления Nested Sets. Теперь попробуем собрать на её основе модуль Perl для работы.

Для начала, определим, сам объект:

package MyModule::NestedSets;
use strict; use warnings; use Carp;
require Exporter;
our @ISA = qw(Exporter);
sub new {
    my $self = shift;
    $self = {
             id     => 'id',
             left   => 'left_key',
             right  => 'right_key',
             level  => 'level', 
             table  => undef,
             DBI    => undef,
            };
    bless $self; 
    return $self;
}

Где:

  • $self->{'id'} - имя поля идентификатора узла  таблицы;
  • $self->{'left'} - имя поля левого ключа узла таблицы;
  • $self->{'right'} - имя поля правого ключа узла  таблицы;
  • $self->{'level'} - имя поля уровня узла  таблицы;
  • $self->{'table'} - имя таблицы;
  • $self->{'DBI'} - ссылка на объект DBI модуля - подключение к;

Пока все тривиально и просто, в объекте описаны имена полей и таблицы, в которой хранится наше дерево каталогов.

Теперь нужно определить какие методы мы будем применять к объекту.

  • создание узла;
  • удаление узла;
  • перемещение узла, подразделяющееся на:
    • установка узла в подчинение другому;
    • установка узла рядом с другим (впереди - за ним);
    • изменение уровня узла (выше, ниже на уровень);

Так же, существует реальная потребность в том, что бы хранить несколько деревьев в одной таблице (далее по тексту: "мультидерево"). Например, если существует два и более раздельных каталогов товаров. Для определения того, одно или несколько разных деревьев в таблице добавим в наш объект еще два свойства:

...
    $self = {
             ...
             type      => 'N',
             multi     => 'class',
            };
...

Где:

  • $self->{'type'} - Флаг определения статуса таблицы (N - одно дерево, M - несколько деревьев);
  • $self->{'multi'} - имя поля идентификатора дерева таблицы;

Объявление объекта можно производить так:

...
use MyModule::NestedSets;
    my $nested = new MyModule::NestedSets;
    $nested->{'table'} = 'catalog_category';
    $nested->{'type'} = 'M';
    $nested->{'DBI'} = $dbh; # $dbh должен быть уже определен как класс DBI 
...

Или, дабы упростить объявление:

...
use MyModule::NestedSets;
    my $nested = new MyModule::NestedSets {table=>'catalog_category', type=>'multi', DBI=>$dbh};
... 

Но при этом в процедуре - new модуля, нужно дополнительно обработать данные:

sub new {
    my ($self, $common) = @_;

    $self = {
              ...
             };
    $self->{'type'} = $$common{'type'} && $$common{'type'} eq 'multi' ? 'M' : 'N';
    $self->{'left'} = $$common{'left'} if $$common{'left'};
    $self->{'right'} = $$common{'right'} if $$common{'right'};
    $self->{'level'} = $$common{'level'} if $$common{'level'};
    $self->{'multi'} = $$common{'multi'} if $$common{'multi'};
    $self->{'table'} = $$common{'table'} if $$common{'table'};
    $self->{'DBI'} = $$common{'DBI'} if $$common{'DBI'};
    bless $self; 
    return $self;
}

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

Сама таблица будет выглядеть так:

CREATE TABLE `catalog_category` (
           `id`         int(11) NOT NULL auto_increment,
           `left_key`   int(11) NOT NULL default '0',
           `right_key`  int(11) NOT NULL default '0',
           `level`      int(11) NOT NULL default '1',
           `class`      int(11) NOT NULL default '1',    
           `name`       varchar(100),
           ...    
           `note`       varchar(100),
PRIMARY KEY (`id`),
KEY `child` (`id`,`left_key`,`right_key`,`class`)
);

Соответственно, если в таблице будет только лишь одно дерево, то поле class - не нужно.

Теперь можно перейти непосредственно к методам нашего объекта:

1. Создание узла

Как показывает практика, иногда требуется создавать узел в начале списка, а иногда - в конце. Причем данный параметр может распространяться как на все дерево, так и непосредственно только на конкретную операцию создания (перемещения). Поэтому добавим еще одно свойство объекта, которое мы будем определять во время его объявления, а так же сделаем возможность указывать данный параметр, во время операции. Изменяем процедуру new модуля:

sub new {
    ...
    $self = {
             ...
             order => 'B', # T - (top) начало списка, B - (bottom) конец списка 
            };
    ...
    $self->{'order'} = $$common{'order'} && $$common{'order'} eq 'top' ? 'T' : 'B';
    ...
}

Для того, что бы создать узел, нам нужны следующие данные:

  • подчинение (родитель) создаваемого узла;
  • идентификатор дерева, в котором создаем узел (если мультидерево)

Действия, которые мы должны произвести во время создания:

  • определить точку, где мы создаем узел;
  • создание "пустого" промежутка в дереве;
  • вставка нового узла в пустой подготовленный промежуток
sub insert_unit {
# Получаем объект, идентификатор родителя и идентификатор дерева
    my ($self, %common)= @_; 
# Инициализируем идентификатор дерева 
    my $catalog = $common{'tree'} || 1;
# Инициализируем идентификатор родителя  
    my $under = $common{'under'} || 'root';
# Определяем порядок создания (место в списке)  
    my $order = $common{'order'} || undef;
# Объявляем локальные переменные
    my ($key, $level);
# Если родитель корень дерева
    if ($under eq 'root') {
# если вставка в конец списка левый ключ создаваемого выбирается как
# максимальный правый ключ дерева + 1, уровень узла - 1 
        if (($order && $order eq 'top') || ($self->{'order'} eq 'T')) {
            $level = 1; $key = 1            
        } else { 
            my $sql = 'SELECT MAX('.$self->{'right'}.') + 1 FROM '.$self->{'table'}.
                ($self->{'type'} eq 'M' ? ' WHERE '.$self->{'multi'}.'= \''.$catalog.'\'' : ''); 
            my $sth = $self->{'DBI'}->prepare($sql); $sth->execute();
            $key = $sth->fetchrow_arrayref()->[0];
            $sth->finish(); 
            $level = 1;
            $key = $key || 1
        }
# Если родитель определен, то левый ключ создаваемого узла будет равным
# правому ключу родительского узла, уровень - родительский + 1 
    } else {
        my $sql = 'SELECT '.$self->{'right'}.', '.$self->{'left'}.', '.$self->{'level'}.
                  ($self->{'type'} eq 'M' ? ', '.$self->{'multi'} : '').
                  ' FROM '.$self->{'table'}.' WHERE '.$self->{'id'}.' = \''.$under.'\'';
        my $sth = $self->{'DBI'}->prepare($sql); $sth->execute();
        my $row = $sth->fetchrow_arrayref(); $sth->finish();
        $key = ($order && $order eq 'top') || ($self->{'order'} eq 'T') ? $$row[1] + 1: $$row[0]; 
        $level = $$row[2] + 1;
# Если у нас мультидерево, то переопределяем идентификатор дерева
# относительно родительского узла 
        $catalog = $$row[3] || undef;
    }
# Обновляем ключи дерева для создания пустого промежутка
# UPD: IF изменен на CASE для адаптирования под другие SQL диалекты
    $self->{'DBI'}->do('UPDATE '.$self->{'table'}.' SET '.
        $self->{'right'}.' = '.$self->{'right'}.' + 2, '.
        $self->{'left'}.' = CASE WHEN '.$self->{'left'}.' >= '.$key.' THEN '.$self->{'left'}.
        ' + 2 ELSE '.$self->{'left'}.' END WHERE '.$self->{'right'}.' >= '.$key.
        ($self->{'type'} eq 'M' ? ' AND '.$self->{'multi'}.'= \''.$catalog.'\'' : ''));
# Создаем новый узел
    $self->{'DBI'}->do('INSERT INTO '.$self->{'table'}.' SET '.
        $self->{'left'}.' = '.$key.', '.$self->{'right'}.' = '.$key.' + 1, '.
        $self->{'level'}.' = '.$level.
        ($self->{'type'} eq 'M' ? ', '.$self->{'multi'}.'= \''.$catalog.'\'' : ''));
# Получаем идентификатор созданного узла и возвращаем его в качестве результата
# UPD: LAST_INSERT_ID изменен на обычный запрос т.к. существует только в MySQL
    my $sth = $self->{'DBI'}->prepare('SELECT MAX('.$self->{'id'}.') 
                                       FROM '.$self->{'table'}.
                                       ($self->{'type'} eq 'M' ? 
                                             ' WHERE '.$self->{'multi'}.'='.$catalog.')' : '')); 
    $sth->execute();
    my $id = $sth->fetchrow_arrayref()->[0]; 
    $sth->finish();
    return $id
}

Вызов данного метода производится так:

...
    my $under = ... ; # Определяем родителя 
    my $tree = ... ;    # Определяем идентификатор дерева 
...  
    use MyModule::NestedSets;
    my $nested = new MyModule::NestedSets {table=>'catalog_category', type=>'M', DBI=>$dbh};
    my $new_unit = $nested->insert_unit(under=>$under, tree=>$tree, order=>'top');
... 

2. Определение узла

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

sub select_unit {
# Получаем объект, идентификатор узла 
    my $self  = shift;
    $self->{'unit'}->{'id'} = shift;
# Производим выборку данных узла*
    my $sql = 'SELECT '.$self->{'left'}.' AS lk, '.
                        $self->{'right'}.' AS rk, '.
                        $self->{'level'}.' AS lv '.
                        ($self->{'type'} eq 'M' ? ', '.$self->{'multi'}.' AS cl' : '').
              ' FROM '.$self->{'table'}.
              ' WHERE '.$self->{'id'}.' = \''.$self->{'unit'}->{'id'}.'\'';
    my $sth = $self->{'DBI'}->prepare($sql); $sth->execute();
    my $row = $sth -> fetchrow_hashref();
    $sth -> finish();
# Если узел существует, то передаем данные в объект 
    if ($row) { 
        $self->{'unit'}->{'left'} = $row->{'lk'}; 
        $self->{'unit'}->{'right'} = $row->{'rk'};
        $self->{'unit'}->{'level'} = $row->{'lv'}; 
        $self->{'unit'}->{'multi'} = $row->{'cl'} if $row->{'cl'};
        return $self
    } else {
        croak("Error: Your cann't select this unit, because unit is not exist!!!")
    } 
}

Хочу обратить внимание на то, что всем полям при выборке мы объявляем псевдонимы, потому как имена полей в таблице могут быт разные. Полученные данные, мы сохраняем в тот же объект $self, поэтому, добавим в описание объекта дополнительные свойства:

sub new {
    ...
    $self = {
             ...
             unit    => { 
                         id      => undef,
                         left    => undef,
                         right   => undef,
                         level   => undef,
                         multi   => undef,
                        },
            };
    ...
}

Вызов данного метода производится так:

...
    my $unit = ... # Определяем идентификатор узла 
...  
use MyModule::NestedSets;
my $nested = new MyModule::NestedSets {table=>'catalog_category', type=>'M', DBI=>$dbh};
$nested->select_unit($unit);
... 

3. Удаление узла

Во время удаления узла нам нужны данные только удаляемого узла, для определения которых мы воспользуемся вышеописанной процедурой. Действия, которые мы будем производить:

  • определение данных узла;
  • удаление узла и ему подчиненных;
  • обновление ключей дерева для устранения промежутка
sub delete_unit {
# Получаем данные: объект и идентификатор удаляемого узла 
    my ($self, $unit) = @_;
# получаем параметры узла 
    if ($unit) {$self = &select_unit($self, $unit)}
    elsif (!$self->{'unit'}->{'id'}) {
        croak("Error: Your must first select unit, for detete it!!!")     } # Определяем смещение ключей после удаления     my $skew = $self->{'unit'}->{'right'} - $self->{'unit'}->{'left'} + 1; # Удаляем узел     $self->{'DBI'}->do('DELETE FROM '.$self->{'table'}.' WHERE '.                        $self->{'left'}.' >= '.$self->{'unit'}->{'left'}.                        ' AND '.$self->{'right'}.' <= '.$self->{'unit'}->{'right'}.                        ($self->{'type'} eq 'M' ?                         ' AND '.$self->{'multi'}.'= \''.$self->{'unit'}->{'multi'}.'\'' : '')                       ); # Обновляем ключи дерева относительно смещения     $self->{'DBI'}->do('UPDATE '.$self->{'table'}.                        ' SET '.                        $self->{'left'}.' = CASE WHEN '.$self->{'left'}.' > '.$self->{'unit'}->{'left'}.                            ' THEN '.$self->{'left'}.' - '.$skew.' ELSE '.$self->{'left'}.' END, '.                        $self->{'right'}.' = '.$self->{'right'}.' - '.$skew.                        ' WHERE '.                        $self->{'right'}.' > '.$self->{'unit'}->{'right'}.' AND '.                        ($self->{'type'} eq 'M' ?                            $self->{'multi'}.'= \''.$self->{'unit_select'}->{'multi'}.'\'' : '')                       );     return 1 }

Вызов данного метода производится так:

...
    my $unit = ... # Определяем идентификатор узла 
... 
use MyModule::NestedSets;
my $nested = new MyModule::NestedSets {table=>'catalog_category', type=>'M', DBI=>$dbh};
$nested->delete_unit($unit);
...

или так:

...
    my $unit = ... # Определяем идентификатор узла 
... 
use MyModule::NestedSets;
my $nested = new MyModule::NestedSets {table=>'catalog_category', type=>'M', DBI=>$dbh};
$nested->select_unit($unit);
$nested->delete_unit;  
...


Другие статьи по теме
Категория: Использование в Perl | Добавил: phoinix (2005-10-31) | Автор: Сергей Томулевич (aka Phoinix)
Просмотров: 2880 | Рейтинг: 0.0 |

Комментарии

 

Бесплатный конструктор сайтов - uCoz