Previous Page TOC Next Page



- 22 -
Using the Remote Data Object


The 32-bit Remote Data Object (RDO) and its associated Remote Data control (RDC) are designed to maximize the performance of 32-bit Visual Basic 4.0 front-ends for client/server RDBMSs. Microsoft aptly characterizes the RDO as "a thin layer over ODBC." The RDO bypasses the Jet 3.0 database engine and relies on SQL passthrough techniques for query processing. What's different about SQL passthrough with the RDO and RDC is that, unlike all Recordset objects created by Jet with SQL passthrough, RDO query result sets can be updatable. The RDC is identical to Visual Basic 4.0's Data control in appearance and functionality, but many of the properties of the RDC are specific to client/server data access through the ODBC API. This chapter begins with examples of the use of the RDO/RDC combination and includes comparisons of the performance of controls bound to the RDC and the Jet 3.0 DAO.



The Remote Data Object and Remote Data control are included with the Enterprise Edition of Visual Basic 4.0. Therefore, you must own the Enterprise Edition to work with the demonstration applications of this chapter. Applications that use the RDO or RDC run only under Windows 95 or Windows NT 3.5+.

The RDO also is intended to take full advantage of the versatility of server-side stored procedures. Jet 3.0's dbPassThrough option of the OpenRecordset method lets you execute simple stored procedures to create a Recordset object or to update table data. If a query or stored procedure returns more than one query result set, Jet 3.0 creates a persistent table for each of the additional result sets. The RDO creates a Recordset equivalent (a temporary rdoResultSet object) for each of the result sets. The RDO also streamlines handling of parameters passed to stored procedures. This chapter also shows you how to take advantage of the more sophisticated features of the RDO, such as rdoPreparedStatements.

Using the Remote Data Control with Bound Controls


The quickest way to evaluate the performance of the Remote Data Object is to use the RDO/RDC combination with a sample database. The examples in the following sections, which compare the 32-bit DAO/Data control and the RDO/RDC combination, are derived from the applications of Chapter 20, "Creating Front-Ends for Client/Server Databases." To use the sample applications, you must first create the Northwind 32-bit ODBC data source by exporting the Northwind.mdb sample database of Access 95 to the Northwind database of Microsoft SQL Server or another client/server RDBMS having a 32-bit ODBC driver. The "Adding the Northwind Database as an ODBC Data Source" and "Exporting Tables from Access 95 to the ODBC Data Source" sections of Chapter 20 describe how to create the Northwind data source.



You must add the Microsoft Remote Data control (MSRDC32.OCX) to the 32-bit Visual Basic toolbox in order to create your own RDO applications. Although the Microsoft Remote Data Object 1.0 (MSRDO32.DLL) appears in the list of the Custom Control dialog's Available Controls, you don't add a reference to MSRDO32.DLL from the Custom Control dialog. (You receive a C:\Win95\System\MSRDO32.DLL could not be loaded message if you try.) To bind the RDO to applications that don't rely on the RDC exclusively, use the References dialog to create a reference to MSRDO32.DLL. The RDO and Jet DAO libraries coexist peacefully.


Connection Properties of the Remote Data Control


You can use the built-in properties sheet of the RDO or Visual Basic's Properties window to set up the connection to your ODBC data source. Figure 22.1 shows the Remote Data Control Properties sheet with settings for the default system administrator (sa) to establish a connection to the SQL Server northwind database with the Northwind data source. In addition to the data source name (DSN), user ID (UID), and password (PWD), you must supply an SQL statement as the value of the SQL property of the RDC. The SQL property of the RDC corresponds to the RecordSource property of the Data control, but you cannot supply only a table name as the value of the SQL property; a valid SQL statement is required.

Figure 22.1. Setting the connection-related properties of the Remote Data control.



Using the Remote Data Control Properties sheet sets values for the DataSourceName, Password, and UserName properties. Alternatively, you can substitute a conventional ODBC connect string (ODBC;DSN=DataSourceName;UID=UserName;PWD=Password) as the value of the ConnectString property.

For initial trials with the RDO/RDC combination, all but one of the default property values of the RDC are adequate. To avoid the appearance of the SQL Server Login dialog, set in Visual Basic's Properties window the value of the Prompt property to 1 - No Prompt. The RDO's Resultset property corresponds to the Recordset property of the Data control. A detailed comparison of the object hierarchies of the DAO and the RDO appears in the "Exploring the Properties and Methods of the RDO" section, later in this chapter.

Comparing RDO and DAO Performance with a DBGrid Control


Chapter 20's Rem_locl.vbp project compared the performance of a local copy of Northwind.mdb with that of server tables linked to a Jet 3.0 database. The RDO_DAO.vbp project in the \DDG_VB4\32_bit\Chaptr22 folder of the accompanying CD-ROM uses the Northwind ODBC data source for both the conventional Data control (dtcDAO) and the Remote Data control (rdcRDO). When you open frmRDO_DAO, each of the controls establishes a connection to the Northwind ODBC data source. You can use Move Last and Scroll buttons for each DBGrid control to compare the relative performance of a DBGrid control with the DAO/Data control or the RDO/RDC combination. Figure 22.2 illustrates the times (in seconds) for the first MoveLast operation after opening the form and the times required to scroll through the 1,000+ records of the Order Details table by invoking the MoveNext method. Figure 22.2 also demonstrates that both the DAO's Recordset object of the Dynaset type and the DTC's Resultset object are updatable; the tentative append record appears in the DBGrid control after the last record.

Figure 22.2. The RDO_DAO form used to compare the performance of the Jet 3.0 DAO/Data control and the RDO 1.0/RDC combination.

The times shown in Figure 22.2 indicate that the RDO/RDC needs only about 70 percent of the time required by the DAO/Data control to traverse the Resultset and Recordset, respectively, by the MoveNext method. Table 22.1 lists typical times for the following three conditions:


Table 22.1. Time in seconds to traverse a DAO Recordset and a RDO Resultset with the Data control and RDC, respectively.

Type of Test DAO/DTC Move Last RDO/RDC Move Last DAO/DTC Scroll RDO/RDC Scroll
DBGrid 1.59 1.21 55.97 39.32
No Data 1.82 1.71 4.72 5.17
Data to Variables 1.32 2.25 58.93 7.96


For consistency between the comparative timing tests, the Connect property value of the Data control and the RDC is ODBC;DSN=Northwind;UID=sa;PWD= in order to open the ODBC data source directly. Using an ODBC table attached to a Jet 3.0 database with the Data control gives results similar to those shown for the DAO/DTC combination in Table 22.1.

Listing 22.1 shows the code used to perform the comparative timing tests, the outcome of which appears in Table 22.1.

Listing 22.1. Code to compare the performance of the DAO/Data control and the RDO/RDC combination with and without bound DBGrid controls.




Option Explicit



Dim sngTime As Single



Dim lngOrderID As Long



Dim intProductID As Integer



Dim curUnitPrice As Currency



Const fUseVariables = False



'Note: Delete DataSource property value when setting True



Private Sub cmdDAOMoveLast_Click()



   'DAO/Data control test



   dtcDAO.Recordset.MoveFirst



   DoEvents



   txtDAOTime.Text = 0



   If fUseVariables Then



      lngOrderID = dtcDAO.Recordset.Fields(0)



      intProductID = dtcDAO.Recordset.Fields(1)



      curUnitPrice = dtcDAO.Recordset.Fields(2)



   End If



   sngTime = Timer



   dtcDAO.Recordset.MoveLast



   If fUseVariables Then



      lngOrderID = dtcDAO.Recordset.Fields(0)



      intProductID = dtcDAO.Recordset.Fields(1)



      curUnitPrice = dtcDAO.Recordset.Fields(2)



   End If



   txtDAOTime.Text = Format(Timer - sngTime, "0.00")



