Chapter 17: Programming for Shared Access

If you create an application that will run on several machines in a network environment, or if several instances of a form will access the same data, then you need to program for shared access. Shared access means providing efficient ways of using and sharing data among users, as well as restricting access when necessary.

Visual FoxPro provides support for shared or exclusive access to data, locking options, data sessions, record and table buffering, and transactions. Although these features are particularly useful in shared environments, you can use them in single-user environments too.

This chapter discusses:

Controlling Access to Data

Since you access data in files, effective data management begins with control over the environment of these files. You must choose how to access the data and how and when to limit that access.

Accessing Data

In a shared environment, you can access data in two ways: from exclusive files or from shared files. If you open a table for shared access, other users also have access to the file. If you open a table for exclusive access, no other user can read or write to that file. Because exclusive use defeats many of the benefits of sharing data on a network, it should be used sparingly.

Using a Table with Exclusive Access

The most restrictive way to open a file is to open it exclusively. When you open a table through the interface, it opens for exclusive use by default. You can also explicitly open a table for exclusive use by using Visual FoxPro commands.

To open a table for exclusive use

The following commands require you to open a table for exclusive use:

Visual FoxPro returns the error, “Exclusive open of file is required,” if you try to execute one of these commands on a shared table.

You can restrict access to a table by using the FLOCK( ) function. If you use FLOCK( ) to lock the table, other users cannot write to the table but they can read it.

Using a Table with Shared Access

When you open a table for shared use, more than one workstation can use the same table at the same time. When you open a table through the interface, you can override the default ON setting for SET EXCLUSIVE. You can explicitly open a table for shared use by using Visual FoxPro commands.

To open a table for shared use

When you add or change data in a shared table, you must first lock the affected record or the entire table. You can lock a record or a table opened for shared use in the following ways:

Associated memo and index files always open with the same share status as their table.

If your application uses a table for lookup purposes only and all users of the application access it, then you can improve performance by marking the table as read-only.

Locking Data

If you share access to files, you must also manage access to data by locking tables and records. Locks, unlike access permissions, can provide both long- and short-term control of data. Visual FoxPro provides both automatic and manual locking.

Choosing Record or Table Locks

Record locking, whether automatic or manual, prevents one user from writing to a record that’s currently being written to by another user. Table locking prevents other users from writing to, but not reading from, an entire table. Because table locking prohibits other users from updating records in a table, it should only be used sparingly.

Choosing Automatic or Manual Locks

In addition to record or table locking, you can also choose automatic or manual locking. Many Visual FoxPro commands automatically attempt to lock a record or a table before the command is executed. If the record or table is successfully locked, the command is executed and the lock is released.

Commands that Automatically Lock Records and Tables

Command Scope of lock
ALTER TABLE Entire table
APPEND Table header
APPEND BLANK Table header
APPEND FROM Table header
APPEND FROM ARRAY Table header
APPEND MEMO Current record
BLANK Current record
BROWSE, CHANGE and EDIT Current record and all records from aliased fields in related tables once editing of a field begins
CURSORSETPROP( ) Depends on parameters
DELETE Current record
DELETE NEXT 1 Current record
DELETE RECORD n Record n
DELETE of more than one record Entire table
DELETE – SQL Current record
GATHER Current record
INSERT Entire table
INSERT - SQL Table header
MODIFY MEMO Current record when editing begins
READ Current record and all records from aliased fields
RECALL Current record
RECALL NEXT 1 Current record
RECALL RECORD n Record n
RECALL of more than one record Entire table
REPLACE Current record and all records from aliased fields
REPLACE NEXT 1 Current record and all records from aliased fields
REPLACE RECORD n Record n and all records from aliased fields
REPLACE of more than one record Entire table and all files from aliased fields
SHOW GETS Current record and all records referenced by aliased fields
TABLEUPDATE( ) Depends on buffering
UPDATE Entire table
UPDATE – SQL Entire table

Record Lock Characteristics

Commands that attempt record locks are less restrictive than commands that lock tables. When you lock a record, other users can still add or delete other records. If a record or table is already locked by another user, an attempted record or table lock fails. Commands that attempt to lock the current record return the error, “Record is in use by another,” if the record cannot be locked.

