Chapter 6 Extending Remote Views with SQL Pass Through 95

advertisement
Chapter 6: Extending Remote Views with SQL Pass Through
95
Chapter 6
Extending Remote Views
with SQL Pass Through
As you learned in Chapter 4, remote views make it very easy to access remote data.
Remote views are actually wrappers for SQL pass through, handling the tasks of
managing connections, detecting changes, formatting UPDATE commands and
submitting them to the server. In this chapter, we’re going to take an in-depth look at
SQL pass through. We’ll see how to connect to the server, submit queries, manage
transactions and more.
There is no doubt about it, remote views offer the easiest way to access remote data. However,
with ease of use comes less flexibility. Visual FoxPro contains another powerful mechanism for
manipulating remote data called SQL pass through (SPT). SQL pass through provides all the
functionality of remote views and more.
With SQL pass through you can:
•
Execute queries other than SELECT.
•
Access back-end-specific functionality.
•
Fetch multiple result sets in a single call.
•
Execute stored procedures.
However:
•
There is no graphical user interface.
•
You must manually manage connections.
•
Result sets are read-only by default and must be configured to be updatable.
The flexibility that SQL pass through allows makes it a powerful tool. It is important for
client/server developers to understand it thoroughly.
Connecting to the server
In order to use SQL pass through, you must make a connection to the server. Unlike remote
views, Visual FoxPro does not manage the connection. The developer must manually make the
connection, configure its behavior, and then disconnect when the connection is no longer
needed. Connection management is very important because making a connection consumes
substantial time and resources on the client and server.
There are two functions that are used to establish the connection with the remote server:
SQLConnect() and SQLStringConnect().
96
Client/Server Applications with Visual FoxPro and SQL Server
The SQLConnect() function
There are two ways to use the SQLConnect() function to connect to a remote data source. The
first requires that you supply the name of a data source as defined in the ODBC Data Source
Administrator applet of the control panel. The following example creates a connection to a
remote server using the ODBCPubs DSN:
LOCAL hConn
hConn = SQLConnect("ODBCPubs", "sa", "")
The second way to use SQLConnect() is to supply the name of a Visual FoxPro
connection that was created using the CREATE CONNECTION command. As you saw in
Chapter 4, “Remote Views,” the CREATE CONNECTION command stores the metadata that
Visual FoxPro needs to connect to a remote data source. The following example creates a
Visual FoxPro connection named VFPPUBS and then connects to the database described by
the connection:
LOCAL hConn
CREATE DATABASE cstemp
CREATE CONNECTION vfppubs ;
DATASOURCE "ODBCPubs" ;
USERID "sa" ;
PASSWORD ""
hConn = SQLConnect("vfppubs")
The SQLStringConnect() function
The other function that can be used to establish a connection to a remote data source is
SQLStringConnect(). Unlike SQLConnect(), SQLStringConnect() requires a single parameter,
a string of semicolon-delimited options that describes the remote data source and optional
connections settings.
The valid options are determined by the requirements of the ODBC driver. Specific
requirements for each ODBC driver can be found in that ODBC driver’s documentation.
Table 1 lists some commonly used connection string options for SQL Server 7.0.
Table 1. Some common SQL Server 7.0 connection string options.
Option
Description
DSN
Driver
Server
UID
PWD
Database
APP
WSID
Trusted_Connection
References an ODBC DSN.
Specifies the name of the ODBC driver to use.
Specifies the name of the SQL Server to connect to.
Specifies the login ID or username.
Specifies the password for the given login ID or username.
Specifies the initial database to connect to.
Specifies the name of the application making the connection.
The name of the workstation making the connection.
Specifies whether the login is being validated by the Windows NT Domain.
Chapter 6: Extending Remote Views with SQL Pass Through
97
Not all of the options listed in Table 1 have to be used for each connection. For
instance, if you specify the Trusted_Connection option and connect to SQL Server using NT
Authentication, there’s no reason to use the UID and PWD options since SQL Server would
invariably ignore them.
The following code demonstrates some examples of using SQLStringConnect().
‡
From this point forward, substitute the name of your server for the string
<MyServer> in code examples.
LOCAL hConn
hConn = SQLStringConnect("Driver=SQL Server;Server=<MyServer>;"+ ;
UID=sa;PWD=;Database=pubs")
hConn = SQLStringConnect("DSN=ODBCPubs;UID=sa;PWD=;Database=pubs")
hConn = SQLStringConnect("DSN=ODBCPubs;Database=pubs;Trusted_Connection=Yes")
Handling connection errors
Both the SQLConnect() and SQLStringConnect() functions return a connection handle. If
the connection is established successfully, the handle will be a positive integer. If Visual
FoxPro failed to make the connection, the handle will contain a negative integer. A simple
call to the AERROR() function can be used to retrieve the error number and message.
The following example traps for a failed connection and displays the error number and
message using the Visual FoxPro MESSAGEBOX() function. Figure 1 shows an example
of the error message.
‡
Visual FoxPro returns error 1526 for all errors against a remote data
source. The fifth element of the array returned by AERROR() contains the
remote data source-specific error.
#define MB_OKBUTTON
#define MB_STOPSIGNICON
0
16
LOCAL hConn
hConn = SQLConnect("ODBCPubs", "bad_user", "")
IF (hConn < 0)
LOCAL ARRAY laError[1]
AERROR(laError)
MESSAGEBOX( ;
laError[2], ;
MB_OKBUTTON + MB_STOPSIGNICON, ;
"Error " + TRANSFORM(laError[5]))
ENDIF
98
Client/Server Applications with Visual FoxPro and SQL Server
Figure 1. The error message returned from SQL Server 7.0 when trying to establish a
connection using an unknown login.
Disconnecting
As mentioned previously, the developer is responsible for connection management when using
SQL pass through. It is very important that a connection be released when it is no longer
needed by the application because connections consume valuable resources on the server, and
the number of connections may be limited by licensing constraints.
You break the connection to the remote data source using the SQLDisconnect() function.
SQLDisconnect() takes one parameter, the connection handle created by a call to either
SQLConnect() or SQLStringConnect(). SQLDisconnect() returns a 1 if the connection was
correctly terminated and a negative value if an error occurred.
The following example establishes a connection to SQL Server 7.0 and then drops
the connection:
LOCAL hConn,lnResult
*hConn = SQLStringConnect("Driver=SQL Server;Server=<MyServer>;"+ ;
UID=sa;PWD=;Database=pubs")
hConn = SQLConnect("ODBCPubs", "sa", "")
IF (hConn > 0)
MESSAGEBOX("Connection established")
lnResult = SQLDisconnect(hConn)
IF lnResult < 0
MESSAGEBOX("Disconnect failed")
ENDIF && lnResult < 0
ENDIF && hConn > 0
If the parameter supplied to SQLDisconnect() is not a valid connection handle, Visual
FoxPro will return a run-time error (#1466). Currently there is no way to determine whether a
connection handle is valid without attempting to use it.
‡
To disconnect all SQL pass through connections, you can pass a value of
zero to SQLDisconnect().
Accessing metadata
VFP has two SQL pass through functions that return information about the database you’ve
connected to. The first, SQLTables(), returns a result set containing information about the
tables and views in the database. The second, SQLColumns(), returns a result set containing
information about a specific table or view.
Chapter 6: Extending Remote Views with SQL Pass Through
99
The SQLTables() function
The following example demonstrates using SQLTables() to retrieve information about the user
tables and views within the SQL Server demo database pubs. Figure 2 shows a portion of the
information returned in a VFP Browse window, and Table 2 lists the definitions of the columns
within the result set.
LOCAL hConn, lnResult
hConn = SQLConnect("odbcpubs", "sa", "")
lnResult = SQLTables(hConn, "'TABLE', 'VIEW'")
SQLDisconnect(hConn)
Figure 2. The results of calling SQLTables() on the pubs database.
Table 2. The description of the columns in the result set.
Column
Description
Table_cat
Object qualifier. However, In SQL Server 7.0, Table_cat contains the
name of the database.
Object owner.
Object name.
Object type (TABLE, VIEW, SYSTEM TABLE or another data-storespecific identifier).
A description of the object. However, SQL Server 7.0 does not return a
value for Remarks.
Table_schema
Table_name
Table_type
Remarks
100
Client/Server Applications with Visual FoxPro and SQL Server
The SQLColumns() function
SQLColumns() returns a result set containing information about each column in the specified
table. This function returns different results depending on a third, optional parameter:
“FOXPRO” or “NATIVE.” The “NATIVE” option formats the result set with information
specific to the remote data source, whereas specifying “FOXPRO” formats the result set with
column information describing the Visual FoxPro cursor that contains the retrieved data. Table
3 lists the columns that are returned by SQLColumns() using the “FOXPRO” option. Table 4
lists the columns returned by SQLColumns() when using “NATIVE” and when attached to a
SQL Server database. Different results are possible depending on the remote data source.
Table 3. A description of the columns returned by the FOXPRO option.
Column
Description
Field_name
Field_type
Field_len
Field_dec
Column name
Visual FoxPro data type
Column length
Number of decimal places
Table 4. A description of the columns returned from SQL Server using SQLColumns()
and the NATIVE option.
Column
Description
Table_cat
Table_schema
Table_name
Column_name
Data_type
Type_name
Column_size
Buffer_length
Decimal_digits
Num_prec_radix
Nullable
Remarks
Column_def
SQL_data_type
SQL_datetime_sub
Character_octet_length
Ordinal_position
Is_nullable
SS_data_type
SQL Server database name
Object owner
Object name
Column name
Integer code for the ODBC data type
SQL Server data type name
Display requirements (character positions)
Storage requirements (bytes)
Number of digits to the right of the decimal point
Base for numeric data types
Integer flag for nullability
SQL Server always returns NULL
Default value expression
Same as Data_type column
Subtype for datetime data types
Maximum length of a character or integer data type
Ordinal position of the column (starting at 1)
Nullability indicator as a string (YES | NO)
SQL Server data type code
The following example demonstrates using the SQLColumns() function to retrieve
information about the authors table in the pubs database. Figure 3 shows a Browse window
containing a result set formatted with the FOXPRO option. Figure 4 shows a subset of the
columns returned by the NATIVE option.
LOCAL hConn, lnResult
hConn = SQLConnect("odbcpubs", "sa", "")
Chapter 6: Extending Remote Views with SQL Pass Through
lnResult = SQLColumns(hConn,
BROWSE NORMAL && display the
lnResult = SQLColumns(hConn,
BROWSE NORMAL && display the
SQLDisconnect(hConn)
101
"authors", "FOXPRO")
results
"authors", "NATIVE")
results
Figure 3. The results of calling SQLColumns() with the FOXPRO option.
Figure 4. A subset of the columns returned by SQLColumns() with the NATIVE option.
(See Table 4 for a complete list of columns.)
Submitting queries
Most interactions with the remote server will be through the SQLExec() function. SQLExec() is
the workhorse of the SQL pass through functions. You’ll use it to submit SELECT, INSERT,
UPDATE and DELETE queries, as well as calls to stored procedures. If the statement is
successfully executed, SQLExec() will return a value greater than zero that represents the
number of result sets returned by the server (more on multiple result sets later). A negative
return value indicates an error. As discussed previously, you can use AERROR() to retrieve
information about the error. It’s also possible for SQLExec() to return a value of zero (0), but
only if queries are being submitted asynchronously. We’ll look at asynchronous queries in a
later section.
Queries that return a result set
As with the SQLTables() and SQLColumns() functions, result sets returned by a query
submitted using SQLExec() are stored in a Visual FoxPro cursor. Also like the SQLTables()
102
Client/Server Applications with Visual FoxPro and SQL Server
and SQLColumns() functions, the name of the result set will be SQLRESULT unless another
name is specified in the call to SQLExec().
For example, the following call to SQLExec() runs a SELECT query against the authors
table in the pubs database.
‡
From this point forward, examples may not include the code that
establishes the connection.
lnResults = SQLExec(hConn, "SELECT * FROM authors")
To specify the name of the result set rather than accept the default, “SQLRESULT,”
specify the name in the third parameter of the SQLExec statement. The following example uses
the same query but specifies that the resultant cursor should be called authors:
lnResult = SQLExec(hConn, "SELECT * FROM authors", "authors")
Figure 5 shows the Data Session window with the single authors cursor open.
Figure 5. The Data Session window showing the single authors cursor.
Retrieving multiple result sets
As discussed in Chapter 3, “Introduction to SQL Server 7.0,” submitting a query to a remote
server can be a very expensive operation. The server must parse the query and check for syntax
errors, verify that all referenced objects exist, optimize the query to determine the best way to
solve it, and then compile and execute. Luckily for us, Visual FoxPro and ODBC provide a
means to gain an “economy of scale” when submitting queries. It is possible to submit multiple
queries in a single call to SQLExec(), as in the following example:
lcSQL = "SELECT * FROM authors; SELECT * FROM titles"
lnResults = SQLExec(hConn, lcSQL)
Chapter 6: Extending Remote Views with SQL Pass Through
103
SQLExec() returns a value (stored in lnResults in this example) containing the number of
cursors returned.
Now you might be wondering what names Visual FoxPro assigns to each cursor. In the
preceding example, the results of the first query (SELECT * FROM authors) will be placed into
a cursor named Sqlresult. The results from the second query will be placed into a cursor named
Sqlresult1. Figure 6 shows the Data Session window with the two cursors open.
Figure 6. The Data Session window showing the two cursors Sqlresult and Sqlresult1.
Visual FoxPro’s default behavior is to wait until SQL Server has returned all the result sets
and then return the result sets to the application in a single action. Alternately, you can tell
Visual FoxPro to return the result sets one at a time as each one is available. This behavior is
controlled by a Connection property, BatchMode. If BatchMode is True (the default), Visual
FoxPro returns all result sets at once; else, if False, Visual FoxPro returns the result sets one at
a time.
Use the SQLSetProp() function to manipulate connection settings. The following example
changes the BatchMode property to False, causing Visual FoxPro to return results sets one at
a time:
lnResult = SQLSetProp(hConn, 'BatchMode', .F.)
As usual, a positive return result indicates success.
SQLSetProp() has a sibling, SQLGetProp(), that returns the current value of a connection
property. The following code checks that the BatchMode property was set correctly:
llBatch = SQLGetProp(hConn, 'BatchMode')
When BatchMode is False, Visual FoxPro automatically returns only the first result set.
The developer must request that Visual FoxPro return additional result sets by calling the
104
Client/Server Applications with Visual FoxPro and SQL Server
SQLMoreResults() function. SQLMoreResults() returns zero (0) if the next result set is not
ready, one (1) if it is ready, two (2) if there are no more result sets to retrieve, or a negative
number if an error has occurred.
The following example demonstrates the SQLMoreResults() function. In this example,
we’re going to retrieve information about a specific book by submitting queries against the
titles, authors, titleauthor and sales tables.
*-- Get information about the book
lcSQL = "SELECT * FROM titles WHERE title_id = 'TC7777'" + ";"
*-- Retrieve the authors
lcSQL = lcSQL + ;
"SELECT * " + ;
"FROM authors INNER JOIN titleauthor " + ;
"ON authors.au_id = titleauthor.au_id " + ;
"WHERE titleauthor.title_id = 'TC7777'"
*-- Retrieve sales information
lcSQL = lcSQL + "SELECT * FROM sales WHERE title_id = 'TC7777'"
lnResult = SQLSetProp(hConn, "BatchMode", .F.)
lnResult = SQLExec(hConn, lcSQL, 'TitleInfo')
DO WHILE .T.
lnResult = SQLMoreResults(hConn)
DO CASE
CASE lnResult < 0
*-- Error condition
CASE lnResult = 0
*-- No result sets are ready
CASE lnResult = 2
*-- All result sets have been retrieved
EXIT
OTHERWISE
*-- Process retrieved result set
ENDCASE
ENDDO
It is important to realize that SQLMoreResults() must continue being called until it returns
a two (2), meaning no more result sets. If any other SQL pass through function is issued before
SQLMoreResults() returns 2, Visual FoxPro will return the error shown in Figure 7.
‡
The preceding statement is not entirely true. You can issue the
SQLCancel() function to terminate any waiting result sets, but we haven’t
introduced it yet.
Chapter 6: Extending Remote Views with SQL Pass Through
105
Figure 7. The results of trying to issue another SQL pass through function while
processing result sets in non-batch mode.
Queries that modify data
INSERT, UPDATE and DELETE queries are submitted in the same way as SELECT queries.
The following example increases the price of all books in the titles table of the pubs database
by 10 percent:
lnResult = SQLExec(hConn, "UPDATE titles SET price = price * 1.1")
In this example, SQLExec() executes a data modification query rather than a SQL
SELECT statement. Therefore, it returns a success indicator (1 for successful execution or a
negative number in the event of an error), rather than the number of result sets. If the query
successfully updates zero, one, or one million rows, SQLExec() will return a value of one. (A
query is considered successful if the server can parse and execute it.)
To determine the number of rows updated, use the SQL Server global variable
@@ROWCOUNT, which performs the same function as Visual FoxPro’s _TALLY global
variable. After executing a query, @@ROWCOUNT contains the number of rows affected by
the query. The value of @@ROWCOUNT can be retrieved by issuing a SELECT query:
lnResult = SQLExec(hConn, "SELECT @@ROWCOUNT AS AffectedRows", "status")
Note that the value of @@ROWCOUNT is returned as a column named “AffectedRows”
in the first (and only) row of a cursor named “Status,” not to the variable lnResult.
Unlike _TALLY, @@ROWCOUNT is not truly global. It is one of several variables
that are scoped to the connection. Therefore, the value of @@ROWCOUNT must be retrieved
on the same connection that executed the query. If you execute the query on one connection
and retrieve the value of @@ROWCOUNT from another connection, the result will not
be accurate.
Also, @@ROWCOUNT is reset after each statement. If you submit multiple queries,
@@ROWCOUNT will contain the affected row count for the last query executed.
Parameterized queries
You previously read about using parameters in views to filter the query. You can use this same
mechanism with SQL pass through.
106
Client/Server Applications with Visual FoxPro and SQL Server
Using a parameterized query might seem unnecessary at first. After all, since you pass the
query as a string, you have complete control over its creation. Consider the following example:
FUNCTION GetTitleInfo(tcTitleID, tcCursor)
LOCAL lcQuery, hConn
lLcQuery = "SELECT * FROM titles WHERE title_id = '" + tcTitleID + "'"
hConn = SQLConnect("odbcpubs", "sa", "")
lnResult = SQLExec(hConn, lcQuery, tcCursor)
SQLDisconnect(hConn)
RETURN .T.
Creating the query using the technique from the previous example works in most
situations. However, there are situations where using a parameterized query makes more sense.
For example, when different back ends impose different requirements for specifying literal
values, it is easier to allow Visual FoxPro to handle the conversion. Consider dates. Visual
FoxPro requires date literals to be specified in the form {^1999-12-31}. SQL Server, on the
other hand, does not recognize {^1999-12-31} as a date literal. Instead you would have to use a
literal similar to ‘12/31/1999’ or ‘19991231’ (the latter being preferred).
The following code shows how the same query would be formatted for Visual FoxPro and
SQL Server back ends:
*-- If accessing Visual FoxPro using the ODBC driver
lcQuery = ;
"SELECT * " + ;
"FROM titles " + ;
"WHERE pubdate BETWEEN {^1998-01-01} AND {^1998-12-31}"
*-- If accessing SQL Server
lcQuery = ;
"SELECT * " + ;
"FROM titles " + ;
"WHERE pubdate BETWEEN '19980101' AND '19981231'"
In this situation, Visual FoxPro converts the search arguments to the proper format
automatically. The following example demonstrates this:
LOCAL ldStart, ldStop, lcQuery
ldStart = {^1998-01-01}
ldStop = {^1998-12-31}
lcQuery = ;
"SELECT * " + ;
"FROM titles " + ;
"WHERE pubdate BETWEEN ?ldStart AND ?ldStop"
The preceding query would work correctly against both Visual FoxPro and SQL Server.
There are other data types that also benefit from the use of parameterization. Visual
FoxPro’s Logical vs. SQL Server’s Bit is another example. A literal TRUE is represented in
Visual FoxPro as .T., while in Transact-SQL it is 1.
Chapter 6: Extending Remote Views with SQL Pass Through
107
The advantage of parameterization
Parameterized queries provide an additional benefit: Parameterized queries execute more
quickly than non-parameterized queries when the query is called repeatedly with different
parameters. This performance benefit occurs because SQL Server does not have to parse,
optimize and compile the query each time it is called—instead, it can reuse the existing
execution plan with the new parameter values.
To demonstrate, we’ll use the SQL Server Profiler, a utility that ships with SQL Server
7.0. SQL Server Profiler, described in greater detail in Chapter 8, “Errors and Debugging,”
is one of the best tools available for debugging and investigation. It logs events that occur on
the server (such as the submission of a query or calling a stored procedure) and collects
additional information.
‡
SQL Server 6.5 has a similar utility called SQLTrace.
Figure 8 shows the output from the SQL Server Profiler for the following query, and
Figure 9 shows the output for the second one:
LOCAL llTrue,lnResult
lnResult = SQLExec(hConn, "SELECT * FROM authors WHERE contract = 1")
llTrue = .T.
lnResult = SQLExec(hConn, "SELECT * FROM authors WHERE contract = ?llTrue")
Figure 8. The SQL Server Profiler output for a non-parameterized query.
Figure 9. The SQL Server Profiler output for a parameterized query.
There is an important difference between how these two queries were submitted to SQL
Server. As expected, the first query was passed straight through. SQL Server had to parse,
108
Client/Server Applications with Visual FoxPro and SQL Server
optimize, compile and execute the query. The next time the same query is submitted, SQL
Server will have to parse, optimize and compile the query again before executing it.
The second query was handled quite differently. When Visual FoxPro (actually, ODBC)
submitted the query, the sp_executesql stored procedure was used to identify the search
arguments to SQL Server. The following is an excerpt from the SQL Server Books Online:
“sp_executesql can be used instead of stored procedures to execute a Transact-SQL
statement a number of times when the change in parameter values to the statement is
the only variation. Because the Transact-SQL statement itself remains constant and
only the parameter values change, the Microsoft® SQL Server™ query optimizer is
likely to reuse the execution plan it generates for the first execution.”
SQL Server will take advantage of this knowledge (the search arguments) by caching the
execution plan (the result of parsing, optimizing and compiling a query) instead of discarding
the execution plan, which is the normal behavior. The next time the query is submitted, SQL
Server can reuse the existing execution plan, but with a new parameter value.
There is a tradeoff—calling a stored procedure has a cost, so you should not blindly write
all your queries using parameters. However, the cost is worth incurring if the query is executed
repeatedly. There are no magic criteria to base your decision on, but some of the things to
consider are the number of times the query is called and the length of time between calls.
Making SQL pass through result sets updatable
By default, the cursor created to hold the results of a SQL pass through query is read-only.
Actually, changes can be made to the data within the cursor, but Visual FoxPro won’t do
anything with the changes. This may sound familiar, as it’s the same behavior exhibited by a
non-updatable view. As it turns out, the same options that make a view updatable will also
work on a cursor created by a SQL pass through query. The following example retrieves all the
authors from the authors table and makes the au_fname and au_lname columns updatable:
lnResult = SQLExec(hConn, "SELECT * FROM authors", "authors")
CURSORSETPROP("TABLES", "dbo.authors", "authors")
CURSORSETPROP("KeyFieldList", "au_id", "authors")
CURSORSETPROP("UpdatableFieldList", "au_lname, au_fname", "authors")
CURSORSETPROP("UpdateNameList", ;
"au_id dbo.authors.au_id, " + ;
"au_lname dbo.authors.au_lname, " + ;
"au_fname dbo.authors.au_fname, " + ;
"phone dbo.authors.phone, " + ;
"address dbo.authors.address, " + ;
"city dbo.authors.city, " + ;
"state dbo.authors.state, " + ;
"zip dbo.authors.zip, " + ;
"contract dbo.authors.contract", "authors")
CURSORSETPROP("SendUpdates", .T., "authors")
Each property plays an important role in the creation of the commands sent to the server.
Visual FoxPro will create an INSERT, UPDATE or DELETE query based on the operations
performed on the cursor. The UpdatableFieldList property tells Visual FoxPro which columns
Chapter 6: Extending Remote Views with SQL Pass Through
109
it needs to track changes to. The Tables property supplies the name of the remote table, and the
UpdateNameList property has the name of the column in the remote table for each column in
the cursor. KeyFieldList contains a comma-delimited list of the columns that make up the
primary key. Visual FoxPro uses this information to construct the WHERE clause of the query.
The last property, SendUpdates, provides a safety mechanism. Unless SendUpdates is marked
TRUE, Visual FoxPro will not send updates to the server.
There are two other properties that you may want to include when making a cursor
updatable. The first, BatchUpdateCount, controls the number of update queries that are
submitted to the server at once. The default value is one (1), but increasing this property can
improve performance. SQL Server will parse, optimize, compile and execute the entire batch of
queries at the same time. The second parameter, WhereType, controls how Visual FoxPro
constructs the WHERE clause used by the update queries. This also affects how conflicts are
detected. Consult the Visual FoxPro online Help for more information on the WhereType
cursor property.
Calling stored procedures
One of the things you don’t normally do with a remote view is call a stored procedure. For that,
you typically use SQL pass through. Calling a stored procedure via SQL pass through is just
like submitting any other query: The SQLExec() function does all the work.
The following example demonstrates calling a SQL Server stored procedure. The stored
procedure being called, sp_helpdb, returns information about the databases residing on the
attached server. Note that we have the same ability to rename the result set returned by the
query/stored procedure.
lnResult = SQLExec(hConn, "EXECUTE sp_helpdb", "dbinfo")
The preceding example uses the Transact-SQL EXECUTE command to call the stored
procedure. You can also call stored procedures using the ODBC escape syntax, as
demonstrated in the following example:
lnResult = SQLExec(hConn, "{CALL sp_helpdb}")
Using the ODBC escape syntax offers two small advantages. First, ODBC will
automatically convert the statement to the format required by the back end you are working
with—as long as that back end supports directly calling stored procedures. Second, it is an
alternate way to work with OUTPUT parameters.
Handling input and output parameters
Stored procedures, like Visual FoxPro’s procedures and functions, can accept parameters. An
example is sp_helprotect, which returns information about user permissions applied to a
specific database object. The following code calls the sp_helprotect stored procedure to obtain
information about user permissions applied to the authors table. The result set will contain all
the users and roles that have been given explicit permissions on the authors table, and whether
those permissions were granted or denied.
110
Client/Server Applications with Visual FoxPro and SQL Server
lnResult = SQLExec(hConn, "EXECUTE sp_helprotect 'authors'")
Using the ODBC calling convention is slightly different from calling a Visual FoxPro
function, as shown here:
lnResult = SQLExec(hConn, "{CALL sp_helprotect ('authors')}")
Additional parameters are separated by commas:
lnResult = SQLExec(hConn, "EXECUTE sp_helprotect 'authors', 'guest'")
lnResult = SQLExec(hConn, "{CALL sp_helprotect ('authors', 'guest')}")
Output parameters
There is another way to get information from a stored procedure: output parameters. An output
parameter works the same way as a parameter passed to a function by reference in Visual
FoxPro: The stored procedure alters the contents of the parameter, and the new value will be
available to the calling program.
The following Transact-SQL creates a stored procedure in the Northwind database that
counts the quantity sold of a particular product within a specified date range:
LOCAL lcQuery, lnResult
lcQuery = ;
"CREATE PROCEDURE p_productcount " + ;
"@ProductId INT, " + ;
"@StartDate DATETIME, " + ;
"@EndDate DATETIME, " + ;
"@QtySold INT OUTPUT " + ;
"AS " + ;
"SELECT @QtySold = SUM(od.Quantity) " + ;
"FROM Orders o INNER JOIN [Order Details] od " + ;
"ON o.OrderId = od.OrderId " + ;
"WHERE od.ProductId = @ProductId " + ;
"AND o.OrderDate BETWEEN @StartDate AND @EndDate "
*-- hConn must be a connection to the Northwind database
lnResult = SQLExec(hConn, lcQuery)
The stored procedure accepts four parameters: the ID of the product, the start and end
points for a date range, and an output parameter to return the total quantity sold.
The following example shows how to call the stored procedure and pass the parameters:
LOCAL lnTotalCnt, lcQuery
lnTotalCnt = 0
lcQuery = "EXECUTE p_ProductCount 72, '19960701', '19960801', ?@lnTotalCnt"
lnResult = SQLExec(hConn, lcQuery)
You can also call the p_ProductCount procedure using ODBC escape syntax, as in the
following code:
Chapter 6: Extending Remote Views with SQL Pass Through
111
lcQuery = "{CALL p_productcount (72, '19960701', '19960801', ?@lnTotalCnt)}"
lnResult = SQLExec(hConn, lcQuery)
‡
Because SQL Server returns result codes and output parameters in the
last packet sent from the server, output parameters are not guaranteed to
be available until after the last result set is returned from the server—that
is, until SQLExec() returns a one (1) while in Batch mode or SQLMoreResults()
returns a two (2) in Non-batch mode.
Transaction management
A transaction groups a collection of operations into a single unit of work. If any operation
within the transaction fails, the application can cause the data store to undo (that is, reverse) all
the operations that have already been completed, thus keeping the integrity of the data intact.
Transaction management is a powerful tool, and the Visual FoxPro community was
pleased to see its introduction into Visual FoxPro.
In Chapter 3, “Introduction to SQL Server 7.0,” we looked at transactions within SQL
Server and identified two types: implicit (or Autocommit) and explicit. To review, implicit
transactions are individual statements that commit independently of other statements in the
batch. In other words, the changes made by one statement are not affected by the success or
failure of a statement that executes later. The following example demonstrates transferring
funds from a savings account to a checking account:
lnResult = SQLExec(hConn, ;
"UPDATE account " + ;
"SET balance = balance – 100 " + ;
"WHERE ac_num = 14356")
lnResult = SQLExec(hConn, ;
"UPDATE account " + ;
"SET balance = balance + 100 " + ;
"WHERE ac_num = 45249")
Even if the two queries are submitted in the same SQLExec() call, as in the following
example, the two queries commit independently of each other:
lnResult = SQLExec(hConn, ;
"UPDATE account " + ;
"SET balance = balance – 100 " + ;
"WHERE ac_num = 14356" + ;
";" + ;
"UPDATE account " + ;
"SET balance = balance + 100 " + ;
"WHERE ac_num = 45249")
Each query is independent of the other. If the second fails, nothing can be done to undo the
changes made by the first except to submit a correcting query.
112
Client/Server Applications with Visual FoxPro and SQL Server
On the other hand, an explicit transaction groups multiple operations and allows
the developer to undo all changes made by all operations in the transaction if any one
operation fails.
In this section, we’re going to look at the SQL pass through functions that manage
transactions: SQLSetProp(), SQLCommit() and SQLRollback().
SQL pass through doesn’t have a function to start an explicit transaction. Instead,
explicit transactions are started by setting the connection’s Transactions property to a two
(2) or DB_TRANSMANUAL (from Foxpro.h). The following example shows how to use
the SQLSetProp() function to start a manual (Visual FoxPro term) or explicit (SQL Server
term) transaction:
#include FOXPRO.h
lnResult = SQLSetProp(hConn, "TRANSACTIONS", DB_TRANSMANUAL)
Enabling manual transaction does not actually start a transaction. The transaction actually
starts only when the first query is submitted. After that, all queries submitted on the connection
will participate in the transaction until the transaction is terminated. You will see exactly how
this works in Chapter 11, “Transactions.” Regardless, if everything goes well and no errors
occur, you can commit the transaction with the SQLCommit() function:
lnResult = SQLCommit(hConn)
If something did go wrong, the transaction can be rolled back and all operations reversed
with the SQLRollback() function:
lnResult = SQLRollback(hConn)
Manual transactions can only be disabled by calling SQLSetProp() to set the Transactions
property back to 1. If you do not reset the Transactions property to 1, the next query submitted
on the connection automatically causes another explicit transaction to be started.
Taking all that into account, the original example can be rewritten as follows:
#include FOXPRO.h
…
lnResult = SQLSetProp(hConn, "TRANSACTIONS", DB_TRANSMANUAL)
lnResult = SQLExec(hConn, ;
"UPDATE account " + ;
"SET balance = balance – 100 " + ;
"WHERE ac_num = 14356" + ;
";" + ;
"UPDATE account " + ;
"SET balance = balance + 100 " + ;
"WHERE ac_num = 45249")
IF (lnResult != 1)
SQLRollback(hConn)
*-- Relay error message to the user
ELSE
SQLCommit(hConn)
Chapter 6: Extending Remote Views with SQL Pass Through
113
ENDIF
SQLSetProp(hConn, "TRANSACTIONS", 1)
RETURN (lnResult = 1)
The code in the preceding example wraps the UPDATE queries within the explicit
transaction and handles an error by rolling back any changes that may have occurred.
Binding connections
Sometimes it’s necessary for two or more connections to participate in the same transaction.
This scenario can occur when dealing with components in a non-MTS environment. To
accommodate this need, SQL Server provides the ability to bind two or more connections
together. Once bound, the connections will participate in the same transaction.
If multiple connections participate in one transaction, any of the participating connections
can begin the transaction, and any participating connection can end the transaction.
Connection binding is accomplished by using two stored procedures: sp_getbindtoken and
sp_bindsession. First, execute sp_getbindtoken against the first connection to obtain a unique
identifier (the bind token, as a string) for the connection. Next, pass the bind token to
sp_bindsession, which is executed against another connection. The second function binds the
two connections. The following example demonstrates the entire process:
LOCAL hConn1, hConn2, hConn3, lnResult, lcToken
lcToken = ""
hConn1 = SQLConnect("odbcpubs", "sa", "")
hConn2 = SQLConnect("odbcpubs", "sa", "")
hConn3 = SQLConnect("odbcpubs", "sa", "")
lnResult = SQLExec(hConn1, "EXECUTE sp_getbindtoken ?@lcToken")
lnResult = SQLExec(hConn2, "EXECUTE sp_bindsession ?lcToken")
lnResult = SQLExec(hConn3, "EXECUTE sp_bindsession ?lcToken")
SQLDisconnect(hConn1)
SQLDisconnect(hConn2)
SQLDisconnect(hConn3)
In the example, three connections are established to the server. In the first call to
sp_getbindtoken, you get the bind token. You must use the ? and @ symbols with the lcToken
variable because the binding token returns through an OUTPUT parameter. You then pass the
bind token to the second and third connections by calling sp_bindsession.
Asynchronous processing
So far, every query that we’ve sent to the server was sent synchronously. Visual FoxPro paused
until the server finished processing the query and returned the result set to Visual FoxPro.
There are times, however, when you may not want Visual FoxPro to pause while the query is
running. For example, you may want to provide some feedback to the user to indicate that the
application is running and has not locked up, or you may want to provide the ability to cancel a
query mid-stream. To prevent Visual FoxPro from pausing, submit the query asynchronously.
Just remember that this approach makes the developer responsible for determining when the
query processing is finished.
114
Client/Server Applications with Visual FoxPro and SQL Server
Switching to asynchronous processing is not complicated. There is a connection property,
Asynchronous, that determines the mode. If Asynchronous is set to FALSE (the default), all
queries will be sent synchronously. Note that Asynchronous is a Visual FoxPro connection
property and therefore is scoped to the connection.
The following example demonstrates making a connection and then using the
SQLSetProp() function to change to asynchronous mode:
hConn = SQLConnect("odbcpubs", "sa", "")
lnResult = SQLSetProp(hConn, "ASYNCHRONOUS", .T.)
There is absolutely no difference between submitting a query in synchronous mode and
submitting a query in asynchronous mode. There is, however, a difference in the way you detect
that the query has completed. If a query is submitted in synchronous mode, SQLExec() will
return a positive value indicating the number of result sets returned or a negative value
indicating an error. In asynchronous mode, that still holds true, but SQLExec() can return a
zero (0) indicating that the query is still being processed. It is up to the developer to poll the
server to determine when the query has been completed.
Polling the server is quite easy. It requires resubmitting the query again. ODBC and the
server realize that the query is not being resubmitted, that this is merely a status check. In fact,
you can simply pass an empty string for the subsequent SQLExec() calls. Regardless, the
following example shows one way to structure this process:
LOCAL llDone, lnResult
llDone = .F.
DO WHILE !llDone
lnResult = SQLExec(hConn, "EXECUTE LongQuery")
llDone = (lnResult != 0)
ENDDO
The loop will stay engaged as long as SQLExec() returns a zero (0) identifying the query
as still being processed.
In the preceding example, the stored procedure being called does not actually run any
queries. The stored procedure LongQuery simply uses the Transact-SQL WAITFOR command
with the DELAY option to pause for a specific period of time (two minutes in this case) before
proceeding. The code for LongQuery is shown here:
lcQuery = ;
[CREATE PROCEDURE longquery AS ] + ;
[WAITFOR DELAY '00:02:00']
*-- hConn should be a connection to the pubs database
lnResult = SQLExec(hConn, lcQuery)
The following program demonstrates calling the LongQuery stored procedure in
asynchronous mode and trapping the Escape key, which is the mechanism provided to
terminate the query:
LOCAL hConn, lcQuery, llCancel, lnResult
lcQuery = "EXECUTE LongQuery"
Chapter 6: Extending Remote Views with SQL Pass Through
115
hConn = SQLConnect("odbcpubs", "sa", "")
SQLSetProp(hConn, "ASYNCHRONOUS", .T.)
SET ESCAPE ON
ON ESCAPE llCancel = .T.
WAIT WINDOW "Press Esc to cancel the query" NOWAIT NOCLEAR
llCancel = .F.
lnResult = 0
DO WHILE (!llCancel AND lnResult = 0)
lnResult = SQLExec(hConn, lcQuery)
DOEVENTS
ENDDO
WAIT CLEAR
IF (llCancel AND lnResult = 0)
WAIT WINDOW "Query being cancelled..." NOWAIT NOCLEAR
SQLCancel(hConn)
WAIT WINDOW "Query cancelled by user"
ELSE
IF (lnResult > 0)
WAIT WINDOW "Query completed successfully!"
ELSE
WAIT WINDOW "Query aborted by error"
ENDIF
ENDIF
SQLDisconnect(hConn)
If the user presses the Escape key, the ON ESCAPE mechanism sets the local variable
llCancel to TRUE, terminating the WHILE loop. The next IF statement tests whether the query
was canceled before the results were returned to Visual FoxPro. If so, the SQLCancel()
function is used to terminate the query on the server.
Asynchronous queries are a bit more complicated to code, but they permit the user to
cancel a query, which adds polish to your applications.
Connection properties revisited
Throughout this chapter you have seen connection properties used to configure the behavior of
SQL pass through. Visual FoxPro has two ways to configure default values for connection
properties. The first and perhaps easiest to use is the Remote Data tab of the Options dialog
(see Figure 10). Note that unless the defaults are written to the registry using the Set As
Default button, changes made using the Remote Data tab will affect all new connections but
will only persist until the current Visual FoxPro session is terminated.
SQLSetProp() can also be used to configure connection defaults through the special
connection handle zero (0)—the environment handle. Unlike the Remote Data tab of the
Options dialog, the changes made to the environment handle using SQLSetProp() cannot be
made to persist beyond the current session. However, you can configure connection properties
explicitly using SQLSetProp() as part of your application startup routine.
116
Client/Server Applications with Visual FoxPro and SQL Server
Figure 10. The Remote Data tab of the Options dialog.
Other connection properties
Earlier in this chapter, we explored several connection properties: Batch Mode, Asynchronous
and Transactions. There are many more connection properties. The Help for SQLSetProp() lists
17 different properties, some of which are seldom, if ever, used (for example, ODBChdbc).
However, some are worthy of mention.
The DispLogin property
The DispLogin connection property controls whether Visual FoxPro prompts the user with the
ODBC login dialog. Table 5 lists the possible values and their descriptions.
Table 5. The legal values for the DispLogin connection property.
Numeric value
Constant from Foxpro.h
Description
1 (Default)
DB_PROMPTCOMPLETE
2
DB_PROMPTALWAYS
3
DB_PROMPTNEVER
Visual FoxPro will display the ODBC
connection dialog if any required connection
information is missing.
Client is always prompted with the ODBC
connection dialog.
Client is never prompted.
Chapter 6: Extending Remote Views with SQL Pass Through
117
The following example demonstrates the effect of DispLogin.
‡
The following example will not work correctly if your ODBC DSN is
configured for NT Authentication.
lnResult = SQLSetProp(0, 'DISPLOGIN', 1) && Set to the default value
hConn = SQLConnect("odbcpubs", "bad_user", "")
You should be prompted with the ODBC connection dialog similar to Figure 11.
Figure 11. Visual FoxPro prompting with the ODBC connection dialog due to missing
login information.
If you execute the following code, you’ll get a different result. Visual FoxPro will not
prompt with the ODBC connection dialog, and SQLConnect() will return a –1.
lnResult = SQLSetProp(0, 'DISPLOGIN', 3) && never prompt
hConn = SQLConnect("odbcpubs", "bad_user", "")
It is highly recommended that you always use the “never prompt” option for this property,
as you should handle all logins to the server through your own code instead of this dialog.
The ConnectionTimeOut property
The ConnectionTimeOut property specifies the amount of time (in seconds) that Visual FoxPro
waits while trying to establish a connection with a remote data source. Legal values are 0 to
600. The default is 15 seconds. You may want to adjust this value upward when connecting to a
server across a slow connection.
The QueryTimeOut property
The QueryTimeOut property specifies the amount of time (in seconds) that Visual FoxPro
waits for a query to be processed. Legal values are 0 to 600. The default is 0 seconds (wait
indefinitely). This property could be used as a governor to terminate long-running queries
before they tie up important server resources.
118
Client/Server Applications with Visual FoxPro and SQL Server
The IdleTimeOut property
The IdleTimeOut property specifies the amount of time (in seconds) that Visual FoxPro will
allow a previously active connection to sit idle before being automatically disconnected. The
default is 0 seconds (wait indefinitely).
Remote views vs. SQL pass through
Developers have strong opinions about the usefulness of remote views. Many members of the
Visual FoxPro community feel that remote views carry too much overhead and that the best
performance is achieved by using SQL pass through. In this section, we’ll again use the SQL
Profiler to capture and evaluate the commands submitted to SQL Server.
‡
Refer to the topic “Monitoring with SQL Server Profiler” in the SQL Server
Books Online for more information on using the SQL Server Profiler.
SQL pass through
Figure 12 shows the commands sent to SQL Server for the following query:
lnResult = SQLExec(hConn, ;
"UPDATE authors " + ;
"SET au_lname = 'White' " + ;
"WHERE au_id = '172-32-1176'")
Figure 12. The commands captured by SQL Profiler for a simple UPDATE query.
Notice that nothing special was sent to the server—the query was truly “passed through”
without any intervention by Visual FoxPro. When SQL Server received the query, it proceeded
with the normal process: parse, name resolution, optimize, compile and execute.
Figure 13 shows the commands sent to SQL Server for a query that uses parameters. This
is the same query as before, except this time we’re using parameters in place of the literals
‘White’ and ‘172-32-1176.’
LOCAL lcNewName, lcAuID
lcNewName = "White"
lcAuID = "172-32-1176"
lnResult = SQLExec(hConn, ;
"UPDATE authors " + ;
"SET au_lname = ?lcNewName " + ;
"WHERE au_id = ?lcAuID")
Chapter 6: Extending Remote Views with SQL Pass Through
119
Figure 13. The commands captured by SQL Profiler for the UPDATE query
with parameters.
This time we see that Visual FoxPro (and ODBC) has chosen to pass the query to SQL
Server using the sp_executesql stored procedure.
There is a benefit to using sp_executesql only if the query will be submitted multiple
times and the only variation will be the values of the parameters/search arguments (refer
back to the section “The advantage of parameterization” for a review of the sp_executesql
stored procedure).
Figure 14 shows the commands sent to SQL Server when multiple queries are submitted in
a single call to SQLExec().
lnResult = SQLExec(hConn, ;
"SELECT * FROM authors; " + ;
"SELECT * FROM titles")
Figure 14. The commands captured by SQL Profiler when multiple queries are
submitted in a single call to SQLExec().
As we hoped, Visual FoxPro (and ODBC) has submitted both queries to SQL Server in a
single submission (batch). For the price of one trip to the server, we’ve submitted two queries,
and the only drawback is the funny name that will be given to the results of the second query
(you may want to review the section “Retrieving multiple result sets” for a refresher).
Remote views
Now that we’ve examined SQL pass through, let’s perform the same exercise using remote
views. The code to create the remote view was generated by GENDBC.PRG and is shown here:
CREATE DATABASE MyDBC && A database must be open
CREATE SQL VIEW "V_AUTHORS" ;
REMOTE CONNECT "ODBCPubs" ;
AS SELECT * FROM dbo.authors Authors
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
'View',
'View',
'View',
'View',
'UpdateType', 1)
'WhereType', 3)
'FetchMemo', .T.)
'SendUpdates', .T.)
120
Client/Server Applications with Visual FoxPro and SQL Server
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
DBSetProp('V_AUTHORS',
'View',
'View',
'View',
'View',
'View',
'View',
'View',
'View',
'View',
'View',
'View',
'UseMemoSize', 255)
'FetchSize', 100)
'MaxRecords', -1)
'Tables', 'dbo.authors')
'Prepared', .F.)
'CompareMemo', .T.)
'FetchAsNeeded', .F.)
'FetchSize', 100)
'Comment', "")
'BatchUpdateCount', 1)
'ShareConnection', .F.)
*!* Field Level Properties for V_AUTHORS
* Props for the V_AUTHORS.au_id field.
DBSetProp('V_AUTHORS.au_id', 'Field', 'KeyField', .T.)
DBSetProp('V_AUTHORS.au_id', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.au_id', 'Field', 'UpdateName', 'dbo.authors.au_id')
DBSetProp('V_AUTHORS.au_id', 'Field', 'DataType', "C(11)")
* Props for the V_AUTHORS.au_lname field.
DBSetProp('V_AUTHORS.au_lname', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.au_lname', 'Field', 'Updatable', .T.)
DBSetProp('V_AUTHORS.au_lname', 'Field', 'UpdateName', 'dbo.authors.au_lname')
DBSetProp('V_AUTHORS.au_lname', 'Field', 'DataType', "C(40)")
* Props for the V_AUTHORS.au_fname field.
DBSetProp('V_AUTHORS.au_fname', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.au_fname', 'Field', 'Updatable', .T.)
DBSetProp('V_AUTHORS.au_fname', 'Field', 'UpdateName', 'dbo.authors.au_fname')
DBSetProp('V_AUTHORS.au_fname', 'Field', 'DataType', "C(20)")
* Props for the V_AUTHORS.phone field.
DBSetProp('V_AUTHORS.phone', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.phone', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.phone', 'Field', 'UpdateName', 'dbo.authors.phone')
DBSetProp('V_AUTHORS.phone', 'Field', 'DataType', "C(12)")
* Props for the V_AUTHORS.address field.
DBSetProp('V_AUTHORS.address', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.address', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.address', 'Field', 'UpdateName', 'dbo.authors.address')
DBSetProp('V_AUTHORS.address', 'Field', 'DataType', "C(40)")
* Props for the V_AUTHORS.city field.
DBSetProp('V_AUTHORS.city', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.city', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.city', 'Field', 'UpdateName', 'dbo.authors.city')
DBSetProp('V_AUTHORS.city', 'Field', 'DataType', "C(20)")
* Props for the V_AUTHORS.state field.
DBSetProp('V_AUTHORS.state', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.state', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.state', 'Field', 'UpdateName', 'dbo.authors.state')
DBSetProp('V_AUTHORS.state', 'Field', 'DataType', "C(2)")
* Props for the V_AUTHORS.zip field.
DBSetProp('V_AUTHORS.zip', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.zip', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.zip', 'Field', 'UpdateName', 'dbo.authors.zip')
DBSetProp('V_AUTHORS.zip', 'Field', 'DataType', "C(5)")
* Props for the V_AUTHORS.contract field.
DBSetProp('V_AUTHORS.contract', 'Field', 'KeyField', .F.)
DBSetProp('V_AUTHORS.contract', 'Field', 'Updatable', .F.)
DBSetProp('V_AUTHORS.contract', 'Field', 'UpdateName', 'dbo.authors.contract')
DBSetProp('V_AUTHORS.contract', 'Field', 'DataType', "L")
Chapter 6: Extending Remote Views with SQL Pass Through
121
Figure 15 shows the commands captured by SQL Profiler when the remote view
was opened.
Figure 15. Opening the remote view.
As with the simple SQL pass through query, nothing special is sent to the server.
Figure 16 shows the results of changing a single row and issuing a TABLEUPDATE().
Figure 16. After changing one row and calling TABLEUDPATE().
Just like the parameterized SQL pass through, Visual FoxPro (and ODBC) uses
sp_executesql to make the updates. In fact, modifying multiple rows and issuing a
TABLEUPDATE() results in multiple calls to sp_executesql (see Figure 17).
Figure 17. The commands sent to SQL Server when multiple rows of a remote view
are modified and sent to the server with a single call to TABLEUPDATE(.T.).
This is exactly the situation for which sp_executesql was created. The au_lname column of
the first two rows was modified. SQL Server will be able to reuse the execution plan from the
first query when making the changes for the next, eliminating the work that would have been
done to prepare the execution plan (parse, resolve, optimize and compile) for the second query.
What have we learned? Overall, remote views and SQL pass through cause the same
commands to be sent to the server for roughly the same situations, so the performance should
be similar. Given these facts, the decision to use one over the other must be made based on
other criteria.
Remote views are a wrapper for SQL pass through and, hence, a handholding mechanism
that handles the detection of changes and the generation of the commands to write those
changes back to the data store. Anything that can be done with a remote view can also be done
using SQL pass through—although it may require more work on the part of the developer.
However, the converse is not true. There are commands that can only be submitted using SQL
pass through. Returning multiple result sets is the most obvious example.
Remote views require the presence of a Visual FoxPro database, which might be a piece
of baggage not wanted in a middle-tier component. On the other hand, the simplicity of
122
Client/Server Applications with Visual FoxPro and SQL Server
remote views makes them a very powerful tool, especially when the query is static or has
consistent parameters.
Using remote views and SPT together
In most cases, you don’t have to choose between using remote views vs. SQL pass through.
Combining the two in a single application is a very powerful technique. All the SQL pass
through functions, including SQLExec(), SQLCommit(), SQLRollback(), SQLGetProp() and
SQLSetProp(), can be called for existing connections. So if a connection to the server is
established by a remote view, then you can use the same connection for SQL pass through.
To determine the ODBC connection handle for any remote cursor, use
CURSORGETPROP():
hConn = CURSORGETPROP("ConnectHandle")
In the following example, the previously described v_authors view is opened, and then its
connection is used to query the titles table:
USE v_authors
hConn = CURSORGETPROP("ConnectHandle", "v_authors")
lnResult = SQLExec(hConn, "SELECT * FROM titles")
If your application uses remote views with a shared connection, then by using this
technique you can use a single ODBC connection throughout the application for views and
SQL pass through. The following sections give some brief examples of how combining remote
views with SQL pass through can enhance your applications.
‡
It is impossible to allow views to use a connection handle that was acquired
by a SQL pass through statement. Therefore, to share connections
between views and SQL pass through statements, you must open a view,
acquire its connection, and then share it with your SQL pass through commands.
Transactions
Even if remote views suffice for all your data entry and reporting needs, you will need
SQL pass through for transactions. Transactions are covered in greater detail in Chapter
11, “Transactions.”
Stored procedures
Consider the example of a form that firefighters use to report their activity on fire incidents.
It uses 45 different views, all of which share a single connection, for data entry. However,
determining which firefighters are on duty when the alarm sounds is too complicated for a
view. A stored procedure is executed with SQLExec() to return the primary keys of the
firefighters who are on duty for a particular unit at a specific date and time. The result set is
scanned and the keys are used with a parameterized view that returns necessary data about
each firefighter.
Chapter 6: Extending Remote Views with SQL Pass Through
123
Filter conditions
Suppose you give a user the ability to filter the data being presented in a grid or report. You can
either bring down all the data and then filter the result set, or let the server filter the data by
sending it a WHERE clause specifying the results the user wants. The latter is more efficient at
run time, but how do you implement it? Do you write different parameterized views for each
possible filter condition? Perhaps, if there are only a few. But what if there are 10, 20 or 100
possibilities? Your view DBC would quickly become unmanageable.
We solved this problem by creating a single view that defines the columns in the result set,
but does not include a WHERE clause. The user enters all of his or her filter conditions in a
form, and when the OK button is clicked, all the filter conditions are concatenated into a single,
giant WHERE clause. This WHERE clause is tacked onto the end of the view’s SQL SELECT,
and the resulting query is sent to the back end with SQLExec(). Here’s an example with a
simple WHERE clause looking for a specific datetime:
*-- Open a view and use it as a template
USE myview IN 0 ALIAS template
lnHandle = CURSORGETPROP("ConnectHandle", "template")
*-- Get the SQL SELECT of the template
lcSelect = CURSORGETPROP("SQL", "template")
*-- Create a WHERE clause and add it to the SQL
lcWhere = " WHERE alarmtime = '" + lcSomeDatetime + "'"
lcSelect = lcSelect + lcWhere
*-- Execute the new query
lnSuccess = SQLExec(lnHandle, lcSelect, "mycursor")
You can even make the new result set updatable by simply copying some of the properties
from the updatable view used as a template:
*-- Copy update properties to the new cursor
SELECT mycursor
CURSORSETPROP("Buffering", 5)
CURSORSETPROP("Tables", CURSORGETPROP("Tables", "template"))
CURSORSETPROP("UpdateNameList", CURSORGETPROP("UpdateNameList", "template"))
CURSORSETPROP("UpdatableFieldList", CURSORGETPROP("UpdatableFieldList",
"template"))
CURSORSETPROP("SendUpdates", .T.)
Summary
In this chapter, we explored the capabilities of SQL pass through and how to use it effectively.
The next chapter takes a look at building client/server applications that can scale down as well
as up, allowing you to use a single code base for all your customers.
124
Client/Server Applications with Visual FoxPro and SQL Server
Download