
The third and final part of a series of articles about lsFusion (links to the
first and
second parts)
It will focus on the physical model: everything that is not connected with the system’s functionality, but is associated with its development and performance optimization, when there is too much data.
')
This article, like the previous ones, is not very suitable for entertaining reading, but, unlike the others, there will be more technical details and “hot” topics (like typing or metaprogramming), plus this article will give a part of the answers to the question, how is it all works inside.
In this article, we’ll do without a picture (there’s no stack like this), but we’ll do a table of contents, as requested in previous articles:
Item Identification
If the project consists of several small files, then problems with the naming of elements usually do not arise. All names are in sight, and it's easy enough to make sure they don't overlap. If the project, on the contrary, consists of many modules developed by a large number of different people, and abstractions in these modules from one domain domain, name conflicts become much more likely. LsFusion has two mechanisms to solve these problems:
- Namespaces - separation of a name into full and short, and the ability to use only a short name when accessing an element
- Explicit typing (to be more precise, function overloading) - the ability to name properties (and actions) in the same way, and then, when accessing them, depending on the classes of arguments, automatically determine which property the call is to
Namespaces
Any complex project usually consists of a large number of elements that must be named. And, if domain domains intersect, very often there is a need to use the same name in different contexts. For example, we have the name of the class or form Invoice (invoice), and we want to use this name in various function blocks, for example: Purchase (Purchase), Sale (Sale), Return of purchase (PurchaseReturn), Return of sale (SaleReturn). It is clear that you can call classes / forms PurchaseInvoice, SaleInvoice, and so on. But, firstly, such names in themselves will be too bulky. And secondly, in one functional block, calls, as a rule, go to the elements of the same functional block, which means that when developing, for example, the Purchase functional block from a constant repetition of the word Purchase, it will simply ripple in your eyes. To prevent this from happening, the platform has such a concept as a namespace. It works as follows:
- every element in the platform is created in some namespace
- if other elements are being accessed during element creation, elements created in the same namespace take precedence
Namespaces in the current version of the language are set for the entire module immediately in the module header. By default, if no namespace is specified, it is created implicitly with a name equal to the module name. If you need to access an element from a non-priority namespace, you can do this by specifying the full name of the element (for example, Sale.Invoice).
Explicit typing
Namespaces are important, but not the only way to make code shorter and more readable. In addition to them, when searching for properties (and actions), it is also possible to take into account the classes of arguments passed to them at the input. For example:
Here, of course, the question may arise: what will happen if the namespace of the desired property is not a priority, but it is better suited for classes? In fact, the general search algorithm is quite complicated (its full description is
here ) and there are a lot of such “ambiguous” cases, so in case of uncertainty it is recommended to either specify namespaces / classes of the desired property explicitly, or double-check in the IDE (using Go to Declaration - CTRL + B) that the property found is exactly what was meant.
Also, it is worth noting that explicit typing in lsFusion is generally not necessary. Parameter classes can be omitted, and if the platform has enough information to find the desired property, it will do so. On the other hand, in really complex projects, it is still recommended to set the parameter classes explicitly, not only from the point of view of brevity of the code, but also from the point of view of various additional features, such as: early error diagnosis, smart auto-completion from the IDE and so on. We had extensive experience working both with implicit typing (the first 5 years) and explicit (remaining time), and I must say that the times of implicit typing are now remembered with a shudder (although it may just “we did not know how to cook it”).
Modularity
Modularity is one of the most important properties of the system, allowing to ensure its extensibility, reuse of code, as well as effective interaction of the development team.
LsFusion provides modularity with the following two mechanisms:
- Extensions - the ability to expand (change) the elements of the system after they are created.
- Modules - the ability to group some functionality together for its further reuse.
Extensions
lsFusion supports the ability to extend classes and forms, as well as properties and actions through the polymorphism mechanism described in the first article.
Also, we note that almost all other platform designs (for example, navigator, form design) are extensible by definition, therefore there is no separate extension logic for them.
Modules
A module is some functionally complete part of a project. In the current version of lsFusion, a module is a separate file consisting of the header and body of a module. The title of the module, in turn, consists of: the name of the module, as well as, if necessary, a list of the modules used and the name of the namespace of this module. The body of the module consists of declarations and / or extensions of system elements: properties, actions, restrictions, forms, metacodes and so on.
Usually modules use elements from other modules to declare their own / expand existing elements. Accordingly, if module B uses elements from module A, then it is necessary to indicate in module B that it depends on A.
Based on their dependencies, all modules in the project are arranged in a certain order in which they are initialized (this order plays an important role when using the aforementioned extension mechanism). It is guaranteed that if module B depends on module A, the initialization of module A will occur earlier than the initialization of module B. Cyclic dependencies between the modules in the project are not allowed.
Dependencies between modules are transitive. That is, if module C depends on module B, and module B depends on module A, then it is considered that module C also depends on module A.
Any module always automatically depends on the system module System, regardless of whether it is indicated explicitly or not.
Metaprogramming
Metaprogramming is a type of programming associated with writing program code that generates other program code as a result. LsFusion uses so-called metacodes for metaprogramming.
The metacode consists of:
- metacode name
- metacode parameters
- body of a metacode - a code block consisting of declarations and / or extensions of system elements (properties, actions, events, other metacodes, etc.)
Accordingly, before starting the main processing of the code, the platform pre-processes it - replaces all uses of metacodes with the bodies of these metacodes. In this case, all metacode parameters used in identifiers / string literals are replaced by the arguments passed to this metacode:
Announcement:
Using:
Resulting Code:
In addition to simply substituting metacode parameters, the platform also allows you to combine these parameters with existing identifiers / string literals (or with each other), for example:
Announcement:
Using:
Resulting Code:
Metacodes are very similar to macros in C, but, unlike the latter, they do not work at the text level (they cannot, for example, pass keywords in the parameter), but only at the level of identifiers / string literals (this restriction, in particular, allows parsing the metacode body in the IDE).
In lsFusion, metacodes solve problems similar to generics in Java (passing classes as parameters) and lambda in FPs (passing functions as parameters), however, they do not do it very beautifully. But, on the other hand, they do this in a much more general case (that is, for example, with the possibility of combining identifiers, use in any syntactic constructions - forms, designs, navigator, etc.)
Note that the "deployment" of metacodes is supported not only in the platform itself, but also in the IDE. So, in the IDE there is a special mode, Enable meta, which generates the resulting code directly in the sources and thereby allows this generated code to participate in the search for uses, auto-completion, etc. In this case, if the body of the metacode changes, the IDE automatically updates all uses of this metacode.

