Using UNDO, LEAVE on a block
- Last Updated: August 16, 2021
- 7 minute read
- OpenEdge
- Version 12.2
- Documentation
Using UNDO, LEAVE on a block
In this section, you try an example that uses UNDO, LEAVE on
a block. To undo and leave the block that updates the OrderLine
table:
- To make this a transaction block of its own, put the
ENDstatement for theDO TRANSACTIONblock after the Order is updated, as you did in Controlling the size of a transaction:... ELSE DO: /* FIND the updated tt rec and save the changes. */ FIND bUpdateOrder WHERE bUpdateOrder.TransType = "U". BUFFER-COPY bUpdateOrder TO Order. /* Save our changes. */ END. /* ELSE DO */ END. /* OF AVAILABLE ttOrder */ END. /* DO Transaction */This makes the
FOR EACHblock that follows a separate transaction. - Add a new variable definition in saveOrder for
an error message to return:
DEFINE VARIABLE cMessage AS CHARACTER NO-UNDO. - Add to the
FOR EACHblock that updates OrderLines the highlighted lines shown:FOR EACH ttOline WHERE ttOline.TransType = "": ... /* Find corresponding bUpdateOline */ FIND OrderLine WHERE OrderLine.OrderNum = ttOline.OrderNum AND OrderLine.LineNum = ttOline.LineNum EXCLUSIVE-LOCK. ... BUFFER-COPY bUpdateOline TO OrderLine. /* Save OrderLine changes. */ RELEASE OrderLine. /* Re-find the db record to capture any changes made by a trigger. */ FIND OrderLine WHERE OrderLine.OrderNum = bUpdateOline.OrderNum AND OrderLine.LineNum = bUpdateOline.LineNum NO-LOCK. BUFFER-COPY OrderLine TO bUpdateOline. IF bUpdateOline.ExtendedPrice > (ttOline.ExtendedPrice * 1.2) THEN DO: cMessage = "Line " + STRING(OrderLine.LineNum) + ": Can't increase price by more than 20%.". UNDO, LEAVE. END. ... END. /* DO FOR EACH ttOline */This code checks to make sure that the ExtendedPrice for an OrderLine is not increased by more than 20%. If this limit is exceeded, then the current iteration of the block is undone and the block is left.
On each iteration, the
FOR EACHblock makes a ttOline record current. Your code uses the second buffer,bUpdateOline, to locate the updated version of that OrderLine temp-table record. It then finds the OrderLine in the database and copies the updates to it. Next it releases the database record to force itsWRITEtrigger to fire, which recalculates the ExtendedPrice field. Then it again finds the database record and copies it back into thebUpdateOlinebuffer to capture the effects of the trigger code, in particular the new ExtendedPrice value. Only now can your program logic compare this to the original value in thettOlinebuffer to see if it exceeds the limit. If it does, then you store off a message, then undo and leave theFOR EACHblock.At this point, following the
UNDOstatement, the whole database change that you wrote out and then read back in is gone—the AVM has restored the database record to its values before the transaction began. - Add code so that after leaving the block, you check if you have
an error message. If so, your code needs to re-find the OrderLine record
with its original values, and copy it back into the
bUpdateOlinebuffer to return to the client for display. It then returns the message as the return value for the procedure:IF cMessage NE "" THEN DO: FIND OrderLine WHERE OrderLine.OrderNum = bUpdateOline.OrderNum AND OrderLine.LineNum = bUpdateOline.LineNum NO-LOCK. BUFFER-COPY OrderLine TO bUpdateOline. RETURN cMessage. END.
Why did you have to re-find the OrderLine record from
the database? The UNDO released it from its buffer,
so it's no longer available after the block ends. Then why did you not have to
re-find the temp-table record? You defined the temp-table as NO-UNDO so
it was left out of normal transaction handling. The temp-table buffer
is scoped to the whole procedure, so the record it contains is still
available after the block ends. If you had defined the temp-table without NO-UNDO,
then the bUpdateOline record would have been
released, as well as the database OrderLine record,
and you would have had to re-establish it in the temp-table as well.
This is an illustration of the kind of overhead you save by using NO-UNDO temp-tables,
and also of the sometimes unintended consequences of having undo
capability on a temp-table that doesn't require it.
The simple diagram in the following figure illustrates the scope of the transactions.

