Перейти к основному содержимому
Версия: 7.0

Brief: domain logic

Классы

Что такое класс

Класс — это множество объектов и базовый элемент, которым типизируется всё остальное: сигнатура свойства или действия — это классы его параметров, а у каждого объекта формы есть свой класс. Классы могут наследоваться, в том числе от нескольких родителей сразу.

CLASS [ABSTRACT] name [caption] [: parent1, ..., parentN];
CLASS [NATIVE] name [caption] [{ objectName1 [objectCaption1], ... }] [: parent1, ..., parentN];

Вторая форма объявляет класс со СТАТИЧЕСКИМИ объектами — фиксированным именованным набором, заданным в коде, а не создаваемым во время работы.

Аналогия: классы ООП, только диспетчеризация множественная — по классам всех параметров, а не одного получателя.

Класс — это не таблица

Это самое дорогое непонимание модели, поэтому стоит сказать прямо. Таблица хранит не объекты класса, а значения свойств. Её ключевые поля хранят идентификаторы объектов, а типизируются эти поля классами параметров тех свойств. Свойство, объявленное без явного TABLE, попадает в таблицу, чьи классы ключей ему подходят. То есть соответствие классов таблицам — следствие того, как объявлены свойства, а не решение объявления класса. См. Brief: физическая модель.

Полиморфизм

Поведение специализируется по классу через ABSTRACT-свойства и действия с реализациями +=, а также через MULTI. См. абстрактные свойства и расширение классов.

Свойства

Выражения и композиция

Свойство принимает набор объектов-параметров и возвращает ровно одно значение — как чистая функция, но вычисляемая сразу на всей базе, как колонка SQL-запроса. Значение либо хранится (первичное свойство, оператор DATA), либо вычисляется выражением: арифметика, логика (AND, OR, NOT), сравнения, операции со строками, проверка и приведение класса (IS, AS). Функции строк, чисел и дат (lpad, substr, mod, currentDate) — не операторы, а свойства модулей Utils и Time.

Подстановка свойства в выражение другого свойства — композиция; оператор JOIN записывает ее явно. Список создающих свойства операторов — Операторы.

price = DATA NUMERIC[14,2] (Item);
vat = DATA NUMERIC[6,2] (Item);
priceWithVAT (Item i) = price(i) * (1 + vat(i) / 100);

Группировка (GROUP)

Оператор GROUP разбивает все наборы объектов на группы и вычисляет для каждой одну агрегирующую функцию — SUM, MAX, MIN, CONCAT, LAST, EQUAL, AGGR / NAGGR, CUSTOM (агрегат СУБД):

GROUP
type [expr1, ..., exprN]
[orderClause]
[TOP topExpr] [OFFSET offsetExpr]
[WHERE whereExpr]
[BY groupExpr1, ..., groupExprM]

Отдельной функции подсчета нет: количество считается как GROUP SUM 1. Аналогия: GROUP BY в SQL, только результат — самостоятельное свойство, а не часть запроса. Механизм — какие наборы объектов попадают в группу, какими получаются параметры создаваемого свойства и где важен порядок — описан в группировке.

sold (Sku s) = GROUP SUM quantity(OrderDetail d) BY sku(d);

Разбиение и сортировка (PARTITION)

Оператор PARTITION тоже разбивает наборы объектов на группы блоком BY, но возвращает результат не на группу, а на каждый набор объектов — по окну внутри его группы, заданному порядком ORDER. Агрегирующие функции здесь — SUM, PREV, LAST, CUSTOM: отсюда места и ранги, сквозная нумерация, накопительные итоги, значение предыдущего или последнего набора в окне. Аналогия: оконные функции SQL (OVER (PARTITION BY ... ORDER BY ...)). Что именно попадает в окно, описано в разбиении / упорядочивании.

Форма PARTITION UNGROUP решает обратную задачу — распределяет значение свойства-источника по наборам объектов группы: пропорционально заданному выражению (PROPORTION) или по порядку (LIMIT).

place (Team t) = PARTITION SUM 1 ORDER DESC points(t), t BY conference(t);

Агрегация объектов (GROUP AGGR, AGGR)

Эти операторы работают не со значениями, а с объектами.