The BROWSE, CHANGE, EDIT, and MODIFY MEMO commands do not lock a record until you edit the record. If you're editing fields from records in related tables, the related records are locked if possible. The lock attempt fails if the current record or any of the related records are also locked by another user. If the lock attempt is successful, you can edit the record; the lock is released when you move to another record or activate another window.

Header and Table Lock Characteristics

Some Visual FoxPro commands lock an entire table while others only lock a table header. Commands that lock the entire table are more intrusive than commands that only lock the table header. When you lock the table header, other users cannot add records, but they can still change data in fields.

Users can share the table without causing a conflict when you issue the APPEND BLANK command, but an error can occur while another user is also appending a BLANK record to the table. You can trap for the error, “File is in use by another,” which is returned when two or more users execute APPEND BLANK simultaneously. Commands that lock an entire table return the error, “File is in use by another,” if the table cannot be locked. To cancel the attempted lock, press ESC.

Example: Automatic Locking

In the following example, the user automatically locks the table header by appending records from another table, even though customer was opened as a shared file:

SET EXCLUSIVE OFF
USE customer
APPEND FROM oldcust FOR status = "OPEN"

Locking Manually

You can manually lock a record or a table with locking functions.

To manually lock a record or a table

RLOCK( ) and LOCK( ) are identical and lock one or more records. FLOCK( ) locks a file. The LOCK( ) and RLOCK( ) functions can apply to a table header. If you provide 0 as the record to LOCK( ) or RLOCK( ) and the test indicates the header is unlocked, the function locks the header and returns true (.T.).

Once you lock a record or table, be sure to release the lock by using the UNLOCK command as soon as possible to provide access to other users.

These manual locking functions perform the following actions:

If an attempt to lock a record or table fails, the SET REPROCESS command and your current error routine determine if the lock is attempted again. SET REPROCESS affects the result of an unsuccessful lock attempt. You can control the number of lock attempts or the length of time a lock is attempted with SET REPROCESS.

Example: Manual Locking

The following example opens the customer table for shared access and uses FLOCK( ) to attempt to lock the table. If the table is successfully locked, REPLACE ALL updates every record in the table. UNLOCK releases the file lock. If the file cannot be locked because another user has locked the file or a record in the file, a message is displayed.

SET EXCLUSIVE OFF
SET REPROCESS TO 0
USE customer    && Open table shared
IF FLOCK()
 REPLACE ALL contact ;    && Replace and unlock
  WITH UPPER(contact) 
 UNLOCK   
ELSE  && Output message
 WAIT "File in use by another." WINDOW NOWAIT
ENDIF

Unlocking Data

After you establish a record or file lock and complete a data operation in a shared environment, you should release the lock as soon as possible. There are several ways to release locks. In some cases, simply moving to the next record is enough to unlock the data. Other situations require explicit commands.

To unlock a record that's been automatically locked, you need only move the record pointer, even if you set MULTILOCKS ON. You must explicitly remove a lock from a record that you’ve manually locked; simply moving the record pointer is not enough.

The following table describes the effects of commands on manual and automatic record and table locks.

Command Effect
UNLOCK Releases record and file locks in the current work area.
UNLOCK ALL Releases all locks in all work areas in the current session.
SET MULTILOCKS OFF Enables automatic release of the current lock as a new lock is secured.
FLOCK( ) Releases all record locks in the affected file before locking the file.
CLEAR ALL, CLOSE ALL,
USE, QUIT
Releases all record and file locks.
END TRANSACTION Releases automatic locks.
TABLEUPDATE( ) Releases all locks after updating the table.

Caution   If a record was automatically locked in a user-defined function and you move the record pointer off and then back on the record, the lock will be released. Use table buffering to avoid this problem.

Using Data Sessions

To ensure that each user in a shared environment has a secure, exact duplicate of the working environment, and to ensure that multiple instances of a form can operate independently, Visual FoxPro provides data sessions.

A data session is a representation of the current dynamic work environment. You might think of a data session as a miniature data environment running inside one open Visual FoxPro session on one machine. Each data session contains:

