DBMS, February 1996
DBMS Online: Desktop DBMS By Tom Spitzer

Going Client/Server with VFP

Flexible ODBC connectivity requires a substantial management effort.

I introduced Visual FoxPro (VFP) back in my column in the May 1995 issue (see "FoxPro Goes Visual," page 97). In that column, I identified the integration of ODBC technology into the product's overall database access model as one of its three key features. Since then, one of my company's development teams has been using VFP to prototype application components where both data and application logic reside on database servers. Our initial challenge was simply to access from VFP our target database servers -- Oracle7 and Microsoft SQL Server --and this challenge continues as we endure continual upgrades to network transport and ODBC libraries. More interesting has been the process of identifying VFP's capabilities, strengths, and weaknesses as a client/server development tool. We are now wrestling with how to preserve the responsiveness of our FoxPro applications, minimize the amount of data we have to move around the network, and maximize data concurrency.

VFP offers two modes for working with server-based data: persistent stored views and SQL pass-through (SPT). When you create a persistent view, VFP stores a view definition in a database structure that it calls a Database Container, or DBC. The definition contains the names of the views' tables and fields and their property settings. When you use the view, the view definition is used to build a SQL statement that defines the view's data set. Although it also provides view-creation facilities, SPT opens up the entire spectrum of server functionality to the VFP application by exposing much of the ODBC API. This breadth of functionality does not come without costs, which manifest themselves in the overhead of having to manage explicitly all of the tasks involved in connecting to the server and requesting services from it.

Three function pairs provide fine control over the behavior of VFP's client/server features. SQLGETPROP( ) and SQLSETPROP( ) query and manipulate the client/server environment and the properties of connections to client/server databases. These connection properties specify the ODBC data source, user, password, and connect string and communications settings such as wait and timeout intervals, network packet size, and whether results are passed back to VFP synchronously or asynchronously. The CURSORGETPROP( ) and CURSORSETPROP( ) functions control the local cursors that are created when data is retrieved from a server. User-manageable properties of cursors let you specify fetch and update rules. Finally, DBSETPROP( ) and DBGETPROP( ) control the properties of connections and views that are stored persistently in database containers. You can set the default values for these properties on the Remote Data Options panel, as shown in Figure 1.

The Database Container

VFP's Database Container is a table in which each row represents an object in your database schema. In this context, objects can be tables or their fields, indexes, and stored server connections, or views and their columns. For each object, the DBC stores a set of predefined properties (in a format that only VFP can read) and a binary representation of any code associated with the object. Fortunately, VFP provides an extensive toolkit of functions for interrogating the database container. Using these tools effectively requires a good deal of work, because each function returns a single piece of information in exchange for one or more parameters (which you must specify).

Named connections function as objects that record the name of an ODBC data source, userID, and password in the DBC structure, along with settings that control the behavior of views using the connection. You can associate each stored view with a named connection; when the view is instantiated, it uses the connectivity parameters defined for the connection. These parameters specify whether the connection is asynchronous, whether the connection operates in batch mode, whether each update request is wrapped in a transaction automatically, and how long processes should wait before they time out. Named connections let the developer or database administrator shield the application user from the ODBC login process, and they create a high-level profile of properties that multiple views can share.

VFP's Stored Views

The interactive view designer and the CREATE SQL VIEW command are equivalent ways of defining a view against either local FoxPro tables or server tables accessed via an ODBC connection. Views based on tables on database servers are called remote views. An application opens, or instantiates, a view by executing a USE <viewname> command. This issues a request for data specified by the view parameters and places all of the qualifying rows into a single cursor on the client. VFP will employ asynchronous progressive fetching while executing the SQL statement that specifies the view, so the application can proceed while the cursor is being filled.

