William R. Vaughn
This white paper is an excerpt from Hitchhiker's Guide to Visual Basic and SQL Server, 5th Edition by William R. Vaughn (MS Press); used with permission by the author. To order the complete book, contact Microsoft Press® (http://mspress.microsoft.com/).
As a client/server developer, one of the challenges you have to face is how to leverage the suite of queries built up over the lifetime of your application. These queries take the form of SQL procedures hard-coded into your application or references to stored procedures maintained on the server. Since the code that references these SQL queries can be deeply impacted in an application, it is especially difficult to maintain and keep current.
In Remote Data Objects (RDO) version 1.0, accessing these stored procedures was easier than ever, but they still required each application to include fairly complex Open Database Connectivity (ODBC) call syntax used with balky rdoPreparedStatement objects—hand-coded for each application and each procedure invocation. To get it all working, you had to build a correct SQL property, make sure all of the rdoParameter objects had the right Direction and hoped that the Type property was set correctly. On top of that, your application was responsible for providing all of the parameters to establish the ODBC connection. This means you had to build a correct connect string and set another half-dozen parameters so you could consistently get connected. This also meant that your developers had to learn a new set of skills to know how and when to use a chosen cursor driver, query type, and other settings for each situation.
Now that event-driven programming has arrived with RDO version 2.0 and Microsoft® Visual Basic® version 5.0, connecting is a little easier, but it still involves writing and maintaining event procedures to handle the contingencies. In addition, all of this must be repeated for every application you write. Sure, you can use class modules to help perform these operations in teams, but this too can be a challenge, as you have to define your own standards to do so.
For some reason, the documentation provided with Visual Basic 5.0 often calls this designer the "User Connection" designer. Granted, that's what gets created—a UserConnection object. However, the Visual Basic version 5.0 user interface (UI) calls it a "Query Connection Designer." You pick.
With RDO 2.0, you now have another option when you need to expose stored procedures or canned queries to your applications—the Query Connection designer (QCD). This new feature as exposed by the add-ins menu in Visual Basic version 5.0 Enterprise Edition, walks you through a wizardlike interface that interactively builds UserConnection objects based on your specifications. Using the dialog boxes and drop-down lists exposed by the designer, you simply choose from the available options, or fill in the properties and options as you go. When you are done, a UserConnection object containing one or more pre-defined queries is added to your application. After that, you simply refer to these queries as methods against your UserConnection object. What used to take forty or more lines of code to create and execute now can be done with two or three.
Does this technology work with other ODBC data sources besides those that support stored procedures? Sure. Any query you can define using SQL that your ODBC driver recognizes will work with the QCD. This means that you can code simple or complex procedures with or without parameters using the QCD.
Yes, building rdoQuery objects manually in code is easier than it was with RDO 1.0, but using the Query Connection designer makes the process even easier and it makes you work smarter. For one thing, the Query Connection designer isolates all of the logic needed to expose these objects into one GUI interface. This means that you can create a custom UserConnection object once and include it in every application your company creates—at least every application that needs access to the same data. This way, a central team can manage it, and if there are changes, these can be incorporated by re-compiling the application. However, even recompiling should not be required if the query code is kept server-side in stored procedures—all you have to do is rebuild the SP. This approach also dramatically reduces the number of lines of code you have to write, test, debug and maintain. While the Query Connection designer will not build your application for you, it goes a long way toward making team development easier.
Before we start building our own sample UserConnection object with the Query Connection designer (QCD), we need to take a brief look at what is going on under the hood. The QCD accepts a number of parameters through its dialog boxes. These are used to feed a precompiled run-time dynamic-link library (DLL) that:
The dialog boxes are designed to facilitate the collection of these parameters, but they do not make the decisions for you as to suitability of the options when used with one another. It is just as easy (okay, easier) to make a mistake when choosing these options as it was when you had to write code for them all. However, it is also far easier to correct these mistakes later—even much later when the developer has been promoted to management (or marketing) to get him or her out of the trenches.
Basically, the process of creating a custom Query Connection object is simple. All you have to do is:
Each of the queries you added to the control are invoked as methods against the UserConnection object—arguments and all. It couldn't be simpler if I sent one of the Visual Basic version 5.0 developers to your office to do it for you.
The following section describes step-by-step how to build a typical Query Connection object. We will be using the sample Biblio database for this example, which was constructed from the Jet BIBLIO.MDB database that ships with Microsoft SQL Server™. You could also run the example against your own database if that seems like too much trouble—or you don't have the space to devote to another test database.
Due to concerns about security and password protection, there are two levels of persistence available for the UserConnection object's UserID and Password properties. By default both of these levels are turned off, so that no caching or persistence of these properties occurs—so no one can dump your code and see these sensitive parameters.
If Save Connection Information for new Run-Mode Class is checked, the user name and password properties are stored in the properties of the actual class and are persisted in the built executable or DLL.
If Save Connection Information for Design Time is checked, the user name and password properties persist only during design-time, and are not written into the built .exe or .dll file.
Figure 1. Insert multiple queries dialog box—Query Connection designer
Figure 2. Insert stored procedures dialog box—Query Connection designer
CREATE PROCEDURE TitlesByAuthor @AuthorWanted VARChar(30)
AS Select Title, Author
From Titles T, Title_Author TA, Authors A
Where T.ISBN = TA.ISBN and
TA.Au_ID = A.Au_ID and
Author like @AuthorWanted
Return @@ROWCOUNT
Figure 3. Query parameters dialog box—Query Connection designer
Figure 4. Query definition advanced tab dialog box—UserConnection designer
We are done with the designer for now, so we can close the designer dialog box. Notice that we now have a UserConnection1 class under the Designers heading in the Project window. You might also discover that the project also includes a reference to RDO 2.0—this was added automatically by the designer. It builds code to handle all of the properties and events associated with the object in RDO. Yes, even the event prototypes are ready to go—all you have to do is supply the code. That's next on the tour.
Note When you build a new table or stored procedure, don't forget to grant permissions on them so the intended user can access or execute them. While your program might work if you log in as system administrator, it won't when you log in as a "normal" user. Sure, even the stored procedures that you don't have permission to access are listed by the UserConnection designer—it's like looking through Tiffany's window at the tiaras.
Now that the UserConnection object is created, we are ready to take it out for a spin. The first thing we need to do is add some code to instantiate the object from its base class. First, create an instance of the UserConnection object we built using the New operator. We also need an rdoResultset object to hold the result set we are about to fetch.
Dim ucTest As New UserConnection1
Dim rs As rdoResultset
Next, we need to open the connection. You can do this anywhere, but in this case it is a one-time operation, so we put the code in the Form_Load event procedure.
Private Sub Form_Load()
ucTest.EstablishConnection
End Sub
Note If you don't want a forward-only, read-only cursor, now is the time to reset the rdoQueries(n).CursorType to some other type of cursor. In addition you'll need to set the rdoQueries(n).LockType to some other type of concurrency too—unless you want rdConcurReadOnly. Once the query is executed, it will be too late.
At this pont the connection is open—if all is going as planned, and we are ready to try executing the query. Since each of the stored procedures we added with the designer are now exposed as methods on the UserConnection object that was created, they are really easy to code. After we declare the return-value variable, start typing the next line of code as shown following . . .
Dim lrc As Long
lrc = ucTest
You should see something similar to Figure 5.
Figure 5. IntelliSense enumerating properties, methods and events
Notice that the stored procedure that we chose is now in the enumerated list of methods. Now select the TitlesByAuthor method (you can just press the space bar to select it) and type the opening parenthesis and see what happens next. Part of Visual Basic version 5.0's Intellisense knows that this method takes a single input argument (a String) and returns an integer (Long). Because of this, Visual Basic can help you write the code for the method by exposingthe following code in Figure 6.
Figure 6. IntelliSense exposing method arguments
We complete the line by filling in the argument to pass to our TitlesByAuthor query. For our example, I am simply hard-coding the parameter. You would probably pass in a parmeter via a Text box or somesuch.
lrc = ucTest.TitlesByAuthor("%Vaughn%")
When this line executes, it runs the query and returns a return status and result set as described in the UserConnection properties. As a word of caution, I have seen a number of strange instances where I trip an "Invalid use of Nulls" error when using the syntax shown above. Apparenty, some of the cursor drivers do not provide the return status value until the query is fully populated and return a Null until that value is available. Well, there are a couple of ways around this. First, you can code the call without the return value:
ucTest.TitlesByAuthor "%Vaughn%"
and fetch it later when the rdoResulset is at end of file (EOF). At this point the rdoQuery object under the UserConnection object contains the return-status value.
lrc = ucTest.rdoQueries(0).rdoParameters(0)
To access the result set, you must address the rdoResultset object just created which is passed in the LastQueryResults property as shown below.
Set rs = ucTest.LastQueryResults
WARNING You can't treat the LastQueryResults property like any rdoResultset object—it is only a temporary pointer to the result set. As soon as it is referenced it is lost. This means that if you use the DataTip pointer to examine its contents, it will be lost. You must set an rdoResultset variable to this property to use it—or you lose it.
At this point you have an rdoResultset object—just as if you had created it with an OpenResultset—so the same rules and techniques apply. The result set might not be fully populated, so you will want to do a MoveLast on it as soon as possible. And no, you can't use the method or the UserConnection again without first dealing with the result set you just created. This means that you have to use the Close method on the rdoResultset or the LastQueryResults property (which exposes the underlying rdoResultset object built by the UserConnection.) To see what ReturnStatus was sent back, we can examine the variable we used when executing the method (lrc
). Notice that the RowCount property returns a -1 as it is not (yet) available.
Debug.Print rs.RowCount, lrc
Wait a minute! There's something missing here. When I create a result set, I like to choose the type of cursor. For some situations, I use cursorless result sets, but other times I need a keyset, static, forward-only or if I have gone completely bonkers, I use a dynamic cursor. Actually, that's a lie. I have never been crazy enough to use a dynamic cursor. Well, anyway, to set the type of cursor that the UserConnection method is to create can be set by changing the CursorType property of the rdoQuery that the UserConnection object creates for the chosen stored procedure. RDO and Visual Basic will help you choose one as it enumerates your choices when you address this property as illustrated in Figure 7.
Figure 7. DataTips exposing enumerated constants
Note I discovered a bug when using this feature. It seems that each time this feature is invoked, the enumerated list box appeared a little more to the right. Apparently, by choosing a proportional font in the Visual Basic Options, I confused the code that positions the dialog boxes. It'll be fixed in the next version.
While this is not as easy as clicking on a tab and selecting a cursor type from the UserConnection designer dialog boxes, it isn't that hard to do in code. Notice that I have addressed the rdoQuery object using the bang (!) syntax. This causes an "early" binding of the rdoQueries collection member and speeds up your code (a tad). I would not like to reference this member by its ordinal number, because the code would break if I added another stored procedure or query ahead of it.
Generally, I recommend that any queries that your application executes should be in the form of stored procedures. In cases where this is not possible, you can use the Query Connection designer's ability to manage queries you provide in the form of T-SQL queries. Since Microsoft Query is cross-connected to the Query Connection designer, it can be used to help you build your own custom queries interactively through a GUI interface. You might also consider using the new Microsoft Visual Database Tools to build your queries. Frankly, I prefer the latter as it has a much better, albeit more complex, interface.
The steps you need to take to get to the Microsoft Query window are fairly straightforward. Let's step through them to make sure you are ready to use this interface when the time comes to build a complex query. Incidentally, once your query has been developed and tested, it would still be a good idea to move it over to SQL Server as an stored procedure. To try out this interface, let's create a variation on our previous query, but this time, let's add the Publishers table. Since we already have a UserConnection object created, we can go right to the designer's main form.
Figure 8. Defining your own SQL query for a user-defined UserConnection object
Figure 9. Using MS Query to define your SQL query—choosing the tables
Figure 10. Using MS Query to define your SQL query—choosing the columns
Figure 11. User-defined SQL query built by MS Query
Figure 12. Tuning query parameters using the Query Connection designer
Note It seems to me that Microsoft Visual Database provides a better way to create these queries.
Of course, you know that SELECT queries aren't the only kind of queries in use today—no, far from it. While fetching rows is important, many of our client/server front-end operations use action queries too. No, I don't think the interactive query development strategy used here in MSQ would be much good for this type of work. However, there is nothing to stop you from creating your own action queries on your own and adding them on to your user connection. Again, Visual Database Tools might also be helpful here.
You can include your own queries instead of pointing to a stored procedure. You can even code queries that contain parameters. I often take this back-road approach when I am developing a new application—before I commit the query to a stored procedure. There are a couple of chuckholes here that you need to watch out for.
Select * From Authors Where Name Like ?
Make sure you know what you are asking for when you make a parameter a CHAR as opposed to a VARCHAR. Remember that CHAR expressions are fixed in length, and VARCHAR is variable in length. This means if the length of the parameter you are passing changes, you should be using a VARCHAR.
The UserConnection object exposes an entire set of events that can help you fine-tune your custom UserConnection object for special circumstances. The events exposed are the same as for the rdoConnection object plus the Terminate event, which is always associated with user-created objects.
At a minimum, I would also add an error-handling routine to deal with the stuff that happens when we work with SQL Server. Since the UserConnection object exposes all of the rdoConnection events, you can simply add some code to the QueryComplete event handler. Consider that this error handler becomes part of the UserConnection object and is carried around in the .dsr file. This means other developers can use your custom UserConnection object and get all of the error-handling code as well.
Private Sub UserConnection_QueryComplete _
(ByVal Query As RDO.rdoQuery, _
ByVal ErrorOccurred As Boolean)
Dim er As rdoError
Dim m As String
If ErrorOccurred Then
For Each er In rdoErrors
m = m & er & vbCrLf
Next er
MsgBox "Something went wrong with the query" _
& vbCrLf & m
End If