Managing Updates Using Transactions
Even with buffering, things can go wrong. If you want to protect update operations and recover from an entire section of code as a unit, use transactions.
Adding transactions to your application provides protection beyond Visual FoxPro record and table buffering by placing an entire section of code in a protected, recoverable unit. You can nest transactions and use them to protect buffered updates. Visual FoxPro transactions are available only with tables and views contained in a database.
Wrapping Code Segments
A transaction acts as a wrapper that caches data update operations to memory or to disk, rather than applying those updates directly to the database. The actual database update is performed at the end of the transaction. If for any reason the system cannot perform the update operations on the database, you can roll back the entire transaction and no update operations are performed.
Note
Buffered update operations made outside a transaction are ignored within a transaction in the same data session.
Commands that Control Transactions
Visual FoxPro provides three commands and one function to manage a transaction.
To |
Use |
---|---|
Initiate a transaction. |
|
Determine the current transaction level. |
|
Reverse all changes made since the most recent BEGIN TRANSACTION statement. |
|
Lock records, commit to disk all changes made to the tables in the database since the most recent BEGIN TRANSACTION, and then unlock the records. |
You can use transactions to wrap modifications to tables, structural .cdx files, and memo files associated with tables within a database. Operations involving variables and other objects don't respect transactions; therefore, you cannot roll back or commit such operations.
Note
When using data stored in remote tables, transaction commands control only updates to the data in the local copy of the view cursor; updates to remote base tables are not affected. To enable manual transactions on remote tables use SQLSETPROP( ), and then control the transaction with SQLCOMMIT( ) and SQLROLLBACK( ).
In general, you should use transactions with record buffers rather than with table buffering, except to wrap TABLEUPDATE( ) calls. If you place a TABLEUPDATE( ) command in a transaction, you can roll back a failed update, address the reason for the failure, and then retry the TABLEUPDATE( ) without losing data. This ensures the update happens as an "all-or-nothing" operation.
Though simple transaction processing provides safe data update operations in normal situations, it doesn't provide total protection against system failures. If power fails or some other system interruption occurs during processing of the END TRANSACTION command, the data update can still fail.
Use the following code template for transactions:
BEGIN TRANSACTION
* Update records
IF lSuccess = .F. && an error occurs
ROLLBACK
ELSE && commit the changes
* Validate the data
IF && error occurs
ROLLBACK
ELSE
END TRANSACTION
ENDIF
ENDIF
The following rules apply to transactions:
A transaction starts with the BEGIN TRANSACTION command and ends with the END TRANSACTION or ROLLBACK command. An END TRANSACTION statement without a preceding BEGIN TRANSACTION statement generates an error.
A ROLLBACK statement without a preceding BEGIN TRANSACTION statement generates an error.
A transaction, once begun, remains in effect until the corresponding END TRANSACTION begins (or until a ROLLBACK command is issued), even across programs and functions, unless the application terminates, which causes a rollback.
Visual FoxPro uses data cached in the transaction buffer before using disk data for queries on the data involved in transactions. This ensures that the most current data is used.
If the application terminates during a transaction, all operations roll back.
A transaction works only in a database container.
You cannot use the INDEX command if it overwrites an existing index file, or if any .cdx index file is open.
Transactions are scoped to data sessions.
Transactions exhibit the following locking behaviors:
Within a transaction, Visual FoxPro imposes a lock at the time a command directly or indirectly calls for it. Any system or user direct or indirect unlock commands are cached until the completion of the transaction by ROLLBACK or END TRANSACTION commands.
If you use a locking command such as FLOCK( ) or RLOCK( ) within a transaction, the END TRANSACTION statement will not release the lock. In that case, you must explicitly unlock any locks explicitly taken within a transaction. You should also keep transactions containing the FLOCK( ) or RLOCK( ) commands as brief as possible; otherwise, users could be locked out of records for a long time.
Nesting Transactions
Nested transactions provide logical groups of table update operations that are insulated from concurrent processes. BEGIN TRANSACTION...END TRANSACTION pairs need not be in the same function or procedure. The following rules apply to nested transactions:
You can nest up to five BEGIN TRANSACTION...END TRANSACTION pairs.
Updates made in a nested transaction aren't committed until the outermost END TRANSACTION is called.
In nested transactions, an END TRANSACTION only operates on the transaction initiated by the last issued BEGIN TRANSACTION.
In nested transactions, a ROLLBACK statement only operates on the transaction initiated by the last issued BEGIN TRANSACTION.
The innermost update in a set of nested transactions on the same data has precedence over all others in the same block of nested transactions.
Notice in the following example that because changes in a nested transaction aren't written to disk but to the transaction buffer, the inner transaction will overwrite the changes made to the same STATUS fields in the earlier transaction:
BEGIN TRANSACTION && transaction 1
UPDATE EMPLOYEE ; && first change
SET STATUS = "Contract" ;
WHERE EMPID BETWEEN 9001 AND 10000
BEGIN TRANSACTION && transaction 2
UPDATE EMPLOYEE ;
SET STATUS = "Exempt" ;
WHERE HIREDATE > {^1998-01-01} && overwrites
END TRANSACTION && transaction 2
END TRANSACTION && transaction 1
The following nested transaction example deletes a customer record and all its related invoices. The transaction will roll back if errors occur during a DELETE command. This example demonstrates grouping table update operations to protect updates from partial completion and to avoid concurrency conflicts.
Example of Modifying Records in Nested Transactions
Code |
Comments |
---|---|
DO WHILE TXNLEVEL( ) > 0 ROLLBACK ENDDO |
Cleanup from other transactions. |
CLOSE ALL SET MULTILOCKS ON SET EXCLUSIVE OFF |
Establish environment for buffering. |
OPEN DATABASE test USE mrgtest1 CURSORSETPROP('buffering',5) GO TOP |
Enable optimistic table buffering. |
REPLACE fld1 WITH "changed" SKIP REPLACE fld1 WITH "another change" MESSAGEBOX("modify first field of both" + ; "records on another machine") |
Change a record. Change another record. |
BEGIN TRANSACTION lSuccess = TABLEUPDATE(.T.,.F.) |
Start transaction 1 and try to update all modified records without force. |
IF lSuccess = .F. ROLLBACK AERROR(aErrors) DO CASE CASE aErrors[1,1] = 1539 ... CASE aErrors[1,1] = 1581 ... CASE aErrors[1,1] = 1582 |
If the update failed, roll back the transaction. Get the error from AERROR( ). Determine the cause of the failure. If a trigger failed, handle it. If a field doesn't accept null values, handle it. If a field rule was violated, handle it. |
CASE aErrors[1,1] = 1585 nNextModified = getnextmodified(0) DO WHILE nNextModified <> 0 GO nNextModified RLOCK() FOR nField = 1 to FCOUNT() cField = FIELD(nField) if OLDVAL(cField) <> CURVAL(cField) |
If a record was changed by another user, locate the first modified record. Loop through all modified records, starting with the first record. Lock each record to guarantee that you can update. Check each field for any changes. Check the buffered value against the value on disk, and then present a dialog box to the user. |
nResult = MESSAGEBOX; ("Data was changed " + ; "by another user — keep"+ ; "changes?", 4+48, ; "Modified Record") |
|
IF nResult = 7 TABLEREVERT(.F.) UNLOCK record nNextModified ENDIF |
If user responded "No," revert the one record and unlock it. |
EXIT ENDIF ENDFOR |
Break out of the "FOR nField..." loop. |
ENDDO |
Get the next modified record. |
BEGIN TRANSACTION TABLEUPDATE(.T.,.T.) END TRANSACTION UNLOCK |
Start transaction 2 and update all non-reverted records with force. End transaction 2. Release the lock. |
CASE aErrors[1,1] = 109 ... CASE aErrors[1,1] = 1583 ... CASE aErrors[1,1] = 1884 ... OTHERWISE MESSAGEBOX( "Unknown error "+; "message: " + STR(aErrors[1,1])) ENDCASE |
If the record is in use by another user, handle it. If a row rule was violated, handle it. If there was a unique index violation, handle it. Otherwise, present a dialog box to the user. |
ELSE END TRANSACTION ENDIF |
End transaction 1. |
Protecting Remote Updates
Transactions can protect you from system-generated errors during data updates on remote tables. The following example uses a transaction to wrap data-writing operations to a remote table.
Example of a Transaction on a Remote Table
Code |
Comment |
---|---|
hConnect = CURSORGETPROP('connecthandle') SQLSETPROP(hConnect, 'transmode', DB_TRANSMANUAL) |
Get the connect handle and enable manual transactions. |
BEGIN TRANSACTION |
Begin the manual transaction. |
lSuccess = TABLEUPDATE(.T.,.F.) IF lSuccess = .F. SQLROLLBACK (hConnect) ROLLBACK |
Try to update all records without force. If the update failed, roll back the transaction on the connection for the cursor. |
AERROR(aErrors) DO CASE |
Get the error from AERROR( ). |
CASE aErrors[1,1] = 1539 ... |
If a trigger failed, handle it. |
CASE aErrors[1,1] = 1581 ... |
If a field doesn't accept null values, handle it. |
CASE aErrors[1,1] = 1582 ... |
If a field rule was violated, handle it. |
CASE aErrors[1,1] = 1585 nNextModified = GETNEXTMODIFIED(0) DO WHILE nNextModified <> 0 GO nNextModified |
If a record was changed by another user, handle it. Loop through all modified records, starting with the first record. |
FOR nField = 1 to FCOUNT() cField = FIELD(nField) IF OLDVAL(cField) <> CURVAL(cField) nResult = MESSAGEBOX; ("Data has been changed ; by another user. ; Keep changes?",4+48,; "Modified Record") |
Check each field for any changes. Check the buffered value against the value on disk, and then present a dialog box to the user. |
IF nResult = 7 TABLEREVERT(.F.) ENDIF EXIT ENDIF ENDFOR nNextModified = ; GETNEXTMODIFIED(nNextModified) ENDDO |
If user responded "No," revert the one record. Break out of the "FOR nField..." loop. Get the next modified record. |
TABLEUPDATE(.T.,.T.) SQLCOMMIT(hConnect) |
Update all non-reverted records with force and issue a commit. |
CASE aErrors[1,1] = 109 * Handle the error |
Error 109 indicates that the record is in use by another user. |
CASE aErrors[1,1] = 1583 * Handle the error |
Error 1583 indicates that a row rule was violated. |
CASE aErrors[1,1] = 1884 * Handle the error |
Error 1884 indicates that the uniqueness of the index was violated. |
OTHERWISE * Handle generic errors. |
|
MESSAGEBOX("Unknown error message:" ; + STR(aErrors[1,1])) ENDCASE |
Present a dialog box to the user. End of error handling. |
ELSE SQLCOMMIT(hConnect) END TRANSACTION ENDIF |
If all errors were handled and the entire transaction was successful, issue a commit and end the transaction. |
See Also
Concepts
Managing Conflicts When Updating Data
Reference
Update Criteria Tab, View Designer