Using the Business Entity Dispatcher
Introduction
The Business Entity Dispatcher (Consultingwerk.OERA.BusinessEntityDispatcher.BusinessEntityDispatcher) is an abstract Business Entity base class that dispatches FetchData and SaveChanges requests to multiple underlying Business Entities that share the same ProDataset schema and maintains a joint result set. To any consumer — a SmartBusinessEntityAdapter, a DatasetModel, the Service Interface — the dispatcher looks and behaves like a single, regular Business Entity.
A typical use case is a unified "sales document" view over orders and invoices: both source Business Entities expose their data through the same temp-table layout, and the dispatcher merges the records from both entities into a single sorted, batched result set. Each record in the joint result set carries an Origin field that identifies the Business Entity the record came from, so update operations can be routed back to the right entity.
A complete sample implementation is available in the Test.BusinessEntityDispatcher package (SalesDocument dispatcher over an Order and an Invoice Business Entity). The code samples in this document are taken from that sample.
Components
Class / Interface | Purpose |
|---|---|
| Abstract base class (inherits |
| Describes the configuration of a dispatcher: the list of dispatched Business Entities. The concrete dispatcher class must assign an implementation of this interface to its |
| A single configuration entry, pairing an |
| Typed list of configuration items, exposed through the configuration's |
| Interface that every dispatched Business Entity must implement, so the dispatcher can retrieve data from it with the dataset passed |
The shared ProDataset schema
All dispatched Business Entities and the dispatcher itself use the same ProDataset definition (typically from a shared include file). The temp-table must provide two infrastructure fields:
Origin— identifies the Business Entity a record was retrieved from. The values used here must match theOriginvalues registered in the dispatcher configuration.RowIdent— a unique row identifier within the origin entity (in the sample, theROWIDvalues of the source buffers). The dispatcher uses this value as the restart position for batching.
The combination of the two fields must be unique; the sample uses them as the unique primary index and as the OpenAPI primary key:
@openapi.openedge.entity.primarykey (fields="Origin,RowIdent").
DEFINE TEMP-TABLE eSalesDocument NO-UNDO BEFORE-TABLE eSalesDocumentBefore
FIELD Origin AS CHARACTER LABEL "Record Origin":T
FIELD RowIdent AS CHARACTER LABEL "Row Ident":T
FIELD DocumentNum AS INTEGER LABEL "Document Number":T
FIELD DocumentDate AS DATE LABEL "Document Date":T
FIELD CustNum AS INTEGER LABEL "Customer Number":T
FIELD CustomerName AS CHARACTER LABEL "Customer Name":T
FIELD DocumentStatus AS CHARACTER LABEL "Document Status":T
INDEX Index1 AS UNIQUE PRIMARY Origin ASCENDING RowIdent ASCENDING
.
DEFINE DATASET dsSalesDocument FOR eSalesDocument .Note that the field names in the shared temp-table are intentionally neutral (DocumentNum, DocumentDate, …) — each Data Access class maps its own database fields (e.g. Order.OrderNum or Invoice.InvoiceNum) onto this common schema.
Implementing the Dispatcher Business Entity
The concrete dispatcher class inherits BusinessEntityDispatcher instead of BusinessEntity. Apart from that it is a regular Business Entity: it passes the dataset handle to the base class and can be registered and consumed by name like any other entity. The one additional responsibility of the constructor is assigning the DispatcherConfiguration property:
USING Consultingwerk.OERA.* FROM PROPATH .
USING Consultingwerk.OERA.BusinessEntityDispatcher.* FROM PROPATH .
USING Test.BusinessEntityDispatcher.* FROM PROPATH .
CLASS Test.BusinessEntityDispatcher.SalesDocument.SalesDocumentBusinessEntity
INHERITS BusinessEntityDispatcher
IMPLEMENTS IFetchDataByReference
USE-WIDGET-POOL:
{ Test/BusinessEntityDispatcher/SalesDocument/dsSalesDocument.i }
CONSTRUCTOR PUBLIC SalesDocumentBusinessEntity ():
SUPER (DATASET dsSalesDocument:HANDLE).
THIS-OBJECT:DataAccessName = "Test.BusinessEntityDispatcher.SalesDocument.SalesDocumentDataAccess":U .
THIS-OBJECT:DispatcherConfiguration = NEW SalesDocumentBusinessEntityDispatcherConfiguration () .
END CONSTRUCTOR.
METHOD PUBLIC FINAL VOID FetchDataByReference (poFetchDataRequest AS Consultingwerk.OERA.IFetchDataRequest,
OUTPUT DATASET dsSalesDocument):
THIS-OBJECT:FetchData (poFetchDataRequest, DATASET dsSalesDocument:HANDLE) .
END METHOD.
END CLASS.Instead of NEW'ing the configuration directly, the configuration instance may also be resolved through the IFactory service — this allows customers or applications to replace the set of dispatched Business Entities without changing the dispatcher class itself:
DEFINE VARIABLE oFactory AS IFactory NO-UNDO .
oFactory = {Consultingwerk/get-mandatory-service.i IFactory} .
THIS-OBJECT:DispatcherConfiguration =
CAST (oFactory:CreateInstance (GET-CLASS (IBusinessEntityDispatcherConfiguration),
"SalesDocument":U),
IBusinessEntityDispatcherConfiguration) .The Dispatcher Configuration
The dispatcher configuration is the central piece that tells the dispatcher which Business Entities to dispatch to and how records are attributed to them. It is a small class implementing IBusinessEntityDispatcherConfiguration, which exposes a single property: the list of configuration items. Each item maps an Origin value to the name of the Business Entity serving that origin:
USING Consultingwerk.OERA.BusinessEntityDispatcher.* FROM PROPATH .
CLASS Test.BusinessEntityDispatcher.SalesDocumentBusinessEntityDispatcherConfiguration
IMPLEMENTS IBusinessEntityDispatcherConfiguration:
DEFINE PUBLIC PROPERTY ConfigurationItems AS ListDispatcherBusinessEntityDispatcherConfigurationItem NO-UNDO
GET.
PRIVATE SET .
CONSTRUCTOR PUBLIC SalesDocumentBusinessEntityDispatcherConfiguration ():
THIS-OBJECT:ConfigurationItems = NEW ListDispatcherBusinessEntityDispatcherConfigurationItem () .
THIS-OBJECT:ConfigurationItems:Add ("Order":U, "Test.BusinessEntityDispatcher.Order.OrderBusinessEntity":U) .
THIS-OBJECT:ConfigurationItems:Add ("Invoice":U, "Test.BusinessEntityDispatcher.Invoice.InvoiceBusinessEntity":U) .
END CONSTRUCTOR.
END CLASS.The configuration plays a role in both directions of the data flow:
During
FetchDatathe dispatcher iterates the configuration items and retrieves data from every registered Business Entity (resolved by itsEntityNamethrough theServiceManager), merging the results. TheOriginvalue of each item is also used as the key under which the per-entity batching context is stored (see below).During
SaveChangesthe dispatcher inspects theOriginfield of the changed records and usesConfigurationItems:Find (cOrigin)to locate the single Business Entity the changes must be routed to.
The Origin values registered in the configuration must exactly match the values the Data Access classes assign to the Origin field of the temp-table records — this string is the link between a record in the joint result set and the Business Entity responsible for it.
The dispatched Business Entities
Each dispatched Business Entity is a regular Business Entity built on the shared dataset include. The only additional requirement is implementing Consultingwerk.OERA.IFetchDataByReference, so that the dispatcher can fetch data with the dataset passed BY-REFERENCE:
CLASS Test.BusinessEntityDispatcher.Order.OrderBusinessEntity
INHERITS BusinessEntity IMPLEMENTS IFetchDataByReference
USE-WIDGET-POOL:
{ Test/BusinessEntityDispatcher/SalesDocument/dsSalesDocument.i }
CONSTRUCTOR PUBLIC OrderBusinessEntity ():
SUPER (DATASET dsSalesDocument:HANDLE).
THIS-OBJECT:DataAccessName = "Test.BusinessEntityDispatcher.Order.OrderDataAccess":U .
END CONSTRUCTOR.
METHOD PUBLIC FINAL VOID FetchDataByReference (poFetchDataRequest AS Consultingwerk.OERA.IFetchDataRequest,
OUTPUT DATASET dsSalesDocument):
THIS-OBJECT:FetchData (poFetchDataRequest, DATASET dsSalesDocument:HANDLE) .
END METHOD.
END CLASS.Data Access requirements
The Data Access class of every dispatched Business Entity is responsible for populating the two infrastructure fields. This is done in an AFTER-ROW-FILL callback — the Origin value must match the value registered in the dispatcher configuration, and the RowIdent must uniquely identify the row within that entity:
METHOD OVERRIDE PROTECTED VOID DefineReadEvents ():
BUFFER eSalesDocument:SET-CALLBACK ("AFTER-ROW-FILL":U, "eSalesDocumentAfterRowFill":U, THIS-OBJECT).
END METHOD.
METHOD PUBLIC VOID eSalesDocumentAfterRowFill (DATASET dsSalesDocument):
ASSIGN eSalesDocument.Origin = "Order":U
eSalesDocument.RowIdent = SUBSTITUTE ("&1&2&3":U, ROWID (Order), CHR(5), ROWID (Customer)) .
END METHOD.Because the dispatcher fills the same dataset from multiple entities, the fill of the temp-table buffer runs in MERGE mode so that a subsequent fill does not empty the records retrieved before:
METHOD PROTECTED OVERRIDE LOGICAL DatasetFill (phDataset AS HANDLE, poFetchDataRequest AS IFetchDataRequest):
BUFFER eSalesDocument:FILL-MODE = "MERGE":U .
RETURN SUPER:DatasetFill (phDataset, poFetchDataRequest) .
END METHOD.The mapping from the entity-specific database tables to the neutral shared schema is regular Data Access field mapping, e.g. in the Order Data Access:
THIS-OBJECT:AttachDataSource (BUFFER eSalesDocument:HANDLE,
DATA-SOURCE src_OrderCustomer:HANDLE, "
DocumentNum,Order.OrderNum,
DocumentDate,Order.OrderDate,
CustNum,Order.CustNum,
CustomerName,Customer.Name,
DocumentStatus,Order.OrderStatus":U) .How FetchData works
When a consumer requests data (with a filter, sort order and batch size), the dispatcher's FetchData implementation:
Creates a clone of the
FetchDataRequest(tables, queries,NumRecords, custom context/parameter) for every Business Entity in the configuration.Distributes the batching context: the dispatcher's
Contextis a JSON object keyed by theOriginvalues, holding the restart position of each dispatched entity individually. Each cloned request receives the context entry of its own entity.Fetches up to
NumRecordsrecords from each entity throughIFetchDataByReferenceand merges all partial results into the dispatcher's dataset.Sorts the merged result according to the
BYclause of the consumer's query and deletes the overflow rows beyondNumRecords, remembering theRowIdentof the first deleted row per origin as that entity's restart position.Returns the combined
NextContext/PrevContextas a JSON object keyed by origin, so the next batch request resumes each entity at exactly the right position.
The result is transparent forward batching over the combined, sorted result set: for a batch size of 50, the consumer receives the 50 "smallest" records (in sort order) across all origins, then the next 50, and so on.
How SaveChanges works
The dispatcher's SaveChanges implementation scans the before-image tables of the dataset to determine the Origin of all changed records (created, modified and deleted rows). It then resolves the matching Business Entity from the configuration (ConfigurationItems:Find (cOrigin)) and delegates the SaveChanges call — including the full change set — to that single entity.
Changes to records of more than one origin within the same transaction are not supported: as a Business Entity boundary is also a transaction boundary, the dispatcher throws a NotSupportedException ("Not supported to save changes to more than a single Business Entity at a time.") when the change set spans multiple origins. Consumers must therefore commit changes per origin.
Consuming the Dispatcher from a client
Consumers use the dispatcher exactly like any other Business Entity — for example a SmartBusinessEntityAdapter in a GUI for .NET form simply references the dispatcher's class name:
THIS-OBJECT:smartBusinessEntityAdapter1:EntityName = "Test.BusinessEntityDispatcher.SalesDocument.SalesDocumentBusinessEntity":U.
THIS-OBJECT:smartBusinessEntityAdapter1:EntityTable = "eSalesDocument":U.
THIS-OBJECT:smartBusinessEntityAdapter1:QuerySort = "BY eSalesDocument.DocumentDate":U.Browsing the resulting query shows orders and invoices merged into a single result set, sorted by document date, with forward batching across both source entities. The Test.BusinessEntityDispatcher.UserInterface.SalesDocumentForm class in the Tests project provides a complete sample form with a grid and viewer bound to the dispatcher.
Known limitations
Currently the Business Entity Dispatcher only supports forward batching. The FetchLastOnServer property of the SmartBusinessEntityAdapter cannot be used with the Business Entity Dispatcher currently.
Fetch-last and find (reposition) requests are not supported — the dispatcher's
FetchDataraises aNotSupportedExceptionfor such requests.SaveChangescan only process changes belonging to a single dispatched Business Entity (a singleOrigin) per call; change sets spanning multiple origins raise aNotSupportedException.All dispatched Business Entities must share the same ProDataset schema; a field mapping between differing schemas is not supported.