End Sub



Private Sub cmdDAOScroll_Click()



   'DAO/Data control test



   dtcDAO.Recordset.MoveFirst



   txtDAOScroll.Text = 0



   sngTime = Timer



   Do Until dtcDAO.Recordset.EOF



      If fUseVariables Then



         lngOrderID = dtcDAO.Recordset.Fields(0)



         intProductID = dtcDAO.Recordset.Fields(1)



         curUnitPrice = dtcDAO.Recordset.Fields(2)



      End If



      dtcDAO.Recordset.MoveNext



      'DoEvents



   Loop



   txtDAOScroll.Text = Format(Timer - sngTime, "0.00")



End Sub



Private Sub cmdRDCMoveLast_Click()



   'RDO/RDC test



   rdcRDO.Resultset.MoveFirst



   DoEvents



   txtRDCTime.Text = 0



   sngTime = Timer



   If fUseVariables Then



      lngOrderID = rdcRDO.Resultset.rdoColumns(0)



      intProductID = rdcRDO.Resultset.rdoColumns(1)



      curUnitPrice = rdcRDO.Resultset.rdoColumns(2)



   End If



   rdcRDO.Resultset.MoveLast



   If fUseVariables Then



      lngOrderID = rdcRDO.Resultset.rdoColumns(0)



      intProductID = rdcRDO.Resultset.rdoColumns(1)



      curUnitPrice = rdcRDO.Resultset.rdoColumns(2)



   End If



   txtRDCTime.Text = Format(Timer - sngTime, "0.00")



End Sub



Private Sub cmdRDCScroll_Click()



   'RDO/RDC test



   rdcRDO.Resultset.MoveFirst



   DoEvents



   txtRDCScroll.Text = 0



   sngTime = Timer



   Do Until rdcRDO.Resultset.EOF



      If fUseVariables Then



         lngOrderID = rdcRDO.Resultset.rdoColumns(0)



         intProductID = rdcRDO.Resultset.rdoColumns(1)



         curUnitPrice = rdcRDO.Resultset.rdoColumns(2)



      End If



      rdcRDO.Resultset.MoveNext



      'DoEvents



   Loop



   txtRDCScroll.Text = Format(Timer - sngTime, "0.00")



End Sub

Using the RDC with a Picklist Form


The Remote Data Object and its Remote Data control offer substantially better performance than Jet 3.0 and the Data control in typical, multi-query decision-support applications. The Picklist project developed in Chapter 20 is easily adapted to a project that compares the performance of the DAO/Data control and RDO/RDC combinations. The PickTest.vbp project and its PickTest.frm are located on the accompanying CD-ROM in the \DDG_VB4\32_bit\Chaptr22 folder.

Figure 22.3 shows the frmPick_DAO form that uses Data controls to provide the RecordSource for the DBCombo (dbcCustomers) and two DBGrid controls (dbgOrders and dbgOrderDetails). Figure 22.4 shows frmPick_RDO, a clone of frmPick_DAO that substitutes RDCs for Jet 3.0 Data controls. The two forms each include a pair of text boxes that report the time required to display all of the orders placed by the customer selected in the DBCombo control and the line items that comprise each order. Table 22.2 lists the comparative times to populate the two DBGrid controls.



Using the rdcCustomers control to position the record pointer of the Recordset over the Customers table results in an error when you query a case-sensitive SQL Server. There is a bug in the first release of the RDC that sends table names to SQL server as an all-lowercase string. You do not encounter this problem when repositioning the record pointer with the rdcOrders or rdcOrderDetails controls. This problem does not occur when using Oracle RDBMSs.

Figure 22.3. The frmPick_DAO form uses Data controls to populate a DBCombo control and two DBGrid controls.

Figure 22.4. Clicking the RDO 1.0 button of the frmPick_DAO form shows the frmPick_RDO form that replaces Data controls with RDCs.

Table 22.2. Time in seconds and ratio of time to display order information using Data controls and RDCs.

Operation DAO/DTC RDO/RDC Ratio
Find All Orders 1.598 0.277 5.7:1
Find Details 0.871 0.168 5.2:1


The times shown in Table 22.2 are for the single instance of the comparison illustrated by Figures 22.3 and 22.4. The average times for multiple comparisons do not differ substantially from the typical elapsed times shown in Table 22.2. Changing the DTC's ResultsetType property value from Keyset (1), the default, to Static (3) provides slightly shorter elapsed times.

The VBA code and SQL statements to populate the two DBGrid controls from queries against the Orders and Order_Details tables appear in Listing 22.2. The only significant difference in the code for use with Data controls and RDCs is the substitution of the RDC's SQL property for the Data control's RecordSource property. Unlike a DBCombo control bound to a Data control, you need to activate the dbcCustomers combo box to display the initial member of the underlying Recordset. The Form_Activate subprocedure uses SendKeys to open the dbcCustomers combo box with the {F4} argument. The RDC accommodates invoking the Refresh method on a Recordset having zero rows; the Data control triggers a trappable error when Refresh is applied to a Recordset with zero rows. You can compare the behavior of the Data control and RDC with a zero-row Recordset by selecting FISSA in the dbcCustomer control.

Listing 22.2. VBA code to determine the comparative performance of a more complex set of RDCs and bound controls.




Option Explicit



Private Sub dbcCustomers_Change()



  'Find latest orders for CustomerID picked in combo box



   Dim strSQL As String



   Dim sngTime As Single



   txtFindOrders.Text = ""



   sngTime = Timer



   strSQL = "SELECT OrderID, OrderDate AS Ordered, "



   strSQL = strSQL & "ShippedDate AS Shipped FROM Orders "



   strSQL = strSQL & "WHERE CustomerID = '"



   strSQL = strSQL & dbcCustomers.BoundText



   strSQL = strSQL & "' ORDER BY OrderDate DESC"



   rdcOrders.SQL = strSQL



   rdcOrders.Refresh



   'DoEvents   'DoEvents causes strange behavior of the dbcCustomer



   txtFindOrders.Text = Format(Timer - sngTime, "0.000")



   If rdcOrders.Resultset.RowCount = 0 Then



      MsgBox "No orders found for '" & dbcCustomers.BoundText & "'."



      Exit Sub



   Else



      Call rdcOrders_Reposition



   End If



End Sub



Private Sub rdcOrders_Reposition()



  'Find detail records for order number clicked in grid



   Dim strSQL As String



   Dim sngTime As Single



   txtFindDetails.Text = ""



   sngTime = Timer



   strSQL = "SELECT Order_Details.Quantity AS Quan, "



   strSQL = strSQL & "Products.ProductName AS Product, "



   strSQL = strSQL & "Order_Details.UnitPrice AS Price "



   strSQL = strSQL & "FROM Order_Details, Products "



   strSQL = strSQL & "WHERE OrderID = " & rdcOrders.Resultset(0)



   strSQL = strSQL & " AND Products.ProductID = "



   strSQL = strSQL & "Order_Details.ProductID"



   rdcOrderDetails.SQL = strSQL



   rdcOrderDetails.Refresh



   txtFindDetails.Text = Format(Timer - sngTime, "0.000")



End Sub



Private Sub cmdJet_Click()



   'Show the DAO form



   Me.Hide



   frmPick_DAO.Show



End Sub



Private Sub Form_Load()



   Me.Top = (Screen.Height - Me.Height) / 2



   Me.Left = (Screen.Width - Me.Width) / 2



End Sub



Private Sub Form_Activate()



   'Select the first entry in the customers list



   dbcCustomers.SetFocus



   SendKeys "{F4}{Enter}", True



   dbgOrders.SetFocus



End Sub

Exploring the Properties and Methods of the RDO


