Update Conflict Resolution in Visual FoxPro
Jim Booth
What does buffering give us?
In the olden days of FoxPro 2.x we used to use indirect
editing, that is editing memvars rather than the fields directly. Why did we do
this? Because this gave us control over when and if the users work would be
saved. The word was that editing the fields directly would make immediate
changes to the file that we could not reverse easily.
Was this true? Well if it was then typing in a GET would
cause the disk light to flash as the disk was written to and this did not
happen. FoxPro was using a memory buffer to hold the file’s data and was
writing it to disk at a later time. The problem was that we had no control over
when and if that buffer would be written to disk.
Data buffering in VFP gives us that control. Data
buffering also allows us to react to and handle the problems that can occur in
multi-user environments.
Buffering Modes
There are four buffering modes in VFP. These modes are
divide into Row or Table and Pessimistic and Optimistic.
Row versus Table
Row buffering allows a single row of data to be “dirty” at
any time. The word dirty means that there is data in the buffer that has
pending changes that have not been either committed or discarded. Table
buffering allows multiple dirty records in the buffer.
Row buffering has the “magic” side effect that if there are
changes and anything moves the record pointer VFP will attempt to do a commit of
the buffer to disk. This can cause unwanted updates to occur.
Pessimistic versus Optimistic Locking Strategies
The other aspect of buffering modes is the locking
strategy. The two strategies are pessimistic and optimistic. Pessimistic
locking secures a lock on a record immediately as an edit of that record
begins. This lock is held until the commit or discard is done.
Optimistic locking does not hold a lock during an edit,
rather when an attempt is made to commit an edit the record is examined to see
if it has changed since the edit began and if it has the commit is frustrated.
Transactions
A transaction in VFP is a mechanism for wrapping multiple
update operations into a single “all or none” operation. These multiple
operations may be updates to multiple tables or even multiple records within a
single table. Transactions can be nested inside each other up to 5 levels deep.
What, exactly, does a transaction do? When VFP enters a
transaction it begins keeping track of all data update operations. It secures
all locks required but does not actually do the update until the
END TRANSACTION
command is encountered. Hitting a
ROLLBACK
command will discard all of the updates and release the locks, but it does not
clean the buffers. You must issue a
TableRevert()
to do that.
Enter the Update Conflict
The function we use to commit buffered data to disk is
TableUpdate().
This function will write the buffered data to disk (of course a transaction will
hold the actual writing to disk until the transaction is completed).
The
TableUpdate()
returns a value that indicates whether the update was successful. Why might the
update fail? There are many reasons that an update might fail, the server is
down, the network cable is broken, but the most common one is that there is an
update conflict. This means that the record on disk does not match the record
that the buffer started with indicating that someone else has changed the record
while it was being edited. VFP’s conflict detection is at the record level.
So what happens when the update conflict occurs
You have some choices; you can discard the user’s work for
them and have them start over again with the new data from disk; you can leave
the record as it currently is in the buffer and let the user choose to discard
their own work by clicking the Revert button; or you could bring the level of
detection down to the field.
Now for the code that demonstrates the field level
detection.
This code makes use of three methods of referring to the
data values;
OldVal()
This function tells us what value a field had in our buffer
before we edited it.
CurVal()
This function tells us what value a field has on disk now.
Alias.FieldName
This reference tells us what value a field has in our
buffer now.
With these three references we can find out if there are
any fields that have changed on disk and that we have also changed. If no field
are in this category then we can force the update to succeed, otherwise we have
to fail the update.
Below is listed code from a Resolve method of a form class
that generically applies this approach.
LPARAMETERS pcAlias, pcDatabase
* Define constants for the arrays
#DEFINE BUFFERNEW laFields( lnCnt, 2 )
#DEFINE TABLENEW laFields( lnCnt, 3 )
LOCAL llRet, laFields(1), lnCnt, lnNext, llGotOne, llView,
lcUser, ;
lcTable
DIMENSION laFields(1)
llRet = .T.
llView = ( CursorGetProp( "SOURCETYPE", pcAlias ) = 1 )
IF llView
* Fail the resoution for updateable views
RETURN .F.
ENDIF
SET DATABASE TO (pcDatabase)
* Attempts to resolve a failed TableUpdate(). Compares OldVal() and Buffer values to find the
* changed values, then checks CurVal() to see if these are among the fields that are different
* in the file. If the changed fields are not among the different file values, the buffer
* is updated from the table and the TableUpdate() is forced. Otherwise the TableUpdate is allowed
* to fail and the SaveChanges will also fail.
* Select the work area being resolved
SELECT (pcAlias)
* Build an array of fields names
AFIELDS(laFields)
* Get the first modified record
lnNext = GETNEXTMODIFIED(0,pcAlias)
* As long as we have a modified record and we have not failed
DO WHILE lnNext <> 0 AND llRet
* Set for no conflicts
llGotOne = .F.
* Move to the modified record
GOTO lnNext
* Check buffer against OLDVAL() to build a list of changed
fields
* result of comparison is stored in column 2 of laFields
and
* check changed fields against CURVAL() to find any
conflicts
* Store conflict detection result in column 3 of laFields
FOR lnCnt = 1 TO ALEN(laFields,1)
BUFFERNEW = OLDVAL( laFields( lnCnt, 1 ) ) <> ;
EVALUATE(pcAlias+"."+laFields(lnCnt,1))
TABLENEW = OLDVAL( laFields( lnCnt, 1 ) ) <> ;
CURVAL( laFields( lnCnt, 1 ) )
IF BUFFERNEW AND TABLENEW AND ;
EVALUATE(pcAlias+"."+laFields(lnCnt,1)) <> ;
CURVAL( laFields( lnCnt, 1 ) )
* If both the buffer and the curval()
are new
* from the oldval()
* and they are not the same value
* Set conflict flag
llGotOne = .T.
ENDIF
ENDFOR
* If any conflicts
IF llGotOne
* Set to fail
llRet = .F.
* LOOP back to exit DO WHILE
LOOP
ELSE
* There are no field collisions so we
will fix the buffere
* field values to match the disk image
for the unedited
* fields
FOR lnCnt = 1 TO ALEN(laFields,1)
IF TABLENEW
REPLACE (pcAlias+"."+laFields(lnCnt,1))
WITH ;
CURVAL( pcAlias+"."+laFields( lnCnt,
1 ) )
ENDIF
ENDFOR
ENDIF
* Force TableUpdate()
IF NOT TableUpdate( .F., .T., pcAlias )
llRet = .F.
LOOP
ENDIF
* Get the next modified record
lnNext = GETNEXTMODIFIED(lnNext,pcAlias)
ENDDO
RETURN llRet