GROUP AGGR — форма оператора GROUP, возвращающая сам объект группы. Получается отображение, обратное перечисленным в BY свойствам, — например, поиск объекта по коду; платформа добавляет ограничение, что в группе такой объект не более одного.

Оператор AGGR идет дальше: он сам создает объект, когда агрегируемое выражение становится не NULL, и удаляет, когда оно снова NULL, заполняя свойства-отображения на параметры. Механизм — агрегации.

countryName = GROUP AGGR Country c BY name(c);
shipment (Invoice i) = AGGR ShipmentInvoice WHERE createShipment(i);

Выбор и переопределение (CASE, IF, OVERRIDE)

Оператор выбора проверяет условия по порядку и возвращает результат первого выполненного; условие выполнено, если его значение не NULL.

  • CASE — явные пары WHEN ... THEN ... и необязательный ELSE.
  • IF — постфиксная одиночная форма result IF condition; IF ... THEN добавляет блок ELSE.
  • OVERRIDE — первый операнд, не равный NULL; так же подставляется значение по умолчанию вместо NULL.
  • EXCLUSIVE — то же плюс декларация, что не NULL не более одного операнда.
  • MULTI — операнд выбирается по совместимости классов аргументов с его сигнатурой.
signedQuantity (Ledger l) = MULTI quantity[InLedger](l), quantity[OutLedger](l);
price (Item i) = OVERRIDE salePrice(i), basePrice(i), 0;

Рекурсия (RECURSION)

Оператор RECURSION создает свойство, вычисляемое итерациями; к нему обращаются на деревьях, графах и транзитивных замыканиях — уровень узла, все предки объекта, достижимость по цепочке ссылок. Его части — STEP, префикс $ перед параметром и опция CYCLES — и то, как считаются итерации, описаны в рекурсии.

level (Group child, Group parent) = RECURSION 1 IF child IS Group AND parent = child
STEP 1 IF parent = parent($parent);

Абстрактные свойства (ABSTRACT)

Оператор ABSTRACT объявляет свойство без реализации: базовый модуль задает класс значения и классы параметров, а другие модули добавляют реализации инструкцией +=. Из них платформа собирает оператор выбора — это расширение свойств, способ снять зависимость между модулями и получить полиморфизм свойств.

Какая реализация выбирается и что требуется от реализаций, задается опциями — MULTI, CASE, VALUE, EXCLUSIVE, OVERRIDE, FULL — и описано в расширении свойств.

name 'Наименование' = ABSTRACT ISTRING[250] (Document);      // базовый модуль
name (Shipment s) += ISTRING[250]('Поставка ' + number(s)); // модуль поставок

Действия

Изменение состояния (<-, NEW, DELETE)

Оператор CHANGE пишет значение в изменяемое свойство, оператор NEW добавляет объект конкретного класса, операторы DELETE и CHANGECLASS удаляют объект или переводят его в другой класс:

[CHANGE] propertyId(expr1, ..., exprN) <- valueExpr [WHERE whereExpr]
NEW className WHERE whereExpr [TO propertyId(prm1, ..., prmN)]
NEW [alias =] className [AUTOSET] action
DELETE expr [WHERE whereExpr]
CHANGECLASS expr TO className [WHERE whereExpr]

Изменение пишется сразу по всем наборам аргументов, удовлетворяющим условию, одной операцией над множеством. Аналогия: UPDATE ... SET ... WHERE, а не присваивание переменной. При смене класса или удалении платформа обнуляет хранимые значения первичных свойств, в которых объект стал недопустим.

setDiscount () {
discount(Customer c) <- 15 WHERE totalOrders(c) > 100;
NEW o = Order { date(o) <- currentDate(); }
}

Вызов и последовательность

Действие двойственно свойству: свойство говорит, чему значение равно, действие — как оно меняется. Объявляется инструкцией ACTION:

name [caption] [(param1, ..., paramN)] { actionBody } [options]

Тело в фигурных скобках — последовательность: вложенные действия выполняются в порядке записи; внутри блока можно объявить свойства LOCAL, живущие только во время его выполнения. Вызов действия записывается именем с аргументами, [EXEC] actionId(expression1, ..., expressionN) [TO toProperty], либо подставляется прямо как значение. Аналогия: вызов процедуры.

Циклы (FOR, WHILE)

