В работе с данными в проектах на Contenido существует два слоя абстракции: дескриптор таблицы и объект данных Contenido. Эти два объекта лежат один на другом (и на таблице оба), как сыр на масле. Следуя аналогии, вы взаимодействуете всегда с сыром, но между ним и таблицей всегда должно быть масло.
Объект Contenido (myproject::Document) |
|
|
Дескриптор таблицы (myproject::SQL::TableDescription) |
|
|
Таблица в БД |
Таблица
В минимальном исполнении должна иметь автоинкрементное поле id, с привязанным к ней primary index. Однако для полноценной интеграции таблицы в фреймворк желательно нагрузить ее следующими полями:
- id – идентификатор записи;
- class – класс документа (может быть опущен только в том случае, если все документы в таблицы принадлежат строго к одному классу; в этом случае в дескрипторе таблицы необходимо унаследовать от SQL::ProtoTable и переписать метод _single_class);
- name – название документа (поле, сильно желательно для документов, но необязательное для таблицы связей);
- ctime – дата/время создания записи;
- mtime – дата/время модификации записи;
- sections – integer или integer[], секция или массив секций, к которому привязан документ (не нужно для связей и секций);
- data – meta-поле для хранения extra-полей (экстра-свойствa или extra-properties).
Пример: таблица documents из стандартной структуры фреймворка:
create table documents
(
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" );
create index documents_dtime on documents (dtime);
Еще пример: таблица связей из стандартной структуры фреймворка:
create table links
(
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(),
status smallint not null default 1,
source_id integer not null,
source_class text not null,
dest_id integer not null,
dest_class text not null,
data text
);
create index links_source on links (source_id);
create index links_dest on links (dest_id);
.
Дескриптор таблицы
Данный объект:
- указывает на таблицу (метод db_table);
- указывает на используемый id-секвенсер (метод db_id_sequence)
- содержит в себе структуру используемых полей (метод required_properties);
- содержит список и описание используемых фильтров (метод available_filters и методы _*_filter) выборки;
- управляет сортировкой (метод _get_orders) данных при выборе из базы;
- содержит прочие методы, необходимые для инициализации и идентификации объектов в конкретной таблице.
Все дескрипторы таблиц из начальной установки Contenido можно найти в исходниках ядра в каталоге src/core/lib/SQL/ (от корня инсталляции). Все дескрипторы проектных таблиц необходимо складывать в каталог src/projects/myproject/lib/myproject/SQL/ (от корня инсталляции).
Проще всего рассмотреть пример описания таблицы на основе входящего в набор для создания проекта семпла SampleTable.pm:
package myproject::SQL::SampleTable;
use base 'SQL::ProtoTable';
sub db_table
{
return 'sample';
}
sub available_filters {
my @available_filters = qw(
_class_filter
_status_filter
_in_id_filter
_id_filter
_class_excludes_filter
_excludes_filter
);
return \@available_filters;
}
# ----------------------------------------------------------------------------
# Свойства храним в массивах, потому что порядок важен!
# Это общие свойства - одинаковые для всех документов.
#
# attr - обязательный параметр, название атрибута;
# type - тип аттрибута, требуется для отображдения;
# rusname - русское название, опять же требуется для отображения;
# hidden - равен 1, когда
# readonly - инициализации при записи только без изменения в дальнейшем
# db_field - поле в таблице
# default - значение по умолчанию (поле всегда имеет это значение)
# ----------------------------------------------------------------------------
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,
'db_field' => 'class',
'db_type' => 'varchar(48)',
'db_opts' => 'not null',
},
{ # Время создания документа, служебное поле...
'attr' => 'start',
'type' => 'datetime',
'rusname' => 'Дата/время начала',
'db_field' => 'start',
'db_type' => 'timestamp',
'db_opts' => 'not null',
},
{ # Время модификации документа, служебное поле...
'attr' => 'finish',
'type' => 'datetime',
'rusname' => 'Дата/время окончания',
'db_field' => 'finish',
'db_type' => 'timestamp',
'db_opts' => 'not null',
},
{ # Одно поле статуса является встроенным...
'attr' => 'external_id',
'type' => 'integer',
'rusname' => 'Внешний id',
'db_field' => 'external_id',
'db_type' => 'integer',
},
{ # Одно поле статуса является встроенным...
'attr' => 'event_id',
'type' => 'integer',
'rusname' => 'Событие',
'db_field' => 'event_id',
'db_type' => 'integer',
},
{ # Одно поле статуса является встроенным...
'attr' => 'place_id',
'type' => 'integer',
'rusname' => 'Место проведения',
'db_field' => 'place_id',
'db_type' => 'integer',
},
);
}
########### FILTERS DESCRIPTION ####################################################################################
sub _excludes_filter {
my ($self,%opts)=@_;
if (exists $opts{excludes}) {
# - исключение из отбора
my @eids = ();
if (ref($opts{excludes}) eq 'ARRAY') {
@eids = @{ $opts{excludes} };
} elsif ($opts{excludes} =~ /[^\d\,]/) {
warn "Contenido Warning: В списке идентификаторов для исключения встречаются нечисловые элементы. Параметр excludes игнорируется (".$opts{excludes}.").\n";
} else {
@eids = split(',', $opts{excludes});
}
my $excludes = join(',', @eids);
# Меняется логика запроса, если это join-запрос.
# Потому что в этом случае гораздо лучше ограничить выборку по таблице links,
# чем производить полную склейку таблиц.
if (@eids) {
if (exists($opts{ldest}) || exists($opts{lsource})) {
if (exists($opts{ldest})) {
return " (l.source_id not in ($excludes)) ";
} elsif (exists($opts{lsource})) {
return " (l.dest_id not in ($excludes)) ";
}
} else {
return " (d.id not in ($excludes)) ";
}
}
}
return undef;
}
sub _get_orders {
my ($self, %opts) = @_;
if ($opts{order_by}) {
return ' order by '.$opts{order_by};
} else {
return ' order by start desc,finish desc';
}
return undef;
}
1;
Данный объект наследуется от SQL::ProtoTable и переопределяет следующие методы:
db_table – возвращает название таблицы, с которой связывается данный дескриптор. В данном случае, таблица sample.
required_properties – возвращает список, формальное описание задействованных полей таблицы sample. На список полей, возвращаемый данным методом накладывается список, возвращаемый методом extra_properties, совместный список, возвращаемый методом structure, является списком полей Contenido-объекта. Каждый элемент списка полей является хешем (или словарем), в котором в обязательном порядке указывается название атрибута (ключ attr) объекта, тип (ключ type) для интерпретации в системе администрирования, смысловое название поля (ключ rusname) для именования в системе администрирования, название поля в БД (ключ db_field); более подробно список распознаваемых и задействованных в Contenido ключей – в отдельном разделе. Для нужд приложения список ключей описателя поля может быть как угодно расширен.
available_filters – список фильтров, которые применимы для создания выборки данных по таблице. Метод возвращает список методов, которые будут последовательно вызваны методом get_items при формировании SQL-запроса, и "сработают" (или нет) на один или несколько параметров, переданных приложением в get_items, и добавят WHERE и JOIN-ы в итоговый запрос.
В большинстве случаев нет необходимости наследовать дескриптор от SQL::ProtoTable, особенно при описании таблиц, схожих с documents или links. Гораздо проще взять таблицу документов (SQL::DocumentTable) или связей (SQL::LinkTable), "нагрузить" ее недостающими полями и при описании собственного дескриптора просто доопределить необходимые методы.
Пример:
package myproject::SQL::DocumentTable;
use strict;
use base 'SQL::DocumentTable';
sub available_filters {
my @available_filters = qw(
_class_filter
_status_filter
_in_id_filter
_id_filter
_name_filter
_class_excludes_filter
_sfilter_filter
_datetime_filter
_date_equal_filter
_date_filter
_previous_days_filter
_s_filter
_excludes_filter
_link_filter
_search_filter
_alias_filter
);
return \@available_filters;
}
sub required_properties
{
my $self = shift;
my @parent_properties = $self->SUPER::required_properties;
return (
@parent_properties,
{
'attr' => 'alias',
'type' => 'string',
'rusname' => 'Веб-алиас страницы',
'column' => 3,
'db_field' => 'alias',
'db_type' => 'text',
'rem' => 'Значение данного поля будет формировать веб-адрес конечной страницы в виде alias.html',
},
{
'attr' => 'subscribe',
'type' => 'checkbox',
'rusname' => 'Включить в рассылку?',
'hidden' => 1,
'db_field' => 'subscribe',
'db_type' => 'integer',
'db_opts' => "not null default 0",
'default' => 0,
},
);
}
sub _alias_filter {
my ($self,%opts)=@_;
return undef unless ( exists $opts{alias} );
return &SQL::Common::_generic_text_filter('d.alias', $opts{alias});
}
1;
.
Объект данных Contenido
Данный объект является верхней абстракцией ORM, с которой работают как система администрирования, так и приложения Contenido. Наследуется либо от Contenido::Document (объекты данных, документы), либо от Contenido::Section (проектные секции), либо от Contenido::Link (для организации связей многие-к-многим). В обязательном порядке содержит методы class_name, class_description; как правило, содержит методы class_table (возвращает имя класса дескриптора соответствующей таблицы) и extra_properties (содержит список дополнительных полей, сериализуемых в таблице в мета-поле data; также метод может переопределять параметры основных полей, возвращаемых методом required_properties дескриптора таблицы).
Все проектные объекты данных складываются в src/projects/myproject/lib/myproject/ (от корня инсталляции)
Пример:
package myproject::Article;
use strict;
use Contenido::Globals;
use base 'Contenido::Document';
sub extra_properties
{
return (
{ 'attr' => 'dtime', 'type' => 'date', 'rusname' => 'Дата', },
{ 'attr' => 'status', 'type' => 'status', 'rusname' => 'Статус',
cases => [
[0, 'Страница не активна'],
[1, 'Страница активна и присутствует в меню'],
[2, 'Страница активна и не присутствует в меню'],
[3, 'К удалению'],
],
},
{ 'attr' => 'abstr', 'type' => 'text', 'rusname' => 'Аннотация', rows => 5 },
{ 'attr' => 'body', 'type' => 'wysiwyg', 'rusname' => 'Полный текст документа', rows => 45 },
{ 'attr' => 'url', 'type' => 'url', 'rusname' => 'Ссылка на оригинал' },
{ 'attr' => 'source_name', 'type' => 'string', 'rusname' => 'Источник / исходящие данные' },
{ 'attr' => 'source_url', 'type' => 'string', 'rusname' => 'URL источника' },
{ 'attr' => 'file_1', 'type' => 'multimedia_new', 'rusname' => 'Файл / документ', softrename => 1 },
{ 'attr' => 'file_2', 'type' => 'multimedia_new', 'rusname' => 'Файл / документ', softrename => 1 },
{ 'attr' => 'file_3', 'type' => 'multimedia_new', 'rusname' => 'Файл / документ', softrename => 1 },
{ 'attr' => 'file_4', 'type' => 'multimedia_new', 'rusname' => 'Файл / документ', softrename => 1 },
{ 'attr' => 'file_5', 'type' => 'multimedia_new', 'rusname' => 'Файл / документ', softrename => 1 },
{ 'attr' => 'icon', 'type' => 'image', 'rusname' => 'Иконка', preview => ['240x240'], crop => ['150x150'] },
{ 'attr' => 'pictures', 'type' => 'images', 'rusname' => 'Список изображений', preview => ['240x240','400x400','900x750'], crop => ['150x150'] },
)
}
sub contenido_status_style
{
my $self = shift;
if ( $self->status == 2 ) {
return 'color:green;';
}
}
sub class_name
{
return 'Статья или веб-страница';
}
sub class_description
{
return 'Статья или веб-страница';
}
sub class_table
{
return 'myproject::SQL::DocumentTable';
}
sub pre_store
{
my $self = shift;
return 1;
}
1;
В примере присутствуют несколько дополнительных методов, унаследованных от Contenido::Object и Contenido::Document и переопределенных. Более подробно со списком методов можно ознакомиться в соответствующих разделах.
.