VFP leverages its own data processing power by storing in local cursors the data sets retrieved by the server. For developers entering the client/server arena from a dBASE or FoxPro background, this approach provides a familiar way to work with server-based data. After a cursor has been created, either with a stored view or a SELECT statement via SPT, an application can manipulate the cursor's data as if it were in a native FoxPro table. To navigate the cursor, you can use the familiar dBASE/FoxPro SKIP command. You may also create an index on a cursor, which enables extremely fast searches. More likely, the cursor will be small enough so that sequential searches using VFP's LOCATE FOR <expression> command are sufficient.

The properties of a stored view determine how the view will behave when it is instantiated. The FetchMemo, FetchSize, and MaxRecords properties let you manage the size of a local cursor when it is created, and thereby manage the amount of time it takes to create it. The BatchUpdateCount, UpdateType, SendUpdates, and WhereType parameters control how updates are managed. To make a view updatable, you must specify key fields, updatable fields, and the rule for detecting update conflicts. For detecting update conflicts, you can compare the key field in the cursor with the key field in the corresponding server row, the key and all updatable fields, the key and modified fields, and the key and timestamp.

VFP's SQL Pass-Through

VFP's SPT library provides a rich set of functions, but it requires the developer to write a substantial amount of code. To initiate an SPT session, an application must request a handle for an active connection using either SQLCONNECT( ) or SQLSTRINGCONNECT( ). If the attempt to connect succeeds, the session returns a numeric connection handle that the application will use to specify the connection for routing subsequent SPT calls. If necessary, the application can request multiple connections for one data source. It can also connect to multiple ODBC data sources by obtaining a different connection handle for each one.

After the application establishes a connection, it can request information about the data source with the SQLTABLES( ) and SQLCOLUMNS( ) functions, or it can issue any of the SPT SQL statement execution and control functions. Of these, the primary function is SQLEXEC( ), which passes SQL statements to the server. A SQLEXEC( ) that passes a SELECT statement or calls a stored procedure that, in turn, executes a SELECT statement creates a local cursor with the SELECT results. When you use a single SQLEXEC( ) to issue multiple SELECT statements, VFP will retrieve result sets into multiple cursors.

Controlling the way VFP retrieves these cursors and how they can be updated requires an extensive amount of parameter-setting code. As with stored views, you need to specify key fields, updatable fields, and the rule for detecting update conflicts. The difference between a stored view and an SPT-established view is that you must set each view option explicitly every time you create that view with SPT. Listing 1 illustrates the parameter-setting code required to do this.

Using SQLEXEC( ) to issue DML commands such as INSERT, UPDATE, and DELETE provides fine control over updates and access to server-specific extensions to these commands. Applications can also pass DDL statements via SQLEXEC( ) to create and modify database schema. Of course, applications operating at this level of sophistication must provide a substantial amount of error trapping. Listing 2 provides an example of using SQLEXEC to create and load a table. SQLEXEC( ) also provides a way to execute blocks of server code and stored procedures. These capabilities support moving application logic from the client application to the server.

Creating a Form-Based Interface to Server Data

Ultimately, our objective in developing an interactive application is to provide a set of forms with which people can work to maintain the server data. VFP offers an object-oriented form design system that provides several ways to associate a data set with a form. The most straightforward way is to associate a stored view with the form as part of the form's Data Environment. We expect most views to accept parameterized criteria, so when we present a form to the user we request criteria for creating a subset (ideally a small subset) of the table. If the criterion produces a cursor with more than one row, we need to provide a way for the user to identify the row that will be the initial focus of the form. After doing so, the user may want to edit one of the columns.

Building on its cursor-oriented architecture, VFP provides a flexible buffering model to support entering and editing data in multiuser and client/server environments. When buffering is enabled, VFP copies the data being edited into a memory buffer. This architecture can buffer individual rows (for record-by-record editing) or entire tables (for multi-record updates and batch-processing operations). If you execute TABLEUPDATE( ) with buffering on, moving off the current row causes VFP to attempt to commit changes from the buffer to the database.

