Release 10.1A: OpenEdge Development:
ProDataSets

> ## Building the server side change handler

> On the server side, there can also be a generic procedure that applies all changes to a ProDataSet. As we explained in Chapter 6, "Updating Data with ProDataSets,", there is a `SAVE-ROW-CHANGES` method to apply one row of changes to the database, but not a `SAVE-CHANGES` method for a temp-table or for the entire ProDataSet. The reason for this is there might be many ways in which you need to control the update process, including:

> - The transaction scope — Are all updates made as part of a single transaction that fails in its entirety if any change is not valid? Are all updates to a single table made together? Or is each individual change a separate transaction that can succeed or fail independent of others? These questions are the reason why we support both an `ERROR` and a `REJECTED` attribute for ProDataSet rows. A change that fails or conflicts with a change made by another user is `REJECTED` and also made an `ERROR`. Any other change that is backed out because it is part of the same transaction is `REJECTED` whether it caused an error or not. You can check these flags back in the caller to understand what made it into the database and what didn’t, and why.

> - The order of applying changes to different tables — In a parent-child ProDataSet or when there are multiple top-level tables, which changes should be applied first? Normally, you would apply changes from the top down, but there might be exceptions to that. And if there are multiple top-level tables, does it matter to you which order changes are applied in? By writing your own code you have complete control over this. No default would satisfy all situations.

> - The order of creates, deletes, and modifies to a single table — Do you want deletes applied before or after creates and updates? This might depend on your data definitions or business logic. The `ROW-STATE` attribute has a numeric value that orders rows so that unmodified rows are first, followed by deleted rows, modified rows, and created rows, in that order. You can take advantage of this default ordering, but you can also change it as you need to.

> This section shows you a sample for a general purpose procedure that makes default decisions about these variants. Each row is a single transaction. Tables are processed from the top of the hierarchy down (and multiple top-level tables are processed in the order in which they appear in the ProDataSet definition). And changes are processed in `ROW-STATE` `Order`. You could, of course, extend or modify this procedure for other defaults and to accept parameters to change the behavior.

> The example procedure is `commitChanges.p`. It handles changes to any ProDataSet with any number of tables in its hierarchy. It takes the ProDataSet handle as its `INPUT`-`OUTPUT` parameter. The variables represent the handle to the current top-level buffer and a buffer counter, as shown:

>     /* commitChanges.p */
>     DEFINE INPUT-OUTPUT PARAMETER DATASET-HANDLE hDataSet.
>     DEFINE VARIABLE hTopBuff    AS HANDLE     NO-UNDO.
>     DEFINE VARIABLE iBuff       AS INTEGER    NO-UNDO.

> The procedure starts its main loop through all top-level buffers, using the `NUM-TOP-BUFFERS` counter and the `GET-TOP-BUFFER` method to return each one in turn. These are the temp-table buffers with no parent. They are returned in the order they appear in the ProDataSet definition. There is one special case for which the loop must check. Because of the `FILL` behavior, children of a `REPOSITION` Data-Relation show up in the list of top-level buffers, even though they aren’t really top-level for the purpose of walking through the hierarchy of the ProDataSet as this procedure does. For this reason, the loop contains a check to see if there is a `PARENT-RELATION` for the buffer. If there is, then it’s really a child of a reposition relation, not a true top-level buffer, and it is skipped.

> For each top-level buffer, the recursive procedure `traverseBuffers` is run to walk down that branch of the ProDataSet definition, as shown:

>     DO  iBuff = 1 TO hDataSet:NUM-TOP-BUFFERS:
>             hTopBuff = hDataSet:GET-TOP-BUFFER(iBuff).

>             IF hTopBuff:PARENT-RELATION NE ? THEN
>                 NEXT.   /* Skip the reposition children. */

>             RUN traverseBuffers (hTopBuff).
>     END.  /* END DO iBuff */

