Данный раздел – практическое руководство, расширяющее базовын понятия, описанные в разделе "Объектная модель данных. Интеграция таблиц в Contenido".
На базе изначально созданных таблиц (или слегка расширив их, как показано в примере о расширении SQL::DocumentTable) можно быстро построить, например, простой корпоративный или новостной сайт. Но рано или поздно придет проджект-менеджер и попросит сделать какой-то функционал, который ну никак не укладывается в набор sections/documents/links. Тогда вы садитесь и начинаете проектировать новые таблицы, подчинения и связи.
Второй пример. Вам достался проект с грамотно спроектированной структурой данных, но написанный на каком-нибудь настолько архаичном диалекте полузабытого языка (или вообще на PHP), что проще сесть и заново написать весь функционал вебсайта.
В общем, приходит время завести в Contenido-проект свои, на сленге разработчиков фреймворка – кастомные (от слова custom) – таблицы.
Как уже упоминалось, для того, чтобы в процессе работы образовать из строк таблицы объекты, необходимо и обязательно определить поля id и class (впрочем, последнее не так уж и обязательно). Для обращения к элементам таблицы из приложения больше ничего и не нужно.
Для системы администрирования (если пользователям необходим доступ для поиска и редактирования) придется добавить еще несколько полей. Чтобы пользователь системы администрирования мог как-то различать документы в списках желательно задействовать поле name (в списках оно автоматически преобразуется в ссылку на редактирование) или какое-нибудь другое (или несколько других) поле, идентифицирующее документ. Чтобы не потерять объекты в структуре секций, необходимо поле sections (и связанный с ним фильтр _s_filter); изначально поле sections спроектировано, как integer[], но можно использовать и обычный тип integer. И поле status – тип integer или smallint, без него в error_log системы администрирования будут сыпаться ошибки.
Последнее поле – data. Опять же, можно обойтись и без него, если вы не планируете использовать extra-поля.
Получается стандартная обвязка Contenido-таблицы.
create table sampletable
(
id integer not null primary key default nextval('public.documents_id_seq'::text),
class text not null,
status smallint not null default 0,
sections integer[],
name text,
data text
);
create index documents_sections on documents using gist ( "sections" "gist__int_ops" );
которую вы вольны расширить любым набором собственных полей. Но в таком аскетическом виде в системе администрирования документы в списках видны не будут, а в error_log будут сыпаться ошибки о неправильных SQL-запросах. Не хватает еще полей: ctime (дата/время создания документа), mtime (дата/время последнего изменения документа) и dtime (просто дата/время). Необходимо либо доопределить их, либо переопределить умолчательный метод сортировки.
Отбросив несвойственный нам аскетизм, получаем... клон таблицы documents:
create table sampletable
(
id integer not null primary key default nextval('public.documents_id_seq'::text),
class text not null,
ctime timestamp not null default now(),
mtime timestamp not null default now(),
dtime timestamp not null default now(),
status smallint not null default 0,
sections integer[],
name text,
data text
);
create index documents_sections on documents using gist ( "sections" "gist__int_ops" );
Дальнейшие шаги точно такие, как и в разделе о расширении свойств SQL::DocumentTable: создание дескриптора, создание Contenido-классов на базе дескриптора.
Создание дескриптора на базе SQL::ProtoTable. Все своими руками
Пример созданного на базе SQL::ProtoTable лежит в свежесозданном проекте в src/projects/myproject/lib/myproject/SQL/. Для разнообразия приведем пример другой таблицы, максимально скромной. Пусть это будет таблица стран:
create table coutries
(
id integer not null primary key default nextval('public.documents_id_seq'::text),
class text not null,
status smallint not null default 0,
sections integer,
pid integer,
name text,
data text
);
Здесь pid – указатель на родительскую страну, протекторат и т.д. (например, Испания для Тенерифе или Великобритания для Англии), взят просто для примера. Кроме того, поле sections имеет класс integer, так как список стран мы поместим в одну единственную секцию, где его можно расширять и редактировать. Поле data оставим для хранения кратких описаний и изображения флагов.
Назовем класс myproject::SQL::CountryTable и поместим его в src/projects/myproject/lib/myproject/SQL/.
package myproject::SQL::CountryTable;
use base 'SQL::ProtoTable';
sub db_table
{
return 'countries';
}
sub available_filters {
my @available_filters = qw(
_class_filter
_status_filter
_in_id_filter
_id_filter
_name_filter
_s_filter
_class_excludes_filter
_excludes_filter
_pid_filter
);
return \@available_filters;
}
sub required_properties
{
return (
{ # Идентификатор документа, сквозной по всем типам...
'attr' => 'id',
'type' => 'integer',
'rusname' => 'Идентификатор документа',
'hidden' => 1,
'auto' => 1,
'readonly' => 1,
'db_field' => 'id',
'db_type' => 'integer',
'db_opts' => "not null default nextval('public.documents_id_seq'::text)",
},
{ # Класс документа...
'attr' => 'class',
'type' => 'string',
'rusname' => 'Класс документа',
'hidden' => 1,
'readonly' => 1,
'column' => 3,
'db_field' => 'class',
'db_type' => 'varchar(48)',
'db_opts' => 'not null',
},
{ # Статус
'attr' => 'status',
'type' => 'status',
'rusname' => 'Статус',
'db_field' => 'status',
'db_type' => 'smallint',
},
{ # Родитель
'attr' => 'pid',
'type' => 'integer',
'rusname' => 'Родительская страна',
'allow_null' => 1,
'db_field' => 'pid',
'db_type' => 'integer',
},
{ # Название
'attr' => 'name',
'type' => 'string',
'rusname' => 'Название страны',
'column' => 1,
'db_field' => 'name',
'db_type' => 'text',
},
{ # Массив секций
'attr' => 'sections',
'type' => 'sections_list',
'rusname' => 'Секции',
'hidden' => 1,
'db_field' => 'sections',
'db_type' => 'integer',
},
);
}
sub _pid_filter {
my ($self,%opts)=@_;
return undef unless ( exists $opts{pid} );
return &SQL::Common::_generic_int_filter('d.pid', $opts{pid});
}
sub _s_filter {
my ($self,%opts)=@_;
return undef unless ( exists $opts{s} );
return &SQL::Common::_generic_int_filter('d.sections', $opts{s});
}
sub _get_orders {
my ($self, %opts) = @_;
if ($opts{order_by}) {
return ' order by '.$opts{order_by};
} else {
return ' order by name';
}
return undef;
}
1;
Переопределенный метод db_table возвращает название таблицы. Переопределенный метод available_filters определяет список фильтров, к которым будет чувствительен построитель запросов, в частности, к списку стандартных фильтров добавлен собственный фильтр _pid_filter, отбирающий документы по родителю.
Поскольку поле sections в данной таблице не массив integer[], а просто integer, то фильтр _s_filter также переопределен.
Добавлен фильтр _pid_filter – близнец фильтра по полю секций, но реагирующий на другой параметр.
И для совместимости с админкой переопределен фильтр _get_orders, отвечающий за сортировку полей, а также за сортировку объектов по умолчанию. Поскольку в данной таблице отсутствует поле dtime, умолчательной принята сортировка по возрастанию по полю name.
Создание дескриптора на базе SQL::DocumentTable
В случае, если кастомная таблица построена с привлечением полной обвязки documents, для построения дескриптора можно наследоваться от класса, обслуживающего данную таблицу. Пример:
CREATE TABLE news (
id integer DEFAULT nextval(('public.documents_id_seq'::text)::regclass) NOT NULL,
class text NOT NULL,
ctime timestamp without time zone DEFAULT now() NOT NULL,
mtime timestamp without time zone DEFAULT now() NOT NULL,
dtime timestamp without time zone DEFAULT now() NOT NULL,
status smallint DEFAULT 0 NOT NULL,
uid integer DEFAULT 0,
ext_id integer,
votes integer,
value integer,
rating integer,
sections integer[],
name text,
data text
);
CREATE INDEX news_sections ON news USING gist (sections);
CREATE INDEX news_dtime ON news USING btree (dtime);
CREATE INDEX news_user ON news USING btree (uid);
Строим дескриптор, наследуя его от SQL::DocumentTable:
package myproject::SQL::NewsTable;
use strict;
use base 'SQL::DocumentTable';
sub db_table
{
return 'news';
}
sub available_filters {
my $self = shift;
my $available_filters = $self->SUPER::available_filters;
push @$available_filters, qw(
_search_filter
_uid_filter
_ext_id_filter
);
return $available_filters;
}
sub required_properties
{
my $self = shift;
my @parent_properties = $self->SUPER::required_properties;
return (
@parent_properties,
{
'attr' => 'uid',
'type' => 'integer',
'rusname' => 'Идентификатор пользователя',
'db_field' => 'uid',
'db_type' => 'integer',
'db_opts' => "not null default 0",
},
{
'attr' => 'external',
'type' => 'integer',
'rusname' => 'Внешний ID новости',
'db_field' => 'ext_id',
'db_type' => 'integer',
'db_opts' => "default 0",
},
{
'attr' => 'votes',
'type' => 'integer',
'rusname' => 'Кол-во голосов',
'db_field' => 'votes',
'db_type' => 'integer',
'default' => 0,
},
{
'attr' => 'value',
'type' => 'integer',
'rusname' => 'Сумма голосов',
'db_field' => 'value',
'db_type' => 'integer',
'default' => 0,
},
{
'attr' => 'rating',
'type' => 'integer',
'rusname' => 'Рейтинг',
'db_field' => 'rating',
'db_type' => 'integer',
'default' => 0,
},
);
}
sub _search_filter {
my ($self, %opts) = @_;
return undef unless exists $opts{search} && $opts{search};
my @fields = qw ( name data );
if ( ref $opts{search} eq 'ARRAY' && @{$opts{search}} ) {
my @str;
map { $_ =~ s/'/''/g } @{$opts{search}};
foreach my $field ( @fields ) {
push @str, (join ' AND ', map { "($field ILIKE '$_')" } @{$opts{search}} );
}
my $str = join ' OR ', map { "($_)" } @str;
return "($str)";
} elsif ( ref $opts{search} eq 'ARRAY' ) {
return undef;
} else {
my $str = join ' AND ', map { "($_ ILIKE '$opts{search}')" } @fields;
return "($str)";
}
return undef;
}
sub _uid_filter {
my ($self,%opts)=@_;
return undef unless ( exists($opts{uid}) );
return &SQL::Common::_generic_int_filter('d.uid', $opts{uid});
}
sub _ext_id_filter {
my ($self,%opts)=@_;
return undef unless ( exists $opts{ext_id} );
return &SQL::Common::_generic_int_filter('d.ext_id', $opts{ext_id});
}
1;
В данном примере методы available_filters и required_properties не переписывают, а доопределяют родительские. Также ненавязчиво продемонстрировано следующее:
- Название поля объекта совсем не обязательно должно совпадать с названием поля таблицы и с названием аргумента вызова фильтра. В примере таблица имеет поле ext_id, которое отображается на поле external Contenido-объекта. В практическом применении – если вам приходится работать с "чужими" талицами, в которых отсутствует какое-либо из стандартных полей (например, вместо поля name используется name_rus), то "втащить" эту таблицу под админку можно, просто поставив соответствие названием атрибута;
- Фильтры могут "захватывать" произвольное количество полей, на одно и то же поле можно накладывать различные фильтры. Пример – фильтр тупого like-поиска по полям name и data (да-да, по всем extra-полям и элементам json сразу);
.
Создание Contenido-объекта, связанного с дескриптором.
Производится в точности, как в примере о расширении свойств SQL::DocumentTable. Переопределяется метод class_table, возвращающий класс дескриптора соответствующей таблицы:
sub class_table
{
return 'myproject::SQL::NewsTable';
}
.