The concept of a data session is easily understood when you consider what happens when you open the same form simultaneously from separate workstations in a multi-user application. In this case, each workstation is running a separate Visual FoxPro session, and therefore has its own set of work areas: cursors representing open base tables, indexes, and relationships.

However, if you open multiple instances of the same form in a single project, on one machine, within the same Visual FoxPro session, the forms share the Default data session, representing a single dynamic work environment. Each instance of the open form open in the same Visual FoxPro session uses the same set of work areas, and actions in one instance of a form that move the record pointer in a work area automatically affect other instances of the same form.

Using Private Data Sessions

If you want to have more control over multiple instances of form, you can implement Private data sessions. When your form uses private data sessions, Visual FoxPro creates a new data session for each instance of the Form, FormSet, or Toolbar control your application creates. Each private data session contains:

The number of available data sessions is limited only by available system memory and disk space.

You implement private data sessions by setting the DataSession property for the form. The DataSession property has two settings:

By default, the DataSession property of a form is set to 1.

To enable private data sessions

Choose one of the following options:

When a form uses private data sessions, each instance of a form open on a single machine in a single Visual FoxPro session uses its own data environment. Using private data sessions is similar to running the same form simultaneously from separate workstations.

Equivalent multiple data sessions

Identifying Data Sessions

Each private data session is identified separately. You can see the contents of each data session in the Data Session window. You can also change the data session description through commands in the Load event code.

You can view the identification number for each data session by using the DataSessionID run-time property. The following example displays the DataSessionID property of a form named frmMyForm:

DO FORM frmMyForm
? frmMyForm.DataSessionID

If you activate the form using the NAME clause, you can use the form’s name to access the DataSessionID property, as in the following code:

DO FORM MyForm NAME one
? one.DataSessionID

The DataSessionID property is designed only to identify a particular data session. Avoid changing the DataSessionID of a form instance because data-bound controls lose their data sources when you change the DataSessionID.

Updating Data Using Multiple Form Instances

While private data sessions generate separate work areas containing separate copies of a form’s open tables, indexes, and relationships, every copy of the form references the same underlying base tables and base index files. When a user updates a record from one instance of a form, the base table referenced by the form is updated. You see the changes made from another instance of the form when you navigate to the changed record.

Locks taken on records or tables in one private data session are respected by other private data sessions. For example, if the user of data session 1 takes a lock on a record, the user in data session 2 cannot lock the record. If the user in session 1 opens a table exclusively, the user in data session 2 cannot open the table. By respecting locks taken by other data sessions, Visual FoxPro protects the integrity of updates to the underlying base tables.

Customizing the Environment of a Data Session

Because data sessions control the scope of certain SET commands, you can use private data sessions to establish custom SET command settings within a single session of Visual FoxPro.

For example, the SET EXACT command, which controls the rules used when comparing character strings of different lengths, is scoped to the current data session. The default setting for SET EXACT is off which specifies that, to be equivalent, expressions must match, character for character, until the end of the expressions on the right side is reached. You might want to enable “fuzzy” or equivalent searches by leaving SET EXACT set to OFF for the default data session; however, your application might contain a specific form that requires exact matches. You could set the DataSession property for the form requiring exact matches to 2, to enable private data sessions, and then SET EXACT to ON for that form. By issuing a SET command only for the form using private data sessions, you preserve the global Visual FoxPro session settings while enabling customized session settings for a specific form.

Overriding Automatic Private Data Session Assignment

When private data sessions for a form are in use, changes you make to data in one form are not automatically represented in other instances of the same form. If you want all instances of a form to access the same data and to immediately reflect changes to common data, you can override automatic data session assignment.

To override automatic data session assignment

Both commands enable the default data session to be controlled by the Command window and the Project Manager.

Buffering Data

If you want to protect data during updates, use buffers. Visual FoxPro record and table buffering help you protect data update and data maintenance operations on single records and on multiple records of data in multi-user environments. Buffers can automatically test, lock, and release records or tables.

With buffering, you can easily detect and resolve conflicts in data update operations: the current record is copied to a memory or disk location managed by Visual FoxPro. Other users can then still access the original record simultaneously. When you move from the record or try to update the record programmatically, Visual FoxPro attempts to lock the record, verify that no other changes have been made by other users, and then writes the edits. After you attempt to update data, you must also resolve conflicts that prevent the edits from being written to the original table.

