Описание схемы данных
Возможно, вы сталкивались с большими программными библиотеками, именуемыми труднопереводимым термином object-relational mapper (ORM). Не исключено также, что словосочетание "Описание схемы данных" напомнило вам характерную часть именно такого ПО. Внешнее сходство, определённо, имеется, но в Eludia всё ровно наоборот.
В директории Model располагаются файлы с мета- и данными, по одному на таблицу. Имена файлов получаются из имён таблиц приписыванием расширения .pm. Содержимое каждого такого файла – иерархический хэш, содержащий от 1 до 5 корневых компонент:
- columns: поля таблицы;
- keys: индексы;
- data: гарантированные данные;
- aliases: логические псевдонимы;
- label: человекочитаемый комментарий.
Вот пример такого описания:
label => 'Справочник валют', columns => { label => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255, REMARKS => 'Наименование валюты'}, code => {TYPE_NAME => 'char', COLUMN_SIZE => 3, REMARKS => 'Код валюты'}, }, keys => { label => 'label', }, data => [ {id => 1, fake => 0, code => 'RUR', label => 'рубли'}, {id => 2, fake => 0, code => 'EUR', label => 'евро'}, {id => 3, fake => 0, code => 'USD', label => 'доллары'}, ], aliases => [ 'currency_pay', 'currency_stock', ],
На первый взгляд, смешение мета- и данных может показатьтся нелогичным. Но в действительности это очень удобно. Дело в том, что описание данных в Eludia активно, именно оно формирует структуру и отчасти содержимое БД. То есть когда программисту требуется завести новую таблицу, добавить поле или построить индекс, ему достаточно дополнить описание модели – и структура БД при ближайшем запросе подстроится автоматически. Eludia-разработчик вообще никогда не отдаёт команду ALTER TABLE ... ADD COLUMN ... – он только пишет, какой в итоге должна стать таблица. При этом совершенно естественно, что данные небольших статических справочников тоже прописываются в конфигурационные файлы.
Сокращённая форма описаний полей
В вышеприведённом примере поля задаются хэшами, набор компонент которых восходит к результату функции DBI::column_info. Такой, вид описания хорош тем, что он явный, однако не совсем удобен для широкого использования: при большом количестве определяемых полей бОльшая часть текста загромождена повторяющимися наборами опций.
Наряду с полной формой описания можно применять сокращённую: просто символическое имя типа и комментарий (именно как комментарий, в смысле Perl)
columns => { label => 'string', # Наименование валюты ... },
Определения типов
Тип 'string', использованный выше, стандартом SQL не предусмотрен. Это одно из символических имён, которые раскрываются в полное описание через таблицу-словарь $conf -> {sql_types}. По умолчанию там определены следующие типы:
int => {TYPE_NAME => 'int', FIELD_OPTIONS => {type => 'string'}}, string => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255}, checkbox => {TYPE_NAME => 'tinyint', NULLABLE => 0, COLUMN_DEF => 0 }, radio => {TYPE_NAME => 'tinyint', NULLABLE => 0, COLUMN_DEF => -1 }, select => {TYPE_NAME => 'int'}, suggest => {TYPE_NAME => 'int'}, text => {TYPE_NAME => 'text'}, ref => {TYPE_NAME => 'int'}, money => { TYPE_NAME => 'decimal', COLUMN_SIZE => 10, DECIMAL_DIGITS => 2, FIELD_OPTIONS => { type => 'string', picture => '### ### ### ###,##' } },
Большинство из них совпадают по именам с типами полей ввода в draw_form — разумеется, не случайно, но подробнее об этом написано чуть ниже.
В Config.pm вы можете дополнить этот список собственными типами:
our $conf = { ... sql_types => { time => {TYPE_NAME => 'time', FIELD_OPTIONS => { type => 'select', values => [map {{id => $_, label => sprintf ('%02d:%02d:%02d', $_)}} (0 .. 23)], empty => '...', }}, }, };
или переопределить стандартные. Если символическое имя типа в $conf -> {sql_types} не найдено, считается, что оно совпадает с именем SQL-типа.
Указание размерности
Строка-описание поля может содержать не только имя типа, но и размерность. В частности,
code => 'char [3]' # Код валюты
равносильно
code => {TYPE_NAME => 'char', COLUMN_SIZE => 3, REMARKS => 'Код валюты'},
а
cost => 'money [5, 1]' # Стомость
разворачивается в
cost => { TYPE_NAME => 'decimal', COLUMN_SIZE => 5, DECIMAL_DIGITS => 1, FIELD_OPTIONS => { type => 'string', picture => '### ### ### ###,#' } },
Связь с отображением полей ввода
Если в описании поля прямо или косвенно определяется опция FIELD_OPTIONS, её значение передаётся в процедуру draw_form в качестве значений по умолчанию для отрисовки соответствующего поля ввода. Комментарий к полю (REMARKS) превращается в подпись поля (опция label).
Тип поля (type) по умолчанию совпадает с символическим именем типа (например, checkbox), но может переопределяться в FIELD_OPTIONS (см. выше money).
Отметим также, что маска вывода чисел (picture) автоматически подстраивается под заявленное число десятичных знаков (DECIMAL_DIGITS).
Поля-ссылки
Для полей типов select, radio и suggest, а также для полей-ссылок, не вводимых напрямую (например, ссылка parent на родительскую запись) бывает удобно указать целевую таблицу. В полном описании это опция ref, в сокращённом — имя таблицы в круглых скобках:
parent => '(mytable)' # Родительская запись id_user => 'select (users)' # Ответственный исполнитель
Если для ссылочного поля заявлена целевая таблица и её имя совпадает с именем соответствующего словаря как компоненты извлечённых данных $data (стандартная ситуация при использовании add_vocabularies), то в draw_form для такого поля можно не указывать опцию values. Для поля id_user в последнем примере можно определить поле как
{ name => 'id_user', },
при этом опции type, label и values будут определены автоматически. Собственно, упоминание данной таблицы-справочника в вызове add_vocabularies необходимо только в том случае, если при этом указываются какие-либо нестандартные параметры (filter, order и т. п.) Если же требуется полный справочник, отсортированный по label, то при отрисовке поля будет автоматически вызван sql_select_vocabulary на нужную таблицу.
Целевая таблица поля-ссылки может использоваться не только в draw_form, но и в sql — для определения условий JOIN.
Условия отключения
Как многое другое в Eludia.pm, описания таблиц и полей могут отключаться в зависимости от контекста приложения, и задаётся это опцией off. Например, тот факт, что при задании параметров связи с LDAP-сервером авторизация происходит вне БД приложения и, соответственно, поле users.password не имеет смысла, выражается следующим образом:
password => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255, off => 0 + %{$preconf -> {ldap}}},
аналогично можно определить опцию off и для целой таблицы, например, __access_log:
off => !$conf -> {core_session_access_logs_dbtable},
Приведённые примеры имеют отношение к таблицам, определяемым в Eludia/GenericApplication/Model относительно директории ядра.
Подробнее о разделе data
Как показано выше, раздел data содержит записи, наличие которых в таблице гарантируется ядром. Конкретно за это отвечает функция wish table_data.
При обновлении данных используется один из двух возможных ключей синхронизации. Ядро проверяет первую попавшуюся запись: если там находится компонента id, то ключом считается id, в противном случае — name. За то, что ключевое поле определено для всех записей, отвечает программист.
Ключ id имеет смысл использовать в тех случаях, когда записей немного, структура таблицы простая, и/или оправдано использование "волшебных" констант в коде программы. Например, если вы пишете систему учёта чего-либо, с вероятностью 0.95 относящегося к одной стране, и 0.05 — к остальному миру, то, определив в справочнике государств voc_countries
{id => 1, label => 'Эта страна', fake => 0},
логично в дальнейшем для всех ссылок id_voc_country проставлять значение по умолчанию 1.
С другой стороны, при описании шагов процессов документооборота числовые id, скорее всего, будут вам только мешать. Вместо них удобно использовать осмысленные символические имена:
{name => 'hr_hire_start', label => 'Начать оформление нового сотрудника', fake => 0},
Если вы используете разбиение кода по тематическим директориям, то имеет смысл разделять и описания таких таблиц, заводя в каждой поддиректории Model свой файл для данной таблицы со специфическим подмножеством data. Например, вышеприведённая строка логично смотрится в файле lib/_hr/Model/task_types.pm, а
{name => 'it_grant_approve', label => 'Подтвердить необходимость доступа к системе', fake => 0},
в lib/_it/Model/task_types.pm.
Поддержка SQL VIEWS
В Model можно описывать не только таблицы, но и VIEWS ("представления", "виды" или как их там ещё назвать по-русски). При этом исходный текст запроса указывается в качестве значения опции sql:
columns => { label => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255}, code => {TYPE_NAME => 'char', COLUMN_SIZE => 3}, }, sql => 'SELECT label, code FROM foreign_schema.not_my_table',
При этом опция data, очевидно, смысла не имеет; keys могла бы иметь: для MATERIALIZED VIEWS в Oracle и INDEXED VIEWS в MS SQL Server — но они пока не поддерживаются.
Практически все диалекты SQL поддерживают создание VIEW без явного указания списка столбцов, но у нас оно в настоящий момент обязательно. Данную метаинформацию, в частности, использует функция sql.
Все VIEW создаются в текущей схеме, предполагается, что текущий пользователь имеет право на их создание и удаление. При любом изменении описания соответствующая VIEW удаляется и пересоздаётся (DROP / CREATE или CREATE OR REPLACE в зависимости от диалекта SQL).
Механизм обновления
Здесь описан вышеупомянутый механизм обновления, составляющий часть стандартного обработчика запроса Eludia.
Параметрические изменения
Перед началом обслуживания каждого HTTP-запроса обработчик удостоверяется в том, что в ОЗУ загружена актуальная версия программного кода. Для запроса с типом $_REQUEST{type} проверяются следующие файлы:
- Content/$_REQUEST{type}.pm;
- Presentation/$_REQUEST{type}.pm.
- Content/subset.pm;
- Content/menu.pm;
- Config.pm;
Для каждого из них время последнего изменения (свойство файла) сравнивается с временем последней загрузки (хранящимся в памяти). Если выясняется, что файл изменился со времени последнего прочтения или не был ещё ни разу загружен, он перезагружается.
Если один из файлов понадобилось перезагрузить, то производится сканирование директории Model. Для каждого файла время последнего изменения (читается из файловой системы) сравнивается с временем последней актуализации (хранящимся в БД). Если выясняется, что файл изменился со времени последней актуализации, он считывается и БД приводится в соответствие с содержащимся в нём описанием:
- если таблицы с таким именем в БД нет, то она создаётся;
- создаются вновь все описанные поля, которых пока нет в БД;
- модифицируются все поля, для которых требуется:
- изменить тип (например, int на varchar);
- увеличить размерность или точность (например, varchar (50) на text или decimal (10,1) на decimal (10,2));
- изменить значение по умолчанию и/или опцию NOT NULL;
- создаются вновь все описанные индексы, которых пока нет в БД;
- пересоздаются все индексы, для которых описание отличается от содержащегося в БД;
- [пере]создаются все записи в соотствии с содержимым раздела data.
Заметим, что в соответствии с Model-описанием производятся только недеструктивные изменения. Поля и индексы, а также словарные строки, не упомянутые в описании, не удаляются. Как правило, это и не требуется: разработчику гораздо спокойнее, когда он уверен, что его опечатка в описании схемы данных никогда не приведёт к уничтожению данных.
Update-скрипты
Увы, не исключена ситуация, когда наличие лишних полей или индексов окажет существенное влияние на эффективность приложения. Или вообще, (что, заметим, происходит гораздо чаще) потребуется некая непараметризуемая модификация данных. Такое случается при заполнении вновь добавленных полей функциями от ранее введённых данных, в частности, при замене отношения 1-к-1 на 1-ко-многим и 1-ко-многим на многие-ко-многим. В общем, нужны скрипты модификации данных.
Такие скрипты содержатся в директории Updates под именами *.pl. Они запускаются при тех же условиях, что актуализируются описания из Model, но после них. Текст Update-скрипта представляет собой Perl-код, который исполняется в контексте приложения, со всеми глобальными переменными и загруженными внешними модулями. Для каждого файла из директорий Model и Updates в БД сохраняется время его последнего использования, а на время актуализации каждого файла механизм обновлений переходит в однозадачный режим, что обеспечивает одноразовость каждого действия.
Если скрипт исполняется достаточно долго, то однозадачность при высокой загрузке системы конкурирующими запросами может привести к переполнению и исчерпанию ресурсов. По этой причине:
- проектируя Update-скрипт, всегда стоит оценивать время его исполнения на рабочей БД;
- публиковать обновления желательно при минимальной нагрузке (например, в нерабочее время);
- если Update-скрипт не может быть исполнен достаточно быстро для того, чтобы это прошло незаметно для пользователей, следует заменить его offline-скриптом и исполнить его самостоятельно: в этом случае сам по себе запуск все запросы к системе не перекроет, хотя не исключены блокировки на уровне БД.