In applications that access local tables, VFP offers a choice of optimistic or pessimistic locking modes. For client/server applications, VFP supports optimistic locking in which the lock is attempted only when the change is about to be committed from the buffer to the source tables. If we expect a great deal of contention, we simulate pessimistic locking by implementing a semaphore-based scheme.

Another group of functions allows us to provide very granular conflict detection. GETFLDSTATE( ) indicates whether the user has changed the contents of the specified field. GETNEXTMODIFIED( ) indicates the next modified record in the current buffer. OLDVAL( ) takes the name of a field or expression and returns the value of that field or expression before the user started to edit. CURVAL( ) reports the current value in the base table, and it can be used to determine whether another user made changes during the current user's edit session. Thus, when the user prepares to commit changes to a buffered table, the application can scan the buffer for new and changed rows and compare them to the corresponding rows on the database server. If the current user changes rows that have also changed on the server, the application can display the initial value and both sets of changed values and allow the user to determine which value to commit. In any case, if an attempt to update a buffered table fails, VFP will report the failure. If table buffering is in effect, you can issue a TABLEREVERT( ) to undo any changes that were committed before the failure occurred.

In addition to data buffering, VFP lets you manage server transactions. The Transactions property of the connection determines how the connection manages transactions on the remote table. If the Transactions property is set to Automatic, each command passed to the server is treated as a separate transaction. If you set the Transactions property to Manual, the application can pass a series of SQL updates to the server and then issue a SQLCOMMIT( ) to advise the server when to commit. If the application determines that the update cannot be completed due to failure of a validation condition or multiuser contention, it can request that the server discard the pending changes with SQLROLLBACK( ).

Data Validation

Assuming that there are no multiuser conflicts when the user attempts to update a table, our application will be ready to handle the next tricky aspect of working with server data via ODBC: validation. There are likely to be validation constraints on the underlying table that will be evaluated when the user tries to commit changes. If an attempted update violates any constraints, the server will reject the change. Our application needs to check for this failure and report it to the user. The mechanism for this is to first detect and identify the error. The text of the error that ODBC returns to FoxPro looks something like this: "Connectivity error: [Visigenic][ODBC Oracle driver][Oracle]ORA-02290: check constraint [TSPI.C_MINPRICE] violated". We would not show this message to a user; rather, we maintain a table of our own error messages and map the error messages returned from the server into the error messages that we want to display.

We also have a stylistic preference for validating data entry sooner rather than later, and would prefer not to make users learn that their changes are invalid after they push the Save button. We are looking at several ways to enable validation when the user makes the invalid entry. Certainly we can hardcode the validation rule into the form, but that's not a good solution for large systems and it defeats the purpose of moving business rules into the database. Alternatively, we can develop an infrastructure for reading constraints from the system catalog and assigning them to the appropriate form controls when the user opens the form. This is a bandwidth- and overhead-intensive solution that may not be feasible.

VFP lets me assign a validation rule to a stored view, or to a field in a stored view, with DBSETPROP( ). We can take advantage of this feature by maintaining a local data dictionary that mirrors the server's schema, and assign rules to our views from the dictionary. We can do this dynamically, and synchronize constraint rules with the server, but that represents a significant amount of work. It's fairly clean where the rule syntax is the same in both VFP and on the server. Where the rules use different syntax (for example, relying on server-specific functions), we must maintain multiple versions of each rule.

VFP poses other challenges for client/server applications. For example, VFP's mode of retrieving data into workstation-based cursors compels us to anticipate how users will work. We must weigh the number of server requests the application will make and the amount of data the application will retrieve against the currency of the data in the workstation's cursors. For example, after disposing of the current row, it's likely that a user will want to do something else. This could involve working on another row in the same view, working with some data in another view, or entering some type of transaction. When anticipating the user's workflow, we must consider carefully how the application should build data sets on the workstation. If the user stays in one task for a long period, we would prefer to retrieve a substantial number of records and allow the work to proceed locally. If the user switches tasks often, we would want to create small views and refresh them each time the task is switched. These kinds client/server considerations add complexity to the design of VFP applications.