Choosing a Buffering Method

Before you enable buffering, evaluate the data environment to choose the buffering method and locking options that best suit the editing needs of your application, the record and table types and sizes, how the information is used and updated, and other factors. Once you enable buffering, it remains in effect until you disable buffering or close the table.

Visual FoxPro has two types of buffering: record and table.

Visual FoxPro record and table buffering

Choosing a Locking Mode

Visual FoxPro provides buffering in two locking modes: pessimistic and optimistic. These choices determine when one or more records are locked, and how and when they're released.

Pessimistic Buffering

Pessimistic buffering prevents other users in a multi-user environment from accessing a particular record or table while you're making changes to it. A pessimistic lock provides the most secure environment for changing individual records but it can slow user operations. This buffering mode is most similar to the standard locking mechanism in previous versions of FoxPro, with the added benefit of built-in data buffering.

Optimistic Buffering

Optimistic buffering is an efficient way to update records because locks are only taken at the time the record is written, thus minimizing the time any single user monopolizes the system in a multi-user environment. When you use record or table buffering on views, Visual FoxPro imposes optimistic locking.

The value of the Buffering property, set with the CURSORSETPROP( ) function, determines the buffering and locking methods.

The following table summarizes valid values for the Buffering property.

To enable Use this value
No buffering. The default value. 1
Pessimistic record locks which lock record now, update when pointer moves or upon TABLEUPDATE( ). 2
Optimistic record locks which wait until pointer moves, and then lock and update. 3
Pessimistic table locks which lock record now, update later upon TABLEUPDATE( ). 4
Optimistic table lock which wait until TABLEUPDATE( ), and then lock and update edited records. 5

The default value for Buffering is 1 for tables and 3 for views. If you use buffering to access remote data, the Buffering property is either 3, optimistic row buffering, or 5, optimistic table buffering. For more information on accessing data in remote tables, see Chapter 6, Querying and Updating Multiple Tables, in the User’s Guide.

Note   Set MULTILOCKS to ON for all buffering modes above 1.

Enabling Record Buffering

Enable record buffering with the CURSORSETPROP( ) function.

To enable pessimistic record locking in the current work area

Visual FoxPro attempts to lock the record at the pointer location. If the lock is successful, Visual FoxPro places the record in a buffer and permits editing. When you move the record pointer or issue a TABLEUPDATE( ) command, Visual FoxPro writes the buffered record to the original table.

To enable optimistic record locking in the current work area

Visual FoxPro writes the record at the location of the pointer to a buffer and permits edits. When you move the record pointer or issue a TABLEUPDATE( ) command, Visual FoxPro attempts a lock on the record. If the lock is successful, Visual FoxPro compares the current value of the record on the disk with the original buffer value. If these values are the same, the edits are written to the original table; if these values are different, Visual FoxPro generates an error.

Enabling Table Buffering

Enable table buffering with the CURSORSETPROP( ) function.

To enable pessimistic locking of multiple records in the current work area

Visual FoxPro attempts to lock the record at the pointer location. If the lock is successful, Visual FoxPro places the record in a buffer and permits editing. Use the TABLEUPDATE( ) command to write the buffered records to the original table.

To enable optimistic locking of multiple records in the current work area

Visual FoxPro writes the records to a buffer and permits edits until you issue a TABLEUPDATE( ) command. Visual FoxPro then performs the following sequence on each record in the buffer:

When table buffering is enabled, Visual FoxPro attempts updates only after a TABLEUPDATE( ) command.

Appending and Deleting Records in Table Buffers

You can append and delete records while table buffering is enabled: appended records are added to the end of the buffer. To access all records in the buffer, including appended records, use the RECNO( ) function. The RECNO( ) function returns sequential negative numbers on records you append to a table buffer. For instance, if you initiate table buffering, edit records 7, 8, and 9, and then append three records, the buffer will contain RECNO( ) values of 7, 8, 9, – 1, – 2, and – 3.

Buffer after editing and appending records

You can remove appended records from the buffer only by using the TABLEREVERT( ) command. For any appended record, both TABLEUPDATE( ) and TABLEREVERT( ) delete the negative RECNO( ) value for that record while maintaining the sequence.

