Release 10.1A: OpenEdge Development:
ProDataSets

> ## Elements of a Business Entity

> In principle, a Business Entity as an object can map to a Progress 4GL procedure. This might in turn be supported by any number of other procedures that provide either specific validation logic for the entity or general support services used by all entities. Each Business Entity is normally paired with a Data Access object that manages the connection to the actual data sources. As discussed earlier, in many cases a single running instance of a Data Access object could provide data for any number of requests from different Business Entity instances.

> In a distributed stateless environment, a server-side Business Entity will normally not live beyond a single request. It can fill its ProDataSet for a particular `Order`, for example, return that `Order` to the client, and then either terminate or remain in memory to be used by another independent request, depending on how the application manages its procedures. On the other hand, if one Business Entity is used by another Business Entity in the same session, as part of its validation logic, for example, then there could be a relationship between the two procedures that would last beyond a single request.

> As with the Data Access object, the Business Entity will typically be a static object. That is, it will be based on one or more static ProDataSet definitions, and contain (directly or indirectly) mostly specific logic to support its use. This logic will include a specific API for the various kinds of requests that can be made of it, and validation logic to apply to update requests. A dynamic Business Entity designed to handle a certain class of similar data objects is also entirely possible, of course.

> Given these basics, the following subsections outline some of the common elements of the Business Entity.

> ##### ProDataSet and temp-table definitions

> A Business Entity will typically contain the same temp-table and ProDataSet definitions as its Data Access object. The significant different is that it is the Business Entity’s instance of the ProDataSet that is actually filled with data used to satisfy a request, when it is passed `BY-REFERENCE` to its Data Access object.

> ##### Relationship to the Data Access object

> If the application model is that an instance of the Data Access object serves a single Business Entity instance, then the Business Entity can run that instance as a persistent procedure, as in this example:

>     RUN OrderSource.p PERSISTENT SET hSourceProc.

> The Entity can then use the API of the Data Access object to run support methods in this procedure handle.

> The Business Entity could also make the Data Access object a super procedure of the Business Entity, as shown:

>     THIS-PROCEDURE:ADD-SUPER-PROCEDURE(hSourceProc).

> This allows the Entity to use the API of the Data Access object as if it were part of its own procedure.

> Using the super procedure technique also changes the nature of calls from outside that are routed from the Business Entity to the Data Access object. This is discussed in the "Data retrieval API" section.

> ##### Attaching the Data-Sources and callbacks

> The entity procedure can, as part of its main block startup code, request that the Data Access object attach Data-Sources and set callback procedures to its ProDataSet instance, as in this example:

>     lError = DYNAMIC-FUNCTION("attachDataSet" IN hSourceProc, hDataSet).

> As was illustrated earlier, this can also be done in each of the methods of the Data Access object, rather than as a separate step.

> ##### Defining a data retrieval API

> Any requests for data should be made to the Business Entity, never directly from outside to the Data Access object. This simply preserves the isolation of the different layers of the application. The `fetchOrder` procedure in `OrderEntity.p` is an example of this:

>     PROCEDURE fetchOrder:
>         DEFINE INPUT PARAMETER piOrderNum AS INTEGER NO-UNDO.
>         DEFINE OUTPUT PARAMETER DATASET FOR dsOrder BY-VALUE.
>         /* This turns around and runs an equivalent procedure in the
>         Data-Source procedure, passing in the static dataSet. */
>         DYNAMIC-FUNCTION("attachDataSet" IN hSourceProc, hDataSet).
>         RUN fetchOrder IN hSourceProc (INPUT piOrderNum,
>             INPUT-OUTPUT DATASET dsOrder BY-REFERENCE).
>         DYNAMIC-FUNCTION("detachDataSet" IN hSourceProc, INPUT hDataSet).
>     END PROCEDURE. /* fetchOrder */

> This turns around and runs an equivalent procedure in the Data Access procedure. Significantly, the ProDataSet parameter is `INPUT-OUTPUT BY-REFERENCE` in the second-level call to the Data Access object. This uses the Business Entity’s ProDataSet and avoids the expense of copying the ProDataSet back and forth. Having avoided this, the overhead of the second procedure call is not very significant.

> Figure 11–2 shows how the ProDataSet is being used in this case.

> Figure 11–2: ProDataSet flow

> Here is an outline of the steps illustrated in Figure 11–2:

> 1. A requesting procedure on the client runs `fetchOrder` in the `Order` Business Entity on the server.

> 2. That `fetchOrder` procedure runs `fetchOrder` in the Data Access object in its session.

> 3. Because the ProDataSet is passed in `BY-REFERENCE`, it is actually the Business Entity’s instance that is used (marked in bold). The dotted lines indicate that this instance is passed without being copied.

> 4. The `fetchOrder` procedure in the Data Access object attaches Data-Sources, which are tables in the database, and also sets any callback procedures for `FILL` events.

> 5. It then does a `FILL`, which actually fills the ProDataSet instance back in the Business Entity.

> 6. It returns the ProDataSet to the requesting procedure. It is copied there rather than being passed `BY-REFERENCE` because the presumption is that the requester is or may be in a different session.

