Pull to refresh
51.56
lsFusion
Не очередной язык программирования

Не очередной язык программирования. Часть 3: Физика

Reading time25 min
Views9.2K


Третья и заключительная часть серии статей о языке lsFusion (ссылки на первую и вторую части)

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

Эта статья, как и предыдущие, не очень подходит для развлекательного чтения, но, в отличие от остальных, тут будет больше технических деталей и «горячих» тем (вроде типизации или метапрограммирования), плюс эта статья даст часть ответов на вопрос, как это все работает внутри.

В этой статье обойдемся без картинки (стека тут как такого нет), но зато сделаем оглавление, о чем просили в предыдущих статьях:


Идентификация элементов


Если проект состоит из нескольких небольших файлов, то проблем с именованием элементов обычно не возникает. Все имена на виду, и достаточно легко сделать так, чтобы они не пересекались. Если же проект, наоборот, состоит из множества модулей, разрабатываемых большим количеством различных людей, и абстракции в этих модулях из одной доменной области, конфликты имен становятся куда более вероятными. Для решения этих проблем в lsFusion есть два механизма:

  • Пространства имен — разделение имени на полное и короткое, и возможность использования при обращении к элементу только короткого имени
  • Явная типизация (если быть более точным, function overloading) — возможность называть свойства (и действия) одинаково, а затем при обращении к ним, в зависимости от классов аргументов, автоматически определять к какому именно свойству идет обращение

Пространства имен


Любой сложный проект обычно состоит из большого количества элементов, которые необходимо именовать. И, если доменные области пересекаются, очень часто возникает необходимость использовать одно и то же имя в различных контекстах. Например, у нас есть имя класса или формы Invoice (накладная), и мы хотим использовать это имя в различных функциональных блоках, например: Закупка (Purchase), Продажа (Sale), Возврат закупки (PurchaseReturn), Возврат продажи (SaleReturn). Понятно, что можно называть классы / формы PurchaseInvoice, SaleInvoice и так далее. Но, во-первых, такие имена сами по себе будут слишком громоздкими. А во-вторых, в одном функциональном блоке обращения, как правило, идут к элементам этого же функционального блока, а значит, при разработке, к примеру, функционального блока Закупки (Purchase) от постоянного повторения слова Purchase будет просто рябить в глазах. Чтобы этого не происходило, в платформе существует такое понятие как пространство имен. Работает это следующим образом:

  • каждый элемент в платформе создается в некотором пространстве имен
  • если в процессе создания элемента идет обращение к другим элементам, элементы созданные в этом же пространстве имен имеют приоритет
MODULE PurchaseInvoice;
NAMESPACE Purchase;
CLASS Invoice 'Накладная (закупка)';
MODULE SaleInvoice;
NAMESPACE Sale;
CLASS Invoice 'Накладная (продажа)';
MODULE PurchaseShipment;
REQUIRE PurchaseInvoice, SaleInvoice;
NAMESPACE Purchase;
// В качестве Invoice будет использован именно Purchase.Invoice, а не Sale.invoice
// так как namespace этого модуля Purchase и элементы с namespace Purchase приоритетнее
shipment(Invoice invoice) = AGGR ShipmentInvoce WHERE createShipment(invoice);
Пространства имен в текущей версии языка задаются для всего модуля сразу в заголовке модуля. По умолчанию, если пространство имен не задано, оно создается неявно с именем равным имени модуля. Если необходимо обратиться к элементу из не приоритетного пространства имен, это можно сделать, указав полное имя элемента (например Sale.Invoice).

Явная типизация


Пространства имен являются важным, но не единственным способом сделать код короче и читабельнее. Помимо них при поиске свойств (и действий) также существует возможность учитывать классы аргументов, передаваемых им на вход. Так, например:
sum = DATA NUMERIC[10,2] (OrderDetail);
sum = GROUP SUM sum(OrderDetail od) BY order(od);
// выберет свойство во второй строке, так как оно принимает на вход класс Order
// в то время как свойство в первой строке принимает на вход класс OrderDetail
CONSTRAINT sum(Order o) < 0 MESSAGE 'Сумма заказа должна быть положительной';
Тут, конечно, может возникнуть вопрос: а что будет, если пространство имен искомого свойства не приоритетное, но оно лучше подходит по классам? На самом деле, общий алгоритм поиска достаточно сложный (полное его описание тут) и таких «неоднозначных» случаев достаточно много, поэтому в случае неуверенности рекомендуется или задавать пространства имен / классы искомого свойства явно, или перепроверять в IDE (при помощи Go to Declaration — CTRL+B), что найденное свойство именно то, которое имелось ввиду.

Также, стоит отметить, что явная типизация в lsFusion в общем случае не обязательна. Классы параметров можно не указывать, и если платформе хватит информации, чтобы найти нужное свойство, она сделает это. С другой стороны, в реально сложных проектах классы параметров все же рекомендуется задавать явно, не только с точки зрения краткости кода, но и с точки зрения различных дополнительных возможностей, таких как: ранняя диагностика ошибок, умное автодополнение со стороны IDE и так далее. У нас был большой опыт работы как с неявной типизацией (первые 5 лет), так и с явной (оставшееся время), и надо сказать, что времена неявной типизации сейчас вспоминают с содроганием (хотя может просто «мы не умели ее готовить»).