Оператор FOR выполняет тело по разу на каждый набор объектов, для которого условие не NULL; оператор WHILE пересчитывает условие на каждом шаге, поэтому изменения из тела учитываются:

FOR expression [ORDER [DESC] orderExpr1, ..., orderExprN]
[TOP topExpr] [OFFSET offsetExpr]
[NEW [alias =] className]
DO action
[ELSE alternativeAction]

WHILE expression [ORDER [DESC] orderExpr1, ..., orderExprN]
[NEW [alias =] className]
DO action

К циклу обращаются, когда тело действительно построчное — диалог, сообщение, внешний вызов. Механизмы — цикл и рекурсивный цикл.

createDetails (Order o) {
FOR in(Sku s) NEW d = OrderDetail DO {
order(d) <- o;
sku(d) <- s;
}
}

Ветвление (CASE, IF)

Ветвление вызывает действие, соответствующее выполнившемуся условию; условие выполнено, если его значение не NULL. В операторах IF ... THEN и CASE условие записывается явно, в операторе MULTI условие — соответствие аргументов вызова сигнатуре действия, то есть диспетчеризация по классу аргумента:

IF condition
THEN action
[ELSE alternativeAction]

CASE [exclusionType]
WHEN condition1 THEN action1
...
WHEN conditionN THEN actionN
[ELSE elseAction]

MULTI [exclusionType] action1, ..., actionN
message (Shape s) { MULTI { message[Square](s); }, { message[Circle](s); } }

Отложенный вариант — абстрактное действие ABSTRACT: базовый модуль объявляет точку расширения, реализации к ней добавляют другие модули (расширение действий).

Управление потоком

  • BREAK выходит из ближайшего цикла, CONTINUE переходит к следующей итерации, RETURN [выражение] выходит из ближайшего вызова действия с указанным значением как результатом.
  • TRY action [CATCH catchAction] [FINALLY finallyAction]CATCH поглощает ошибку, давая доступ к ней через messageCaughtException[] и lsfStackTraceCaughtException[]; FINALLY выполняется в любом случае. Аналогия: try / catch / finally.
  • NEWTHREAD action [dispatchClause] — выполнение в отдельном потоке, сразу или по расписанию (SCHEDULE: задержка и период). NEWEXECUTOR выбирает, куда отправить поток: в серверный пул, где тело работает в сессии изменений вызывающего кода, или в клиентское соединение (CLIENT), где оно получает собственную новую сессию в навигаторе этого соединения.

Действия на форме

  • SHOWоткрытие формы в интерактивном представлении; переданные объекты становятся текущими.
  • DIALOG — то же открытие как диалог ввода значения: каждый объект, помеченный INPUT или CHANGE, возвращает в блок DO свое последнее текущее значение.
  • ACTIVATEактивация формы, вкладки, свойства или набора объектов в группе объектов.
  • EXPAND / COLLAPSE — разворачивание и сворачивание контейнера формы и узлов дерева объектов.
  • MESSAGE и INPUT — сообщение и ввод значения без отдельной формы.
ACTIVATE FORM formName
ACTIVATE TAB formName.componentSelector
ACTIVATE PROPERTY formPropertyId

ACTIVATE [seekDirection] formObjectId = expr
ACTIVATE [seekDirection] formGroupObjectId [OBJECTS formObject1 = expr1, ..., formObjectK = exprK]

События

События данных (WHEN)

Событие выполняет заданное действие — свою обработку — при изменении данных. Блок описания события задает, глобальное событие или локальное — для всей базы или в пределах сессии изменений — ключевыми словами GLOBAL и LOCAL, ограничивает событие заданными формами через FORMS и упорядочивает его обработку относительно других через AFTER.

Событие на изменение данных создают три инструкции:

  • WHENпростое событие: обработка выполняется на каждый набор объектов, на котором условие не NULL;
  • <- WHENвычисляемое событие: вместо обработки задается изменение первичного свойства;
  • ON — событие общего вида: обработка выполняется один раз на все изменения.
WHEN eventClause eventExpr [ORDER [DESC] orderExpr1, ..., orderExprN] DO eventAction;
propertyId(param1, ..., paramN) <- valueExpr WHEN eventExpr;
ON eventClause eventAction;

