ДокументацияРазработка → Объектная модель данных. Интеграция таблиц в Contenido

Объектная модель данных. Интеграция таблиц в Contenido

В работе с данными в проектах на 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 и переопределенных. Более подробно со списком методов можно ознакомиться в соответствующих разделах.

.

Напишите нам

Практический пример. Расширение свойств SQL::DocumentTable
Документы, построенные на собственных (кастомных) таблицах
Связи, построенные на собственных (кастомных) таблицах
Дескриптор атрибутов объектов