RecordContainers
A RecordContainer is the ADITO model that defines how the data of an Entity is retrieved (loaded) and persisted (saved). There are various types of RecordContainers, which are described in the following chapters.
Depending on the purpose, it may be appropriate to define more than one RecordContainer per Entity. An example of this is KeywordEntry_entity in the ADITO xRM project.
⚠️ Important
A RecordContainer includes the option to utilize a cache in order to improve the performance of the ADITO system for repeated requests of the same data. This is described in the chapter RecordContainerCache.
Database RecordContainer
A Database RecordContainer (dbRecordContainer) is a RecordContainer that enables an easy-to-establish mapping of specific EntityFields and specific database columns.
Example of a mapping of an EntityField (right) and a database column (left)
The database tables selectable in property recordfield are loaded from the Alias Definition, not directly from the database. Therefore, make sure that the Alias Definition is synchronized with the database. If required, update the Alias Definition, in order to make all respective tables/columns available in property recordfield.
On the basis of these "RecordFieldMappings", the Database RecordContainer automatically generates all required SQL statements for
- loading (
SELECT), - updating (
UPDATE), - saving (
INSERT), - ordering (
ORDER BY), and - deleting (
DELETE) data.
Furthermore, you can optionally write specific parts of the SQL statement yourself, particularly by using:
- the RecordContainer's properties
fromClauseProcess(FROM)conditionProcess(WHERE)orderClauseProcess(ORDER BY)
- a RecordFieldMapping's property
expression(e.g., subselect oder SQL calculation)
💡 Tip
You can find detailed additional information in the sub-chapter Basic SQL Statement.
COUNT queries
When a dbRecordContainer loads data from the database, in many cases, a SELECT COUNT(*) statement is executed automatically before the SELECT statement of the actual data is executed.
Purpose
The automatic SELECT COUNT(*) queries serve several purposes, particularly:
- the number of datasets is stored in specific variables, e.g.,
$sys.datarowcount - the
SELECTstatement for retrieving the actual data is skipped ifSELECT COUNT(*)returns 0 (i.e., no datasets are available)
minimizeCountQueries
Usually, a SELECT COUNT(*) statement consumes only minimal system resources. If you still want to reduce the frequency of these queries, you can set the dbRecordContainer's property minimizeCountQueries to true.
However, before using this property, read its property description carefully to avoid unwanted side effects.
Caching not required
When your dbRecordContainer utilizes a cache, please note:
Even though caching is active, a SELECT COUNT(*) statement is still executed.
The reason is that SELECT COUNT(*) queries are generally excluded from caching, as they are assumed to consume only minimal system resources.
JDitoRecordContainer
Introduction
While a dbRecordContainer has a database as its data source, a JDitoRecordContainer uses JDito code as its data source. (Of course, the JDito code itself can include loading from the database.) The result of this code is an array. This array must provide the data in a specific order, which is configured in the JDitoRecordContainer’s recordFieldMappings property. The data source array is the result object of the contentProcess property.
How to establish a JDitoRecordContainer?
To establish a JDitoRecordContainer, proceed as follows:
-
Open the respective Entity in the Navigator window.
-
Right-click on it and choose New RecordContainer from the context menu.
-
A dialog appears, in which you choose JDitoRecordContainer as the type and enter an arbitrary name (the convention is, to name it "jdito").
-
The new RecordContainer will appear as a sub-node of RecordContainers. Click on it and edit its properties:
-
recordFieldMappings: Add the EntityFields to be filled by the
JDitoRecordContainer.
⚠️ Important:
- The order of the fields must match the order of the data in the array built in the
contentProcess(see below).- An EntityField named
UID(spelled exactly like this!), with contentTypeTEXTmust always be present and included in the field mapping.
- contentProcess: This is the core logic of a JDito RecordContainer. This code's result must always be a nested array, acting as the data source.
⚠️ Important: The order of the data in the array must be exactly the order of the fields in the
recordFieldMappings.
In principle, the contentProcess looks like this:
var entityField1value1 = (...);
var entityField2value1 = (...);
var entityField3value1 = (...);
var myDataArray = [];
myDataArray.push([entityField1value1, entityField2value1, entityField3value1]);
myDataArray.push([entityField1value2, entityField2value2, entityField3value2]);
myDataArray.push([entityField1value3, entityField2value3, entityField3value3]);
result.object(myDataArray);
Example from xRM:
In Turnover_entity (Context "Turnover"), the contentProcess fills a variable chartData using:
// EntityFields: UID, PARENT, CATEGORY, X, Y
chartData.push([key, countDataSet.parent, countDataSet.category, countDataSet.x, countDataSet.count]);
Finally, return the array:
result.object(chartData);
The structured data is displayed in charts within a GroupLayout, e.g., Sales > Opportunity > MainView > tab Forecast.
Advanced Explanations
The JDitoRecordContainer is one of four RecordContainer types and provides flexibility due to its programmable data source. However, you must implement sorting, paging, and filtering manually, using the contentProcess.
You may source data from a database using SqlBuilder or from a web service via the net module.
⚠️ Important: You must handle sorting, paging, and filtering manually.
Important Properties
jDitoRecordAlias: Default alias used in database access methods, often set toData_alias.
recordFieldMappings: MapsEntityFieldsto the array values of thecontentProcess. ⚠️ Warning: Order must match exactly.
isPageable: Enables paging. Access$local.pageand$local.pagesizeincontentProcess.
isFilterable: Enables filtering. Access$local.filter(map of field/operator/value).
isRequireContainerFiltering: Enables server-side filtering for performance.
isSortable: Enables sorting. Access$local.order(map of field and direction).
contentProcess: Core process for providing the data. Returns a nested array:
var data = [
["UID1","VALUE1.1","VALUE2.1","VALUE3.1"],
["UID2","VALUE1.2","VALUE2.2","VALUE3.2"],
["UID3","VALUE1.3","VALUE2.3","VALUE3.3"]
];
result.object(data);
rowCountProcess: Returns row count to avoid double execution ofcontentProcess.
hasDependentRecords: For hierarchical structures (e.g., trees). Re-executescontentProcessafter deletion.
onInsert: Handles new records using$local.rowdata:
var rowdata = vars.get("$local.rowdata");
var columns = ["UID", "C1", "C2", "C3"];
var values = [rowdata["UID.value"], rowdata["C1.value"], rowdata["C2.value"], rowdata["C3.value"]];
new SqlBuilder().insertData("YOURTABLE", columns, null, values);
⚠️ Important: Use
$local.rowdata, not$field.
onUpdateUpdates changed data fields:
var changedFields = vars.get("$local.changed");
var rowData = vars.get("$local.rowdata");
var columns = [];
var data = [];
for (let field in changedFields) {
columns.push(changedFields[field].split(".")[0]);
data.push(rowData[changedFields[field]]);
}
newWhereIfSet("YOURTABLEID = '" + vars.get("$local.uid") + "'")
.updateData(true, "YOURTABLE", columns, null, data);
⚠️ Use
$local.rowdata, not$field.
onDelete: Deletes a record:
newWhereIfSet("YOURTABLEID = '" + vars.get("$local.uid") + "'")
.deleteData(true, "YOURTABLE");
⚠️ Use
$local.uid, not$field.
⚠️ For web services, adapt
onInsert,onUpdate, andonDeleteaccordingly.
Step-by-step example see examples.
Filtering a JDitoRecordContainer
The previous example shows basic filter integration. For complex filtering, use additional helper functions from JDitoFilter_lib, especially FilterSqlTranslator, to manually filter data or build SQL conditions.
You’ll find practical implementations of such advanced filtering in JDitoRecordContainers from contexts like Attribute, Manager, or Workflow.
IndexRecordContainer
In ADITO, the "index" is a kind of parallel data container that can be filled with selected data from connected ADITO databases (e.g., name and address of contact persons or companies). Due to data reduction, a special data structure, and a dedicated database (comparable to a NoSQL database), the data included in the index can be scanned and read very quickly using the Apache Solr search engine.
Commonly used for Global Search
An IndexRecordContainer can be used in various ways, the most common of which is the Global Search: If you click on the search button ("magnifying glass") in the web client's Global Bar, a search field opens, in which you can enter search terms, e.g., the name Smith. In the result, among others, all persons or companies are listed that have a name including Smith. By clicking on one of them, the corresponding dataset is opened, ready for further processing.
Besides the Global Search, an IndexRecordContainer can also be used as an alternative data source for EntityFields, particularly if using a DbRecordContainer or JDitoRecordContainer is not suitable or does not provide the required performance. In the ADITO xRM project, several FilterViews are filled using an IndexRecordContainer.
Connected to audit process
To keep the index up to date, it is connected to ADITO's audit process. This process is triggered on every change in the database. In the Projects window, you can find the audit process under:
process > internal > process_audit
📄 For detailed explanations on the purpose and usage of an IndexRecordContainer, see the ADITO Information Document AID093 "Indexsearch".
DatalessRecordContainer
A DatalessRecordContainer makes it possible to use an Entity without loading or writing data via the RecordContainer. This enables the mere entry of data, which can then be read and processed, e.g., via an Action.
Example: If you want to request specific information before opening the actual Context, you could realize this via an Entity having a DatalessRecordContainer. Via an Action, the information could subsequently be extracted from the EntityFields and then be passed to another Entity via method neon.openContextWithRecipe, using a Parameter.
Examples in ADITO xRM:
There are several examples of the usage of a DatalessRecordContainer in the ADITO xRM project – just do a full-text search for the term "datalessRecordContainer".
An easy-to-understand example is BulkMailTesting_entity. This Entity's View BulkMailTesting_view is opened when, in the FilterView of Context BulkMail, a dataset is selected and Action "Test email" is called via the three-dotted button in the CardViewTemplate of the PreviewView – see BulkMail_entity.testMail.onActionProcess.
BulkMailTesting_view is only required for selecting a contact and entering the recipient email address – both are temporary data that does not need to be stored anywhere. After the user has pressed the button "Test email", the data is extracted in process BulkMailTesting_entity.testMail.onActionProcess (do not mistake this process with the onActionProcess quoted before) and used for sending out the test email.
If the user had set the switch "Save settings" to true, then CONTACT_ID and the email recipient are stored in the database table BULKMAIL – i.e., the table referring to BulkMail_entity. Thus, BulkMailTesting_entity itself does not need database access and therefore uses a DatalessRecordContainer.
Note
It stands out that a DatalessRecordContainer also features the property"alias"(for specifying the default alias), when in fact there is no database access in this case.
The reason is consistency:"alias"must be a common property of all types of RecordContainers.
This has technical reasons, because the existence of a RecordContainer with an"alias"property is a prerequisite for all methods that allow database access on Entity level (e.g., in Actions).
Note
If you open the PreviewView of an Entity connected to a DatalessRecordContainer, and there is an Action involved that does not cause a "jump" to another Entity, then make sure that the PreviewView is closed via the following code line at the end of theonActionProcess:neon.closeImage(vars.get("$sys.currentimage"), true);Otherwise, the PreviewView will remain open, and the user will not get any feedback when the Action is finished, but must close the window manually.