In the previous figure, the first transaction, which saves changes
to the Order, completes at the END statement
for the DO TRANSACTION block. At the end of the FOR
EACH block, the transaction to save the current OrderLine ends,
committing those changes to the database, releasing the OrderLine record,
and then going back to the beginning of the block. Each OrderLine is saved
in a separate transaction.
The UNDO statement rolls back the transaction
for the current OrderLine and leaves the FOR EACH block,
which skips any remaining ttOline records.
But any previously committed OrderLine changes
remain in the database. For example, in the following figure, the
user changes the Price for Line
1 from 7.49 to 7.60, for Line 2 from
23.00 to 30.00, and for Line 3 from 13.98
to 13.50.

The first and third changes are valid, but the second one is not. It increases the ExtendedPrice by too much. So when the user clicks Save, the user sees an error message for Line 2, as shown in the following figure.

But if the user presses the Fetch button to refresh all the OrderLines:
- The change to Line 1 is committed, because it is in its own transaction.
- The change to Line 2 is undone, because it failed the constraint.
- The change to Line 3 is never even applied because the code left the block after the error on Line 2.
The following figure shows the result.

This might not be the behavior you want. On the one hand, you might want all OrderLines to be committed together or all undone if any one fails. In another case, you might want to process each OrderLine as a separate transaction, but keep going if one of them fails. Look at both of those cases.
In the first case, you want all the OrderLine updates to either succeed or fail together. If any one fails its validation, all are rolled back.
To commit all OrderLine records together or undo if any one record update fails:
- Define the transaction scope to be greater than a single
iteration of the
FOR EACHblock by putting a newDO TRANSACTIONblock around it. Then add a label for that new block to identify how much to undo in yourUNDOstatement:OlineBlock: DO TRANSACTION: FOR EACH ttOline WHERE ttOline.TransType = "": - Change the
UNDOstatement to undo the entire larger transaction and to leave that outer block as well:UNDO OlineBlock, LEAVE OlineBlock.Remember that the default is to undo and leave the innermost block with the error property, the
FOR EACHblock. - Add another
ENDstatement to match the newDO TRANSACTIONstatement that begins the new block:END. /* ELSE DO If we updated the OrderLine */ END./* DO FOR EACH ttOline */ END. /* new DO TRANSACTION block */Note that a block label, such as OlineBlock:, does not require a matching
ENDstatement. It is simply an identifier for a place in the code and does not actually start a block of its own. - Make a change to the code that restores the original values
to the temp-table if there's an error. Because the error might not
be on the line that's current when you leave the block, you need
to re-read all the OrderLines for the Order and
buffer-copy their values back into the update copies of each of
the temp-table records, in a
FOR EACHloop:IF cMessage NE "" THEN DO: FOR EACH OrderLine WHERE OrderLine.OrderNum = bUpdateOline.OrderNum NO-LOCK: FIND bUpdateOline WHERE OrderLine.LineNum = bUpdateOline.LineNum AND bUpdateOline.TransType = "U". BUFFER-COPY OrderLine TO bUpdateOline. END. RETURN cMessage. END.Now if you make changes to three OrderLines, and the second of the three is invalid, then all three changes are rolled back because they're all part of one large transaction. You see this reflected in your window.
The following figure shows a sketch of what this variation looks like.

Now look at the second case. You'd like each OrderLine committed
independently of the others, and you'd like to keep going if one
is invalid. In this case, you can use the NEXT option
on the UNDO statement instead of LEAVE.
If an error is found, the current transaction is undone and your
code continues on to the next ttOline record.
To commit each OrderLine record independently and continue if any one update fails:
- Remove the OlineBlock block label,
along with the
DO TRANSACTIONblock header and its matchingENDstatement, from around theFOR EACHblock. - Change the
UNDO, LEAVEstatement toUNDO, NEXT. - Since it is now possible to get errors on more than one OrderLine at a time, you should be prepared to accumulate multiple error messages in your message string.
- Append each new message to the end of the string by using the
plus sign (+) as a concatenation symbol for the character variable (
cMessage = cMessage + ...). - Put a newline character at the end of each message, using the
CHRfunction to append the ASCII character whose numeric value is 10 to the string:IF bUpdateOline.ExtendedPrice > (ttOline.ExtendedPrice * 1.2) THEN DO: cMessage = cMessage + "Line " + STRING(OrderLine.LineNum) + ": Can't increase price by more than 20%." + CHR(10). UNDO, NEXT. END. - Run the window.
- Enter a valid price for Line 1 and invalid
prices for Lines 2 and 3.
You see error messages for both of these:

You can also see that the valid change for Line 1 is kept
because you're back to making each iteration of the FOR EACH block
its own transaction.
The following figure shows a sketch of this variation.