> Procedure `traverseBuffers` serves only to recurse down through any number of levels of parent-child tables. It runs another internal procedure `saveBuffer` on itself and then loops through each child of the current buffer. `NUM-CHILD-RELATIONS` returns the number of relations for which this buffer is the parent, and `GET-CHILD-RELATION` returns each one in turn. Since `GET-CHILD-RELATION` returns the handle of the Data-Relation object, you need to follow that to its `CHILD-BUFFER` to get the buffer handle of each of the current’s buffer’s children, as shown:

>     PROCEDURE traverseBuffers:
>        DEFINE INPUT  PARAMETER phBuffer AS HANDLE     NO-UNDO.
>        DEFINE VARIABLE iChildRel AS INTEGER    NO-UNDO.

>        RUN saveBuffer(phBuffer).
>        DO iChildRel = 1 TO phBuffer:NUM-CHILD-RELATIONS:
>           RUN traverseBuffers
>               (phBuffer:GET-CHILD-RELATION(iChildRel):CHILD-BUFFER).
>           END.         /* END DO iChildRel */
>     END PROCEDURE.   /* traverseBuffers */

> Procedure `saveBuffer` does the actual work of running `SAVE-ROW-CHANGES`, as shown:

>     PROCEDURE saveBuffer:
>     DEFINE INPUT  PARAMETER phBuffer AS HANDLE     NO-UNDO.
>     DEFINE VARIABLE hBeforeBuff AS HANDLE     NO-UNDO.
>     DEFINE VARIABLE hBeforeQry  AS HANDLE     NO-UNDO.

>     hBeforeBuff = phBuffer:BEFORE-BUFFER.

> The buffer handle passed in is actually the after-table buffer for the current table. You want to run `SAVE-ROW-CHANGES` on the before-table buffer, especially since in the case of a `Delete` there is no after-table row to process. Variable `hBeforeBuff` points to that buffer.

> The `VALID-HANDLE` test checks whether there is a before-table for this temp-table at all. If the table does not have a `BEFORE-TABLE` in its definition and has not been enabled for update by setting `TRACKING-CHANGES` to true, then there will not be a before-table. Otherwise, the before-table could contain zero, one, or many rows, depending on how many rows in that table have been changed. The query simply walks through all those rows and runs `SAVE-ROW-CHANGES` on each one. `SAVE-ROW-CHANGES` starts its own transaction if there is none active, so each change is saved independently. If the save fails because of invalid data or because it had been changed by another user, Progress sets the `ERROR` attribute on the row. The procedure checks this and also sets the `REJECTED` attribute, so that the caller knows row by row which rows were successfully updated into the database and which ones were not, as shown:

>      IF VALID-HANDLE(hBeforeBuff) THEN
>             DO:
>                 CREATE QUERY hBeforeQry.
>                 hBeforeQry:ADD-BUFFER(hBeforeBuff).
>                 hBeforeQry:QUERY-PREPARE("FOR EACH " + hBeforeBuff:NAME).
>                 hBeforeQry:QUERY-OPEN().
>                 hBeforeQry:GET-FIRST().
>                 DO WHILE NOT hBeforeQry:QUERY-OFF-END:
>                     hBeforeBuff:SAVE-ROW-CHANGES().
>                     /* If there was an error signal that this row
>                        did not make it into the database. */
>                     IF hBeforeBuff:ERROR THEN
>                        hBeforeBuff:REJECTED = YES.
>                     hBeforeQry:GET-NEXT().
>                 END.  /* END DO WHILE NOT QUERY-OFF-END */
>                 DELETE OBJECT hBeforeQry.
>             END.      /* END DO IF VALID-HANDLE */
>     END PROCEDURE. /* saveChanges */

> Remember that `SAVE-ROW-CHANGES` takes two optional arguments:

> - The buffer to save, if there is more than one buffer in the Data-Source and the modified buffer isn’t the first one.

> - The name of a field not to copy to the database buffer because its value is assigned by a database trigger. Normally this is the primary key for a newly created row.

> Because this is a generic save procedure, there is no straightforward way to specify these parameters if they’re needed. This example uses the default.

> That’s the end of the code for `commitChanges.p`.

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