save-row-changes and transaction scope

Posted by Admin on 12-Feb-2009 08:49

Because I am going to write a server-side program that uses datasets and that will be based on OERA, I took a look at some of the sample code that comes with the OERA white papers. I use version 10.1C of OpenEdge.

Recently, I performed some testsusing de code of the Advanced Business Logic sample (OERA white paper 7). The aspect I need is that of saving all changes in a prodataset within one transaction. The sample code has for that pupose the commitAllChanges procedure.

Now I see the following happen. In the UI I change the quantity (Qty) of two order line records. Next, I change the quantity field of the first record

in the database from within a different session. When I save the changes from the UI (saveChanges button)

I see Progress error 11913 stating that a database record has been changed by another user, as expected. When I re-fetch the order, however,

I see that the order line that was not changed by another used has made it from the dataset to the database. This, I don't understand.

In fact, when I place a message statement in commitAllChanges (within the second DO WHILE NOT hBeforeQry:QUERY-OFF-END-block)

I see a message twice, despite the fact that first iteration generates the error and the transaction should be undone.

The listing generated via the compile statement confirms that the only transaction in the procedure is the one started

by the do transaction-block which is not a iterating block; the nested do-blocks do not start transactions.

However, when I place a catch-block at the end of the do transaction-block the

procedure behaves as I expected: after the error is generated

the transaction is undone an the transaction-block is left. No changes in the dataset made it to the database.

(No messages this time because of precense of the catch-block. The catch-block only contains a statement to delete the error object.)

Also, I made a version of the commitAllChanges procedure not using the save-row-changes() method but code which

performs the same function as save-row-changes(). The actions are descibed in chapter 6 of the prodatasets book (pages 6-32 - 6-45).

(See below for a code snippet.)

This time I generate an error by releasing an OrderLine record too soon which in my case leads to a unique key violation error (message 132).

Not using a catch-block this time the transaction is backed out because of the error condition and none of the changes in the prodataset make

it to the database.

What is it that I don't understand? Should the (original) commitAllChanges procedure not back out the transaction in case

a record in de database was changed by another user? Why does the code behave so differently when one uses a catch-block

within de transaction-block? Is there something special about the save-row-changes() and the error condition?

Code snippet I used for a test version of the commitAllChanges procedure. Method save-row-changes() is replaced by the

code in DO iDSBuffer = 1 ... block.

DO iDSBuffer = 1 TO hDataSource:NUM-SOURCE-BUFFERS: /* one buffer for order line temp-table */

find OrderLine where OrderLine.Ordernum = hBeforeBuff::OrderNum

and OrderLine.Linenum = hBeforeBuff::LineNum

exclusive-lock.

if avail OrderLine then

do:

if not buffer Orderline:buffer-compare(hBeforeBuff) then

assign

hBeforeBuff:error-string = "Someone else changed it"

hBeforeBuff:error = true

hBeforeBuff:table-handle:error = true

hBeforeBuff:dataset:error = true

.

else

do:

hAfterBuff:find-by-rowid(hBeforeBuff:after-rowid).

/* I changed Qty for order lines 1 and 2 in de UI. */

if OrderLine.Linenum = 1 then

release OrderLine. /* to force the error condition in the next statement */

buffer OrderLine:buffer-copy(hAfterBuff). /* generates unique key violation */

validate OrderLine.

hAfterBuff:buffer-copy(buffer OrderLine:handle).

release OrderLine.

end.

end.

END. /* END DO iBuffer */

All Replies

Posted by Håvard Danielsen on 12-Feb-2009 20:02

> What is it that I don't understand? Should the (original) commitAllChanges procedure not back out the transaction in case a record in de database was changed by another user? Why does the code behave so differently when one uses a catch-block within de transaction-block? Is there something special about the save-row-changes() and the error condition?

Traditionally ABL methods do not raise an error condition when they fail and will not cause an undo of a transaction. One used to handle this by checking whether the method returned false or some other condition, like the buffer's error attribute in the case of save-row-changes and then do an undo.

Adding a catch to an associated block changes this behavior and causes the ABL method to raise/throw an error.

Posted by Admin on 13-Feb-2009 06:26

Thanks Havard for the reply. This explains a lot but it also raises a new question. I have made a small program to test about ABL methods and the error condition. I found that for instance the sax-parse() method does raise error when something goes wrong, but the connect() method of a socket object does not.

(For these tests I used the no-error option on the statement and then tested the error attribute of the error-status handle.)

Can this be explained by the fact that sax-parse() does not return anything and connect() returns a logical so that in the latter case one can test if the method was successful?

Returning to my original post: the test which uses the buffer-copy() method raises error despite the fact that this method returns a logical. How does this relate to the tests with the sax-parse() and connect() method?

Posted by Håvard Danielsen on 13-Feb-2009 10:03

Let me rephrase that: Traditionally many ABL methods do not raise an error condition when they fail...

The sax methods are designed so that you can return error from your call backs.

Transactional methods like buffer-copy, buffer-delete, buffer-release and buffer-create are also treated specially even if they return a logical. This is a good traditional ABL default, but it does make them harder to control and you would typically wrap them inside of a small dedicated do on error block if more granular control is needed. (Also note that you cannot really rely on buffer-copy always raising an error, since write triggers fire when the record goes out of scope. I believe mandatory field errors also are delayed until the release.)

save-row-changes resolves many of these issues and more and together with the error, rejected and data-source-modified attributes on the buffer they give you far more control with less code.

Posted by Tim Kuehn on 14-Feb-2009 13:40

(Also note

that you cannot really rely on buffer-copy always

raising an error, since write triggers fire when the

record goes out of scope. I believe mandatory field

errors also are delayed until the release.)

Write trigger firing is based on

1) the buffer going out of scope,

2) the record being 'hit' at the db level (ie FIND CURRENT table-name),

3) on VALIDATE of the record (ie VALIDATE table-name), or

4) the TX ending - although the trigger could still wait to fire a bit after the ending of a TX block, generally because of one of the previously listed events happening.

I've seen conditions where a given record's write trigger has fired multiple lines after the a value's been changed in the record.

Posted by Håvard Danielsen on 16-Feb-2009 09:43

Write trigger firing is based on

1) the buffer going out of scope,

2) the record being 'hit' at the db level (ie FIND

CURRENT table-name),

3) on VALIDATE of the record (ie VALIDATE

table-name), or

4) the TX ending - although the trigger could still

wait to fire a bit after the ending of a TX block,

generally because of one of the previously listed

events happening.

I've seen conditions where a given record's write

trigger has fired multiple lines after the a

value's been changed in the record.

Yes, and in spite of the fact that triggers fire according to these defined rules, the actual default behavior is hard to guess/predict and, most importantly, might make it (close to) impossible to catch error messages returned from triggers or mandatory field errors.

The common practice (when not using save-row-changes) is to use either validate (or buffer-validate) or release (or buffer-release) to ensure controlled firing of triggers.

Maybe find current also can be used, but there are certain dynamic find variations, like find-by-rowid, that do not fire triggers. (Note I have not tested this for years, so it might have changed).

If you don't use validate or release the trigger may fire for any of the mentioned reasons, but it should always fire before the transaction is over though. In the old PIP (programming in progress) classes we used to joke that the trigger fires at the period of the end statement of the transaction... but not later... But this is too late to get hold of error messages returned from the triggers.

Message was edited by:

trigger fires... not transaction fires

Havard Danielsen

This thread is closed