Knotty Issues

As I have shown, developing client/server applications with VFP centers on managing the client-side cursors for each view. There are no provisions for read-only views against server-side tables. Also, no support exists for server-side views or cursors that do not require retrieving complete data sets across the network. You cannot take advantage of server features such as server-side views and engine-based cursors in VFP applications. Because SELECT result tables and error messages are the only information you can get back from the server, server-side code must put results into tables instead of into server-side variables.

When VFP creates a cursor, the data specified by the view must be copied from the server to the client, where it resides for as long as the view is open. Once the view is established on the local cursor, the connection between the cursor and the server tables is weak. There are several implications of this model. Obviously, developers must specify view parameters -- columns and selection criteria -- that minimize the view's size. Because SELECT is the primary interface between the application and the server, we must design applications with server and network performance in mind.

Because the data in the cursor is static and there is no automatic notification when the local data becomes obsolete, you must establish and follow rules for refreshing data. This will ensure that what the user sees and edits matches data on the server. Some synchronization options include issuing timed REFRESH requests for the current and surrounding rows, detecting changes and reissuing queries, or providing a way for the user to initiate a refresh. To implement distributed applications with VFP, we are considering keeping large working data sets in persistent local tables and developing an engine for replicating data among the server and the local databases when network bandwidth is available.

The burden of replicating data on the client highlights the desirability of having a keyset model similar to the one that Microsoft's JET engine employs. In JET, establishing a view retrieves only the keys of qualifying rows in the view. In that model, the values of the current row's columns are refreshed automatically whenever the user visits them. We can simulate this model by establishing our local cursors so that they only contain key values, and by issuing subsequent SELECTS for the balance of the record when it's needed. VFP gives us the tools to build such a mechanism, and the responsibility of managing it.


Tom Spitzer is managing consultant for application solutions in the San Francisco office of AmeriData Consulting. You can email Tom at tspitzer@ameridata.com.

Figure 1.


--In the VFP Remote Data Options dialog, you set default values for cursor (top half) and connection (bottom half) properties.


LISTING 1
Setting properties of an SBT cursor to make it updatable

* First, create a cursor named c_cust
=SQLEXEC(lnHandle,"Select cu_custid,cu_company,cu_stdate from bkcust where cu_company like 'A%'","c_cust")
* Turn on multi-row locking, required for buffering
SET MULTILOCKS ON
* Turn on optimistic row buffering
= CURSORSETPROP("buffering",3,"c_cust")
* Map cursor to BKCUST table, identify key and updateable fields
= CURSORSETPROP("Tables","bkcust"," c_cust ")
= CURSORSETPROP("KeyFieldList","cu_custid"," c_cust ")
= CURSORSETPROP("UpdatableFieldList","cu_company,cu_stdate"," c_cust ")
* Map column names in cursor to column names in source table = CURSORSETPROP("UpdateNameList","cu_company bkcust.cu_company,cu_stdate bkcust.cu_stdate,cu_custid bkcust.cu_custid"," c_cust ")
* Set conflict resolution mechanism to check key and modified fields
= CURSORSETPROP("WhereType",3," c_cust ")
* Make view updateable
= CURSORSETPROP("SendUpdates",.T.," c_cust ")
* Update with SQL UPDATE
= CURSORSETPROP("UpdateType, 1, "c_cust")

RETURN


LISTING 2.
Creating and loading a table with SQL pass-through

FUNCTION CheckExec
PARAMETERS phConnect, pcExecString, pcCursor
lnHandle = SQLCONNECT(SBTHQORA", "TSPI", "password")
IF .NOT. SQLEXEC(lnHandle, "DROP TABLE DEPT")
DO sqlerror
ENDIF
lcCreaDept = "CREATE TABLE DEPT ( " + ;
" DEPTNO NUMBER(2) NOT NULL, " + ;
" DNAME VARCHAR2(14)," + ;