Аналогия: триггер базы данных, но условием служит произвольное свойство над всей базой.

sum(OrderDetail d) <- quantity(d) * price(d) WHEN CHANGED(quantity(d)) OR CHANGED(price(d));

События формы (ON)

События формы возникают на открытой форме: в точке её жизни (INIT, APPLY, CANCEL, CLOSE, DROP), на действие пользователя либо по таймеру через SCHEDULE. Обработка подключается опцией ON — в блоках событий, свойств и действий и объектов инструкции FORM либо в опциях свойства.

Что событие обслуживаетСобытия
форму целикомINIT, QUERYCLOSE, QUERYOK, OK, APPLY, CANCEL, CLOSE, DROP, SCHEDULE
объект формыCHANGE
группу объектовFILTER, ORDER, SELECT, FILTERS, ORDERS
группу фильтровFILTERGROUPS
свойство или действиеCHANGE, CHANGEWYS, GROUPCHANGE, EDIT, CONTEXTMENU, KEYPRESS, FILTERS PROPERTY, SELECT PROPERTY
контейнерEXPAND, COLLAPSE, TAB

Постфиксы BEFORE и AFTER дают моменты до и после операции. Для событий изменения свойства есть стандартные обработки READONLY, READONLYIF и SELECTOR.

FORM sku 'Товар'
OBJECTS s = Sku
PROPERTIES(s) price ON CHANGE changePrice(s)
EVENTS ON INIT initSku()
;

Порядок выполнения

Обработки локальных событий выполняются не в момент изменения данных, а в определенные моменты жизни сессии — см. выполнение локальных событий. Обработки синхронных глобальных событий выполняются внутри транзакции применения изменений, вместе с проверками ограничений.

Порядок между обработками, реагирующими на одно и то же изменение, определяют зависимости по данным; явно он задается ключевым словом AFTER (синоним GOAFTER) в блоке описания события.

Ограничения

Ограничения (CONSTRAINT)

Ограничение — свойство, значение которого всегда должно быть NULL. Проверяется оно на событии, заданном блоком описания события инструкции, а по умолчанию — на глобальном событии APPLY; если к этому моменту оно стало не NULL хотя бы на одном наборе объектов, платформа показывает сообщение с этими наборами и отменяет изменения. Создается инструкцией CONSTRAINT:

CONSTRAINT [eventClause] constraintExpr [CHECKED [BY propertyId1, ..., propertyIdN]] MESSAGE messageExpr
[PROPERTIES outExpr1, ..., outExprM];

Опция CHECKED BY заставляет диалог изменения перечисленных свойств отфильтровывать значения, нарушающие ограничение.

Простые ограничения задаются видом связи между свойствами, а не выражением, Реализованы два вида: следствие — инструкция => — и определенность — опция NONULL. Следствие умеет разрешать нарушение само: его клауза RESOLVE [LEFT] [RIGHT] указывает платформе, какую сторону исправлять. У NONULL такой клаузы нет — у него есть [DELETE], удаляющий нарушившие объекты.

Аналогия: CHECK и NOT NULL в SQL, но условием служит произвольное свойство над всей базой.

CONSTRAINT balance(Sku s, Stock st) < 0 MESSAGE 'Остаток не может быть отрицательным';

Агрегируемые объекты и ограничения

Агрегация создает агрегируемый объект, когда агрегируемое свойство становится не NULL на наборе значений параметров, и удаляет этот объект, когда свойство снова становится NULL. Создает и удаляет объект только оператор AGGR; GROUP AGGR строит обратное свойство — от значений параметров к объекту. Оба описаны в Brief: свойства.

Для целостности это еще один инвариант, который платформа поддерживает сама: на набор значений параметров приходится не более одного агрегируемого объекта. GROUP AGGR добавляет его как ограничение на свою группу, а AGGR держит в его рамках соответствие между объектами и значениями параметров. Как это ограничение поддерживается, описано в агрегациях и простых ограничениях.

// поставка по инвойсу создается, когда у инвойса ставится признак создания поставки, и удаляется, когда он снимается
shipment (Invoice i) = AGGR ShipmentInvoice WHERE createShipment(i);

Сессии изменений

Что такое сессия