Модульность


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

В lsFusion модульность обеспечивается следующими двумя механизмами:

  • Расширения — возможность расширять (изменять) элементы системы после их создания.
  • Модули — возможность группировать некоторый функционал вместе для его дальнейшего повторного использования.

Расширения


lsFusion поддерживает возможность расширения классов и форм, а также свойств и действий через механизм полиморфизма, описанный в первой статье.
sum = DATA NUMERIC[10,2] (OrderDetail);
sum = GROUP SUM sum(OrderDetail od) BY order(od);
// выберет свойство во второй строке, так как оно принимает на вход класс Order
// в то время как свойство в первой строке принимает на вход класс OrderDetail
CONSTRAINT sum(Order o) < 0 MESSAGE 'Сумма заказа должна быть положительной';
CLASS ABSTRACT Shape;
CLASS Box : Shape;

CLASS Quadrilateral;
EXTEND CLASS Box : Quadrilateral; // Добавляем наследование

CLASS ShapeType {
    point 'Точка',
    segment 'Отрезок'

 
EXTEND CLASS ShapeType { // Добавляем статический объект
    circle 'Окружность'
}

CLASS ItemGroup;
name = DATA ISTRING[100] (ItemGroup);

itemGroup = DATA ItemGroup (Item);

EXTEND FORM items
    PROPERTIES(i) NEWSESSION DELETE // добавляем на форму кнопку удаления
    
    OBJECTS g = ItemGroup BEFORE i // добавляем на форму объект группы товаров перед товаром
    PROPERTIES(g) READONLY name
    FILTERS itemGroup(i) == g // если бы объект был добавлен после объекта с товарами, то фильтрация шла бы по группе товаров, а не по товарам
;
Также, отметим, что практически все остальные конструкции платформы (например, навигатор, дизайн формы) расширяемы по определению, поэтому для них не существует отдельной логики расширений.

Модули


Модуль — это некоторая функционально законченная часть проекта. В текущей версии lsFusion модуль — это отдельный файл, состоящий из заголовка и тела модуля. Заголовок модуля, в свою очередь, состоит из: имени модуля, а также, при необходимости, списка используемых модулей и имени пространства имен этого модуля. Тело модуля состоит из объявлений и / или расширений элементов системы: свойств, действий, ограничений, форм, метакодов и так далее.

Обычно модули для объявления своих / расширения существующих элементов используют элементы из других модулей. Соответственно, если модуль B использует элементы из модуля A, то в модуле B необходимо указать, что он зависит от A.

На основании своих зависимостей все модули в проекте выстраиваются в некотором порядке, в котором происходит их инициализация (этот порядок играет важную роль при использовании вышеупомянутого механизма расширений). Гарантируется, что если модуль B зависит от модуля A, то инициализация модуля A произойдет раньше, чем инициализация модуля B. Циклические зависимости между модулями в проекте не допускаются.

Зависимости между модулями являются транзитивными. То есть, если модуль C зависит от модуля B, а модуль B зависит от модуля A, то считается, что и модуль С также зависит от модуля A.

Любой модуль всегда автоматически зависит от системного модуля System, вне зависимости от того, указано это явно или нет.
MODULE EmployeeExample;         // Задаем имя модуля

REQUIRE Authentication, Utils;         // Перечисляем модули, от которых зависит модуль Employee
NAMESPACE Employee;             // Задаем пространство имен
 
CLASS Employee 'Сотрудник';    // Создаем класс
CLASS Position 'Должность'// Создаем еще один класс
 
employeePosition(employee) = DATA Position (Employee); // Создаем свойство

Метапрограммирование


Метапрограммирование — это вид программирования, связанный с написанием программного кода, который в качестве результата порождает другой программный код. В lsFusion для метапрограммирования используются так называемые метакоды.

Метакод состоит из:

  • имени метакода
  • параметров метакода
  • тела метакода — блока кода, состоящего из объявлений и / или расширений элементов системы ( свойств, действий, событий, других метакодов и т.д.)

Соответственно, перед тем как начать основную обработку кода, платформа выполняет его предобработку — заменяет все использования метакодов на тела этих метакодов. При этом все параметры метакода, использованные в идентификаторах / строковых литералах, заменяются на переданные этому метакоду аргументы:

Объявление:
META addActions(formName)
    EXTEND FORM formName
        PROPERTIES() showMessage, closeForm
    ;
END
Использование:
@addActions(documentForm);
@addActions(orderForm);
Результирующий код:
EXTEND FORM documentForm
    PROPERTIES() showMessage, closeForm
;  
EXTEND FORM orderForm
    PROPERTIES() showMessage, closeForm
;
Кроме просто подстановки параметров метакода, платформа также позволяет объединять эти параметры с существующими идентификаторами / строковыми литералами (или друг с другом), например:

Объявление:
META objectProperties(object, caption)
    object##Name 'Имя '##caption = DATA BPSTRING[100](object);
    object##Type 'Тип '##caption = DATA Type (object);
    object##Value 'Стоимость '##caption = DATA INTEGER (object);
END
Использование:
@objectProperties(document, 'документа');
Результирующий код:
DocumentName 'Имя документа' = DATA BPSTRING[100](Document);
DocumentType 'Тип документа' = DATA Type (Document);
DocumentValue 'Стоимость документа' = DATA INTEGER (Document);
Метакоды очень похожи на макросы в C, но, в отличии от последних, работают не на текстовом уровне (в них нельзя, к примеру, передавать параметром ключевые слова), а только на уровне идентификаторов / строковых литералов (это ограничение, в частности, позволяет парсить тело метакода в IDE).

В lsFusion метакоды решают задачи, схожие с generics в Java (передача классов в качестве параметров) и lambda в ФП (передачи функций в качестве параметров), правда, делают это не очень красиво. Но, с другой стороны, они это делают в существенно более общем случае (то есть, например, с возможностью объединения идентификаторов, использования в любых синтаксических конструкциях — формах, дизайнах, навигаторе и т.п.)

Отметим, что «разворачивание» метакодов поддерживается не только в самой платформе, но и в IDE. Так, в IDE есть специальный режим Enable meta, который генерирует результирующий код прямо в исходниках и тем самым позволяет участвовать этому сгенерированному коду в поиске использований, автодополнении и т.п. При этом, если тело метакода изменяется, IDE автоматически обновляет все использования этого метакода.



Также метакоды можно использовать не только для автоматической, но и для ручной кодогенерации (в качестве шаблонов). Для этого достаточно вместо одной @ написать @@ — и сразу после того, как строка использования метакода будет полностью введена (вплоть до точки с запятой), IDE заменит это использование метакода на сгенерированный по этому метакоду код:



Интеграция


Интеграция включает в себя все то, что связано с взаимодействием системы lsFusion с другими системами. С точки зрения направления этого взаимодействия интеграцию можно разделить на:

  • Обращение к lsFusion системе из другой системы.
  • Обращение из lsFusion системы к другой системе.

С точки зрения физической модели интеграцию можно разделить на:

  • Взаимодействие с системами, выполняющимися в «той же среде», что и lsFusion система (то есть, в виртуальной Java машине (JVM) lsFusion-сервера и/или использующими тот же SQL-сервер, что и lsFusion система).
  • Взаимодействие с удаленными системами по сетевым протоколам.

Соответственно, первые системы будем называть внутренними, вторые — внешними.

Таким образом в платформе существует четыре различных вида интеграции:

  • Обращение к внешней системе
  • Обращение из внешней системы
  • Обращение к внутренней системе
  • Обращение из внутренней системы

Обращение к внешней системе


Обращение к внешним системам в lsFusion в большинстве случаев реализуется при помощи специального оператора EXTERNAL. Этот оператор выполняет заданный код на языке / в парадигме заданной внешней системы. Кроме того, этот оператор позволяет передавать объекты примитивных типов в качестве параметров такого обращения, а также записывать результаты обращения в заданные свойства (без параметров).

На данный момент в платформе поддерживаются следующие типы взаимодействий / внешних систем:

HTTP — выполнение http-запроса Web-сервера.

Для этого типа взаимодействия необходимо задать строку запроса (URL), которая одновременно определяет как адрес сервера, так и непосредственно запрос, который необходимо выполнить. Параметры могут передаваться как в строке запроса (для обращения к параметру используется спецсимвол $ и номер этого параметра, начиная с 1), так и в его теле (BODY). Предполагается, что в BODY передаются все параметры, не использованные в строке запроса. Если в BODY больше одного параметра, тип контента BODY при передаче устанавливается равным multipart/mixed, а параметры передаются как составные части этого BODY.

При обработке параметров файловых классов (FILE, PDFFILE и т.п.) в BODY, тип контента параметра определяется в зависимости от расширения файла (в соответствии со следующей таблицей). Если расширение файла отсутствует в этой таблице, тип контента устанавливается равным application/<расширение файла>.

При необходимости, при помощи специальной опции (HEADERS) можно задать заголовки выполняемого запроса. Для этого нужно указать свойство с ровно одним параметром строкового класса, в котором будет храниться название заголовка, и значением строкового класса, в котором будет храниться значение этого заголовка.

Результат http-запроса обрабатывается аналогично его параметрам, только в обратную сторону: к примеру, если тип контента результата или присутствует в следующей таблице, или равен application/*, то считается, что полученный результат — это файл и должен записываться в свойство со значением FILE. Заголовки результата http-запроса обрабатываются по аналогии с заголовками самого этого запроса (с той лишь разницей, что опция называется HEADERSTO, а не HEADERS).
EXTERNAL HTTP GET 'https://www.cs.cmu.edu/~chuck/lennapg/len_std.jpg' TO exportFile; 
open(exportFile()); 

LOCAL headers = STRING (STRING);
headers('Authentication : Bearer') <- 'd43ks43ds343dd233'';
EXTERNAL HTTP 'http://tryonline.lsfusion.org/exec?action=getExamples' 
    HEADERS headers 
    HEADERSTO headers 
    PARAMS JSONFILE('\{"mode"=1\}'
    TO exportFile;

IMPORT FROM exportFile() FIELDS () STRING caption, STRING code DO
    MESSAGE 'Example : ' + caption + ', code : ' + code;
FOR v = headers(STRING s) DO
    MESSAGE 'Result Header is : Key - ' + s + ', Value - ' + v;
SQL — выполнение команды SQL-сервера.

Для этого типа взаимодействия задается строка подключения и SQL-команда(ы), которую необходимо выполнить. Параметры могут передаваться как в строке подключения, так и в SQL-команде. Для обращения к параметру используется спецсимвол $ и номер этого параметра (начиная с 1).

Параметры файловых классов (FILE, PDFFILE и т.п.) можно использовать только в SQL-команде. При этом, если какой-либо из параметров при выполнении является файлом формата TABLE (TABLEFILE или FILE с расширением table), то такой параметр считается таблицей и в этом случае:

  • перед выполнением SQL-команды значение каждого такого параметра загружается на сервер во временную таблицу
  • при подстановке параметров подставляется не само значение параметра, а имя созданной временной таблицы

Результатами выполнения являются: для DML-запросов — числа, равные количеству обработанных записей, для SELECT-запросов — файлы формата TABLE (FILE с расширением table), содержащие результаты этих запросов. При этом порядок этих результатов совпадает с порядком выполнения соответствующих запросов в SQL-команде.
externalSQL ()  { 
    EXPORT TABLE FROM bc=barcode(Article a) WHERE name(a) LIKE '%Мясо%'// получаем все штрих-коды товаров с именем мясо
    EXTERNAL SQL 'jdbc:mysql://$1/test?user=root&password=' 
        EXEC 'select price AS pc, articles.barcode AS brc from $2 x JOIN articles ON x.bc=articles.barcode'
        PARAMS 'localhost',exportFile() 
        TO exportFile; // читаем цены для считанных штрих-кодов
    
    // для всех товаров с полученными штрих-кодами записываем цены
    LOCAL price = INTEGER (INTEGER);
    LOCAL barcode = STRING[30] (INTEGER);
    IMPORT FROM exportFile() TO price=pc,barcode=brc;
    FOR barcode(Article a) = barcode(INTEGER i) DO 
        price(a) <- price(i);
}
LSF — вызов действия другого lsFusion-сервера.

Для этого типа взаимодействия задается строка подключения к lsFusion-серверу (или его веб-серверу, при наличии такового), действие, которые необходимо выполнить, а также список свойств (без параметров), в значения которых будут записаны результаты обращения. Передаваемые параметры должны по количеству и по классам совпадать с параметрами выполняемого действия.

Способ задания действия в этом типе взаимодействия полностью соответствует способу задания действия при обращении из внешней системы (про этот тип обращения в следующем разделе).
externalLSF()  { 
    EXTERNAL LSF 'http://localhost:7651' EXEC 'System.testAction[]'
}
По умолчанию этот тип взаимодействия реализуется по протоколу HTTP с использованием соответствующих интерфейсов обращений к / из внешней системы.

В том случае, если нужно обратиться к системе по протоколу, отличному от вышеперечисленных, это всегда можно сделать, создав действие на Java и реализовав это обращение там (но об этом чуть позже в разделе «Обращение к внутренним системам»)

Обращение из внешней системы


Платформа предоставляет возможность внешним системам обращаться к разработанной на lsFusion системе с использованием сетевого протокола HTTP. Интерфейсом такого взаимодействия является вызов некоторого действия с заданными параметрами и, при необходимости, возврат значений некоторых свойств (без параметров) в качестве результатов. Предполагается, что все объекты параметров и результатов являются объектами примитивных типов.

Вызываемое действие может задаваться одним из трех способов:

  • /exec?action=<имя действия> — задается имя вызываемого действия.
  • /eval?script=<код> — задается код на языке lsFusion. Предполагается, что в этом коде присутствует объявление действия с именем run, именно это действие и будет вызвано. Если параметр script не задан, то предполагается, что код передается первым параметром BODY.
  • /eval/action?script=<код действия> — задается код действия на языке lsFusion. Для обращения к параметрам можно использовать спецсимвол $ и номер параметра (начиная с 1).

Во втором и третьем случае, если параметр script не задан, то предполагается, что код передается первым параметром BODY.

Обработка параметров и результатов симметрична обращению к внешним системам по протоколу HTTP (с той лишь разницей, что параметры при этом обрабатываются как результаты, и, наоборот, результаты обрабатываются как параметры), поэтому особо повторяться не будем.

Например, если у нас есть действие:
importOrder(INTEGER no, DATE date, FILE detail) {
   NEW o = FOrder {
       no(o) <- no;
       date(o) <- date;
       LOCAL detailId = INTEGER (INTEGER);
       LOCAL detailQuantity = INTEGER (INTEGER);
       IMPORT FROM detail TO detailId, detailQuantity;
       FOR imported(INTEGER i) DO {
           NEW od = FOrderDetail {
               id(od) <- detailId(i);
               quantity(od) <- detailQuantity(i);
               price(od) <- 5;
               order(od) <- o;
           }
       }
       APPLY;
       EXPORT JSON FROM price = price(FOrderDetail od), id = id(od) WHERE order(od) = o;
       EXPORT FROM orderPrice(o), exportFile();
   }
}
То к нему можно обратиться при помощи POST-запроса у которого:
  • URL — хттп://адрес_сервера/exec?action=importOrder&p=123&p=2019-01-01
  • BODY — json-файл со строками запроса

Пример обращения на Python
import json
import requests
from requests_toolbelt.multipart import decoder
 
lsfCode = ("run(INTEGER no, DATE date, FILE detail) {\n"
           "    NEW o = FOrder {\n"
           "        no(o) <- no;\n"
           "        date(o) <- date;\n"
           "        LOCAL detailId = INTEGER (INTEGER);\n"
           "        LOCAL detailQuantity = INTEGER (INTEGER);\n"
           "        IMPORT JSON FROM detail TO detailId, detailQuantity;\n"
           "        FOR imported(INTEGER i) DO {\n"
           "            NEW od = FOrderDetail {\n"
           "                id(od) <- detailId(i);\n"
           "                quantity(od) <- detailQuantity(i);\n"
           "                price(od) <- 5;\n"
           "                order(od) <- o;\n"
           "            }\n"
           "        }\n"
           "        APPLY;\n"
           "        EXPORT JSON FROM price = price(FOrderDetail od), id = id(od) WHERE order(od) == o;\n"
           "        EXPORT FROM orderPrice(o), exportFile();\n"
           "    }\n"
           "}")
 
order_no = 354
order_date = '10.10.2017'
order_details = [dict(id=1, quantity=10),
                 dict(id=2, quantity=15),
                 dict(id=5, quantity=4),
                 dict(id=10, quantity=18),
                 dict(id=11, quantity=1),
                 dict(id=12, quantity=3)]
 
order_json = json.dumps(order_details)
 
url = 'http://localhost:7651/eval'
payload = {'script': lsfCode, 'no': str(order_no), 'date': order_date,
           'detail': ('order.json', order_json, 'text/json')}
 
response = requests.post(url, files=payload)
multipart_data = decoder.MultipartDecoder.from_response(response)
 
sum_part, json_part = multipart_data.parts
sum = int(sum_part.text)
data = json.loads(json_part.text)
 
##############################################################
 
print(sum)
for item in data:
    print('{0:3}: price {1}'.format(int(item['id']), int(item['price'])))
 
##############################################################
# 205
#   4: price 5
#  18: price 5
#   3: price 5
#   1: price 5
#  10: price 5
#  15: price 5

Обращение к внутренней системе


Существует два типа внутреннего взаимодействия:

Java-взаимодействие

Этот тип взаимодействия позволяет вызвать код на языке Java внутри JVM lsFusion-сервера. Для этого необходимо:

  • обеспечить, чтобы скомпилированный Java-класс был доступен в classpath сервера приложений. Также необходимо, чтобы этот класс наследовал lsfusion.server.physics.dev.integration.internal.to.InternalAction.
    Пример Java-класса
    import lsfusion.server.data.sql.exception.SQLHandledException;
    import lsfusion.server.language.ScriptingErrorLog;
    import lsfusion.server.language.ScriptingLogicsModule;
    import lsfusion.server.logics.action.controller.context.ExecutionContext;
    import lsfusion.server.logics.classes.ValueClass;
    import lsfusion.server.logics.property.classes.ClassPropertyInterface;
    import lsfusion.server.physics.dev.integration.internal.to.InternalAction;
     
    import java.math.BigInteger;
    import java.sql.SQLException;
     
    public class CalculateGCD extends InternalAction {
     
        public CalculateGCD(ScriptingLogicsModule LM, ValueClass... classes) {
            super(LM, classes);
        }
     
        @Override
        protected void executeInternal(ExecutionContext<ClassPropertyInterface> context) throws SQLException, SQLHandledException {
            BigInteger b1 = BigInteger.valueOf((Integer)getParam(0, context));
            BigInteger b2 = BigInteger.valueOf((Integer)getParam(1, context));
            BigInteger gcd = b1.gcd(b2);
            try {
                findProperty("gcd[]").change(gcd.intValue(), context);
            } catch (ScriptingErrorLog.SemanticErrorException ignored) {
            }
        }
    }
    
  • зарегистрировать действие при помощи специального оператора внутреннего вызова (INTERNAL)
    calculateGCD 'Рассчитать НОД' INTERNAL 'CalculateGCD' (INTEGERINTEGER);
  • зарегистрированное действие, как и любое другое, можно вызывать при помощи оператора вызова. Выполняться при этом будет метод executeInternal(lsfusion.server.logics.action.controller.context.ExecutionContext context) заданного Java-класса.
    // на форме
    FORM gcd 'НОД'
        OBJECTS (a = INTEGER, b = INTEGERPANEL
        PROPERTIES 'A' = VALUE(a), 'B' = VALUE(b)
        
        PROPERTIES gcd(), calculateGCD(a, b)
    ;

    // в другом действии
    run() {
        calculateGCD(100200);
    }
SQL-взаимодействие

Этот тип взаимодействия позволяет обращаться к объектам / синтаксическим конструкциям SQL-сервера, используемого разрабатываемой lsFusion-системой. Для реализации такого типа взаимодействия в платформе используется специальный оператор — FORMULA. Этот оператор позволяет создавать свойство, вычисляющее некоторую формулу на языке SQL. Формула задается в виде строки, внутри которой для обращения к параметру используется спецсимвол $ и номер этого параметра (начиная с 1). Соответственно, количество параметров у полученного свойства будет равно максимальному из номеров использованных параметров.
round(number, digits) = FORMULA 'round(CAST(($1) as numeric),$2)';  // свойство с двумя параметрами: округляемым числом и количеством знаков после запятой
jumpWorkdays = FORMULA NULL DATE PG 'jumpWorkdays($1, $2, $3)'MS 'dbo.jumpWorkdays($1, $2, $3)'// свойство с двумя различными реализациями для разных диалектов SQL
Использовать этот оператор рекомендуется только в случаях, когда задачу невозможно решить при помощи других операторов, а также, если гарантированно известно, какие конкретно SQL сервера могут быть использованы, или используемые синтаксические конструкции соответствуют одному из последних стандартов SQL.

Обращение из внутренней системы


Тут все симметрично обращению к внутренней системе. Есть два типа взаимодействия:

Java-взаимодействие

В рамках такого типа взаимодействия внутренняя система может обращаться непосредственно к Java-элементам lsFusion-системы (как к обычным Java объектам). Таким образом, можно выполнять все те же операции как и с использованием сетевых протоколов, но при этом избежать существенного оверхеда такого взаимодействия (например, на сериализацию параметров / десериализацию результата и т.п). Кроме того, такой способ общения гораздо удобнее и эффективнее, если взаимодействие очень тесное (то есть в процессе выполнения одной операции требуется постоянное обращение в обе стороны — от lsFusion системы к другой системе и обратно) и / или требует доступа к специфическим узлам платформы.

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

  • Если первоначально обращение идет из lsFusion системы (через описанный выше механизм), то в качестве «объекта поиска» можно использовать объект действия, «через которое» идет это обращение (класс этого действия должен наследоваться от lsfusion.server.physics.dev.integration.internal.to.InternalAction, у которого, в свою очередь, есть все необходимые интерфейсы).
  • Если объект, из метода которого необходимо обратиться к lsFusion системе, является Spring bean'ом, то ссылку на объект бизнес-логики можно получить, используя dependency injection (соответственно bean называется businessLogics).

Пример Java-класса
import lsfusion.server.data.sql.exception.SQLHandledException;
import lsfusion.server.data.value.DataObject;
import lsfusion.server.language.ScriptingErrorLog;
import lsfusion.server.language.ScriptingLogicsModule;
import lsfusion.server.logics.action.controller.context.ExecutionContext;
import lsfusion.server.logics.classes.ValueClass;
import lsfusion.server.logics.property.classes.ClassPropertyInterface;
import lsfusion.server.physics.dev.integration.internal.to.InternalAction;
 
import java.math.BigInteger;
import java.sql.SQLException;
 
public class CalculateGCDObject extends InternalAction {
 
    public CalculateGCDObject(ScriptingLogicsModule LM, ValueClass... classes) {
        super(LM, classes);
    }
 
    @Override
    protected void executeInternal(ExecutionContext<ClassPropertyInterface> context) throws SQLException, SQLHandledException {
        try {
            DataObject calculation = (DataObject)getParamValue(0, context);
            BigInteger a = BigInteger.valueOf((Integer)findProperty("a").read(context, calculation));
            BigInteger b = BigInteger.valueOf((Integer)findProperty("b").read(context, calculation));
            BigInteger gcd = a.gcd(b);
            findProperty("gcd[Calculation]").change(gcd.intValue(), context, calculation);
        } catch (ScriptingErrorLog.SemanticErrorException ignored) {
        }
    }
}

SQL-взаимодействие

Системы имеющие доступ к SQL-серверу lsFusion-системы (одной из таких систем, к примеру, является сам SQL-сервер), могут обращаться непосредственно к таблицам и полям, созданным lsFusion-системой, средствами SQL-сервера. При этом необходимо учитывать, что, если чтение данных относительно безопасно (за исключением возможного удаления / изменения таблиц и их полей), то при записи данных не будут вызваны никакие события (и, соответственно, все элементы их использующие — ограничения, агрегации и т.п.), а также не будут пересчитаны никакие материализации. Поэтому записывать данные напрямую в таблицы lsFusion-системы крайне не рекомендуется, а если это все же необходимо, важно учесть все вышеупомянутые особенности.

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

Миграция


На практике часто возникают ситуации когда по различным причинам необходимо изменять имена уже существующих элементов системы. Если элемент, который необходимо переименовать, не связан ни с какими первичными данными, это можно сделать без каких-либо лишних телодвижений. Но если этим элементом является первичное свойство или класс, то такое «тихое» переименование приведет к тому, что данные этого первичного свойства или класса просто-напросто исчезнут. Чтобы этого не произошло, разработчик может создать специальный миграционный файл migration.script, поместить его в classpath сервера, и в нем указать, как старые имена элементов соответствуют новым именам. Работает это все следующим образом:

Миграционный состоит из блоков, которые описывают изменения, произведенные в указанной версии структуры базы данных. При старте сервера применяются все изменения из миграционного файла, которые имеют версию выше, чем версия, хранящаяся в базе данных. Изменения применяются в соответствии с версией, от меньшей версии к большей. Если изменение структуры БД происходит успешно, то максимальная версия из всех примененных блоков записывается в базу данных в качестве текущей. Синтаксис описания каждого блока выглядит следующим образом:

V<номер версии> {
    изменение1
    ...
    изменениеN
}

Изменения, в свою очередь, бывают следующих типов:
DATA PROPERTY oldNS.oldName[class1,...,classN] -> newNS.newName[class1,...,classN]
CLASS oldNS.oldName -> newNS.newName
OBJECT oldNS.oldClassName.oldName -> newNS.newClassName.newName

TABLE oldNS.oldName -> newNS.newName
PROPERTY oldNS.oldName[class1,...,classN] -> newNS.newName[class1,...,classN]
FORM PROPERTY oldNS.oldFormName.oldName(object1,...,objectN) -> newNS.newFormName.newName(object1,...,objectN) 
NAVIGATOR oldNS.oldName -> newNS.newName
Для миграции пользовательских данных актуальны только первые три типа изменений (изменения первичных свойств, классов, статических объектов). Оставшиеся четыре типа изменений нужны:

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

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

Пример миграции
V0.3.1 {
    DATA PROPERTY Item.gender[Item.Article] -> Item.dataGender[Item.Article] // изменение имени DATA свойства
    PROPERTY System.SIDProperty[Reflection.Property] -> Reflection.dbNameProperty[Reflection.Property] // одновременный перенос в другое пространство имен и изменение имени свойства
    FORM PROPERTY Item.itemForm.name(i) -> Item.itemForm.itemName(i)
}
  
V0.4 {
    FORM PROPERTY Document.documentForm.name(i) -> Document.itemForm.itemName(i)
    FORM PROPERTY Item.itemForm.itemName(i) -> Item.itemForm.iname // добавление явного имени для свойства на форме: iname = itemName(i)
    CLASS Date.DateInterval -> Date.Interval
    OBJECT Geo.Direction.North -> Geo.Direction.north
    TABLE User.oldTable -> User.newTable
}


Стоит отметить, что обычно большинство работ по генерации миграционных скриптов выполняется при помощи IDE. Так, при переименовании большинства элементов, можно указать специальную галочку Change migration file (включена по умолчанию), и IDE сгенерирует все нужные скрипты автоматически.

Интернационализация


На практике иногда возникает ситуация, когда необходимо иметь возможность использовать одно приложение на разных языках. Эта задача обычно сводится к локализации всех строковых данных, которые видит пользователь, а именно: текстовых сообщений, заголовков свойств, действий, форм и т.д. Все эти данные в lsFusion задаются при помощи строковых литералов (строк в одиночных кавычках, например 'abc'), соответственно, их локализация осуществляется следующим образом:
  • в строке вместо текста, который необходимо локализовать, указывается идентификатор строковых данных, заключенный в фигурные скобки (например, '{button.cancel}').
  • при передаче этой строки клиенту на сервере осуществляется поиск всех встречаемых в строке идентификаторов, затем осуществляется поиск каждого из них во всех ResourceBundle файлах проекта в нужной локали (то есть локали клиента), и при нахождении нужного варианта идентификатор в скобках заменяется на соответствующий текст.
script '{scheduler.script.scheduled.task.detail}' = DATA TEXT (ScheduledTaskDetail);
CONSTRAINT script(ScheduledTaskDetail d) AND action(d) MESSAGE '{scheduler.constraint.script.and.action}';
FORM scheduledTask '{scheduler.form.scheduled.task}';
ServerResourceBundle.properties:
scheduler.script.scheduled.task.detail=Script
scheduler.constraint.script.and.action=In the scheduler task property and script cannot be selected at the same time
scheduler.form.scheduled.task=Tasks

ServerResourceBundle_ru.properties
scheduler.script.scheduled.task.detail=Скрипт
scheduler.constraint.script.and.action=В задании планировщика не могут быть одновременно выбраны свойство и скрипт
scheduler.form.scheduled.task=Задания

Оптимизация производительности проектов с большим количеством данных


Если система небольшая и данных в ней относительно немного, как правило, она работает достаточно эффективно без каких-либо дополнительных оптимизаций. Если же логика становится достаточно сложной, а количество данных значительно возрастает, иногда имеет смысл подсказать платформе, как лучше хранить и обрабатывать все эти данные.

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

  • Материализации. Если чтений свойства достаточно много (значительно больше, чем изменений), можно существенно улучшить производительность операций, использующих такое свойство, материализовав его.
  • Индексы. Если свойство часто участвует в вычислениях других свойств, как правило, имеет смысл построить индекс по этому свойству.
  • Таблицы. Если для одного и того же набора объектов часто читаются / изменяются одновременно одни и те же свойства, хранить каждое такое свойство отдельно может быть достаточно неэффективно. Поэтому в платформе хранение свойств можно «группировать» в таблицы.

Материализации


Практически любое агрегированное свойств в платформе можно материализовать. В этом случае свойство будет храниться в базе данных постоянно и автоматически обновляться при изменении данных, от которых это свойство зависит. При этом при чтении значений такого материализованного свойства, эти значения будут читаться непосредственно из базы, как если бы свойство было первичным (а не вычисляться каждый раз). Соответственно, все первичные свойства являются материализованными по определению.

Свойство можно материализовать тогда и только тогда, когда для него существует конечное число наборов объектов, для которых значение этого свойства не NULL

Вообще, тема материализаций достаточно обстоятельно разбиралась в недавней статье про баланс записи и чтения в базах данных, поэтому останавливаться на ней подробно здесь, на мой взгляд, особого смысла не имеет.

Индексы


Построение индекса по свойству позволяет хранить в базе все значения такого свойства в упорядоченном виде. Соответственно, индекс обновляется при каждом изменении значения индексированного свойства. Благодаря индексу, если, например, идет фильтрация по индексированному свойству, можно очень быстро найти нужные объекты, а не просматривать все существующие объекты в системе.

Индексировать можно только материализованные свойства (из раздела выше).

Индекс также может быть построен сразу по нескольким свойствам (это эффективно, если, к примеру, фильтрация идет сразу по этим нескольким свойствам). Кроме того, в такой составной индекс можно включать параметры свойств. Если указанные свойства хранятся в разных таблицах, то при попытке построения индекса будет выдана соответствующая ошибка.
INDEX customer(Order o);

date = DATA DATE (Order);
INDEX date(Order o), o;

INDEX name(Sku s), price(s, DATE d), d;

Таблицы


Для хранения и вычисления значений свойств платформа lsFusion использует реляционную базу данных. Все первичные свойства, а также все агрегированные свойства, которые помечены как материализованные, хранятся в полях таблиц базы данных. Для каждой таблицы существует набор ключевых полей с именами key0, key1, ..., keyN, в которых хранятся значения объектов (например, для пользовательских классов — идентификаторы этих объектов). Во всех остальных полях хранятся значения свойств таким образом, что в соответствующем поле каждого ряда находится значение свойства для объектов из ключевых полей.

При создании таблицы необходимо указать список классов объектов, которые будут ключами в этой таблице.
TABLE book (Book);

in = DATA BOOLEAN (Sku, Stock);
TABLE skuStock (Sku, Stock); // в ней будет храниться свойство in

price = DATA NUMERIC[10,2] (Sku, DATE);
TABLE skuDate (Sku, DATE); // в ней будет храниться свойство Sku

TABLE sku (Sku);
Для каждого свойства можно указать, в какой таблице оно должно храниться. При этом количество ключей таблицы должно совпадать с количеством параметров свойства, а классы параметров должны подходить к классам ключей этой таблицы. Если для свойства таблица, в которой оно должно храниться, не задана явно, свойство автоматически будет помещено в «ближайшую» существующую в системе таблицу (то есть, количество ключей которой совпадает с количеством параметров свойства, и классы ключей которой ближе всего подходят к классам параметров).

Имена таблиц и полей, в которых хранятся свойства, в СУБД формируются в соответствии с заданной политикой именования. На текущий момент в платформе поддерживается три стандартных политики именования.
Политика Имя таблицы Имя поля
Полное с сигнатурой (по умолчанию) ПространствоИмен_ИмяТаблицы ПространствоИмен_ИмяСвойства_ИмяКласса1
_ИмяКласса2..._ИмяКлассаN
Полное без сигнатуры ПространствоИмен_ИмяТаблицы ПространствоИмен_ИмяСвойства
Краткое ИмяТаблицы ИмяСвойства
При необходимости, для каждого свойства разработчик может явно указать имя поля, в котором будет храниться это свойство. Кроме того, существует возможность создать свою политику именования полей свойств, если вышеперечисленные по каким-то причинам не подходят.

При выборе политики именования важно иметь в виду, что использование слишком краткой политики именования свойств, если количество материализованных свойств достаточно большое, может значительно усложнить именование этих свойств (так, чтобы оно было уникальным), или, соответственно, приводить к слишком частой необходимости явно именовать поля, в которых будут храниться эти свойства.

Заключение


Как говорилось в одном известном мультике: «мы строили, строили и наконец построили». Может, конечно, немного поверхностно, но разобраться с основными возможностями языка / платформы lsFusion по этим трем статьям, я думаю, можно. Пришла пора переходить к самой интересной части — сравнению с другими технологиями.

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

Так что совсем скоро появится еще несколько статей в стиле: «Почему не ...?», и, я уверен, они будут значительно более интересными, чем эти весьма занудные tutorial'ы.
Tags:
Hubs:
+6
Comments122

Articles

Information

Website
lsfusion.org
Registered
Founded
Employees
51–100 employees
Location
Беларусь
Representative
NitroJunkie