On the server side, there can also be a generic procedure that applies all changes to a ProDataSet. As we explained in UpdatingData 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 did not, 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 necessary.
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:
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 are not 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 is really a child of a reposition relation, not a true top-level buffer, and it is skipped.
For each top-level buffer, the iterative 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).
/* Skip the reposition children. */
IF hTopBuff:PARENT-RELATION NE ? THEN NEXT.
RUN traverseBuffers (hTopBuff).
END. /* END DO iBuff */
Procedure traverseBuffers serves only to iterate 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, the AVM 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. For example:
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 = TRUE.
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 is not 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 are needed. This example uses the default.