DBGrid and DBCombo controls bound to RDCs are useful for testing the performance of the RDO and quickly prototyping high-performance Visual Basic client/server front-ends. Using unbound controls and manipulating the RDO with VBA code, however, is a much better approach for production decision support and online transaction-processing applications. Writing code to manage the RDO offers better control of connections to the RDBMS and lets you take advantage of prepared statements and server stored procedures to speed the processing of SELECT, INSERT, UPDATE, and DELETE queries. The sections that follow compare the object structure of RDO 1.0 and Jet 3.0's DAO and describe the properties and methods that are unique to the RDO.



You can create a reference to and use the Microsoft Remote Data Object 1.0 in any 32-bit application that supports VBA. As an example, it is possible to use the RDO with Microsoft Access 95, Excel 95, and Project 4.1. Redistribution of the RDO library (Msrdo32.dll), however, is restricted to use in conjunction with 32-bit runtime Visual Basic 4.0 applications. If you want to use the RDO in a commercial application other than Visual Basic 4.0, contact Microsoft for licensing requirements.


The Object Hierarchy of the RDO


The rdoEngine object, which is modeled on Jet 3.0's DBEngine object, has only a total of eight different objects, each of which is contained within a corresponding collection. Figure 22.5 shows the relatively simple object hierarchy of the RDO. RDO uses query result set column and row terminology in place of Jet's references to fields and records. The rdoColumns collection, which is the default collection for rdoPreparedStatement, rdoTable, and rdoResultset objects, has identical properties and methods for each of these three object types. Value is the default property for rdoColumn objects.

Figure 22.5. The object hierarchy of the Remote Data Object 1.0.

The primary collections below the rdoEngine object are comparable to objects of the Jet 3.0 DAO, as illustrated by Figure 22.6. Like DAO's Workspaces collection, each member of RDO's rdoEnvironments collection supports an isolated transaction space to allow multiple, simultaneous transactions. The rdoEnvironment object is defined by an ODBC environment (hEnv) handle, accessed by the hEnv property. Each member of the rdoConnections collection represents a single connection within an rdoEnvironment object and the corresponding ODBC database connection (hDbc) handle. You apply the OpenResultset method to a rdoConnection object in the same way you invoke the OpenRecordset method of Jet's Database object. DAO Recordsets and rdoResultsets have a superficial similarity; rdoResultset objects, however, offer a variety of new features that make SQL passthrough much more flexible, especially if you've upgraded to SQL Server 6+. Unlike many DAO collections, members of RDO collections are not persistent in the long term and cannot be saved as named objects in a database.

Figure 22.6. Correspondence of the object collections of the Jet 3.0 DAO and RDO 1.0.



Several objects in the RDO hierarchy, such as rdoEnvironment, rdoConnection, and rdoPreparedStatement objects, exhibit short-term persistence. Short-term persistence is defined as the lifetime of an instance of a compiled application or an instance of Visual Basic in design/run mode. Short-term persistence is discussed in the "Opening a Connection and Applying the OpenResultset Method" and "Using the rdoPreparedStatement Object with Parameters" sections, later in this chapter.


Properties of the RDC Shared with RDO Objects


Most Visual Basic developers first are likely to use the RDC to explore the features of the RDO, then write VBA code to implement RDO operations.(See Table 22.3.)

Table 22.3. Properties of the RDC and RDO that are not duplicated by the Jet 3.0 Data control and DAO.

Property/Object Purpose
CursorDriver
rdoEnvironment
Specifies whether to use engine-driven (server-side) or ODBC cursors. SQL Server 6.0's engine-driven cursors provide better performance with large rdoResultset objects. The default (rdUseIfNeeded) uses server-side cursors if the RDBMS supports them.
DataSourceName
RDC only
The name of the ODBC data source. Not required if you use the Connect property of the RDC to specify the DSN= value.
ErrorThreshold
rdoPrepared-Statement
Specifies the level of error severity that causes a trappable error, if the ODBC driver supports this feature. (The 32-bit ODBC 2.5 SQL Server driver supports error severity.) All errors, regardless of severity, appear as members of the rdoErrors collection. The default (-1) causes all errors to be trapped.
KeysetSize
rdoPrepared-Statement
The default (0) specifies keyset-driven cursors only. Values greater than the RowsetSize value create mixed cursors, which are not supported by ODBC 2.5 but are expected to be added to the forthcoming ODBC 3.0. (Setting KeysetSize to a value other than 0 generates an error with ODBC 2.5.)
LockType
rdoPrepared-Statement
Determines if the cursor is read-only (rdReadOnly, 1) or uses pessimistic (rdConcurLock, 2), optimistic row-ID (rdConcurRowVer, 3), or optimistic row-value (rdConcurValues, 4) locking.
LogMessagesrdo Prepared-Statement A "True" (string) value causes ODBC messages to be logged to a text file; an empty string turns off logging.
MaxRows rdoPrepared-Statement Specifies the maximum number of rows of a rdoResultset, regardless of the number of rows generated by a query. The default value (-1) returns all rows.
Options RDC only query; the StillExecuting property returns True while the query is executing. The QueryCompleted event triggers after the first rdoResultset is returned.
Password rdoEnvironment A string entry specifies the value of the Password of the current rdoEnvironment object (which is not required if you use the Connect property to specify the PWD= value). The rdoDefaultPassword property of the rdoEngine object sets the password for all rdoEnvironment objects if no PWD= value is provided in the connect string. (Both properties are write-only for security purposes.)
Prompt RDC only Determines the behavior of the ODBC administrative layer. If you supply all required information to the driver manager, using the rdDriverNoPrompt value (1) completes the connection without displaying a dialog. Several other prompt options are provided that require entry of user ID and password values.
QueryTimeout rdoPrepared-Statement, rdoConnection Specifies the time in seconds beyond which an error occurs if the query has not begun executing.
ResultsetType RDC only Specifies the type of cursor. A keyset cursor (rdOpenKeyset, 1) is unique in that updates made to underlying tables by other users appear in the rdoResultset as pairs of "holes" (empty rows) and new rows. A static cursor (rdOpenStatic, 3) is similar to a DAO Recordset of the Snapshot type, except that the rdoResultset is updatable; changes made by others to the underlying tables don't appear in a static rdoResultset. A dynamic cursor (rdOpenDynamic, 2) is equivalent to a DAO Dynaset; changes made by others appear in the data. Omitting the ResultsetType value or setting the value to 0 (rdOpenForwardOnly) specifies a forward-only rdoResultset object. The RDC is restricted to keyset or forward-only cursors.
RowsetSize rdoPrepared- Statement Specifies the number of rows of data in the cursor that are returned to the rdoResultset object as a result of cursor navigation operations. The default value is 100.
SQL rdoPrepared-Statement member of the rdoPreparedStatements collection. If you bind the RDC to a control, you ordinarily supply a conventional SQL SELECT statement.
UserName rdoEnvironment The user ID for logging into the database of the specified data source, a property of the current rdoEnvironment object (not required if you use the Connect property to specify the UID= value).


The LockType property for an rdoResultset object is implemented by the intLockType argument of the OpenResultset method. Similarly, the ResultsetType property of the RDC is implemented by OpenResultset's intType argument, and the Options property value is set by the intOption argument. The SQL property of the RDC is implemented by the strName property of the OpenResultset method applied to a rdoConnection object. The Connect and Prompt properties of the RDC are equivalent to the strConnect and intPrompt arguments, respectively, of the OpenConnection method of the rdoEnvironment object.


Opening a Connection and Applying the OpenResultset Method


The formal method for opening a connection is to create an new rdoEnvironment object and then open a rdoConnection within the new environment with the following two statements:




Set envEnvironment = rdoCreateEnvironment(strName, _



strUserID, strPassword)



Set conConnect = envEnvironment.OpenConnection(strName _



[, intPrompt[, blnReadonly[, strConnect]]])