Also, metacodes can be used not only for automatic, but also for manual code generation (as templates). To do this, it’s enough to write @@ instead of one @ and immediately after the metacode usage string has been completely entered (up to the semicolon), the IDE will replace this metacode usage with the code generated by this metacode:

Integration
Integration includes everything related to the interaction of the lsFusion system with other systems. From the point of view of the direction of this interaction, integration can be divided into:
- Accessing the lsFusion system from another system.
- Access from lsFusion system to another system.
From the point of view of the physical model, integration can be divided into:
- Interaction with systems running in the “same environment” as the lsFusion system (that is, in the Java Virtual Machine (JVM) of the lsFusion server and / or using the same SQL server as the lsFusion system).
- Interaction with remote systems via network protocols.
Accordingly, the first systems will be called internal, the second - external.
Thus, there are four different types of integration in the platform:
- Appeal to an external system
- Appeal from an external system
- Appeal to the internal system
- Appeal from the internal system
Appeal to an external system
Access to external systems in lsFusion is in most cases implemented using the special EXTERNAL operator. This operator executes the given code in the language / in the paradigm of the given external system. In addition, this operator allows you to transfer objects of primitive types as parameters of such a call, as well as write the results of the call to the specified properties (without parameters).
Currently, the platform supports the following types of interactions / external systems:
HTTP - Performs an http request from a web server.For this type of interaction, you must specify a query string (URL), which simultaneously determines both the server address and the request that needs to be executed. Parameters can be transmitted both in the query line (to refer to the parameter, the special character $ and the number of this parameter, starting with 1) are used, and in its body (BODY). It is assumed that all parameters not used in the query string are passed to BODY. If there is more than one parameter in BODY, the BODY content type during transmission is set to multipart / mixed, and the parameters are transferred as components of this BODY.
When processing parameters of file classes (FILE, PDFFILE, etc.) in BODY, the content type of the parameter is determined depending on the file extension (in accordance with the following
table ). If the file extension is not in this table, the content type is set to application / <file extension>.
If necessary, using the special option (HEADERS), you can set the headers of the executed request. To do this, specify a property with exactly one parameter of the string class in which the title will be stored, and the value of the string class in which the value of this header will be stored.
The result of the http-request is processed in the same way as its parameters, only in the opposite direction: for example, if the content type of the result is either present in the following
table or equal to application / *, then it is considered that the result obtained is a file and should be written to a property with the value FILE . The headers of the result of the http-request are processed by analogy with the headers of this request itself (with the only difference being that the option is called HEADERSTO, not HEADERS).
SQL - executing a SQL server command.For this type of interaction, the connection string and the SQL command (s) to be executed are specified. Parameters can be passed both in the connection string and in the SQL command. To access the parameter, the special character $ and the number of this parameter are used (starting from 1).
Parameters of file classes (FILE, PDFFILE, etc.) can be used only in the SQL command. At the same time, if any of the parameters at execution is a TABLE file (TABLEFILE or FILE with the table extension), then this parameter is considered a table in this case too:
- before executing the SQL command, the value of each such parameter is loaded onto the server in a temporary table
- when substituting parameters, it is not the parameter value itself that is substituted, but the name of the created temporary table
The execution results are: for DML queries - numbers equal to the number of processed records, for SELECT queries - TABLE format files (FILE with the table extension) containing the results of these queries. The order of these results coincides with the order of execution of the corresponding queries in the SQL command.
LSF - call the action of another lsFusion server.For this type of interaction, the connection string to the lsFusion server (or its web server, if any) is set, the action to be performed, as well as a list of properties (without parameters), in the values ​​of which the results of the call will be written. The parameters to be transferred must coincide in number and class with the parameters of the action being performed.
The method of setting the action in this type of interaction is fully consistent with the method of setting the action when accessing from an external system (about this type of access in the next section).
By default, this type of interaction is implemented using the HTTP protocol using the appropriate interfaces for accessing to / from an external system.
In the event that you need to access the system using a protocol different from the above, you can always do this by creating an action in Java and implementing this call there (but more on that later in the section “Accessing Internal Systems”)
Appeal from an external system
The platform allows external systems to access the system developed on lsFusion using the HTTP network protocol. The interface of this interaction is to call some action with the given parameters and, if necessary, return the values ​​of some properties (without parameters) as results. It is assumed that all objects of parameters and results are objects of primitive types.
The called action can be set in one of three ways:
- / exec? action = <action name> - sets the name of the called action.
- / eval? script = <code> - sets the code in lsFusion. It is assumed that in this code there is a declaration of an action with the name run, it is this action that will be called. If the script parameter is not specified, it is assumed that the code is passed as the first BODY parameter.
- / eval / action? script = <action code> - sets the action code in lsFusion. To access the parameters, you can use the special character $ and the parameter number (starting from 1).
In the second and third cases, if the script parameter is not specified, then it is assumed that the code is passed by the first BODY parameter.
The processing of parameters and results is symmetric to accessing external systems using the HTTP protocol (with the only difference being that the parameters are processed as results, and, on the contrary, the results are processed as parameters), so we will not repeat ourselves much.
For example, if we have an action:
Then you can access it using a POST request which:
- URL - http: // server_address / exec? Action = importOrder & p = 123 & p = 2019-01-01
- BODY - json file with query strings
Python call example 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)
Appeal to the internal system
There are two types of internal interaction:
Java interoperabilityThis type of interaction allows you to call Java code inside the JVM lsFusion server. To do this, you must:
- ensure that the compiled Java class is available in the classpath of the application server. It is also necessary that this class inherit lsfusion.server.physics.dev.integration.internal.to.InternalAction.
Java class example 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) { } } }
- register an action using the special internal call operator (INTERNAL)
- a registered action, like any other, can be called using the call operator. In this case, the executeInternal method (lsfusion.server.logics.action.controller.context.ExecutionContext context) of the specified Java class will be executed.
SQL interactionThis type of interaction allows you to access the objects / syntax constructs of the SQL server used by the developed lsFusion system. To implement this type of interaction, the platform uses a special operator - FORMULA. This operator allows you to create a property that evaluates some formula in the SQL language. The formula is specified as a string, inside which the special character $ and the number of this parameter are used to access the parameter (starting from 1). Accordingly, the number of parameters of the obtained property will be equal to the maximum of the numbers of the parameters used.
It is recommended to use this operator only in cases where the task cannot be solved with the help of other operators, as well as if it is guaranteed which specific SQL servers can be used, or the syntax constructs used comply with one of the latest SQL standards.
Appeal from the internal system
Everything is symmetrical to the appeal to the internal system. There are two types of interaction:
Java interoperabilityWithin the framework of this type of interaction, the internal system can directly access the Java elements of the lsFusion system (like ordinary Java objects). Thus, you can perform all the same operations as using network protocols, but at the same time avoid significant overhead of such interaction (for example, serialization of parameters / deserialization of the result, etc.). In addition, this method of communication is much more convenient and efficient if the interaction is very close (that is, during the execution of one operation, constant contact is required in both directions - from the lsFusion system to another system and vice versa) and / or requires access to specific platform nodes.
In order to access Java elements of an lsFusion system directly, you first need to get a link to some object that will have interfaces for finding these Java elements. This is usually done in one of two ways:- If initially the call comes from the lsFusion system (through the mechanism described above), then as the “search object” you can use the action object “through which” this call goes (the class of this action should be inherited from lsfusion.server.physics.dev.integration. internal.to.InternalAction, which, in turn, has all the necessary interfaces).
- If the object from whose method it is necessary to access the lsFusion system is a Spring bean, then a link to the business logic object can be obtained using dependency injection (the bean is called businessLogics, respectively).
Java class example 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 interactionSystems that have access to the SQL server of an lsFusion system (one of such systems, for example, is the SQL server itself), can access directly the tables and fields created by the lsFusion system using SQL server tools. It should be borne in mind that if reading data is relatively safe (with the exception of the possible deletion / modification of tables and their fields), then no events will be triggered during data recording (and, accordingly, all elements using them - restrictions, aggregations, etc. n.), and also no materializations will be recounted. Therefore, writing data directly to the tables of the lsFusion system is highly discouraged, and if it is still necessary, it is important to take into account all of the above features.Note that this direct interaction (but only for reading) is especially convenient for integration with various OLAP systems, where the whole process should occur with a minimal overhead.Migration
In practice, situations often arise when for various reasons it is necessary to change the names of existing elements of the system. If the element to be renamed is not associated with any primary data, this can be done without any unnecessary gestures. But if this element is a primary property or class, then such a “quiet” renaming will lead to the fact that the data of this primary property or class will simply disappear. To prevent this, the developer can create a special migration file migration.script, place it in the server classpath, and indicate in it how the old element names correspond to the new names. It all works as follows:Migration consists of blocks that describe the changes made in the specified version of the database structure. When starting the server, all changes from the migration file that have a version higher than the version stored in the database are applied. Changes are applied according to the version, from a smaller version to a larger one. If the change in the database structure is successful, then the maximum version of all applied blocks is written to the database as the current one. The syntax for the description of each block is as follows: V< > { 1 ... N }
Changes, in turn, are of the following types:For the migration of user data, only the first three types of changes are relevant (changes in primary properties, classes, static objects). The remaining four types of changes are needed:- for metadata migration (security policies, table settings, etc.)
- to optimize the migration of user data (so as not to recalculate aggregations and not transfer data between tables once again).
Accordingly, if the migration of metadata is not needed or there is not much data, such changes in the migration script can be omitted.It is worth noting that usually most of the work on the generation of migration scripts is performed using the IDE. So, when renaming most elements, you can specify the special checkbox Change migration file (enabled by default), and the IDE will generate all the necessary scripts automatically.Internationalization
In practice, sometimes a situation arises when it is necessary to be able to use one application in different languages. This task usually comes down to localizing all the string data that the user sees, namely: text messages, property headers, actions, forms, etc. All this data in lsFusion is set using string literals (single-quoted strings, for example 'abc'), respectively, their localization is carried out as follows:- instead of the text to be localized, the string identifies the string data, enclosed in braces (for example, '{button.cancel}').
- when this line is transmitted to the client on the server, all identifiers found in the line are searched, then each of them is searched in all the ResourceBundle project files in the necessary locale (that is, the client locale), and when the right option is found, the identifier in brackets is replaced with the corresponding text.
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=
Optimize the performance of large data projects
If the system is small and there is relatively little data in it, as a rule, it works quite efficiently without any additional optimizations. If the logic becomes quite complicated, and the amount of data increases significantly, sometimes it makes sense to tell the platform how best to store and process all this data.The platform has two main mechanisms for working with data: properties and actions. The first is responsible for storing and computing data, the second is for transferring the system from one state to another. And if the work of actions can be optimized quite limitedly (including due to the aftereffect), then for properties there is a whole set of features that allow you to both reduce the response time of specific operations and increase the overall system performance:- . ( , ), , , .
- . , , .
- . / , . «» .
Almost any aggregated properties in the platform can be materialized. In this case, the property will be stored in the database constantly and automatically updated when the data on which this property depends is changed. Moreover, when reading the values ​​of such a materialized property, these values ​​will be read directly from the database, as if the property was primary (and not calculated each time). Accordingly, all primary properties are materialized by definition.A property can be materialized if and only if for it there is a finite number of sets of objects for which the value of this property is not NULLGenerally, the topic of materialization was examined in sufficient detail in a recent articleabout the balance of writing and reading in databases, so to dwell on it in detail here, in my opinion, does not make much sense.Indices
Building an index by property allows you to store in the database all the values ​​of this property in an ordered manner. Accordingly, the index is updated each time the value of the indexed property changes. Thanks to the index, if, for example, filtering by an indexed property is in progress, you can very quickly find the necessary objects, rather than view all existing objects in the system.Only materialized properties can be indexed (from the section above).An index can also be built on several properties at once (this is effective if, for example, filtering is carried out immediately on these several properties). In addition, property parameters can be included in such a composite index. If the specified properties are stored in different tables, then an attempt to build the index will result in the corresponding error.Tables
LsFusion uses a relational database to store and calculate property values. All primary properties, as well as all aggregated properties that are marked as materialized, are stored in the fields of the database tables. For each table, there is a set of key fields with the names key0, key1, ..., keyN, in which the values ​​of objects are stored (for example, for custom classes, the identifiers of these objects). All other fields store property values ​​in such a way that in the corresponding field of each row is the property value for objects from key fields.When creating a table, you must specify a list of classes of objects that will be the keys in this table.For each property, you can specify in which table it should be stored. In this case, the number of table keys must match the number of property parameters, and parameter classes must match the key classes of this table. If the table in which it is to be stored is not set explicitly for the property, the property will automatically be placed in the "nearest" table existing in the system (that is, the number of keys of which coincides with the number of property parameters, and whose key classes are closest to parameter classes )The names of tables and fields in which properties are stored in the DBMS are formed in accordance with the specified naming policy. Currently, the platform supports three standard naming policies.If necessary, for each property, the developer can explicitly specify the name of the field in which this property will be stored. In addition, it is possible to create your own policy for naming property fields if the above for some reason is not suitable.When choosing a naming policy, it is important to keep in mind that using too short a property naming policy, if the number of materialized properties is large enough, can greatly complicate the naming of these properties (so that it is unique), or, accordingly, lead to too often the need to explicitly name The fields in which these properties will be stored.Conclusion
As they said in one famous cartoon: "we built, built, and finally built." It may, of course, be a bit superficial, but I think you can figure out the basic features of the lsFusion language / platform for these three articles. It's time to move on to the most interesting part - comparing with other technologies.As experience and the format of the Habr showed, doing it more efficiently playing not on your own, but on a foreign field. That is, to go not from opportunities, but from problems and, accordingly, to talk not about their advantages, but about the disadvantages of alternative technologies, and in their own terminology. Such an approach is usually much better perceived in conservative markets, and it is such a market, in my opinion, that is the market for the development of information systems, at least in the post-Soviet space.So very soon there will be several more articles in the style of: “Why not ...?”, And I’m sure they will be much more interesting than these very boring tutorials.