Сессия изменений — место, где изменения накапливаются локально, а не пишутся в базу сразу. В нее попадают изменения первичных свойств, в том числе локальных, и изменения классов объектов — созданные и удаленные объекты, CHANGECLASS. Пока изменения не применены, они остаются в этой сессии; что из них увидит другая сессия, решают вместе открывающий ее оператор — NESTEDSESSION показывает все изменения верхней сессии, NEWSESSION читает из базы, перенося локальные свойства, названные его NESTED — списком в скобках либо все сразу через NESTED LOCAL, — изменения классов, если задан CLASSES, и, перечислено оно или нет, всё объявленное DATA LOCAL NESTED, — и объявления самих свойств, ведь объявленное DATA LOCAL NESTED переносится само; текущую сессию действию задает контекст выполнения — сессия формы, сессия вызывающего действия либо сессия, переданная платформой.

Значение свойства на начало сессии возвращает оператор PREV, а производные от него операторы изменений SET, DROPPED, CHANGED, SETCHANGED, DROPCHANGED, SETDROPPED отвечают, что именно изменилось в сессии.

Аналогия: незафиксированная транзакция базы данных, живущая все время работы пользователя с формой.

NEWSESSION и NESTEDSESSION

Оператор NEWSESSION выполняет вложенное действие в отдельной сессии, изолированной от текущей; оператор NESTEDSESSION — во вложенной, которая копирует в себя изменения текущей сессии, а применение внутри нее возвращает изменения обратно. Если любой из этих операторов выполняется во время транзакции применения текущей сессии, в этот момент сессия не создается — вложенное действие откладывается и выполняется в текущей сессии внутри той же транзакции.

NEWSESSION [NEWSQL] [FORMS formId1, ..., formIdM] [NESTED [nestedPropertySelector] [CLASSES]] [SINGLE] action
NESTEDSESSION [SINGLE] action

Опция NESTED перечисляет переносимые локальные свойства, NEWSQL открывает сессию на отдельном SQL-соединении. Они взаимоисключающи: написанные вместе, они разберутся, но победит NEWSQL, а вся клауза NESTED будет проигнорирована.

К новой сессии обращаются, когда действие — отдельная единица работы: диалог, импорт, обработка файла, фоновая запись.

logError (STRING message) {
NEWSESSION NEWSQL {
NEW e = LogEntry { text(e) <- message; }
APPLY;
}
}

APPLY и CANCEL

Оператор APPLY применяет изменения — записывает накопленное в базу, выполняя по дороге обработки глобальных событий и проверки ограничений. Во вложенной сессии он не пишет в базу ничего: изменения копируются обратно в сессию, в которую вложена эта. Оператор CANCEL отменяет изменения — очищает сессию, кроме случая внутри транзакции применения, где он вместо этого отменяет выполняющийся APPLY.

APPLY [NESTED [nestedPropertySelector] [CLASSES]] [SINGLE] [SERIALIZABLE] [action]
CANCEL [NESTED [nestedPropertySelector] [CLASSES]]

Итог применения читается из свойств System.canceled[] и System.applyMessage[].

Аналогия: COMMIT и ROLLBACK.

Видимость изменений между сессиями

Вне транзакции применения то, что видит новая сессия, задают вместе создавший ее оператор и объявления самих свойств, между двумя краями: сессия, наследующая непримененные изменения верхней, и сессия, которая их не видит и читает зафиксированное состояние базы. Это не переключатель — NESTED (...) переносит перечисленные локальные свойства, а CLASSES независимо переносит изменения классов, так что унаследовать можно и часть состояния.

Оператор — не единственное место, где это решается. Свойство, объявленное DATA LOCAL NESTED, переносится само, без перечисления в операторе, а MANAGESESSION / NOMANAGESESSION в этом объявлении сужают перенос до APPLY / CANCEL или до NEWSESSION соответственно (DATA, NEWSESSION).

NESTEDSESSION — один край: видны все изменения верхней сессии. NEWSESSION — другой: свойства читаются из базы, кроме объявленных DATA LOCAL NESTED — те переносятся сами, — а опции NESTED и CLASSES расширяют перенос. NEWSQL, наоборот, сужает его: на собственном соединении не переносится ничего, объявлено NESTED или нет.

Что дает каждое сочетание, описано в создании сессий и в операторе NEWSESSION.