The strName argument lets you create persistent rdoEnvironment and rdoConnection objects that you can refer to by strName, rather than by ordinal position in the rdoEnvironments and rdoConnections collections, respectively. If you provide a logon dialog that supplies the values of the strUserID and strPassword arguments to rdoCreateEnvironment, use rdDriverNoPrompt as the value of the intPrompt argument of the OpenConnection method. Setting the blnReadonly argument to True speeds execution of queries, but you cannot execute action queries over a read-only connection. You need only supply DSN=Datasource as the value of the strConnect argument if you provide the strUserID and strPassword values when you create a new user environment. (You must add ;DATABASE=DatabaseName to the connect string if your ODBC data source doesn't specify a particular database.) The easiest way to gain familiarity with rdoEnvironment and rdoConnect objects is to use the Object Browser to explore the objects' property values and methods (see Figure 22.7).

Figure 22.7. Using the Object Browser to display the syntax of the OpenResultset method of the rdoConnection object.

Alternatively, you can use Default_Environment (rdoEnvironments(0)), which is automatically provided by the rdoEngine object, with the following single instruction:




Set conConnect = rdoEngine.Environments(0).OpenConnection(strDSN [, _




intPrompt[, blnReadonly[, strConnect]]])

You can use the preceding single-line instruction if you don't need to isolate transactions within multiple environments. In this case, add ;UID=Username;PWD=Password to the DSN=Datasource connect string.



The RDO is designed for use with ODBC connections only, so don't include ODBC; at the beginning of strConnect. RDO doesn't support the ODBC; connect string prefix.

The OpenResultset method of the rdoConnection object offers the following two syntax alternatives:




Set rslResultset = conConnect.OpenResultset(strName[, intType[, _



   intLockType[, intOption]]])



Set rslResultset = {tblTable|prsPrepStatement}.OpenResultset([intType [, _



   intLocktype[, intOption]]])

Use the first of the preceding instructions with a SQL statement as the value of the strName argument. The Name property of the rdoResultset object is derived from the first 255 characters of the SQL statement. The intType argument specifies the type of cursor, as described in the ResultsetType item of Table 22.3. Permissible values of the intLockType argument appear in Table 22.3's LockType item. Use the second OpenResultset instruction to create a rdoResultset object over a rdoTable object or a previously created rdoPreparedStatement object.



The rdoTable object corresponds to the Jet 3.0 DAO's TableDef object. When you establish a connection to a data source, the rdoTables collection automatically is populated with a rdoTable object for each table of the database.


Creating a Simple Decision-Support Front-End with the RDO


One of the primary objectives of the RDO is to improve the performance of client/server front-end applications. Although DBCombo and DBGrid controls bound to RDCs are faster in operation than the same controls bound to Data controls, unbound ComboBox and Grid controls usually offer better performance. In addition, the unbound Grid control provides a degree of content flexibility not offered by the DBGrid control. The sections that follow describe a modification of the PickTest decision-support front-end that uses the RDO and substitutes unbound for bound controls. The RDO_Test.vbp project and its associated frmLogOn and frmRDO_Test forms are located in the \DDG_VB4\32_bit\Chaptr22 folder of the accompanying CD-ROM.

Adding a Logon Dialog to Open a rdoConnection


It's necessary in most cases to provide a user ID and password to log users onto the server RDBMS. Figure 22.8 shows the simple Logon to SQL Server dialog (frmLogOn) that requests a user ID and password to provide values to the strUserID and strPassword of the rdoCreateEnvironment method. Listing 22.3 shows the VBA code that creates the user's rdoEnvironment and rdoConnection objects. The connection is opened for read-only access, which improves overall performance by a small margin.

Figure 22.8. A simple logon dialog to provide user ID and password values to a rdoEnvironment or rdoConnection object.



If your DBA uses Windows NT Server's domain-managed security (integrated or mixed security), which uses your Windows NT logon ID and password for SQL Server access, you assign empty strings for the user ID and password (strUserID = "" and strPassword = "", or DSN=Datasource;UID=;PWD=). In this case, you don't need a logon dialog, and you create the rdoEnvironment and rdoConnection objects in the Form_Load or Form_Activate event-handling subprocedures.

Listing 22.3. VBA code for a simple logon dialog to create rdoEnvironment and rdoConnection objects for the user.




Option Explicit



Private Sub Form_Load()



   'Position the logon dialog



   Me.Top = (Screen.Height - Me.Height) / 3



   Me.Left = (Screen.Width - Me.Width) / 2



End Sub



Private Sub cmdOK_Click()



   'Attempt to create a user environment and connection



   Dim strConn As String



   strConn = "DSN=Northwind"



   On Error GoTo LogOnError



   'Create an environment (hEnv) for the user



   Set envUser = rdoCreateEnvironment("User", _



      txtUID.Text, txtPWD.Text)



   On Error GoTo ConnectError



   'Create a read-only connection (hDbc) for the user



   Set conUser = envUser.OpenConnection("Northwind", _



      rdDriverNoPrompt, True, strConn)



   Unload Me



   frmRDO_Test.Show



   Exit Sub



LogOnError:



   MsgBox Error$, vbExclamation, "Log On Error"



   If rdoEngine.rdoEnvironments.Count > 1 Then



      rdoEngine.rdoEnvironments("User").Close



   End If



   Exit Sub



ConnectError:



   MsgBox Error$, vbExclamation, "Connect Error"



   If rdoEngine.rdoEnvironments.Count > 1 Then



      rdoEngine.rdoEnvironments("User").Close



   End If



   Exit Sub



End Sub



Private Sub cmdCancel_Click()



   'Close the user's environment and connection, if created



   If rdoEngine.rdoEnvironments.Count > 1 Then



      If rdoEngine.rdoEnvironments("User") _



            .rdoConnections.Count > 0 Then



         rdoEngine.rdoEnvironments("User") _



            .rdoConnections("Northwind").Close



      End If



      rdoEngine.rdoEnvironments("User").Close



   End If



   End



End Sub


You must close named environments and named connections during development because named objects are persistent for the current instance of Visual Basic. As an example, if you don't close the User environment when canceling a logon, the next time you attempt to log on, you receive an Object with name 'User' exists in collection message.


Populating a Combo Box and Grid Controls from rdoResultSet Objects


Here are two of the guiding principles of client/server front-end development:

Unlike the RDC, using RDO code gives you precise control over connection management. The frmRDO_Test form uses unbound ComboBox and Grid controls, in conjunction with arrays that store undisplayed values, to provide a noticeably faster response time than the PickTest application that uses RDCs and bound controls. Another advantage of the frmRDO_Test form is that it opens faster than frmPickTest. The improved performance of frmRDO_Test is due to use of fewer and relatively "lightweight" unbound controls, plus a reduction in the network traffic necessary to fill the controls. Figure 22.9 shows typical times to populate the cboCustomers ComboBox control, and the grdOrders and grdDetails Grid controls. The addition of a calculated column for extended unit prices and rows to display Subtotal, Freight, and Order Total values demonstrates the added versatility of Grid controls in decision-support applications.

Figure 22.9. The frmRDO_Test form displaying typical times to execute rdoResultset queries.



The initial times to open the frmRDO_Test form and load the cboCustomers combo box, as well as to populate the two Grid controls depends on the time required to establish a connection to the server, plus server data caching that occurs in conjunction with the first execution of the query set. You can use the Refresh button to recreate the rdoResultset objects without the need to reopen the connection to the server.

Listing 22.4 shows the code required by frmRDO_Test to emulate the operation of frmPickList, plus display the time required to load the cboCustomers combo box. The Form_Load event handler formats the two Grid controls, and the Form_Activate event handler contains the code to populate the combo box with the list of all customer names. Unlike DBCombo controls, you can't specify separate fields for values in the list and in the BoundText property. Thus, the astrCustID array is populated with CustomerID values corresponding to the customer names in the combo box. When you select a customer in cboCustomers, the cboCustomers_Click event-handler populates the grdOrders grid and the asngFreight array that stores values of the Freight field for the selected orders, in a manner similar to populating astrCustID. Clicking a row of the grdOrders control returns the detail records for the selected order and computes order summary amounts.

Listing 22.4. VBA code to populate unbound controls with data from multiple rdoResultset objects.




Option Explicit



Dim astrCustID() As String       'CustomerID array



Dim asngFreight() As Single      'Freight charge array



Dim rslCustomers As rdoResultset 'Fills customers combo



Dim rslOrders As rdoResultset    'Fills orders grid



Dim rslDetails As rdoResultset   'Fills order details grid



Dim fInProcess As Boolean        'Don't process details flag



Private Sub Form_Load()



   'Set up grid headings and formatting



   grdOrders.Row = 0



   grdOrders.Col = 0



   grdOrders.Text = "Order ID"



   grdOrders.Col = 1



   grdOrders.Text = "Ordered"



   grdOrders.Col = 2



   grdOrders.Text = "Shipped"



   grdOrders.ColWidth(0) = 600



   grdOrders.ColWidth(1) = 720



   grdOrders.ColWidth(2) = 720



   grdOrders.ColAlignment(1) = 1



   grdOrders.ColAlignment(2) = 1



   grdDetails.Row = 0



   grdDetails.Col = 0



   grdDetails.Text = "Quan"



   grdDetails.Col = 1



   grdDetails.Text = "Product"



   grdDetails.Col = 2



   grdDetails.Text = "Price"



   grdDetails.Col = 3



   grdDetails.Text = "Exten."



   grdDetails.ColWidth(0) = 440



   grdDetails.ColWidth(1) = 1440



   grdDetails.ColWidth(2) = 600



   grdDetails.ColWidth(3) = 750



   grdDetails.ColAlignment(0) = 1



   grdDetails.ColAlignment(2) = 1



   grdDetails.ColAlignment(3) = 1



   Me.Top = (Screen.Height - Me.Height) / 2



   Me.Left = (Screen.Width - Me.Width) / 2



End Sub



Private Sub Form_Activate()



   'Load the combo with CompanyNames



   Dim sngTime As Single



   Dim strSQL As String



   sngTime = Timer



   strSQL = "SELECT CustomerID, CompanyName FROM Customers"



   Set rslCustomers = conUser.OpenResultset(strSQL, rdOpenStatic)



   'Get RowCount from resultset



   rslCustomers.MoveLast



   ReDim astrCustID(rslCustomers.RowCount)



   rslCustomers.MoveFirst



   'Required for refresh operation



   cboCustomers.Clear



   'Add the combo box items and load the CustomerID array



   Do Until rslCustomers.EOF



      cboCustomers.AddItem rslCustomers(1)



      astrCustID(cboCustomers.ListCount - 1) = rslCustomers(0)



      rslCustomers.MoveNext



   Loop



   'Load the first customer into the combo box



   cboCustomers.ListIndex = 0



   rslCustomers.Close



   Set rslCustomers = Nothing



   txtCustTime.Text = Format(Timer - sngTime, "0.000")



End Sub



Private Sub cboCustomers_Click()



  'Find latest orders for CustomerID picked in combo box



   fInProcess = True



   Dim strSQL As String



   Dim intCol As Integer



   Dim sngTime As Single



   txtOrdersTime.Text = ""



   sngTime = Timer



   strSQL = "SELECT OrderID, OrderDate, "



   strSQL = strSQL & "ShippedDate, Freight FROM Orders "



   strSQL = strSQL & "WHERE CustomerID = '"



   strSQL = strSQL & astrCustID(cboCustomers.ListIndex)



   strSQL = strSQL & "' ORDER BY OrderDate DESC"



   Set rslOrders = conUser.OpenResultset(strSQL, rdOpenStatic)



   If rslOrders.RowCount = 0 Then



      grdOrders.Rows = 1



      grdDetails.Rows = 1



      MsgBox "No orders found for '" & _



         astrCustID(cboCustomers.ListIndex) & "'."



      Exit Sub



   ElseIf rslOrders.RowCount > 13 Then



      'Provide space for vertical scroll bar



      grdOrders.Width = 2360



   Else



      grdOrders.Width = 2108



   End If



   'Move last to obtain valid RowCount



   rslOrders.MoveLast



   grdOrders.Rows = rslOrders.RowCount + 1



   ReDim asngFreight(grdOrders.Rows)



   rslOrders.MoveFirst



   grdOrders.Row = 0



   Do Until rslOrders.EOF



      grdOrders.Row = grdOrders.Row + 1



      For intCol = 0 To 2



         grdOrders.Col = intCol



         'Grid won't accept Null ShippedDate value



         If Not IsNull(rslOrders(intCol)) Then



            grdOrders.Text = rslOrders(intCol)



         End If



      Next intCol



      'Store freight charge in array



      asngFreight(grdOrders.Row) = rslOrders(3)



      rslOrders.MoveNext



   Loop



   'Formal method of closing rdoResultset



   rslOrders.Close



   Set rslOrders = Nothing



   txtOrdersTime.Text = Format(Timer - sngTime, "0.000")



   'Set the selection to the first order



   grdOrders.SelStartRow = 1



   grdOrders.SelStartCol = 0



   grdOrders.SelEndRow = 1



   grdOrders.SelEndCol = 0



   fInProcess = False



   Call grdOrders_SelChange



End Sub



Private Sub grdOrders_SelChange()



   'Find detail records for order number clicked in grid



   If fInProcess Then



      'Don't process during loading operation



      Exit Sub



   End If



   Dim strSQL As String



   Dim intCol As Integer



   Dim sngTotal As Single



   Dim sngTime As Single



   sngTime = Timer



   grdOrders.Col = 0



   txtDetailsTime.Text = ""



   'Get details for selected order



   strSQL = "SELECT Order_Details.Quantity, "



   strSQL = strSQL & "Products.ProductName, "



   strSQL = strSQL & "Order_Details.UnitPrice "



   strSQL = strSQL & "FROM Order_Details, Products "



   strSQL = strSQL & "WHERE OrderID = " & grdOrders.Text



   strSQL = strSQL & " AND Products.ProductID = "



   strSQL = strSQL & "Order_Details.ProductID"



   Set rslDetails = conUser.OpenResultset(strSQL, rdOpenStatic)



   rslDetails.MoveLast



   'Clear the detail grid, but save fixed row backcolor



   grdDetails.Rows = 2



   'Extra rows are for Subtotal, freight, and order total



   grdDetails.Rows = rslDetails.RowCount + 4



   rslDetails.MoveFirst



   grdDetails.Row = 0



   'Load the details grid and calculate order subtotal



   Do Until rslDetails.EOF



      grdDetails.Row = grdDetails.Row + 1



      For intCol = 0 To 3



         grdDetails.Col = intCol



         If intCol = 2 Then



            'Format unit price currency column



            If grdDetails.Row = 1 Then



               grdDetails.Text = Format(rslDetails(2), "$0.00")



            Else



               grdDetails.Text = Format(rslDetails(2), "0.00")



            End If



         ElseIf intCol = 3 Then



            'Format extended amount column



            If grdDetails.Row = 1 Then



               grdDetails.Text = Format(rslDetails(2) * _



                  rslDetails(0), "$0.00")



            Else



               grdDetails.Text = Format(rslDetails(2) * _



                  rslDetails(0), "0.00")



            End If



         Else



            grdDetails.Text = rslDetails(intCol) & " "



         End If



      Next intCol



      'Accumulate subtotal



      sngTotal = sngTotal + (rslDetails(2) * rslDetails(0))



      rslDetails.MoveNext



   Loop



   'Add subtotal, freight, and order total to detail grid



   grdDetails.Row = grdDetails.Row + 1



   grdDetails.Col = 1



   grdDetails.Text = "Subtotal"



   grdDetails.Col = 3



   grdDetails.Text = Format(sngTotal, "$0.00")



   grdDetails.Row = grdDetails.Row + 1



   grdDetails.Col = 1



   grdDetails.Text = "Freight"



   grdDetails.Col = 3



   grdDetails.Text = Format(asngFreight(grdOrders.Row), "0.00")



   grdDetails.Row = grdDetails.Row + 1



   grdDetails.Col = 1



   grdDetails.Text = "Order Total"



   grdDetails.Col = 3



   grdDetails.Text = Format(asngFreight(grdOrders.Row) _



      + sngTotal, "$0.00")



   rslDetails.Close



   Set rslDetails = Nothing



   txtDetailsTime.Text = Format(Timer - sngTime, "0.000")



End Sub



Private Sub Form_Unload(Cancel As Integer)



   'Formally close the connection and environment



   conUser.Close



   envUser.Close



End Sub



Private Sub Command1_Click()



   Call Form_Activate



End Sub


Static (or keyset) cursors are needed in the preceding code because you can't determine the RowCount value of a rdoResultset object without traversing the result set, which prevents the use of a faster forward-only cursor. A valid RowCount value is required to ReDim arrays and to set the Rows property value of the Grid controls. If you don't need an array to hold additional values, you can populate combo and list boxes with a rdoResultset that uses a forward-only cursor.


Using the rdoPreparedStatement Object with Parameters


The rdoPreparedStatement object is similar in concept to the QueryDef object of the Jet DAO. Both rdoPreparedStatement and QueryDef objects are preprocessed (also called precompiled) queries that are designed to improve execution time, especially of parameterized queries. QueryDef objects are persistent for the lifetime of the .mdb file in which the query is store or until explicitly deleted from the QueryDefs collection. The persistence of rdoPreparedStatement objects is limited to the lifetime of an instance of a Visual Basic executable application or, in design/run mode, the current instance of Visual Basic 4.0, unless you write code to delete an object from its collection. The sections that follow describe the characteristics of rdoPreparedStatement objects and how you use preprocessed queries to improve client/server front-end performance.

The CreatePreparedStatement Method and the Parameters Collection


The primary benefit of taking the extra step to create a rdoPreparedStatement object and then opening an rdoResultset based on the rdoPreparedStatement object is that you create the prepared statement once, regardless of the number of times it is executed during your application's lifetime. The syntax for the CreatePreparedStatement method of the rdoConnection object is as follows:




Set prsPrepStmt = _



conConnection.CreatePreparedStatement(strName, strSQL)

The strName and strSQL arguments are required, but may be empty strings (""). If you use an empty string as the value of strSQL, you must set the value of the SQL property of the rdoPreparedStatement prior to executing the prepared statement by opening a rdoResultset or applying the Execute method.

Few applications require repetitive execution of identical queries, but many front-ends involve queries that are identical except for the value of one or more WHERE clause criteria. Therefore, most developers use rdoPreparedStatement objects in conjunction with one or more replaceable parameters (designated by ?) in the passthrough strSQL statement, as in the following example:




SELECT OrderID, OrderDate, ShippedDate, Freight



   FROM Orders



   WHERE CustomerID = ?



   ORDER BY OrderDate DESC

Each ? in the strSQL statement (or the SQL statement assigned to the value of the rdoPreparedStatement's SQL property) automatically adds a member to the object's Parameters collection, beginning with the Parameter(0) member. The instruction for assigning a value to a Parameter object is




prsPrepStmt.Parameters(n) = typValue

where n is the ordinal position of each ? in the SQL statement, beginning with 0 for the first ?. As an example, to return the orders for Alfred Futterkiste (customer code ALFKI), the first customer in the Customers table of Northwind.mdb, the instruction to set the parameter value of a prepared statement having the preceding SQL property value is this:




prsOrders.Parameters(0) = "ALFKI"


You don't need to be concerned with data types when assigning parameter values. The Type property of a rdoParameter object is read-only. All parameters are passed to the ODBC driver as Variant data. Numeric values are not enclosed within double quotes, but you must use quoted date values for literals in a format that's accepted by the server. Using Jet SQL's # date delimiter, as in #01/01/95#, returns a syntax error message from SQL server.

To execute the parameterized query, you apply the OpenResultset method to the rdoPreparedStatement object as in the following example:




prsOrders.OpenResultset(rdOpenForwardOnly, _



   rdConcurReadOnly, rdAsyncEnable)

You can omit the OpenResultset arguments, but not the empty parenthesis, to create a default read-only keyset result set that operates synchronously.

Properties and Methods of the rdoPreparedStatement Object


The rdoPreparedStatement object has variety of properties, many of which let you control the behavior of rdoResultset objects created from rdoPreparedStatement objects. Table 22.4 lists the properties of the rdoPreparedStatement object, the data type of the property, and a brief description of the purpose of the property, including default values, where applicable. Table 22.5 lists the four methods applicable to the rdoPreparedStatement object.

Table 22.4. Properties of the rdoPreparedStatement object.

Property Name Data Type Purpose
BindThreshold Long Sets or returns the maximum number of bytes returned by a column without using the GetChunk method. The default value is 1,024 bytes.
Connect String The ODBC connect string (without the ODBC; prefix). The value is read-only.
ErrorThreshold Long A value representing the level of severity of an error necessary to trigger a trappable error. The default value is -1, which ignores the error threshold.
hStmt Long The handle to the ODBC statement for the object.
KeysetSize Long The number of rows included in the keyset or static cursor for the rdoResultset object. The default is 0 (all rows use the keyset cursor).
LockType Long The type of locking employed by the rdoResultset object specified by the constants rdConsurReadOnly (default), rdConcurLock (pessimistic locking), rdConcurRowVer (optimistic locking based on row ID), rdConcurValues (optimistic locking based on row values).
LogMessages String The path to and name of a text file to log ODBC messages. A Null value turns off logging.
MaxRows Long The maximum number of rows to be returned by a query. The default value is -1, which returns all rows.
Name String The name of the object specified by the strName argument of the CreatePreparedStatement method.
QueryTimeout Long The number of seconds before an error is generated if execution is not complete. The default value is 0 (unlimited).
RowsAffected Long The number of rows modified by application of the Execute method (for action queries only).
RowsetSize Long The number of keyset rows buffered by the applications. The default value is 100.
SQL String A valid SQL statement or ODBC escape statement that determines the content of the rdoResultset object, the action to be performed, or the stored procedure to execute preceded by EXEC.
StillExecuting Boolean True if the query is in the process of execution; False otherwise. (Valid only if the rdOpenAsynch option is specified.)
Type Integer rdQSelect (0) for a SELECT query, rdQAction (1) for an action query, rdQProcedure (2) for execution of a stored procedure. The value is read-only.
Updatable Boolean True if the rdoResultset object is updatable; False otherwise. The value is read-only.

Table 22.5. Methods of the rdoPreparedStatement object.

Method Name Purpose
Cancel Cancels processing of an asynchronous query (specified by the rdOpenAsync option).
Close Closes an open rdoPreparedStatement object.
Execute Executes a rdoPreparedStatement object of the rdQAction type.
OpenResultset Opens a rdoResultset based on the SQL property of the object for the rdQSelect type or, for the rdQProcedure type, a stored procedure that returns rows.

Creating Prepared Statements on the Server


Prepared statements without parameters create a temporary stored procedure in the database specified by the ODBC datasource or the DATABASE=DatabaseName entry in the connect string of the rdoConnection object. The following is an example of a temporary stored procedure (also called a server prepared statement) created in the Northwind SQL Server database by a rdoPreparedStatement object without a replaceable parameter:




/* Remove the following three lines if you do not want _



   the object dropped. */



IF EXISTS (SELECT * FROM sysobjects WHERE id = _



      object_id('dbo.odbc#saddf6693'))



   DROP procedure dbo.odbc#saddf6693



GO



create proc odbc#saddf6693 as SELECT CustomerID, _



   CompanyName FROM Customers


Visual Basic line continuation pairs appear in the preceding SQL statement because line breaks are added to meet publishing line length limitations. The SQL statement displayed in the Manage Stored Procedures window of SQL Object Manager or SQL Enterprise Manager has a total of six lines, one of which is empty.

Once the odbc#saddf6693 procedure is created, subsequent execution of the query occurs with a Transact-SQL EXEC odbc#saddf6693 statement, instead of the SELECT statement. (Transact-SQL is the extended SQL dialect of SQL Server.) When you close the Visual Basic executable application or close the current design/run instance of Visual Basic, the temporary stored procedure is DROPped. The advantages of the use of server stored procedures are discussed in the "Using Parameterized Server Stored Procedures with Prepared Statements" section, near the end of this chapter.



If your application or Visual Basic crashes after having created the server stored procedure, "orphan" stored procedures will remain in the database. Periodically use the Manage | Stored Procedures command of SQL Object Manager or SQL Enterprise Manager to check for orphan stored procedures when developing Visual Basic applications that use the rdoPreparedStatement object.


Adapting the RDO_Test Application to Use Parameterized Prepared Statements


It's a relatively simple process to alter the code of the frmRDO_Test form to use parameterized rdoPreparedStatement objects to retrieve orders (prsOrders) for a selected customer and to display order details (prsDetails). The step-by-step modifications are as follows:

  1. Move the code that creates the two SQL statements for the rslOrders and rslDetails rdoResultset objects to a new subprocedure, PrepareStatements.

  2. Replace the WHERE clause criteria (astrCustID(cboCustomers.ListIndex and grdOrders.Text) with ?.

  3. Modify the OpenResultset statements to add preceding Parameters(0) = ParamValue statements with the previous WHERE clause criteria.

  4. Change the object of the OpenResultset statements to the appropriate rdoPreparedStatement object and delete the strSQL argument.

  5. Add Call PrepareStatements as the last statement in the Form_Load event-handler.

Listing 22.5 shows the code representing the changes described in the preceding steps, except the added Call statement. Code that's the same (as in the cboCustomers_Click() and grdOrders_SelChange() event-handlers of frmRDO_Test) is replaced by an ellipsis to minimize duplication. The code of Listing 22.5 is incorporated in frmRDO_Prep of the RDO_Prep.vbp project in the \DDG_VB4\32_bit\Chaptr22 folder of the accompanying CD-ROM.

Listing 22.5. Code modifications required to use parameterized rdoPreparedStatement objects.




Private Sub PrepareStatements()



   'Create rdoPreparedStatements



   'Called at end of Form_Load subprocedure



   Dim strSQL As String



   strSQL = "SELECT OrderID, OrderDate, "



   strSQL = strSQL & "ShippedDate, Freight FROM Orders "



   strSQL = strSQL & "WHERE CustomerID = ? "



   strSQL = strSQL & "ORDER BY OrderDate DESC"



   Set prsOrders = conUser.CreatePreparedStatement("Orders", _



      strSQL)



   strSQL = "SELECT Order_Details.Quantity, "



   strSQL = strSQL & "Products.ProductName, "



   strSQL = strSQL & "Order_Details.UnitPrice "



   strSQL = strSQL & "FROM Order_Details, Products "



   strSQL = strSQL & "WHERE OrderID = ? "



   strSQL = strSQL & "AND Products.ProductID = "



   strSQL = strSQL & "Order_Details.ProductID"



   Set prsDetails = conUser.CreatePreparedStatement("Details", _



      strSQL)



End Sub



Private Sub cboCustomers_Click()



  'Find latest orders for CustomerID picked in combo box



   fInProcess = True



   Dim intCol As Integer



   Dim sngTime As Single



   txtOrdersTime.Text = ""



   sngTime = Timer



   'Set parameter value for the selected customer



   prsOrders.rdoParameters(0) = astrCustID(cboCustomers.ListIndex)



   Set rslOrders = prsOrders.OpenResultset(rdOpenStatic)



   ... (Same code as for frmRDO_Test)



End Sub



Private Sub grdOrders_SelChange()



   'Find detail records for order number clicked in grid



   If fInProcess Then



      'Don't process during loading operation



      Exit Sub



   End If



   Dim intCol As Integer



   Dim sngTotal As Single



   Dim sngTime As Single



   sngTime = Timer



   grdOrders.Col = 0



   txtDetailsTime.Text = ""



   'Set parameter value for selected order



   prsDetails.rdoParameters(0) = grdOrders.Text



   Set rslDetails = prsDetails.OpenResultset(rdOpenStatic)



   ... (Same code as for frmRDO_Test)



End Sub

Figure 22.10 illustrates the improved performance achieved by using parameterized rdoPreparedStatement objects in conjunction with rdoResultset objects. Query execution speed of frmRDO_Prep to populate the two Grid controls is faster than frmRDO_Test by a significant margin. The much better refresh time for grdDetails is attributable to the more complex SQL statement required to populate the order details grid.

Figure 22.10. The frmRDO_Prep form displaying typical times to execute parameterized rdoPreparedStatement queries.

Using Parameterized Server Stored Procedures with Prepared Statements


Server stored procedures are the client/server counterpart of Jet's QueryDef object. Stored procedures are compiled by the server's operating system and, like QueryDefs, are optimized in accordance with an execution plan. Also like QueryDefs, stored procedures are saved in the database that contains the associated tables; thus, stored procedures have long-term persistence. (SQL Server's system stored procedures are stored in the master database.) Stored procedures offer the following advantages compared with sending conventional SQL statements to the server:

Although you can execute passthrough queries with the Jet DAO, Jet passthrough syntax only supports input parameters that pass values to the stored procedure. The rdoPreparedStatement object and its Parameters collection are designed specifically to accommodate all three types of stored procedure parameters: input, output, and return value. The sections that follow describe how to create rdoPreparedStatement objects that use input parameters and the ODBC syntax for dealing with input, output, and return value parameters. The RDO_SPs.vbp project, which contains demonstration code for using SQL Server stored procedures, is included in the \DDG_VB4\32_bit\Chaptr22 folder of the accompanying CD-ROM.

Creating Stored Procedures with Transact-SQL


Before you can run the RDO_SPs.vbp project, you must add server stored procedures to return the rdoResultset objects that populate the ComboBox and two Grid controls of the frmRDO_SPs form. The following are the three Transact-SQL statements required to create the sp_customers, sp_orders, and sp_details stored procedures:




CREATE PROCEDURE sp_customers AS



   SELECT CustomerID, CompanyName FROM Customers



CREATE PROCEDURE sp_orders @customerid varchar(5) AS



   SELECT OrderID, OrderDate, ShippedDate, Freight



      FROM Orders WHERE CustomerID = @customerid



      ORDER BY OrderDate DESC



CREATE PROCEDURE sp_details @orderid int AS



   SELECT Order_Details.Quantity, Products.ProductName,



         Order_Details.UnitPrice



      FROM Order_Details, Products



      WHERE OrderID = @orderid



         AND Products.ProductID = Order_Details.ProductID

The first of the preceding statements creates a simple, non-parameterized stored procedure, sp_customers, which is used to create the rslCustomers result set that populates the cboCustomers combo box. The sp_orders and sp_details procedures populate the two Grid controls. The sp_orders and sp_details procedures each have a single parameter specified by the @paramname datatype prefix to the AS reserved word, after which appears the SQL statement for the procedure. A datatype keyword, such as char(n), varchar(n), or int, is required for each parameter. (Multiple parameters are separated by commas.) You can specify a default value by appending an equal sign and the default value, as in




CREATE PROCEDURE sp_orders @customerid varchar(5) = '%' AS ...

which returns orders for all customers if the parameter value is not supplied. You specify an output parameter by appending OUT[put], as in




CREATE PROCEDURE sp_orders ..., @count int OUT AS ...


The sp_ prefix ordinarily is used to specify SQL Server system stored procedures. In the examples of this chapter, the sp_ prefix denotes stored procedure. When creating production stored procedures, avoid using the sp_ prefix. Many developers use as a prefix an abbreviation of the database name, such as nw_orders.


Special ODBC Syntax to Execute Server Stored Procedures


You can execute a non-parameterized stored procedure from the isql prompt by the procedure's name only; however, you must use the EXEC(ute) prefix with rdoPreparedStatement object, as in EXEC sp_name. When executing parameterized stored procedures, ODBC uses escape syntax. ODBC escape syntax, which surrounds non-conforming SQL statements with French braces ({}), is briefly described in the "Differences Between Jet SQL and ANSI/ODBC SQL Statements Created with Microsoft Query" of Chapter 19, "Using the Open Database Connectivity API." The general syntax for the SQL property of a rdoPreparedStatement object with one or more parameters is




strSQL = "{ [? = ]call proc_name (? [, ?[, ?]]) }"

where [ ? = ] represents a return value parameter and the remaining ? symbol(s) represent one or more input or output parameters. Input and/or output parameters must be enclosed within parentheses and separated by commas. The ordinal value n of the Parameters(n) collection is determined by the left to right position of the ? in the statement.



The call keyword of the ODBC escape syntax is the equivalent of the EXEC(ute) reserved word of Transact-SQL. You must include call in the SQL property value for rdoPreparedStatements that execute parameterized stored procedures.

You must specify the Direction property of the rdoParameter object when using output or return value parameters. For an input/output or output-only parameter, set the Direction property value to rdParamInputOutput or rdParamOutput; for a return value parameter, use rdParamReturnValue. The default Direction property value is input (rdParamInput, 0).

Modifying Prepared Statement Code to Use Server Stored Procedures


The changes to adapt the code of the frmRDO_Prep form to substitute stored parameterized procedure calls for conventional SQL statements is shown in Listing 22.6. The PrepareStatements subprocedure of the frmRDO_SPs form of the RDO_SPs.vbp project is much simpler than that of the frmRDO_Prep form. To conserve space, Listing 22.6 includes only the changes to the VBA code of frmRDO_Prep required to accommodate the three stored procedures. The parameters passed to the prsOrders and prsDetails prepared statements are the same for rdoPreparedStatement objects that use stored procedures or conventional SQL statements.

Listing 22.6. Code modifications required to use parameterized rdoPreparedStatement objects to execute stored procedures.




Private Sub PrepareStatements()



   'Create rdoPreparedStatements



   'Called at end of Form_Load subprocedure



   Dim strSQL As String



   strSQL = "EXEC sp_customers"



   Set prsCustomers = conUser.CreatePreparedStatement("Customers", _



      strSQL)



   strSQL = "{ call sp_orders (?) }"



   Set prsOrders = conUser.CreatePreparedStatement("Orders", strSQL)



   strSQL = "{ call sp_details (?) }"



   Set prsDetails = conUser.CreatePreparedStatement("Details", _



      strSQL)



End Sub



Private Sub Form_Activate()



   'Load the combo with CompanyNames



   Dim sngTime As Single



   sngTime = Timer



   'Execute sp_customers stored procedure



   Set rslCustomers = prsCustomers.OpenResultset(rdOpenStatic)



   ... (Same code as for frmRDO_Test)



End Sub

Figure 22.11 shows the improved query execution speed when filling the grdOrders and grdDetails Grid controls with rdoResultset objects based on server stored procedures. The time to fill the cboCustomers ComboBox control is about the same as that for a rdoPreparedStatement that uses an SQL statement, because both prepared statements are stored on the server.

Figure 22.11. Improved query execution times resulting from execution of SQL Server stored procedures.

Running rdoPreparedStatement Action Queries


You can apply the Edit, AddNew, Delete, and Update methods to any rdoResultset object that returns for the corresponding rdoPreparedStatement object an Updatable property value of True. There are no substantial differences in the application of methods to updatable rdoResultset and to DAO's Recordset objects. As a general practice, however, it is a more common practice to employ SQL action queries, such as UPDATE, INSERT, and DELETE queries, wrapped in BEGIN TRANS(action) and COMMIT TRANS(action) statements. For simple, single-table INSERT, UPDATE, and DELETE operations, parameterized stored procedures offer a slight performance edge. Listing 22.7 is typical of the VBA code to add a new order and its associated line items to the Orders and Order_Details table of the Northwind data source by executing an rdoPreparedStatement object.

Listing 22.7. An example of VBA code for adding a new order that contains multiple line items stored in a temporary local Recordset.




Private Sub AddNewOrder ()



   'INSERT the order information (one very long SQL statement)



   strSQL = "BEGIN TRAN INSERT Orders VALUES(" & txtOrderID & ", '"



   strSQL = strSQL & txtCustomerID & "', "



   strSQL = strSQL & cboEmployeeID & ", '"



   strSQL = strSQL & txtShipName & "', '"



   strSQL = strSQL & txtShipAddress & "', '"



   strSQL = strSQL & txtShipCity & "', '"



   strSQL = strSQL & txtShipRegion & "', '"



   strSQL = strSQL & txtShipPostalCode & "', '"



   strSQL = strSQL & txtShipCountry & "', "



   strSQL = strSQL & cboShipVia & ", '"



   strSQL = strSQL & txtOrderDate & "', '"



   strSQL = strSQL & txtRequiredDate & "', "



   strSQL = strSQL & "null, null) "



   'INSERT the line item information for each record



   rsLineItems.MoveFirst



   Do Until rsLineItems.EOF



      strSQL = strSQL & "INSERT Order_Details VALUES(" & _



         lngOrder_ID & ", "



      strSQL = strSQL & rsLineItems!ProductID & ", "



      strSQL = strSQL & rsLineItems!UnitPrice & ", "



      strSQL = strSQL & rsLineItems!Quantity & ", "



      strSQL = strSQL & rsLineItems!Discount & ") "



      rsLineItems.MoveNext



      If rsLineItems.EOF Then



         'Finish the transaction



         strSQL = strSQL & "COMMIT TRAN"



      End If



   Loop



   'Execute the SQL statement



   Set prsNewOrder = conUser.CreatePreparedStatement("NewOrder", _



      strSQL)



   prsNewOrder.Execute



End Sub

Summary


The 32-bit Remote Data Object of Visual Basic 4.0's Enterprise Edition demonstrates improved performance compared with Jet 3.0's Data Access Object when accessing client/server databases. This chapter provided a series of demonstration applications to compare the speed of query processing using the Remote Data control with DBCombo and DBGrid controls and the RDO's rdoResultset object to populate unbound ComboBox and Grid controls. The chapter closed with examples of the use of server stored procedures with rdoPreparedStatement objects and how to write code to execute an INSERT-type rdoPreparedStatement.

The next two chapters cover subjects that are of primary interest to developers of enterprise-scale applications: designing three-tier client/server architecture that uses Visual Basic 4.0 OLE servers as the middle tier and taking advantage of the project management features of Visual SourceSafe 4.0 in a multi-developer environment.

Previous Page Page Top TOC Next Page