Brief: physical model
Исполнение
Хранимые данные и материализации
В базе данных хранятся первичные (DATA) свойства — все, кроме локальных, которые хранят значения только в рамках сессии, — и вычисляемые свойства, помеченные опцией MATERIALIZED: их значения лежат в поле таблицы, автоматически обновляются при изменении данных, от которых свойство зависит, и читаются прямо из базы. Отдельными полями хранятся принадлежность объекта классу (_CLASS_ИмяТаблицы) и признак полноты таблицы (_FULL_ИмяТаблицы) — последний только там, где полнота не обеспечена уже самим содержимым таблицы: таблица, хранящая принадлежность классу для всех потомков класса своего ключа, помечается полной сама и такого поля не получает.
Материализовать свойство можно тогда и только тогда, когда наборов объектов с не NULL значением у него конечное число. Материализация переносит стоимость с чтения на запись: значение не вычисляется при каждом обращении, но обновляется при каждом изменении исходных данных. Механизм — материализации.
sum = GROUP SUM sum(OrderDetail od) BY order(od) MATERIALIZED;
Аналогия: материализованное представление базы данных, обновляемое сразу при изменении исходных данных.
Таблицы и имена в БД
Таблица объявляется инструкцией TABLE:
TABLE name [dbName] (className1, ..., classNameN) [FULL | NODEFAULT];
Классы задают ключевые поля key0, ..., key(N-1) — по одному на класс, с нумерацией от нуля, — остальные поля — значения свойств. Таблицу свойства задаёт опция TABLE; без неё свойство попадает в ближайшую по классам ключей таблицу, NODEFAULT исключает таблицу из выбора, а без подходящей создаётся таблица _auto_.... FULL означает, что таблица содержит все объекты классов своих ключей, и влияет только на выполнение запросов. Механизм — таблицы.
Имена зависят от политики именования (db.namingPolicy):
| Политика | Таблица | Поле |
|---|---|---|
| С сигнатурой (умолчание) | Пространство_Таблица | Пространство_Свойство_Класс1_..._КлассN |
| Без сигнатуры | Пространство_Таблица | Пространство_Свойство |
| Краткая | Таблица | Свойство |
Индексы
Индекс по одному свойству создаётся опцией INDEXED, индекс по произвольному списку полей одной таблицы — инструкцией INDEX:
INDEX [dbName] [indexType] field1, ..., fieldN;
Индексировать можно только материализованные свойства. В составной индекс входят и материализованные свойства, и параметры, указывающие на ключевые поля; он должен содержать хотя бы одно материализованное свойство, а все свойства в нём — храниться в одной таблице и использовать один и тот же набор параметров. Типы LIKE и MATCH сохраняют обычный индекс и пытаются добавить специализированные — LIKE добавляет индекс LIKE, а MATCH на строковом поле добавляет сразу MATCH и LIKE, — что на строковых полях происходит только если у текущего адаптера БД включена поддержка триграмм / полнотекстового поиска, а на одиночном поле TSVECTOR MATCH создаёт только специализированный GIN-индекс. Уникальный индекс по всем ключам таблицы и индексы по суффиксам ключей keyK, ..., keyN создаются автоматически. Механизм — индексы.
orderDate = DATA DATE (Order) INDEXED;
INDEX supplier(Sku s, DATE d), s, price(s, d), d;
Пересчёт материализаций
Оператор RECALCULATE создаёт действие, которое заново вычисляет хранимые значения материализованного свойства из его определения:
RECALCULATE [CLASSES | NOCLASSES] propertyId(expr1, ..., exprN) [WHERE whereExpr]
К пересчёту обращаются, когда хранимые значения могли разойтись с определением, — после изменения определения свойства или прямого исправления данных в базе. WHERE ограничивает пересчитываемые наборы аргументов, CLASSES пересчитывает только данные о классах, NOCLASSES — только значения.
recalculateSum() {
RECALCULATE sum(Order o);
}
Модули
Модули и порядок
Модуль — функционально законченная часть проекта: объявления классов, свойств, действий, форм, событий, ограничений (модули). Один модуль — один файл .lsf, начинающийся с заголовка модуля: MODULE, REQUIRE, PRIORITY, NAMESPACE.
REQUIRE перечисляет модули, от которых текущий зависит. Зависимость транзитивна, циклы не допускаются, и по ней строится порядок инициализации: модуль инициализируется после всех своих зависимостей. От модуля System зависит любой модуль. Зависимость определяет и видимость: элемент можно найти по имени только в модуле-зависимости, поэтому расширять чужой функционал позволяет техника расширений, см. Brief: расширения.
Проект — совокупность модулей и сопутствующих файлов; по умолчанию модулями считаются все файлы .lsf в classpath сервера приложений, а параметры запуска logics.includePaths, logics.topModule, logics.orderDependencies сужают этот набор и переопределяют порядок.
Аналогия: пакет или сборка.
MODULE Sale;
REQUIRE System, Utils, Item;
NAMESPACE Sale;
Системные модули
Системные модули поставляются вместе с платформой — это стандартная библиотека, которую проект подключает через REQUIRE и не переопределяет (системные модули). Двенадцать из них платформа загружает сама — System, Service, Reflection, Authentication, Security, SystemEvents, Email, Icon, Scheduler, Time, Utils, UserEvents, — но загруженность не означает зависимость: неявной зависимостью каждого модуля является только System, а объявление из любого другого требует REQUIRE.
| Модуль | Для чего |
|---|---|
System | корневые типы, базовые классы, инфраструктура |
Utils | вспомогательные свойства и действия общего назначения |
Time | свойства и операции над датой и временем |
Authentication | пользователи, контакты, вход в систему |
Security | роли и политики доступа |
Service | сервисные действия и настройки сервера |
SystemEvents | события жизненного цикла сервера |
UserEvents | программный доступ к фильтрам и сортировкам формы |
Reflection | метаданные о навигаторе, формах, свойствах, таблицах |
Scheduler | запуск действий по расписанию |
Email | отправка и приём почты |
Icon | каталог иконок интерфейса |
Прикладные дополнения: Backup, Chat, Eval, Excel, Document / Word, Image / OpenCV, I18n, Integration, MasterData, Numerator, Hierarchy, Historizable, Geo, Printer / QZTray / Sound / Com, ProcessMonitor / Profiler, RabbitMQ / WebSocket, Messenger со своими Telegram / Slack / Viber / Whatsapp / Skype, SQLUtils, DefaultData, Schedule.
Всё это — обычные объявления в .lsf, а не примитивы языка: lpad, currentDate, currentUser — свойства, искать их следует в ветке paradigm.
Метапрограммирование
META и вызов метакода (@)
Метакод — блок кода lsFusion с параметрами, порождающий при использовании другой код. Объявляется инструкцией META — имя, список параметров, последовательность инструкций, завершающее END; используется инструкцией @, которая называет метакод и передаёт аргументы: каждый параметр метакода заменяется на переданный аргумент во всех местах использования.
Кроме подстановки, в метакоде есть операции над лексемами: ## объединяет две соседние лексемы в одну, ### делает то же, переводя первый символ второй лексемы в верхний регистр, кроме случая, когда все слева оказалось пустым, а сам токен не начинается с ###: тогда капитализация пропускается, и prefix###name при пустом prefix даёт name, а не Name. Ими собираются имена порождаемых элементов. Механизм — метапрограммирование.
Аналогия: макрос с текстовой подстановкой параметров.
META objectProperties(object, caption)
object##Name 'Имя '##caption = DATA BPSTRING[100](object);
object##Value 'Стоимость '##caption = DATA INTEGER (object);
END
@objectProperties(Document, 'документа'); // DocumentName 'Имя документа' = ...
Порождаемый код и аргументы вызова
Тело метакода состоит из инструкций уровня модуля, и сама инструкция @ пишется на уровне модуля: метакод порождает объявления элементов системы, а не операторы внутри тела действия.
Отсюда два типовых применения: семейство однотипных объявлений для переданного класса или префикса и набор элементов, добавляемый на произвольную форму через EXTEND FORM с именем формы в параметре (Brief: расширения).
Аргументом вызова может быть составной ID, идентификатор класса, литерал или пустой параметр — но не значение свойства: выбор поведения по данным делается не метакодом, а абстрактными свойствами и действиями.
Среда разработки дописывает после вызова @ порождённый код в фигурных скобках, чтобы работали навигация и анализ; платформа этот блок игнорирует и формирует код заново, поэтому править его вручную нельзя.
Расширения
Расширение классов
Инструкция EXTEND CLASS донаследует уже объявленный класс от новых родительских классов и добавляет ему новые статические объекты.
Структурно это выносит связи между классами в отдельный модуль: базовый модуль объявляет класс, зависящий от него модуль добавляет родителя, ничего в базовом не меняя. Механизм — расширение классов.
CLASS Box : Shape;
CLASS Quadrilateral;
EXTEND CLASS Box : Quadrilateral; // добавляем наследование
EXTEND CLASS ShapeType { // добавляем статический объект
circle 'Окружность'
}
Расширение свойств и действий
Абстрактное свойство или действие — объявленная точка расширения: базовый модуль оператором ABSTRACT задаёт контракт — классы параметров, вид выбора реализации и класс результата, который у свойства есть всегда, а у действия — только если оператор его назвал, — а другие модули добавляют реализации инструкцией += для свойств и инструкцией ACTION+ для действий. Виды выбора и опции — в Brief: свойства.
Структурно это отложенная сборка обычного оператора: для свойства — оператора выбора, для действия — оператора ветвления или последовательности. Реализации хранятся упорядоченным списком, и новая добавляется в его начало или конец.
Отсюда основной приём модульности: базовый модуль объявляет точку расширения, зависящие от него модули добавляют поведение, и обратной зависимости не возникает.
Расширение форм
Инструкция EXTEND FORM дополняет форму, объявленную в другом модуле, — теми же блоками, что и объявление FORM (см. Brief: формы): объекты, отображаемые свойства и действия, фильтры, сортировки. Отдельные блоки расширения меняют элементы, уже добавленные на форму; дизайн формы тоже настраивается извне.
Добавляемый элемент можно поставить до или после конкретного элемента формы либо в начало или в конец; для объектов эта позиция задаёт их место в порядке групп объектов, от которого зависят группа отображения свойства и группа объектов для фильтра. Механизм — расширение форм.
EXTEND FORM items
PROPERTIES(i) NEWSESSION DELETE // кнопка удаления
OBJECTS g = ItemGroup BEFORE i // группа товаров перед товаром
PROPERTIES(g) READONLY name
FILTERS itemGroup(i) = g
;
Отдельной логики расширения для навигатора и дизайна формы нет: эти конструкции расширяемы по определению (расширения).
Идентификация элементов
Идентификаторы и пространства имён
Именованные элементы системы — свойства, действия, пользовательские классы, формы, элементы навигатора, группы свойств и действий, окна, таблицы, метакоды: к каждому обращаются по имени (идентификация элементов).
Элемент создаётся в пространстве имён, которое задаёт модуль инструкцией NAMESPACE (по умолчанию — имя модуля), а инструкция PRIORITY перечисляет дополнительные пространства имён, приоритетные при поиске; обе — в заголовке модуля.
Обращение записывается простым идентификатором — name — или составным, с явным пространством имён — Sale.Document. Строка <пространство имён>.<имя> — полное имя элемента (именование).
Аналогия: пакеты и импорты, только приоритетное пространство задаётся один раз в заголовке модуля.
MODULE Sale;
REQUIRE System, Utils;
NAMESPACE Sale;
CLASS Document 'Документ'; // полное имя — Sale.Document
Именование
Именование рекомендует начинать имя элемента системы с маленькой буквы, имя класса — с большой, каждое следующее слово в имени — с большой: myFirstName, MySuperClass; сама грамматика идентификатора регистр не ограничивает. Уникальность требуется среди элементов одного типа, а не по всей системе: класс и форма могут носить одно и то же полное имя, и свойство с действием тоже. Внутри одного типа есть свои исключения — метакоды могут делить полное имя, если принимают разное количество параметров, свойства и действия — если различаются сигнатурой, — так что полное имя свойства само по себе не уникально.
Каноническое имя — строка, уникально определяющая элемент среди элементов того же типа. Для классов, групп свойств, элементов навигатора, окон и таблиц это полное имя; для свойств и действий к нему добавляется сигнатура — канонические имена классов параметров в квадратных скобках.
Sale.Document // класс
Item.gender[Item.Article] // свойство: полное имя и сигнатура
Каноническое имя связывает элемент с настройками политики безопасности и данными рефлексии. Для свойства, хранимого в базе данных, оно же задаёт имя поля по умолчанию — разработчик может указать это имя явно.
Поиск элементов
По короткому имени элемент находит алгоритм поиска, учитывающий: само имя (регистрозависимо), зависимость модулей — кандидат должен быть объявлен в модуле, от которого зависит модуль поиска, — явно указанное пространство имён, а при его отсутствии приоритет собственного пространства модуля и пространств из PRIORITY.
Для свойств и действий к этому добавляются классы параметров: подходят кандидаты, чьей сигнатуре соответствуют классы аргументов, из них выбирается наиболее конкретный. Сигнатуру можно указать в самом обращении: name[AClass](b).
Если подходящих кандидатов несколько или ни одного, платформа выдаёт ошибку при запуске.
Переименование и migration.script
Переименование элемента или перенос в другое пространство имён меняет каноническое имя, а значит разрывает связь элемента с уже сохранёнными данными и настройками. Соответствие старого и нового имён описывает файл migration.script в CLASSPATH сервера приложений: блоки V<номер версии> { ... }, из которых при старте применяются блоки с версией выше сохранённой в базе. Механизм — миграция.
| Тип изменения | Что сохраняет |
|---|---|
PROPERTY | настройки политики безопасности и рефлексии для свойства или действия |
STORED PROPERTY | то же плюс переименование поля в базе, то есть данные первичного свойства |
FORM PROPERTY | настройки таблицы для свойства или действия на форме |
CLASS | объекты пользовательского класса и связанные с ними данные — но не канонические имена первичных свойств, у которых класс входит в сигнатуру: они меняются вместе с ним, автоматически не отслеживаются и требуют собственных записей STORED PROPERTY, иначе сервер переименует осиротевшую колонку в _DELETED_ плюс ее прежнее имя в базе — а если колонка с таким именем уже есть, просто удалит ее, — и новое свойство начнет с пустого |
OBJECT | данные, связанные со статическим объектом |
TABLE | переименование таблицы вместо переноса записей |
NAVIGATOR | настройки политики безопасности для элемента навигатора |
V0.3.1 {
STORED PROPERTY Item.gender[Item.Article] -> Item.dataGender[Item.Article]
CLASS Date.DateInterval -> Date.Interval
}