Using the Business Entity Dispatcher

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

Class / Interface

Purpose

BusinessEntityDispatcher

Abstract base class (inherits Consultingwerk.OERA.BusinessEntity). Overrides FetchData and SaveChanges to fan out / route requests to the configured Business Entities.

IBusinessEntityDispatcherConfiguration

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 DispatcherConfiguration property.

BusinessEntityDispatcherConfigurationItem

A single configuration entry, pairing an Origin value (e.g. "Order") with the EntityName (the fully qualified class name) of the Business Entity providing records with that origin.

ListDispatcherBusinessEntityDispatcherConfigurationItem

Typed list of configuration items, exposed through the configuration's ConfigurationItems property. Provides Add (pcOrigin, pcEntityName) and Find (pcOrigin).

Consultingwerk.OERA.IFetchDataByReference

Interface that every dispatched Business Entity must implement, so the dispatcher can retrieve data from it with the dataset passed BY-REFERENCE (avoiding deep copies).

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 the Origin values registered in the dispatcher configuration.

  • RowIdent — a unique row identifier within the origin entity (in the sample, the ROWID values 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 FetchData the dispatcher iterates the configuration items and retrieves data from every registered Business Entity (resolved by its EntityName through the ServiceManager), merging the results. The Origin value of each item is also used as the key under which the per-entity batching context is stored (see below).

  • During SaveChanges the dispatcher inspects the Origin field of the changed records and uses ConfigurationItems: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:

  1. Creates a clone of the FetchDataRequest (tables, queries, NumRecords, custom context/parameter) for every Business Entity in the configuration.

  2. Distributes the batching context: the dispatcher's Context is a JSON object keyed by the Origin values, holding the restart position of each dispatched entity individually. Each cloned request receives the context entry of its own entity.

  3. Fetches up to NumRecords records from each entity through IFetchDataByReference and merges all partial results into the dispatcher's dataset.

  4. Sorts the merged result according to the BY clause of the consumer's query and deletes the overflow rows beyond NumRecords, remembering the RowIdent of the first deleted row per origin as that entity's restart position.

  5. Returns the combined NextContext / PrevContext as 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 FetchData raises a NotSupportedException for such requests.

  • SaveChanges can only process changes belonging to a single dispatched Business Entity (a single Origin) per call; change sets spanning multiple origins raise a NotSupportedException.

  • All dispatched Business Entities must share the same ProDataset schema; a field mapping between differing schemas is not supported.