> Alternatively, if the Data Access object is a super procedure of the Business Entity, then in cases where the Business Entity version of a procedure like `fetchOrder` doesn’t do any additional work of its own, it could be dispensed with, and a call to `fetchOrder` from another procedure would be handled automatically by the Data Access object. In this case, there are a couple of things to consider:

> - First, the Data Access object version of `fetchOrder` would need to make its ProDataSet parameter `OUTPUT` instead of `INPUT-OUTPUT`, because it would be passed back directly to the caller. The caller would not be passing in a ProDataSet of its own. In this case, the Data Access object’s ProDataSet instance is the one that is used to satisfy the request. It then becomes the Data Access object’s responsibility to make sure the Data-Sources are attached, as illustrated in the second version of `fetchOrder` in the "Data Access object template" section.

> - Second, if there is any reason for the Business Entity procedure to run `fetchOrder` in the Data Access procedure, or to provide extended behavior for a call from outside to `fetchOrder`, then this arrangement becomes inefficient, because the ProDataSet would be copied from the Data Access procedure to the Business Entity procedure. For example, consider this alternative to `fetchOrder` in the Entity:

>     PROCEDURE fetchOrder:  /* Alternative OrderEntity.p version for
>                               comparison only ! */
>         DEFINE INPUT  PARAMETER piOrderNum AS INTEGER    NO-UNDO.
>         DEFINE OUTPUT PARAMETER DATASET FOR dsOrder.
>         /* fetchOrder does some prep work here, for example validating the
>            INPUT data or the requester’s privileges to make the request */
>         /* Then it just does a RUN SUPER to invoke the standard behavior. */
>         RUN SUPER (INPUT piOrderNum, OUTPUT DATASET dsOrder).
>         /* The RUN SUPER is done instead of running it directly as in the
>            original version:
>            RUN fetchOrder IN hSourceProc (INPUT piOrderNum,
>            INPUT-OUTPUT DATASET dsOrder BY-REFERENCE).
>         */
>     END PROCEDURE. /* fetchOrder */

> If the parameter definition for the ProDataSet in `fetchOrder` in the procedure `OrderSource.p` is changed to be `OUTPUT` instead of `INPUT-OUTPUT`, and `hSourceProc` is a super procedure of `OrderEntity.p`, and `fetchOrder` in `OrderSource.p` does the `attachDataSet`, then this arrangement works fine.

> However, the ProDataSet is copied from `Order`Source to `Order`Entity before being copied back to the caller. This is not a good thing. Because of this, and because of the potential for confusion between when the Data Access ProDataSet instance is being used and when the Business Entity instance is being used, making the data access procedure a super procedure may not be a good practice.

> Figure 11–3 illustrates what happens if the Data Access object is a super procedure of the Business Entity, and it has the only implementation of procedure `fetchOrder`.

> Figure 11–3: Data Access object as super procedure

> Here are the steps illustrated in Figure 11–3:

> 1. The requesting procedure runs `fetchOrder` in the Business Entity as before.

> 2. There is no `fetchOrder` procedure in the Business Entity. However, since the Data Access object is a super procedure, Progress runs `fetchOrder` there.

> 3. This means that it is the Data Access object’s ProDataSet instance that is used for the request.

> 4. The `fetchOrder` procedure attaches Data-Sources and callback procedures to this ProDataSet.

> 5. It then fills the ProDataSet.

> 6. The `fetchOrder` procedure then returns this ProDataSet to the original caller as an `OUTPUT` parameter. The ProDataSet instance in the Business Entity is not used.

> This all works correctly in this case, but could be a source of confusion and errors because the ProDataSet instance being used is not consistent.

> By contrast, Figure 11–4 shows the case where, once again, the Data Access object is a super procedure of the Business Entity, and `fetchOrder` in the Business Entity does a `RUN SUPER` to run the standard attach and fill behavior.

> Figure 11–4: Data Access object as super procedure with RUN SUPER

> Here are the steps illustrated in Figure 11–4:

> 1. The requesting procedure runs `fetchOrder` in the Business Entity as before. There is an implementation of `fetchOrder` there, so it is executed.

> 2. Procedure `fetchOrder` does a `RUN SUPER`, which runs `fetchOrder` in the Data Access object. Because the parameter definitions must be consistent in this case, the ProDataSet is simply an `OUTPUT` parameter.

> 3. Because of this, the Data Access object uses its own ProDataSet instance.

> 4. It attaches Data-Sources to its own ProDataSet instance.

> 5. It fills its own ProDataSet instance.

> 6. It returns its ProDataSet as an `OUTPUT` parameter to the Business Entity, copying it to the Business Entity’s ProDataSet instance.

> 7. The Business Entity then returns it to the original caller as `OUTPUT`, again copying the ProDataSet.

> Because of the extra copy operation, this is not a good configuration. This is something you need to keep in mind when you design your procedures and decide how they are related.

> This discussion may seem complex, but the intention is to make you aware of some of the issues and how you should consider them when you’re designing your application. As with every other aspect of design, once you have thought through an appropriate solution to a part of your design, if you keep to your solution consistently, then you won’t have to worry about it anymore, and developers writing business logic don’t need to be concerned about the details of the Data Access architecture supporting them.