Buffer after editing, deleting an appended record, and appending another

While using a table buffer, you can use the GO command with the negative RECNO( ) value to access a specific appended record. For instance, using the previous example, you can type:

GO 7      && moves to the 1st buffered record
GO -3      && moves to the 6th buffered record (3rd appended)

To append records to a table buffer

Appended records have sequential ascending negative RECNO( ) numbers.

To remove an appended record from a table buffer

  1. Use the GO command with a negative value to position the record pointer at the record to be deleted.

  2. Use the DELETE command to mark the record for deletion.

  3. Use the TABLEREVERT( ) function to remove the record from the buffer.

    Note   The TABLEREVERT( ) function also affects the status of deleted and changed rows.

To remove all appended records from a table buffer

TABLEREVERT( ) removes appended records from a table buffer without writing the records to the table. TABLEUPDATE( ) writes all current buffered records to a table, even if they’ve been marked for deletion.

Updating Data

To update data, you can use buffers, transactions, or views.

Performing Updates with Buffers

After choosing the buffering method and the type of locking, you can enable record or table buffering.

To enable buffering

Choose one of the following options:

You then place code for the update operations in the appropriate method code for your controls.

To write edits to the original table, use TABLEUPDATE( ). To cancel edits after a failed update operation in a table constrained by rules, use TABLEREVERT( ). TABLEREVERT( ) is valid even if explicit table buffering isn’t enabled.

The following sample demonstrates how to update records when pessimistic record buffering is enabled.

Example of Updating Using Record and Table Buffers

Code Comment
OPEN DATABASE testdata
USE customers
CURSORSETPROP('Buffering', 2)         
In the form Init code, open the table and enable pessimistic record buffering.
lModified = .F.
FOR nFieldNum = 1 TO FCOUNT()         
   IF GETFLDSTATE(nFieldNum) = 2      
      lModified = .T.
      EXIT
   ENDIF
ENDFOR

Go through fields, checking for any field that's been modified.

Note   This code might be in the Click event of a “Save” or “Update” command button.

IF lModified
   nResult = MESSAGEBOX;
      ("Record has been modified. Save?", ;
      4+32+256, "Data Change")
Locate the next modified record.
   IF nResult = 7                  
      TABLEREVERT (.F.)            
   ENDIF
ENDIF
Present the current value and give the user the option to revert the change to the current field.
SKIP                           
IF EOF()
   MESSAGEBOX( "already at bottom")
   SKIP -1
ENDIF 
THISFORM.Refresh



SKIP guarantees that the last change is written.

Managing Updates with 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 BEGIN TRANSACTION
Determine the current transaction level TXNLEVEL( )
Reverse all changes made since the most recent BEGIN TRANSACTION statement ROLLBACK
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 END TRANSACTION

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

Using Transactions

The following rules apply to transactions:

Transactions exhibit the following locking behaviors:

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:

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.

Managing Performance

Once you have a working multi-user application, you can use the following suggestions to improve performance:

Place Temporary Files on a Local Drive

Visual FoxPro creates its temporary files in the Windows default Temp directory. Text editing sessions can also temporarily create a backup copy of the file being edited (a .bak file).

If local workstations have hard drives with plenty of free space, you can improve performance by placing these temporary work files on the local drive or in a RAM drive. Redirecting these files to a local drive or a RAM drive increases performance by reducing access to the network drive.

You can specify an alternate location for these files by including the EDITWORK, SORTWORK, PROGWORK and TMPFILES statements in your Config.fpw configuration file. For more information about managing files, see Chapter 4, Optimizing Your System, in the Installation Guide.

Choose between Sorting and Indexing Files

When the data contained in a table is relatively static, processing sorted tables sequentially without an order set improves performance. This doesn't mean that sorted tables cannot or should not take advantage of index files — the SEEK command, which requires an index, is incomparable for locating records quickly. However, once you locate a record with SEEK, you can turn ordering off.

Schedule Exclusive Access to Files

Commands that run when no other users require access to the data, such as overnight updates, can benefit by opening the data files for exclusive use. When files are open for exclusive use, performance improves because Visual FoxPro doesn't need to test the status of record or file locks.

