В предыдущей статье мы рассмотрели теорию управления 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;
...
Другие статьи по теме
|