> ##### Defining a generic update API

> As the sample procedures in earlier chapters show, you can create a general-purpose update API that can take changes made to any ProDataSet and apply them to the database, even executing validation procedures for the specific ProDataSet if they conform to a standard naming convention. This approach is very much like how the SmartDataObject and SmartBusinessObject in the ADM2 handle their update logic.

> The generic update procedure in the samples is `commitChanges.p`. The sample entity procedure, `OrderEntity.p`, wraps this in a call of its own to provide an API for other procedures to use, as shown:

>     PROCEDURE saveChanges:
>         DEFINE INPUT-OUTPUT PARAMETER DATASET FOR dsOrder.
>         DEFINE VARIABLE hDataSet AS HANDLE     NO-UNDO.
>         hDataSet = DATASET dsOrder:HANDLE.
>         DYNAMIC-FUNCTION("attachDataSet" IN hSourceProc,
>                          INPUT hDataSet).
>         RUN commitChanges.p (INPUT-OUTPUT DATASET dsOrder BY-REFERENCE).
>         DYNAMIC-FUNCTION("detachDataSet" IN hSourceProc,
>                          INPUT hDataSet).
>     END PROCEDURE. /* saveChanges */

> This wrapper procedure attaches the Data-Sources and event handlers, runs `commitChanges.p`, and then detaches the Data-Sources. This could as easily be incorporated directly into a procedure like `commitChanges` if it knows where to run the attach and detach methods. Beyond that, having a wrapper procedure gives the specific object an opportunity to add special commit logic before or after the standard code.

> ##### Validation procedures for the generic update API

> For example, the `OrderEntity` procedure has this sample validation procedure which based on its name will be executed whenever a row in the `ttOline` table in the ProDataSet is modified:

>     PROCEDURE ttOlineModify:
>         DEFINE INPUT PARAMETER DATASET FOR dsOrder.
>         /* If the customer doubled the quantity ordered, then
>            increase the discount by 20%. */
>         IF ttOline.Qty >= (ttOlineBefore.Qty * 2) AND
>            ttOline.Discount = ttOlineBefore.Discount
>            THEN
>            ttOline.Discount = ttOlineBefore.Discount * 1.2.
>         ELSE IF ttOline.Qty <= (ttOlineBefore.Qty * .5) THEN
>             ASSIGN BUFFER ttOline:ERROR = YES
>                    BUFFER ttOline:ERROR-STRING =
>                      "Line " + STRING(ttOline.LineNum) +
>                      ": You can't drop the Qty that much!".
>         RETURN.
>     END PROCEDURE. /* ttOlineModify */

> The basic principles of writing procedures such as this one are:

> - You can pass in the entire ProDataSet `BY-REFERENCE` from another procedure in the same session without the cost of copying it, so that the validation logic can examine any part of the ProDataSet that it needs to.

> - The current rows in the buffers will be those that were current in the caller. In particular, the current row in the table that triggered the event is the one that was modified, so you can examine its values without having to `FIND` it.

> Validation procedures can set the `ERROR` and/or `ERROR-STRING` attributes for the row to communicate status information. Setting `ERROR-STRING` without setting error lets you return an informational or warning message without causing an actual error.

> ##### Defining a custom update API

> In addition to using a standard update mechanism for general updates, there can of course be additional more specific update API calls in your Business Entity that handle particular situations where the default behavior is not sufficient.

> ##### Managing Business Entity instances

> Managing Business Entities and their Data Access objects needs to be coordinated in some fashion, so that entities can locate each other, can be accessed from client requests, and can be started and stopped when appropriate.

> In general, Business Entities and their APIs should be designed such that there are no dependencies between requests. This is especially necessary for a stateless distributed environment where a client cannot expect to be given access to the same procedure instance on an AppServer in successive calls. Thus, there is little reason to leave data in a Business Entity on the AppServer after a request completes.

> This means that a Business Entity instance can be left running on an AppServer to service any number of unrelated requests. If the Business Entity procedures themselves are not enormous amount of r-code, it may be just as efficient to destroy each instance after its use and start a fresh instance when needed. It takes very little time to load a compiled procedure into memory. Any significant overhead is in startup code for the object, and this should be minimized. Keep in mind that while only one business instance is needed in an AppServer session to satisfy any number of successive requests from client sessions, the business logic within a session may need to use (and possibly start) other Business Entities to execute its own logic.

> There should be a mapping between Business Entity names as used in business logic and actual procedure names, so that objects do not need to be aware of other procedure names to run them directly. This can be repository-based, and managed by a single session management utility within the session that accepts requests for entities and either locates or starts them and returns their handles. For example, an `Order` entity should be able to make a request of the `Customer` entity without having to know an actual procedure name to run. It should be the responsibility of the entity manager to handle this.

> Support procedures that require only a single instance within a session, such as Data Access procedures, and most supporting business logic and update validation procedures, cam be started by the Business Entities or started by the entity management utility the first time they are needed, and then left running for the duration of the session, or else shut down on some form of LRU basis if memory becomes a problem.

Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095