Time the Locking of Files

To reduce contention between users for write access to a table or record, shorten the amount of time a record or table is locked. You can do this by locking the record only after it's edited rather than during editing. Optimistic row buffering gives you the shortest lock time.

For more information on improving performance, see Chapter 4, Optimizing Your System, in the Installation Guide. You can also find information on improving performance in your client/server applications in Chapter 22, Optimizing Client/Server Performance.

Managing Updates with Views

You can use the update conflict management technology built into Visual FoxPro views to handle multi-user access to data. Views control what is sent to the base tables underlying the view by using the WhereType property. You can set this property for both local and remote views. The WhereType property provides four settings:

By choosing one of these four settings, you control how Visual FoxPro builds the WHERE clause for the SQL Update statement sent to the view’s base tables. You can choose the setting you want using the Update Criteria tab of the View Designer, or you can use DBSETPROP( ) to set the WhereType for a view definition. To change the WhereType setting for an active view cursor, use CURSORSETPROP( ).

For example, suppose you have a simple remote view based on the Customer table that includes seven fields: cust_id, company, phone, fax, contact, title, and timestamp. The primary key for your view is cust_id.

The Update Criteria tab displays the updatable fields in your view.

You've made only two fields updatable: contact_name and contact_title. You want the user to be able to change the company contact and their title from the view. However, if other facts about the company change, such as the company address, you want the changes to go through a coordinator who’ll identify the impact of the changes on your company, such as whether the sales region for the customer will change. Now that your view has been set up to send updates, you can choose the WhereType according to your preferences.

Now suppose that you change the name in the contact field for a customer, but you don’t change the value in the other updatable field, title. Given this example, the following section discusses how the WhereType setting would impact the WHERE clause that Visual FoxPro builds to send the new contact name to the base tables.

Comparing the Key Field Only

The least restrictive update uses the DB_KEY setting. The WHERE Clause used to update remote tables consists of only the primary key field specified with the KeyField or KeyFieldList property. Unless the value in the primary key field has been changed or deleted in the base table since you retrieved the record, the update goes through.

In the case of the previous example, Visual FoxPro would prepare an update statement with a WHERE clause that compares the value in the cust_id field against the cust_id field in the base table row:

WHERE OLDVAL(customer.cust_id) = CURVAL(customer_remote_view.cust_id)

When the update statement is sent to the base table, only the base table’s key field is verified.

The key field in your view is compared against its base table counterpart.

Comparing the Key Field and Fields Modified in the View

The DB_KEYANDMODIFIED setting, the default, is slightly more restrictive than DB_KEY. DB_KEYANDMODIFIED compares only the key field and any updatable fields you’ve modified in the view against their base table counterparts. If you modify a field in the view, but the field isn’t updatable, the fields are not compared to the base table data.

The WHERE clause used to update base tables consists of the primary fields specified with the KeyFieldList property and any other fields that are modified in the view. In the case of the previous example, Visual FoxPro would prepare an update statement that compares the values in the cust_id field because it is the key field, and the contact field because the contact name has been changed. Even though the title field is updatable, title is not included in the update statement because we haven’t modified it.

The key and modified fields in your view are compared against their base table counterparts.

Comparing the Key Field and All Updatable Fields

The DB_KEYANDUPDATABLE setting compares the key field and any updatable fields (whether modified or not) in your view against their base table counterparts. If the field is updatable, even if you haven’t changed it in the view, if anyone else has changed that field in the base table, the update fails.

The WHERE clause used to update base tables consists of the primary fields specified with the Key Field or KeyFieldList property and any other fields that are updatable. In the case of the example, Visual FoxPro would prepare an update statement that compares the values in the cust_id, contact, and title fields against the same fields in the base table row.

All the updatable fields in your view are compared against their base table counterparts.

Comparing the Timestamp for All Fields in the Base Table Record

The DB_KEYANDTIMESTAMP is the most restrictive type of update, and is only available if the base table has a timestamp column. Visual FoxPro compares the current timestamp on the base table record against the timestamp at the time the data was fetched into the view. If any field in the base table’s record has changed, even if it’s not a field you're trying to change, or even a field in your view, the update fails.

In the case of the example, Visual FoxPro prepares an update statement that compares the values in the cust_id field and the value in the timestamp field against those fields in the base table row.

The timestamp for your view’s record is compared against the timestamp on the base table record.

In order to successfully update data using the DB_KEYANDTIMESTAMP setting with a multi-table view, you must include the timestamp field in your view for each table that is updatable. For example, if you have three tables in a view and want to update only two of them, and you choose the DB_KEYANDTIMESTAMP setting, you must bring down the timestamp fields from the two updatable tables into your result set. You can also use logical values in the CompareMemo property to determine whether memo fields are included in conflict detection.

Managing Conflicts

Whether you choose buffering, transactions, or views, you must manage conflicts during the update process.

Managing Buffering Conflicts

You can make data update operations more efficient by carefully choosing how and when to open, buffer, and lock data in a multi-user environment. You should limit the time a record or table is subject to access conflicts. Still, you must anticipate and manage the inevitable conflicts that result. A conflict occurs when one user tries to lock a record or table that’s currently locked by another user. Two users cannot lock the same record or table at the same time.

Your application should contain a routine to manage these conflicts. If your application doesn't have a conflict routine, the system can lock up. A deadlock occurs when one user has locked a record or a table and tries to lock another record that’s locked by a second user who, in turn, is trying to lock the record that’s locked by the first user. While such occurrences are rare, the longer that a record or table is locked, the greater the chance of deadlock.

Trapping Errors

Designing a multi-user application or adding network support to a single-user system requires that you deal with collisions and trap for errors. Using Visual FoxPro record and table buffers simplifies some of this work.

If you attempt to lock a record or table already locked by another user, Visual FoxPro returns an error message. You can use SET REPROCESS to automatically deal with unsuccessful lock attempts. This command, in combination with an ON ERROR routine and the RETRY command, enables you to continue or cancel the lock attempts.

The following example demonstrates automatic reprocessing of a failed operation, using SET REPROCESS.

Using SET REPROCESS and ON ERROR to Manage User Collisions

Code Comment
ON ERROR DO err_fix WITH ERROR(),MESSAGE()
SET EXCLUSIVE OFF   
SET REPROCESS TO AUTOMATIC
USE customer
IF !FILE('cus_copy.dbf')
   COPY TO cus_copy
ENDIF
This routine runs if an error occurs.
Open the files non-exclusively.
Reprocessing of unsuccessful locks is automatic.
Open the table.

Create the APPEND FROM table if needed.
DO app_blank
DO rep_next
DO rep_all
DO rep_curr
DO add_recs
The main routine starts here.
These commands are examples of codes that could be executed in the course of your program.
ON ERROR
The main routine ends here.
PROCEDURE app_blank      
   APPEND BLANK
RETURN
ENDPROC
Routine to append a blank record.
PROCEDURE rep_next
   REPLACE NEXT 1 contact WITH ;
      PROPER(contact)
RETURN
ENDPROC
Routine to replace data in the current record.
PROCEDURE rep_all      
   REPLACE ALL contact WITH ;
      PROPER(contact)
   GO TOP
RETURN
ENDPROC
Routine to replace data in all records.
PROCEDURE rep_curr      
   REPLACE contact WITH PROPER(contact)
RETURN
ENDPROC
Routine to replace data in the current record.
PROCEDURE add_recs      
   APPEND FROM cus_copy
RETURN
ENDPROC
Routine to append records from another file.

The following example demonstrates an error procedure that starts when the user presses ESC.

Error Handling Using the ESC Key

Code Comment
PROCEDURE err_fix
   PARAMETERS errnum, msg
This program is called when an error is encountered and the user escapes from the wait process.
DO CASE
Figure out what kind of error this is.
Is it “File is in use by another”?
   CASE errnum = 108      
      line1 = "File cannot be locked."
      line2 = "Try again later..."
 
   CASE errnum = 109 .OR. errnum = 130
      line1 = "Record cannot be locked."
      line2 = "Try again later."
Or “Record is in use by another”?
   OTHERWISE            
      line1 = msg + " "
      line2 = ;
         "See your system administrator."
ENDCASE
Or is it unknown?


=MESSAGEBOX( line1 + line2, 48, "Error!" )
RETURN                  
Display the error message in a dialog box with an exclamation point and an OK button.

Detecting and Resolving Conflicts

During data update operations, especially in shared environments, you might want to determine which fields have changed or what the original or the current values are in changed fields. Visual FoxPro buffering and the GETFLDSTATE( ), GETNEXTMODIFIED( ), OLDVAL( ) and CURVAL( ) functions, enable you to determine which field has changed, find the changed data, and compare the current, original, and edited values so you can decide how to handle an error or conflict.

To detect a change in a field

GETFLDSTATE( ) works on unbuffered data; however, this function is even more effective when you've enabled record buffering. For instance, use GETFLDSTATE( ) in the code of a Skip button on a form. When you move the record pointer, Visual FoxPro checks the status of all fields in the record as in the following example:

lModified = .F.
FOR nFieldNum = 1 TO FCOUNT( ) && Check all fields 
   if GETFLDSTATE(nFieldNum) = 2  && Modified
      lModified = .T.
      EXIT && Insert update/Save routine here.
   ENDIF && See the next example
ENDFOR

To detect and locate a changed record in buffered data

GETNEXTMODIFIED( ), with zero as a parameter, finds the first modified record. If another user makes changes to the buffered table, any changes encountered by a TABLEUPDATE( ) command in your buffer will cause conflicts. You can evaluate the conflicting values and resolve them using the CURVAL( ), OLDVAL( ), and MESSAGEBOX( ) functions. CURVAL( ) returns the current value of the record on disk, while OLDVAL( ) returns the value of the record at the time it was buffered.

To determine the original value of a buffered field

OLDVAL( ) returns the value of a buffered field.

To determine the current value of a buffered field on disk

CURVAL( ) returns the current value on disk of a buffered field before any edits were performed.

You can create an error-handling procedure that compares the current and original values, enabling you to determine whether to commit the current change or to accept an earlier change to data in a shared environment.

The following example uses GETNEXTMODIFIED( ), CURVAL( ), and OLDVAL( ) to provide the user with an informed choice in an update operation. This example continues from detection of the first modified record and might be contained in an Update or Save button on a form.

Click Event Code for an Update or Save Button

Code Comment
DO WHILE GETNEXTMODIFIED(nCurRec) <> 0   
   GO nCurRec
   RLOCK( )
Loop through buffer.

Lock the modified record.
   FOR nField = 1 TO FCOUNT(cAlias)   
      cField = FIELD(nField)
      IF OLDVAL(cField) <> CURVAL(cField)
         nResult = MESSAGEBOX("Data was ;
            changed by another user. ;
            Keep changes?", 4+48+0, ;
            "Modified Record")
Look for conflict.

Compare the original value to the current value on the disk, and then ask the user what to do about the conflict.
         IF nResult = 7   
            TABLEREVERT(.F.)   
            UNLOCK RECORD nCurRec   
         ENDIF
      ENDIF
   ENDFOR
   nCurRec = GETNEXTMODIFIED(nCurRec)
ENDDO
If the user selects “No,” revert this record, and then remove the lock.




Find the next modified record.
TABLEUPDATE(.T., .T.)   
Force update to all records.

Detecting Conflicts using Memo Fields

You can use the CompareMemo property to control when memo fields are used to detect update conflicts. This view and cursor property determines whether memo fields (types M or G) are included in the update WHERE clause. The default setting, True (.T.), means that memo fields are included in the WHERE clause. If you set this property to False (.F), memo fields don’t participate in the update WHERE clause, regardless of the settings of UpdateType.

Optimistic conflict detection on Memo fields is disabled when CompareMemo is set to False. For conflict detection on memo values, set CompareMemo to True (.T.).

Rules for Managing Conflicts

Managing conflicts encountered in multi-user environments can require extensive and repetitive code. A complete conflict management routine does the following:

For an example of a conflict management routine, see the data checker class in Samples.vcx, located in the Visual Studio …\Samples\Vfp98\Classes directory. Just add the class to a form and call the CheckConflicts method before any operation that writes buffered data to the table, for example moving the record pointer if you're using row buffering, closing a table, or issuing TABLEUPDATE( ).