Chapter 17. ADO and OLE DB

What Is ADO?

In this chapter, we will discuss Microsoft’s latest database programming object model, called ActiveX Data Objects, or ADO. This object model is a successor to DAO and is intended to replace DAO. Of course, the arrival of ADO raises the question of whether to redo existing DAO applications in ADO, as well as whether to write new applications in ADO.

As to the former, I can’t see any immediate need to do so unless the application would benefit by some new feature of ADO. One possibility is that ADO may provide superior performance, but this is an ad hoc issue that will require experimentation in each situation. As to the latter, this decision is somewhat of a moving target. While DAO is more established and has proven to be reliable and stable, ADO is Microsoft’s current wave of the future. For instance, the new VB6 DataBinding object model is just a frontend for an OLE DB data client and is designed to use ADO. In order to keep up with Microsoft’s latest technologies—clearly a desirable goal—we will need to get on the ADO bandwagon. We can only hope that Microsoft will offer us other good reasons to join this bandwagon.

Actually, ADO is the immediate successor to Remote Data Objects (RDO), which is, in turn, the immediate successor to DAO. Since RDO did not get much first-string playing time, we will not discuss it in this book. My plan is to discuss the terminology related to ADO and its underlying technology, called OLE DB. Then we will look at the ADO object model and do a few examples, such as connecting to a Jet database, an Excel spreadsheet, and a text file. This will give you a solid foundation in ADO and OLE DB — certainly enough to understand the documentation (such as it is) and dig more deeply if the need arises.

It appears from the documentation that I have seen (from Microsoft and others) that most writers feel that the most important use of ADO is to connect to an SQL Server data provider. However, in my consulting practice, I seldom encounter SQL Server (or perhaps I just unconsciously avoid it). Much more often, I encounter the need to connect to an Excel spreadsheet, for instance. A great many business clients like to do database management in Excel, probably because they are familiar with that application, since they use it for financial analysis (which is its intended purpose). It seems that it is only the VBA consultant, and not those who hire her, who appreciates how limited Excel is when it comes to database management!

There seem to be three approaches to dealing with Excel “databases” (and I have used all three):

  • We can twist and coerce Excel into doing more database management than it is intended to do. However, this creates bloated Excel workbooks with code that runs at a snail’s pace.

  • We can migrate the data from Excel into Access, where it really belongs.

  • We can connect directly to an Excel spreadsheet using Open Database Connectivity (ODBC) for programming in ADO (or DAO).

We will discuss the latter approach in this chapter. This does seem to work, but for major data manipulation, I definitely prefer the second alternative.

Installing ADO

I should mention a word about installing ADO. ADO is installed along with Office 2000, but not with Office 97.

To see if you have ADO installed on your system, first open an Access code module, and then open the References dialog box, under the Tools menu. If you see an entry such as the one highlighted in Figure 17-1, you’re all set.

Reference to the ADO object library
Figure 17-1. Reference to the ADO object library

If, on the other hand, you have no such listing, you might want to do a file search of your hard disk, looking for MSADOxx.DLL. If you don’t have the file, then you can download the required software components from Microsoft’s web site. At the time of this writing, the URL is http://www.microsoft.com/data/. (If this URL is no longer valid, try searching for ADO or MDAC, which stands for Microsoft Data Access Components.) Note that the small version of the software kit is over 5 MB! Enjoy.

Note also that there is considerable confusion when it comes to versions of ADO, a situation that Microsoft does not seem to want to clarify. Version 2.0 refers to the following items, as reported by the type library itself (or the VBA IDE References dialog box). Note the different version numbers:

  • Implementation: msado15.dll

  • Object library name: msado15.dll

  • Object Library Version: 2.0

  • Documentation String: Microsoft ActiveX Data Objects 1.5 Library

  • Help File: msado10.hlp

On the other hand, Version 2.1 of ADO refers to the following items:

  • Implementation: msado15.dll

  • Object library name: msado20.tlb

  • Object Library Version: 2.0

  • Documentation String: Microsoft ActiveX Data Objects 2.0 Library

  • Help File: (none)

Thus, Version 2.1 uses the same implementation as Version 2.0, which is presumably the same as Version 1.5! (Put another way, referring to Figure 17-1, if you highlight a reference to ADO 2.1, you will still see a reference to the msado15.dll library!)

The type library has changed for Version 2.1 of ADO, having been extracted from within the implementing DLL. However, this new type library does not report a help file, although the file ado20.chm appears to be such a file. (Accordingly, the type library contains no context-sensitive help references.)

Frankly, this situation does not seem to make much sense to me, but the bottom line is that ADO appears to be implemented by the same file (msado15.dll ) through several “versions.”

ADO and OLE DB

As we have seen, the DAO model is the programming interface for the Jet database engine. On the other hand, ADO has a more ambitious goal—it is the programming model for a universal data-access interface called OLE DB. Simply put, OLE DB is a technology that is intended to be used to connect to any type of data—traditional database data, spreadsheet data, web-based data, text data, email data, and so on.

Technically speaking, OLE DB is a set of COM interfaces. An interface is just a collection of functions, also called services, with a similar purpose. The term COM refers to the Component Object Model , which is Microsoft’s model for communication between software components. Thus, simply put, OLE DB is a set of functions or services.

Figure 17-2 gives an overview of ADO and OLE DB from a VB programmer’s perspective.

OLE DB and ADO
Figure 17-2. OLE DB and ADO

Data Stores

The purpose of OLE DB is to provide applications with universal data access—that is, with a common method for accessing data in essentially any format, including traditional database formats, text formats, spreadsheet formats, email formats, file system formats, web-based formats, and more. OLE DB uses the term data store to refer to any data that can be accessed through the OLE DB services. The term data source seems to be a synonym for data store, although this term is used in different ways in other related contexts (such as the VB6 DataBinding object model). Indeed, the term “data source” is one of the most abused in Microsoft’s arsenal.

Data Providers

In order to create access to a particular type of data, a developer must write an OLE DB data provider for that type of data store. This is usually done in a C-type development environment such as Visual C++, but it can be done in VB as well.

The purpose of an OLE DB data provider is to expose the data in data stores of a particular type in tabular format, with rows (records) and columns (fields). In other words, the role of a data provider is to make data from a data store look like a table, even if the raw format does not resemble a table. For this reason, a data provider usually has direct access to the data in data stores of that type.

Note that some data providers may also implement more sophisticated data-retrieval and manipulation techniques, such as SQL. However, this is not a requirement. This is in distinction to ODBC, where an ODBC data provider must implement a form of SQL. (For more on this, see Appendix C.)

Here is a sampling of the OLE DB data providers available at the time of this writing:

  • Microsoft OLE DB Simple Provider (a JavaBeans-related interface)

  • Microsoft OLE DB Provider for ODBC Drivers (for Open Database Connectivity)

  • Microsoft OLE DB Provider for Oracle (for Oracle databases)

  • Microsoft Jet 3.51 OLE DB Provider (for Jet databases)

  • Microsoft OLE DB Provider for SQL Server (for SQL Server databases)

  • Microsoft OLE DB Provider for Directory Services (provides directory services— that is, logon, administration, and replication services—for Windows NT Server networks)

Two of these providers are especially interesting for us: the Microsoft Jet 3.51 OLE DB Provider and the Microsoft OLE DB Provider for ODBC Drivers. The ODBC provider is the default data provider and can be used to connect to a variety of data sources, such as an Excel spreadsheet or a text file, through ODBC. We will consider examples of how to use these providers later in the chapter.

It seems as though the distinction between data provider and data store (or data source) is often blurred. Thus, the term “data provider” may refer to a combination of both the data store (the raw data) and the data provider (the software component that implements OLE DB for that type of data store).

Data Consumers

An OLE DB data consumer is a software component that communicates with a data provider in order to gain access to and manipulate a data store. To a data consumer, all OLE DB data has a tabular format, with rows and columns.

Service Providers

In addition to the standard data providers, a developer may implement custom service providers (see Figure 17-2), which do not have direct access to the data (in the parlance of OLE DB, service providers do not own data). The purpose of a service provider is to provide additional services (features) for that particular type of data store through the use of OLE DB interfaces.

Here are some examples of OLE DB data services:

The Microsoft Data Shaping Service for OLE DB

Provides support for the construction of hierarchical (shaped) Recordset objects from one or more data providers. A hierarchical recordset is one in which the value in a particular field can be another Recordset object, which would then be considered a child of the first (parent) recordset.

The Microsoft OLE DB Persistence Provider

Provides support for saving a Recordset object to a file and restoring a Recordset object from a file.

The Microsoft OLE DB Remoting Provider

Enables a user on a local machine to invoke data providers that reside on a remote machine.

Actually, an OLE DB service provider is both an OLE DB consumer and an OLE DB data provider. For example, consider a heterogeneous query processor. (The term heterogeneous refers to the fact that the query processor can process queries that reference data in more than one data source.) When a consumer asks the query processor to provide data from multiple OLE DB data sources, the query processor acts like a consumer when it submits the query to multiple data providers and retrieves the data from the data sources (through each source’s data provider), and it acts like a provider when it returns the results of the query to the consumer that requested the data.

The ADO Object Model

OLE DB is designed for C programmers. In order to make it accessible to VB programmers, Microsoft created the ADO object model. This model gives VB programmers access to certain aspects of the OLE DB paradigm, by allowing the programmer to program an object model, rather than having to use the OLE DB API functions directly. For instance, a VB programmer can get access to a data provider by creating a Connection object and setting its Provider property. Thus, the Connection object represents a connection to a data store through a data provider.

The ADO object model is actually quite small, even smaller than the DAO object model. Table 17-1 shows the complete list of ADO objects (along with corresponding collection objects).

Table 17-1. The ADO objects

Command

Connection

Error (Errors)

Field (Fields)

Parameter (Parameters)

Property (Properties)

Recordset

The ADO object model is shown in Figure 17-3. Unlike the DAO model, which has a single object (DBEngine) at the top of the model, the ADO object model is headed by a triumvirate of three externally creatable objects: Command, Connection, and Recordset. (The Parameter object is also externally creatable.)

An externally creatable object is an object that can be created directly using the VBA New operator, as in:

Dim rs As New Recordset

or, alternatively:

Dim rs As Recordset
Set rs = New Recordset

Thus, as we will see, unlike DAO, a Recordset object can be created independently at the “beginning” of an ADO session.

Let us emphasize that while DAO is centered around the DBEngine object, through which almost all action begins, in ADO, as we will soon see, the “action” can begin with any of the three main ADO objects: Connection, Command, or Recordset. If you are accustomed to programming in DAO, this can take a bit of getting used to.

Incidentally, the tree-like view of the ADO object model shown in Figure 17-3 is from my Object Browser software program. For more on this, please see the card at the end of the book. You can also get more information on this object browser at my web site: http://www.romanpress.com.

The ADO object model
Figure 17-3. The ADO object model

Our plan is to take a look at the Command, Connection, Field, Property, and Recordset objects, along with their properties and methods. (We will also touch lightly upon the Parameter object.)

It is important to emphasize that some features (objects, properties, or methods) of the ADO object model may not be implemented (or implemented fully) by a particular data provider. This is in contrast to the DAO object model, where the entire model is implemented. This is important enough to bear repeating:

To a large extent, it is up to a data provider to decide which features of the ADO object model to support.

There are potentially four ways in which to determine whether a particular feature is supported by a particular data provider:

  • Check the documentation for the data provider (if you can find it).

  • Use the Supports method of the Recordset object to determine whether certain features are supported (but this only applies to the Recordset object).

  • Use dynamic properties, discussed later.

  • Experiment. If you get the error message shown in Figure 17-4, then you know that the operation that caused the message is not supported!

An “operation not supported” message
Figure 17-4. An “operation not supported” message

Note that we will discuss most of the properties and methods in the ADO object model, with the primary exception of those that relate to batch processing or transaction processing.

For the record, batch processing refers to sending multiple commands at one time. When communication between consumer and provider takes place over a network, this can save considerable time. Transaction processing refers to the grouping of multiple operations into a single transaction. At the end of the transaction, the programmer can commit the operations or rollback the data source to its state prior to any of the operations in the transaction. One use for this is in updating related tables (as in transferring money from one table to another). If the entire group of operations is not completed successfully, then a rollback is probably desirable.

The Three-Pronged Approach to Data Manipulation

As far as data manipulation is concerned (as opposed to data definition), the main purpose of ADO is to create a recordset that provides access to the data. As is indicated by the object model in Figure 17-3, there are three ways to obtain a Recordset object. The three methods are:

  • Create a Recordset object directly, and use its Open method, as in:

    Dim rs As ADODB.Recordset
    Set rs = New ADODB.Recordset
    
    rs.Open ...
  • Create a Connection object, and use its Execute method to return a recordset, as in:

    Dim cn As ADODB.Connection
    Dim rs As ADODB.Recordset
    
    cn.Provider = ...
    cn.ConnectionString = ...
    cn.Open
    
    Set rs = cn.Execute(...)
  • Create a Command object:

    Dim cmd As ADODB.Command
    Dim rs As ADODB.Recordset
    
    Set cmd = New ADODB.Command
    Set cmd.ActiveConnection = ...
    cmd.CommandText = ...
    
    Set rs = cmd.Execute

    Note that we will tend to qualify all ADO objects with the prefix ADODB. This will help distinguish between ADO objects and DAO objects of the same name. In fact, the line:

    Dim rs As Recordset

    will be interpreted by VBA as either an ADO or a DAO recordset depending on which of the references to the corresponding object library has higher priority in the References dialog box (under the Tools menu). Since it is a dangerous practice to rely on this priority (which can easily differ from system to system), it is best to always qualify:

    Dim rs1 As ADODB.Recordset
    Dim rs2 As DAO.Recordset

The RecordsetExample procedure shown in Example 17-1 illustrates each of the previous approaches to creating a recordset. Note, however, that only the first method (using the Open method of the Recordset object) allows us to set various recordset options. The other methods create read-only, forward-only recordsets. We will discuss this issue in detail at the appropriate time.

Example 17-1. Three methods of creating a Recordset object
Sub RecordsetExample(  )

' Creating recordsets in different ways

Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection

' Set up connection
Set cn = New ADODB.Connection
cn.Provider = "Microsoft Jet 3.51 OLE DB Provider"

cn.ConnectionString = "Data Source=D:BkAccessIIAccessCode.mdb"
cn.Open

' --------------------------------
' Use rs.Open with table (or SQL)
' This is the most flexible method
' --------------------------------
Set rs = New ADODB.Recordset
rs.Open "Names", cn, adOpenDynamic, adLockReadOnly, adCmdTable

rs.MoveFirst
Debug.Print "Use rs.Open: "
Debug.Print "ActiveConnection: " & rs.ActiveConnection
Debug.Print "Source: " & rs.Source
rs.Close

' ---------------------------------------
' Use cn.Execute
' Always a read-only, forward only cursor
' ---------------------------------------
Set rs = cn.Execute("SELECT * FROM Names")

rs.MoveFirst
Debug.Print
Debug.Print "Use cn.Execute: "
Debug.Print "ActiveConnection: " & rs.ActiveConnection
Debug.Print "Source: " & rs.Source
rs.Close

' ---------------------------------------
' Use Command object
' Always a read-only, forward only cursor
' ---------------------------------------
Dim cmd As ADODB.Command
Set cmd = New ADODB.Command
Set cmd.ActiveConnection = cn
cmd.CommandText = "SELECT * FROM Names"
Set rs = cmd.Execute

rs.MoveFirst
Debug.Print
Debug.Print "Use Command object: "
Debug.Print "ActiveConnection: " & rs.ActiveConnection
Debug.Print "Source: " & rs.Source
rs.Close

cn.Close

End Sub

For future reference, let us note the output from the Debug.Print statements in Example 17-1. In each case, the ActiveConnection property of the recordset is the same. I have broken the string into multiple lines to aid readability:

Provider=Microsoft.Jet.OLEDB.3.51;
Persist Security Info=False;
User ID=Admin;
Data Source=D:BkAccessIIAccessCode.mdb;
Mode=Share Deny None;
Extended Properties=";
COUNTRY=0;
CP=1252;
LANGID=0x0409";
Locale Identifier=1033;
Jet OLEDB:System database="";
Jet OLEDB:Registry Path="";
Jet OLEDB:Database Password="";
Jet OLEDB:Global Partial Bulk Ops=2

As we will see when we discuss connection strings in more detail later in the chapter, this after-the-fact approach is one of the best (read: only) ways to actually see what a complete connection string looks like.

As for the Source property, here is the output:

Use rs.Open with table: 
Source: select * from Names

Use cn.Execute: 
Source: SELECT * FROM Names

Use Command object: 
Source: SELECT * FROM Names

We will refer to this output when we discuss the Source property.

Let us now take a look at the various objects in the ADO object model. Our intention is not to be comprehensive, but to cover the main objects and their main properties and methods. After looking at the ADO model, we will look at several examples of connecting to a variety of data sources.

The Connection Object

The Connection object represents a connection to a data store through a data provider.

Properties of the Connection object

The main properties of the Connection object are:

CommandTimeout

Sets the length of time to wait for a response to a command from the data source before issuing a timeout error message.

ConnectionString

Holds the information needed to make the connection. This may include the name of the data provider, the name of the data source, a password, and a user ID. We will discuss connection strings at some length later in the chapter.

ConnectionTimeout

Sets the length of time to wait for a connection to be made before issuing a timeout error message.

CursorLocation

Sets a recordset’s cursor (which is a device used to traverse the recordset and which defines the current recordset) to reside on the client side of the connection or on the server side. Typically, client-side cursors offer more capabilities than server-side, but server-side cursors may be better at reflecting changes to the data source made by other users. Ultimately, the choice of which type of cursor to use depends on the capabilities of the data provider and on the particular needs at the time. We will see examples of using both types of cursors later on.

DefaultDatabase

By setting a default database for a particular connection, avoids the need to qualify each table name in an SQL statement with the database name.

Errors

Returns the Errors collection of all Error objects (if any) for the previous command.

Mode

Specifies the access mode for the connection and can be set to any one of the following:

adModeUnknown

Signals that permission has not yet been set or cannot be determined. This is the default.

adModeRead

Is read-only permission.

adModeWrite

Is write-only permission.

adModeReadWrite

Is read/write permission.

adModeShareDenyRead

Prevents other users from opening the connection with read permission.

adModeShareDenyWrite

Prevents other users from opening the connection with write permission.

adModeShareExclusive

Prevents other users from opening the connection.

adModeShareDenyNone

Prevents other users from opening the connection with any permission.

Provider

Specifies the data provider. Note that the data provider can alternatively be specified in the ConnectionString property.

State

Returns the state of the connection (read-only). The possible values are given by the following enum:

Enum ObjectStateEnum
     adStateClosed = 0
     adStateOpen = 1
     adStateConnecting = 2
     adStateExecuting = 4
     adStateFetching = 8
End Enum
Version

Returns the ADO version number as a string.

Methods of the Connection object

The main methods of the Connection object are:

Close

Closes the connection. Its syntax is simply:

cn.Close
Execute

Executes a command. A command can be a database query, an SQL statement, a stored procedure, or a provider-specific command in text form. We emphasize that the form of command depends on the data provider. For instance, not all data providers support stored procedures or even SQL statements.

Note that some commands return a recordset and some do not. Accordingly, there are two syntaxes for the Execute method:

' Syntax for a non recordset-returning command

ConnectionObject.ExecuteCommandText, RecordsAffected, Options

' Syntax for a recordset-returning command
Dim rs As ADODB.Recordset
Set rs = ConnectionObject.Execute(CommandText, RecordsAffected, Options)

We will see several examples of the use of the Execute method. RecordsAffected is a Long parameter that we must supply. ADO will fill this variable with the number of records that are affected by the command. The optional Options parameter can assume a variety of values indicating how the data provider should interpret the CommandText argument. The possible values are:

adCmdText

CommandText is a textual definition of a command.

adCmdTable

CommandText is a table name. The rows of this table should be returned by an SQL query created internally by ADO.

adCmdTableDirect

CommandText is a table name. The provider should return all rows from this table.

adCmdStoredProc

CommandText is the name of a stored procedure.

adCmdUnknown

The type of command in the CommandText argument is not known.

adAsyncExecute

The command should execute asynchronously. (This means that the command will execute and then fire the ExecuteComplete event to signal that it has completed.)

adAsyncFetch

The remaining rows after the initial quantity specified in the CacheSize property should be fetched asynchronously.

Open

Opens a connection; that is, it creates an actual connection to the data provider. Its syntax is:

connection.OpenConnectionString, UserID, Password, Options

where all parameters are optional. The ConnectionString parameter is the tricky one here. We will discuss connection strings at length later in the chapter. Note that the Connection object has a ConnectionString property that can be used to set the connection string as well. However, the ConnectionString parameter will override any setting of the ConnectionString property.

Microsoft warns that we should not pass UserID and password values in both the ConnectionString property and the ConnectionString parameter of the Open method, for this may lead to unpredictable results. (And here I thought that computers did not produce unpredictable results.)

Note that it is important to close a connection using the Close method when the connection is no longer required. However, closing the connection does not remove the Connection object from memory, so its properties may still be accessed or altered. In order to remove the Connection object from memory, we must set the variable that references the Connection object to Nothing.

The Options parameter can assume one of the following values:

adConnectUnspecified

The default value. Opens the connection synchronously. Code execution pauses until the connection is made.

adAsyncConnect

Opens the connection asynchronously. The ConnectComplete event is fired when the connection is complete.

OpenSchema

Gets database information from the data provider. The simplest syntax for this method is:

ConnectionObject.OpenSchema(QueryType)

where QueryType can be one of several constants specifying the type of information to retrieve. The method returns a Recordset object with the requested data.

For instance, the following code lists the tables in a Jet database:

' Get list of tables
Set rs = cn.OpenSchema(adSchemaTables)

Do While Not rs.EOF
    Debug.Print rs!TABLE_NAME & "  Type: " & rs!TABLE_TYPE
    rs.MoveNext

Loop

The Recordset Object

A Recordset object represents a recordset. To quote the documentation, “When you use ADO, you manipulate data almost entirely using Recordset objects.”

Recordsets are created using the Open method with code such as:

Dim rs As ADODB.Recordset

Set rs = New ADODB.Recordset

rs.CursorType = adOpenDynamic
rs.CursorLocation = adUseServer
rs.Open "SELECT * FROM Names", cn

As we have seen, a Recordset object may also be created using the Execute method of the Connection object or the Command object.

Let us reiterate that even though the raw data in a particular data store (such as a text file or mail store) may not have the appearance of a traditional table with rows and columns, all ADO recordsets are structured with rows (records) and columns (fields). In fact, that is the primary purpose of ADO—to give all forms of raw data a table-like format.

Cursors

A recordset cursor is a device that is used to traverse the records (or rows) in a recordset. Recordsets (and their cursors) can reside on the client side of the connection or on the server side . Although we will not discuss remote connections—that is, connections over a network—in this introduction to ADO, the terminology is still valid. For instance, if we connect to a local Excel spreadsheet using the OLE DB provider for ODBC, then the dividing line between client and server is still the connection, even though both “sides” of this connection are on the same computer.

The cursor location is set using the CursorLocation property of the Recordset object; its value can be adUseClient or adUseServer.

ADO supports four types of cursors, determined by the CursorType property setting:

Dynamic cursor (CursorType = adOpenDynamic)

This type of cursor is automatically updated to show additions, deletions, and edits to the recordset made by other users. It also permits all forms of movement through the recordset that do not use bookmarks, as well as those that do use bookmarks if the provider supports bookmarks. (Note, however, that the provider must support bookmarks or backward cursor movement in order to use the MovePrevious method.)

Keyset cursor (CursorType = adOpenKeyset)

This type of cursor is similar to a dynamic cursor, except that it does not show records that have been added by other users, nor does it allow access to records that have been deleted by other users. However, edits by other users are visible. Keyset cursors must support bookmarks and therefore allow all forms of movement through the recordset.

Static cursor (CursorType = adOpenStatic)

This type of cursor provides a static copy of a set of records. This is like a snapshot DAO recordset. Static cursors are used to find data or to generate reports. They must support bookmarks and therefore allow all forms of recordset movement. However, additions, deletions, and edits by other users are not visible. Note that all client-side cursors are static cursors. Even if we specify a different type of cursor for a client-side cursor, ADO will open a static cursor instead.

Forward-only cursor (CursorType = adOpenForwardOnly)

This type of cursor behaves identically to a dynamic cursor except that it permits only forward scrolling. This is the analog of supplying the dbForwardOnly constant as an argument to the DAO OpenRecordset method. As with forward-only DAO recordsets, forward-only cursors perform more efficiently when we need to make only a single pass through the recordset.

LockType

The LockType property is a key property for recordsets. This property indicates the type of lock that is placed on the records during editing. It can be one of the following values:

adLockReadOnly

Records are read-only. Note that this is the default value, which means that if we want to do any editing, we must set this property to another value.

adLockPessimistic

In this case, the data provider ensures successful editing of records, usually by locking records at the data source as soon as the Edit method is called. This is termed pessimistic locking. It occurs on a record-by-record basis.

adLockOptimistic

In this case, the provider locks records only when the Update method is called. This is termed optimistic locking. It occurs on a record-by-record basis.

adLockBatchOptimistic

Optimistic batch updates are required for batch update mode.

I emphasize that adLockReadOnly is the default value, which means that if we want to do any editing, we must set this property to another value.

Properties of the Recordset object

The main properties of the Recordset object are described here:

AbsolutePage, PageCount, and PageSize

To help the user page through the data in a recordset (especially when that data is intended to be displayed on the Web), ADO allows us to group the data into logical pages . (The page count starts at 1, by the way.) The PageSize property is used to specify the number of records per page (the default is 10 records per page).

The PageCount property returns the number of pages in the recordset. If a data provider does not support pages, it will indicate this by always returning a PageCount value of -1.

The AbsolutePage property is used either to set the current record at the beginning of a page or to return the page number of the current record. The return value of AbsolutePage may be a page number or one of the following values:

adPosUnknown

Indicates that the current position is unknown, the recordset is empty, or the data provider does not support pages.

adPosBOF

Indicates that the current record pointer is pointed at BOF (BOF is True).

adPosEOF

Indicates that the current record pointer is pointed at EOF (EOF is True).

AbsolutePosition

This property works like the corresponding DAO property; namely, it provides the ordinal position of the current record in the recordset (the first position is position 1). As with DAO, however, the AbsolutePosition property can change when another record is deleted or if the recordset is refreshed. Thus, we cannot rely on the value of AbsolutePosition to return to a given record at a later time. To mark a record for later retrieval, we should use bookmarks.

ActiveConnection

The ActiveConnection property of a recordset returns the connection string for the corresponding connection. If there is no active connection, it returns Nothing. For instance, in the code:

Dim rs As New ADODB.Recordset

Debug.Print rs.ActiveConnection
Debug.Print rs.ActiveConnection Is Nothing

the second line will produce a runtime error, whereas the third line will return True.

Thus, if the recordset rs is associated with the connection cn, then the following values are the same:

cn.ConnectionString
rs.ActiveConnection

For an open recordset, this property is read-only (as you would expect). However, for a closed recordset, we can set the ActiveConnection property to a valid connection string, and ADO will open the connection for us automatically. Setting the property to Nothing will disconnect the recordset from any provider.

Note that the ActiveConnection property can be set either to a string that specifies the connection or to a valid Connection object variable name.

We will have much more to say about connection strings later in the chapter. For now, we refer the reader to the RecordsetExample subroutine in Example 17-1 for an example of the ActiveConnection property. As mentioned earlier, querying the ActiveConnection property is one of the best ways to get the full syntax of a connection string for a data provider. Needing to resort to this technique is a reflection on the poor quality of the documentation for OLE DB data providers, especially when it comes to connect strings.

BOF and EOF

As with DAO, these Boolean properties indicate whether the current record pointer lies before the first record (BOF is True) or after the last record (EOF is True). In either case, there is no current record.

Bookmark

Each record in an ADO recordset has a bookmark associated with it. (A bookmark has Variant data type.) We can retrieve this bookmark and store it in a variable with code such as:

bk = rs.Bookmark

We can then return to this record at any time by writing:

rs.Bookmark = bk
CacheSize

This specifies the number of records that will be placed in the client-side memory buffer at one time. Put another way, it is the number of records that are fetched from the data store at one time.

CursorLocation

As discussed earlier, this property specifies the location of the cursor: client-side or server-side.

CursorType

As discussed earlier, this property specifies the type of cursor: dynamic, keyset, static, or forward-only.

EditMode

Like DAO, ADO uses a temporary editing buffer for the current record. The EditMode property indicates the current status of the data in this buffer. Its possible values are:

adEditNone

Indicates that no editing operation is in progress.

adEditInProgress

Indicates that the data in the current record buffer has been modified but has not yet been saved.

adEditAdd

Indicates that the AddNew method has been invoked and the new data in the current record buffer has not yet been saved.

adEditDelete

Indicates that the current record has been deleted.

Fields

This returns the Fields collection for the given recordset. We will discuss Field objects later in the chapter.

Filter

Filters the current recordset by restricting the records that are visible. Thus, for instance, after executing the code:

rs.Filter = "Lastname = 'Smith' OR FirstName Like 'A*'"

the recordset referenced by rs is filtered so that we have access only to those records that meet the filter condition. We can release the filter by writing:

rs.Filter = ""

Note that after setting a filter, the current record pointer is moved to the first record that fits the filter criteria. Note also that Microsoft warns that it is preferable to define and open a new recordset on the data source than to make extensive use of filters.

LockType

This property, discussed earlier, indicates the type of lock that is placed on the records during editing.

MaxRecords

This limits the number of records returned by a query. The default value of 0 indicates that all matching records should be returned. This property is read-only for an open recordset.

RecordCount

This indicates the number of records in an open recordset. The property returns -1 when ADO cannot determine the number.

Note that if the recordset supports either approximate positioning or bookmarks (as indicated, for example, by the Supports method discussed later), then the RecordCount value is always correct regardless of whether the recordset has been fully populated by using the MoveLast method. Thus, if neither positioning nor bookmarks are supported, the only way to make sure that the RecordCount property is accurate is to populate the recordset fully, which may place a significant drain on resources because all records in the recordset will need to be retrieved from the data source.

Source

This Variant property gives the source of the data for the recordset. It is read-only when the recordset is open. It can be set to a valid Command object variable name, an SQL statement, a table name, or a stored procedure call. (As always with ADO, this depends on the level of support from the data provider.) See the RecordsetExample subroutine in Example 17-1 for examples of the Source property.

State

This read-only property returns the state of the recordset. The possible values are given by the following enum :

Enum ObjectStateEnum
     adStateClosed = 0
     adStateOpen = 1
     adStateConnecting = 2
     adStateExecuting = 4
     adStateFetching = 8
End Enum

Methods of the Recordset object

The main methods of the Recordset object are described in this section.

AddNew

Adds new records to a recordset, provided that the data provider and the current cursor type support this feature, of course. The general syntax is:

recordset.AddNewFields, Values

where Fields is an optional single field name or an array of field names and the optional Values is the corresponding value (for a single field) or value array (for a field array) to assign to the fields in the new record. For instance, the code:

rs.AddNew Array(LastName, FirstName), Array("Einstein", "Albert")

adds a new record with values LastName = "Einstein" and FirstName = "Albert".

Clone

Creates a new Recordset object that is a duplicate of the Recordset object to which it is applied. It is important to note, however, that a cloned Recordset object is not entirely independent of its parent. Here is what the documentation says about cloned recordsets:

Changes made to one Recordset object are visible in all of its clones regardless of cursor type. However, after you execute Requery on the original Recordset, the clones will no longer be synchronized to the original.

Closing the original Recordset does not close its copies; closing a copy does not close the original or any of the other copies.

You can only clone a Recordset object that supports bookmarks. Bookmark values are interchangeable; that is, a bookmark reference from one Recordset object refers to the same record in any of its clones.

Close

Closes the recordset.

Delete

Deletes one or more records. Its syntax is:

rs.Delete AffectRecords

where AffectRecords is one of the following constants:

adAffectCurrent

Deletes the current record.

adAffectGroup

Causes all records that match the current filter only to be deleted.

adAffectAll

Deletes all records.

adAffectAllChapters

Deletes all chapter records.

GetRows

Retrieves multiple records into an array. The syntax is:

array = recordset.GetRows(Rows, Start, Fields)

Rows is an optional Long parameter that specifies the number of rows to retrieve. Its default is adGetRowsRest, indicating that the method should retrieve all of the remaining records in the recordset. The optional Start parameter specifies the starting row to retrieve. It should be either a bookmark or one of the values: adBookmarkCurrent (start at the current record; this is the default), adBookmarkFirst (start at the first record), or adBookmarkLast (start at the last record). Finally, Fields can be a single field name (or ordinal position) or an array of field names (or ordinal positions). If the Fields parameter is not missing, only those fields will be returned; otherwise, all fields will be returned.

Note that the DAO version of the GetRows method has a different syntax.

Here are some things to keep in mind concerning the GetRows method:

  • The first subscript in the array identifies the field, and the second identifies the record. This is counterintuitive.

  • The lower bound on the returned array is 0, whereas the upper bound is one less than the number of records actually returned. Thus, if we specify more rows than are returned, the upper bound provides a way to get the number of rows actually returned. (Use the VBA UBound function to get the upper bound, and add 1 to get the number of records returned.)

  • After a call to GetRows, the current record is the next unread record, or EOF if there are no more records. Thus, subsequent calls to GetRows can be made without specifying the Start parameter.

Move, MoveFirst, MoveLast, MoveNext, MovePrevious

Are used to move the current record pointer.

The Move method has the syntax:

recordset.MoveNumRecords, Start

where NumRecords is a Long specifying the number of records to move the current record pointer relative to Start, which is either a bookmark or one of the values adBookmarkCurrent, adBookmarkFirst, or adBookmarkLast.

According to the documentation, “the Move method is supported on all Recordset objects.” Of course, exactly what this means is unclear. Does it refer to all types of recordsets for a provider that supports the Move method, or does it mean that all providers must support this method?

If you are experiencing performance problems with Move, you might want to consider whether the CacheSize setting is causing too many retrievals. It may be possible to improve performance by setting the CacheSize value to a larger number. This is a tradeoff between performance and memory usage (as always).

Note that if the Recordset object to which we apply the Move method is forward-only, we can still pass a NumRecords value that is less than zero, provided that the destination is within the current set of cached records. If not, an error will occur. On the other hand, a call to MovePrevious will generate an error even if the resulting move lies within the currently cached group of records.

The MoveFirst, MoveLast, MoveNext, and MovePrevious methods work similarly to those methods in DAO. Note, however, that the Recordset object must support bookmarks or backward cursor movement in order to use the MovePrevious method. Otherwise, the method will generate an error. On the other hand, the MoveFirst method will work on a forward-only recordset, but it may cause the provider to re-execute the command that retrieved the Recordset object in the first place.

NextRecordset

Makes it possible to set up a compound command that contains several individual commands. For instance, the statement:

SELECT * FROM table1;SELECT * FROM table2

consists of two separate SQL statements. If we execute this command using the Execute method, ADO will execute and retrieve only the first SQL statement. To execute the second command and get the corresponding recordset, we use the NextRecordset method. For more on this, we refer the reader to the ADO documentation.

Open

Opens a recordset. The full syntax is:

recordset.OpenSource, ActiveConnection, CursorType, LockType, Options

As with the Connection object, the parameters are optional and can be specified separately using properties of the Recordset object.

The Source parameter specifies the data source. Setting this parameter will override the setting of the Source property (if any). The parameter can be set to a Variant that identifies a valid Command object variable name, or to an SQL statement, a table name, or a stored procedure call (if supported by the data provider, as usual).

Setting the ActiveConnection parameter will override the current value of the ActiveConnection property (if any). The setting can be the name of a valid Connection object variable or a string that describes the connection. This will cause ADO to establish (open) the connection.

For a discussion of the CursorType and LockType parameters, see Section 17.4.3.1 and Section 17.4.3.2 in the discussion of Section 17.4.3 earlier in this chapter. Note that if we set either of these parameters, the setting will also be made automatically in the corresponding property value.

The Options parameter is used when Source is a string (not a Command object) to identify the type of the Source argument. It can be one of the following values:

adCmdText

Treats the Source argument as a text string that describes a command.

adCmdTable

Treats the Source argument as a table name. ADO should generate an SQL query to return the table rows.

adCmdTableDirect

Treats the Source argument as a table name and returns all rows.

adCmdFile

Returns a recordset from the file named by Source.

adCmdStoredProc

Treats the Source argument as the name of a stored procedure.

adCmdUnknown

The Source argument type is unknown.

These values can be combined with values that relate to asynchronous fetching of records:

adAsyncExecute

The Source should be executed asynchronously. A FetchComplete event will fire when the operation is complete.

adAsyncFetch

After the initial quantity specified in the Initial Fetch Size property is fetched, any remaining rows are fetched asynchronously. If a required row has not yet been fetched, further code execution is blocked (halted) until the requested row becomes available.

adAsyncFetchNonBlocking

This is similar to adAsynchFetch, except that further code execution is never blocked. If the requested row has not been fetched, the current row automatically moves to the end of the file.

It is important to close a recordset using the Close method when the recordset is no longer required. However, closing the recordset does not remove the Recordset object from memory, so its properties may still be accessed or altered. In order to remove the Recordset object from memory, we must set the recordset variable that references the object to Nothing.

Requery

Updates the recordset by requerying the data source.

Resync

Resynchronizes the recordset with the underlying data. It differs from the Requery method in that it does not re-execute the original query that produced the recordset. Hence, it will cause any changes to existing records to be visible, but it will not show any new records.

Supports

Gets information on what features are supported for recordsets of the specified type by the data provider. The syntax is:

boolean = recordset.Supports(CursorOptions)

The return value is True if the feature described by CursorOptions is supported and False otherwise.

Here is a list of the possible values for CursorOptions:

adAddNew

The AddNew method is supported.

adApproxPosition

The AbsolutePosition and AbsolutePage methods are supported.

adBookmark

The Bookmark property is supported.

adDelete

The Delete method is supported.

adHoldRecords

With respect to transaction processing, we can retrieve more records or change the next retrieve position without committing all pending changes.

adMovePrevious

The MovePrevious method is supported. Also, Move and GetRows can be used to move the current record pointer backwards without requiring the use of bookmarks.

adResync

The Resync method is supported.

adUpdate

The Update method is supported.

adUpdateBatch

Batch updating is supported.

adSeek

The Seek method is available.

adIndex

The Index property with which to name an index is available (ADO 2.1 only).

To illustrate, the SupportsExample procedure in Example 17-2 compares static and dynamic cursors for a Jet connection.

Example 17-2. The SupportsExample procedure
Sub SupportsExample(  )

' Compares support options for static and dynamic cursors

Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Dim lRecordsAffected As Long

' Set up connection
Set cn = New ADODB.Connection
cn.Provider = "Microsoft Jet 3.51 OLE DB Provider"
cn.ConnectionString = "Data Source=D:BkAccessIIAccessCode.mdb"
cn.Open

Set rs = New ADODB.Recordset

' ---------------------------------------------------
' Check support options for server-side static cursor
rs.CursorLocation = adUseServer
rs.Open "SELECT * FROM Names", cn, adOpenStatic, adLockOptimistic
' Get recordset support
Debug.Print
Debug.Print "Server-Side Static Recordset:"
Debug.Print "adAddNew: " & rs.Supports(adAddNew)
Debug.Print "adBookmark: " & rs.Supports(adBookmark)
Debug.Print "adDelete: " & rs.Supports(adDelete)
Debug.Print "adFind: " & rs.Supports(adFind)
Debug.Print "adUpdate: " & rs.Supports(adUpdate)
Debug.Print "adMovePrevious: " & rs.Supports(adMovePrevious)

rs.Close

' ----------------------------------------------------
' Check support options for server-side dynamic cursor
rs.CursorLocation = adUseServer
rs.Open "SELECT * FROM Names", cn, adOpenDynamic, adLockOptimistic
' Get recordset support
Debug.Print
Debug.Print "Server-Side Dynamic Recordset:"
Debug.Print "adAddNew: " & rs.Supports(adAddNew)
Debug.Print "adBookmark: " & rs.Supports(adBookmark)
Debug.Print "adDelete: " & rs.Supports(adDelete)
Debug.Print "adFind: " & rs.Supports(adFind)
Debug.Print "adUpdate: " & rs.Supports(adUpdate)
Debug.Print "adMovePrevious: " & rs.Supports(adMovePrevious)

rs.Close

cn.Close

End Sub

The output is:

Server-Side Static Recordset:
adAddNew: True
adBookmark: True
adDelete: True
adFind: True
adUpdate: True
adMovePrevious: True

Server-Side Dynamic Recordset:
adAddNew: True
adBookmark: False
adDelete: True
adFind: True
adUpdate: True
adMovePrevious: True

Thus, we can see that static cursors support bookmarks, whereas dynamic cursors do not.

Update

Updates the current record after editing. This method can be used to set values as well, since its general syntax is:

recordset.UpdateFields, Values

where Fields is a single field name or an array of field names, and Values are the corresponding values to assign to the fields in the record. For instance, the code:

rs.Update Array(LastName, FirstName), Array("Einstein", "Albert")

updates the record by setting LastName = "Einstein" and FirstName = " Albert".

The Command Object

A Command object represents a definition of a command that may be executed by a data provider. We have seen an example (the RecordsetExample subroutine in Example 17-1) of how a Command object can be used to create a recordset. The RecordsetExample procedure also demonstrates that a Command object is not always required in order to execute a command. However, a Command object is required when we want to execute the same command more than once. Also, a Command object is needed to pass parameters to a query.

Command objects and connections

The ActiveConnection property is used to specify the connection over which the command will pass. The ActiveConnection property can be set either to a text string that describes the connection or to a Connection object variable that refers to a valid connection.

It is important to note that if we want to assign a single connection to multiple commands (at different times), a Connection object variable should be used. For if we use a text string, ADO will create a new Connection object for each command, even if the connection string is the same.

Setting the ActiveConnection property to Nothing disassociates the Command object from the current connection and causes the data provider to release any associated resources on the data source. This may or may not be required, depending on the data provider, before associating a new Connection object to the command.

Properties of the Command object

Let us discuss the main properties of the Command object.

ActiveConnection

Sets the connection over which the command will be sent. As discussed earlier, it can be a text string (a connection string) or a Connection object variable.

CommandText

Sets (or retrieves) the actual command. This is usually an SQL statement, but it can be any string that is recognized as a command by the data provider (such as a stored procedure call). According to the documentation, some data providers may alter the text of a command string. We can view any changes by examining the value of the CommandText property.

CommandTimeout

Sets or returns the length of time to wait for the command to execute before displaying a timeout error. The default is 30 seconds.

CommandType

Sets the type of command; it has the same values as the Options parameter in the Open method of the Recordset object:

adCmdText

A text string that describes a command.

adCmdTable

A table name whose records are returned by generating an internal SQL query.

adCmdTableDirect

A table name whose records are returned.

adCmdFile

The name of a file containing a recordset.

adCmdStoredProc

The name of a stored procedure.

adExecuteNoRecords

CommandText is a command or stored procedure that does not return rows. This value is always combined with either adCmdText or adCmdStoredProc.

adCmdUnknown

Unknown type.

Name

Can be used to assign a name to a command.

Parameters

Returns a Parameters collection, which contains the parameters that are required by the command (if any). We will not discuss parameterized queries for ADO in this book.

Prepared

If set to True, the data provider will compile the command specified in the CommandText property, assuming that it supports this feature. This may slow execution the first time that the command is executed. However, subsequent executions of the same command should proceed more quickly. Note that if the data provider does not support command compilation, it may return an error as soon as this property is set to True, or it may simply ignore the request to prepare the command and set the Prepared property to False.

Methods of the Command object

Let us discuss the main methods of the Command object.

CreateParameter

Creates a Parameter object. A Parameter object represents a parameter that is associated with a parameterized query. We will not discuss parameterized queries for ADO in this book.

Execute

Executes the command represented by the Command object. As with the Execute method of the Connection object, there are two possible syntaxes based on whether or not the command returns a recordset:

' Syntax for a non recordset-returning command
CommandObject.ExecuteRecordsAffected, Parameters, Options

' Syntax for a recordset-returning command
Dim rs As ADODB.Recordset
Set rs =
CommandObject.Execute(RecordsAffected, Parameters,
Options )

Note that all parameters are optional.

The RecordsAffected parameter is a Long that returns the number of records affected by the command. The Parameters parameter is a Variant array of parameters that may be required by the SQL statement (if any). The values in this array will override any parameter values set through the Parameters property. (The order of parameters in the array is the order in which the parameters are passed.)

Finally, the Options parameter is equivalent to the CommandType property (and has the same possible values).

The Property Object and Dynamic Properties

The ADO objects:

Recordset
Parameter
Field
Connection
Command

each have a Properties property that returns a Properties collection. This collection contains a object for each dynamic property of the object.

ADO objects can have two types of properties: built-in and dynamic. Built-in properties are the familiar properties implemented by ADO itself. These are the properties that we have been discussing up to now. Note that the Properties collection does not contain Property objects for built-in properties.

On the other hand, dynamic properties are defined by the data provider and are thus specific to a particular data provider. There is one Property object in the Properties collection for each dynamic property, and this Properties collection provides the only method for referencing a dynamic property, as in:

Object.Properties(PropertyName)

or:

Object.Properties(PropertyIndex)

Dynamic properties have four built-in properties of their own:

Name

Identifies the property, as in the previous code.

Type

An integer that specifies the data type of the property. It can be one of the values in Table 17-2.

Table 17-2. The values of the Type property

adEmpty = 0

adIUnknown = 13

adNumeric = 131

adSmallInt = 2

adDecimal = 14

adUserDefined = 132

adInteger = 3

adTinyInt = 16

adDBDate = 133

adSingle = 4

adUnsignedTinyInt = 17

adDBTime = 134

adDouble = 5

adUnsignedSmallInt = 18

adDBTimeStamp = 135

adCurrency = 6

adUnsignedInt = 19

adVarChar = 200

adDate = 7

adBigInt = 20

adLongVarChar = 201

adBSTR = 8

adUnsignedBigInt = 21

adVarWChar = 202

adIDispatch = 9

adGUID = 72

adLongVarWChar = 203

adError = 10

adBinary = 128

adVarBinary = 204

adBoolean = 11

adChar = 129

adLongVarBinary = 205

adVariant = 12

adWChar = 130

 

Note also that the Type property can be set to a disjunction (ORing) of one of the constants in Table 17-2 and one of the following values:

adArray

Indicates that the Type value is an array of values.

adByRef

Indicates that the Type value is a pointer to a value.

adVector

Indicates that the Type value is a DBVECTOR structure, as defined by OLE DB. This structure contains a count of elements and a pointer to data of type DBTYPE_VECTOR. For more on this, see the ADO documentation.

For example, the value:

adInteger OR adArray

represents an array of integers.

Value

A Variant containing the value of the dynamic property.

Attributes

A Long that describes attributes of the property. It can be a sum of one or more of the following values:

adPropNotSupported

The property is not supported by the data provider.

adPropRequired

The user must specify a value for this property before the data source is initialized.

adPropOptional

The property is optional.

adPropRead

The property can be read.

adPropWrite

The property can be set.

To illustrate, consider the PropertiesExample procedure shown in Example 17-3.

Example 17-3. The PropertiesExample procedure
Sub PropertiesExample(  )

Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Dim prop As ADODB.Property

' Set up connection
Set cn = New ADODB.Connection
cn.Provider = "Microsoft Jet 3.51 OLE DB Provider"

cn.ConnectionString = "Data Source=d:BkAccessIIAccessCode.mdb"
cn.Open

' Open recordset
Set rs = New ADODB.Recordset
rs.Open "Names", cn, adOpenDynamic, adLockReadOnly, adCmdTable

For Each prop In rs.Properties
   Debug.Print prop.Name
Next

rs.Close
cn.Close
End Sub

This procedure prints a list of dynamic property names for a Jet recordset. The rather impressive output is:

Preserve on Abort
Blocking Storage Objects
Use Bookmarks
Skip Deleted Bookmarks
Bookmark Type
Cache Deferred Columns
Fetch Backwards
Hold Rows
Scroll Backwards
Column Privileges
Preserve on Commit
Defer Column
Delay Storage Object Updates
Immobile Rows
Literal Bookmarks
Literal Row Identity
Maximum Open Rows
Maximum Pending Rows
Maximum Rows
Column Writable
Memory Usage
Notification Phases
Bookmarks Ordered
Others' Inserts Visible
Others' Changes Visible
Own Inserts Visible
Own Changes Visible
Quick Restart
Reentrant Events
Remove Deleted Rows
Report Multiple Changes
Row Privileges
Row Threading Model
Objects Transacted
Updatability
Strong Row Identity
IAccessor
IColumnsInfo
IColumnsRowset
IConnectionPointContainer
IRowset
IRowsetChange
IRowsetIdentity
IRowsetInfo
IRowsetLocate
IRowsetResynch
IRowsetScroll
IRowsetUpdate
ISupportErrorInfo
ILockBytes
ISequentialStream
IStorage
IStream
IRowsetIndex
Column Set Notification
Row Delete Notification
Row First Change Notification
Row Insert Notification
Row Resynchronization Notification
Rowset Release Notification
Rowset Fetch Position Change Notification
Row Undo Change Notification
Row Undo Delete Notification
Row Undo Insert Notification
Row Update Notification
Append-Only Rowset
Change Inserted Rows
Return Pending Inserts
IConvertType
Notification Granularity
Access Order
Lock Mode
Jet OLEDB:Partial Bulk Ops
Jet OLEDB:Pass Through Query Connect String
Jet OLEDB:ODBC Pass-Through Statement
Jet OLEDB:Grbit Value
Jet OLEDB:Use Grbit
Jet OLEDB:3.5 Enable IRowsetIndex
Bookmarkable

Of course, getting documentation on these properties is another matter. Let me know if you find any.

The Field Object

The Field object represents a field (or column) in a recordset. The Fields property of the Recordset object returns the Fields collection of all Field objects for that recordset.

The Field object has but two methods, AppendChunk and GetChunk , which are used with large text or binary fields. The reader should refer to the documentation for more on these methods.

Properties of the Field object

Here are the properties of the Field object:

ActualSize and DefinedSize

The DefinedSize property is used to set the size of a field as it is defined. The ActualSize property returns the size of the actual data stored in that field for the current record. Thus, for example, a String field named FirstName may have DefinedSize 25, but if the actual data in a given record at a particular time is "Albert", then the ActualSize property will return 6.

Attributes

The Attributes property of a Field object can be a sum of the following values. Note that for a Field object, the Attributes property is read-only.

adFldMayDefer

The field is deferred; that is, the field values are not retrieved from the data source when the record is retrieved. Instead, we must explicitly request the values.

adFldUpdatable

The field value is writable.

adFldUnknownUpdatable

The provider cannot determine if we can write to the field.

adFldFixed

The field contains fixed-length data.

adFldIsNullable

The field accepts Null values.

adFldMayBeNull

Null values can be read from the field.

adFldLong

The field is a long binary field. Hence, the AppendChunk and GetChunk methods are available for this field.

adFldRowID

The field contains some type of record ID, such as a record number or unique identifier.

adFldRowVersion

The field contains a time or date stamp used to track updates.

adFldCacheDeferred

The provider caches field values and subsequent reads are done from the cache.

Name

This is the name of the field. Note that the Name property is read-only for Field objects.

NumericScale and Precision

The read-only NumericScale property is used to return the number of digits to the right of the decimal place that is used to represent numeric values. The read-only Precision property returns the total number of digits used to represent a numeric value. Both are Byte properties.

Value, UnderlyingValue, and OriginalValue

The Value property sets or returns the value of the field for the current record.

The UnderlyingValue property returns the current field value from the database. This value may be the result of a recent update to the recordset by another transaction, whereas the OriginalValue property returns the original value that was retrieved from the recordset and thus does not reflect any updates by another transaction.

The UnderlyingValue and OriginalValue properties are read-only. To set a value, we must use the Value property.

Type

This specifies the data type for the field. The possible values are listed earlier in Table 17-1.

Finding OLE DB Providers

It is clearly important to be able to determine which OLE DB providers are installed on a particular system. The Windows registry contains entries for each installed OLE DB provider. An example is shown in Figure 17-5.

Registry entry for an OLE DB provider
Figure 17-5. Registry entry for an OLE DB provider

Unfortunately, Windows does not make it a simple matter to extract this registry information using code. The ListDPs procedure shown in Example 17-4 will do the trick. You don’t need to worry about all of the coding details related to the registry, but you may want to change some of the code, since it currently just prints the list of data providers to the Immediate window. Also, don’t forget to include the code in the declarations section, also shown in Example 17-4.

Example 17-4. The ListDPs procedure
' Declarations for ListDPs

Type FILETIME
     dwLowDateTime As Long
     dwHighDateTime As Long
End Type

Public Const HKEY_CLASSES_ROOT = &H80000000

Public Const ERROR_SUCCESS = 0&
Public Const KEY_QUERY_VALUE = &H1
Public Const KEY_ENUMERATE_SUB_KEYS = &H8
Public Const KEY_NOTIFY = &H10
Public Const SYNCHRONIZE = &H100000
Public Const STANDARD_RIGHTS_READ = &H20000
Public Const KEY_READ = ((STANDARD_RIGHTS_READ Or KEY_QUERY_VALUE Or _
       KEY_ENUMERATE_SUB_KEYS Or KEY_NOTIFY) And (Not SYNCHRONIZE))

Public Const REG_SZ = 1

Declare Function RegOpenKeyEx Lib "advapi32.dll" Alias _
    "RegOpenKeyExA" (ByVal hKey As Long, ByVal lpSubKey As String, _
    ByVal ulOptions As Long, ByVal samDesired As Long, _
    phkResult As Long) As Long
Declare Function RegCloseKey Lib "advapi32.dll" _
    (ByVal hKey As Long) As Long
Declare Function RegEnumKeyEx Lib "advapi32.dll" Alias _
    "RegEnumKeyExA" (ByVal hKey As Long, ByVal dwIndex As Long, _
    ByVal lpName As String, lpcbName As Long, _
    ByVal lpReserved As Long, ByVal lpClass As String, _
    lpcbClass As Long, lpftLastWriteTime As FILETIME) As Long
Declare Function RegQueryValueEx Lib "advapi32.dll" Alias _
    "RegQueryValueExA" (ByVal hKey As Long, ByVal lpValueName As String, _
    ByVal lpReserved As Long, lpType As Long, lpData As Any, _
    lpcbData As Long) As Long
Declare Function RegQueryValueExStr Lib "advapi32.dll" Alias _
    "RegQueryValueExA" (ByVal hKey As Long, ByVal lpValueName As String, _
    ByVal lpReserved As Long, lpType As Long, ByVal lpData As String, _
    lpcbData As Long) As Long


Private Sub ListDPs(  )

' Search the registry for Data Providers

Const BUF_LEN As Long = 2048

Dim lret As Long, lret2 As Long, lret3 As Long
Dim hCLSIDKey As Long, hClassKey As Long, hClassSubKey As Long

Dim lbufKeyName As Long
Dim bufKeyName As String * BUF_LEN
Dim lbufClassName As Long
Dim bufClassName As String * BUF_LEN

Dim lbufKeyName2 As Long
Dim bufKeyName2 As String * BUF_LEN
Dim lbufClassName2 As Long
Dim bufClassName2 As String * BUF_LEN

Dim lbufValue As Long
Dim bufValue As String * BUF_LEN

Dim ft As FILETIME, ft2 As FILETIME
Dim lxKey As Long, lxKey2 As Long
Dim lValueType As Long

Dim bProvider As Boolean
Dim sDPs As String
Dim sName As String

' --------------
' Open CLSID key
' --------------
lret = RegOpenKeyEx(HKEY_CLASSES_ROOT, "CLSID", 0, KEY_READ, hCLSIDKey)

If lret <> ERROR_SUCCESS Then
   MsgBox "Cannot open CLSID key", vbCritical
   Exit Sub
End If

lxKey = 0
Do
   lbufKeyName = BUF_LEN
   bufKeyName = String(BUF_LEN, Chr$(0))
   lbufClassName = BUF_LEN
   bufClassName = String(BUF_LEN, Chr$(0))
   lret = RegEnumKeyEx(hCLSIDKey, lxKey, bufKeyName, lbufKeyName, _
        0, bufClassName, lbufClassName, ft)
   lxKey = lxKey + 1
   DoEvents
   If lret = ERROR_SUCCESS Then
      ' We have a subkey of CLSID (a class key) -
      '   check its subkeys for OLE DB Provider key
      lret2 = RegOpenKeyEx(HKEY_CLASSES_ROOT, "CLSID" & _
              Left$(bufKeyName, lbufKeyName), 0, KEY_READ, hClassKey)
      If lret2 <> ERROR_SUCCESS Then
         MsgBox "Cannot open key " & Left$(bufKeyName, lbufKeyName)
         RegCloseKey hCLSIDKey
         Exit Sub
      End If
      
      ' Got a class key, check its subkeys
      ' We compile the subkeys and their default values in sDPs
      ' to be discarded if the class is not a provider
      sDPs = ""
      bProvider = False
      lxKey2 = 0
      Do
         lbufKeyName2 = BUF_LEN
         bufKeyName2 = String(BUF_LEN, Chr$(0))
         lbufClassName2 = BUF_LEN
         bufClassName2 = String(BUF_LEN, Chr$(0))
         lret2 = RegEnumKeyEx(hClassKey, lxKey2, bufKeyName2, _
            lbufKeyName2, 0, bufClassName2, lbufClassName2, ft2)
         If lret2 = ERROR_SUCCESS Then
            ' Test for OLE DB Provider
            If LCase$(Left$(bufKeyName2, lbufKeyName2)) = _
                     "ole db provider" Then
               bProvider = True
               Exit Do
            End If
         End If
         lxKey2 = lxKey2 + 1
      Loop While lret2 = ERROR_SUCCESS
      ' Finished looping through subkeys of the class key
      ' If a provider, display all key values
      If bProvider Then
         Debug.Print ""
         Debug.Print "***NEW PROVIDER***"
         Debug.Print "CLSID = " & Left$(bufKeyName, lbufKeyName)
         lxKey2 = 0
         Do
            lbufValue = 0  '''this causes a GPF --> BUF_LEN
            bufValue = String(BUF_LEN, Chr$(0))
            lbufKeyName2 = BUF_LEN
            bufKeyName2 = String(BUF_LEN, Chr$(0))
            lbufClassName2 = BUF_LEN
            bufClassName2 = String(BUF_LEN, Chr$(0))
            lret2 = RegEnumKeyEx(hClassKey, lxKey2, bufKeyName2, _
                lbufKeyName2, 0, bufClassName2, lbufClassName2, ft2)
            If lret2 = ERROR_SUCCESS Then
               ' Open the key and get the default value
               lret3 = RegOpenKeyEx(HKEY_CLASSES_ROOT, _
                    "CLSID" & Left$(bufKeyName, lbufKeyName) & "" & _
                    Left$(bufKeyName2, lbufKeyName2), _
                    0, KEY_QUERY_VALUE, hClassSubKey)
               If lret3 = ERROR_SUCCESS Then
                  sName = ""
                  ' Get the length and check for string
                  lret3 = RegQueryValueEx(hClassSubKey, sName, 0&, _
                    lValueType, 0&, lbufValue)

                  ' Check for string
                  If lValueType = REG_SZ Then
                  
                     If lbufValue <> 0 Then
                        lret3 = RegQueryValueExStr(hClassSubKey, sName, _
                            0&, lValueType, bufValue, lbufValue)
                     End If
                     
                     If Left$(bufKeyName2, lbufKeyName2) <> _
                             "ExtendedErrors" Then
                        Debug.Print Left$(bufKeyName2, lbufKeyName2) & _
                            " = " & Left$(bufValue, lbufValue)
                     End If
                  End If   ' string
                  RegCloseKey hClassSubKey
               End If
            End If
            lxKey2 = lxKey2 + 1
         Loop While lret2 = ERROR_SUCCESS
         
      End If
      
      RegCloseKey hClassKey
   End If
Loop While lret = ERROR_SUCCESS

RegCloseKey hCLSIDKey

End Sub

Here is the output of ListDPs on my system:

***NEW PROVIDER***
CLSID = {0C7FF16C-38E3-11d0-97AB-00C04FC2AD98}
InprocServer32 = C:Program FilesCommon Filessystemole dbSQLOLEDB.DLL 
OLE DB Provider = Microsoft OLE DB Provider for SQL Server 
ProgID = SQLOLEDB.1 
VersionIndependentProgID = SQLOLEDB 

***NEW PROVIDER***
CLSID = {3449A1C8-C56C-11D0-AD72-00C04FC29863}
InprocServer32 = C:Program FilesCommon FilessystemmsadcMSADDS.DLL 
OLE DB Provider = MSDataShape 
ProgID = MSDataShape.1 
VersionIndependentProgID = MSDataShape 

***NEW PROVIDER***
CLSID = {c8b522cb-5cf3-11ce-ade5-00aa0044773d}
InprocServer32 = C:Program FilesCommon FilesSystemOLE DBMSDASQL.DLL 
OLE DB Provider = Microsoft OLE DB Provider for ODBC Drivers 
ProgID = MSDASQL.1 
VersionIndependentProgID = MSDASQL 

***NEW PROVIDER***
CLSID = {dee35060-506b-11cf-b1aa-00aa00b8de95}
InprocServer32 = C:Program FilesCommon Filessystemole dbMSJTOR35.DLL 
OLE DB Provider = Microsoft Jet 3.51 OLE DB Provider 
ProgID = Microsoft.Jet.OLEDB.3.51 
VersionIndependentProgID = Microsoft.Jet.OLEDB 

***NEW PROVIDER***
CLSID = {dfc8bdc0-e378-11d0-9b30-0080c7e9fe95}
InprocServer32 = C:Program FilesCommon Filessystemole dbMSDAOSP.DLL 
OLE DB Provider = Microsoft OLE DB Simple Provider 
ProgID = MSDAOSP.1 
VersionIndependentProgID = MSDAOSP 

***NEW PROVIDER***
CLSID = {e8cc4cbe-fdff-11d0-b865-00a0c9081c1d}
InprocServer32 = C:Program FilesCommon Filessystemole dbMSDAORA.DLL 
OLE DB Provider = Microsoft OLE DB Provider for Oracle 
ProgID = MSDAORA.1 
VersionIndependentProgID = MSDAORA 

***NEW PROVIDER***
CLSID = {E8CCCB79-7C36-101B-AC3A-00AA0044773D}
InprocServer32 = C:oledbsdkinSAMPPROV.DLL 
OLE DB Provider = Microsoft OLE DB Sample Provider 
ProgID = SampProv 
VersionIndependentProgID = SampProv

With reference to this output, a CLSID is a number that is intended to identify the data provider (in this case) or any software component (in more general settings) throughout the universe. This is why it is also referred to as a globally unique identifier (GUID). We have no use for this value, however.

The InprocServer32 entry shows the fully qualified name of the DLL that actually implements the data provider. For instance, the Jet provider has the filename C:Program FilesCommon Filessystemole dbMSJTOR35.DLL.

The OLE DB Provider entry is the name of the provider. This can be used with the Provider property of the Connection object. The ProgID entry is the provider’s programmatic ID , an identifying string that is friendlier than the CLSID and is supposed to be unique as well. The ProgID can also be used as the value of the Provider property.

A Closer Look at Connection Strings

It seems fair to say that the most confusing aspect of using ADO is determining the correct connection string required to establish a connection to an OLE DB provider. Certainly, this is one of the first confusing aspects of ADO, if not the only one.

In the beginning, there was only one OLE DB provider—Microsoft OLE DB Provider for ODBC Drivers. This was a good way for Microsoft to introduce OLE DB, because it meant that any ODBC provider automatically became an OLE DB provider.

Today, the list of OLE DB providers has grown to include the following (and presumably there are more of which I am not aware):

  • Microsoft OLE DB Simple Provider (a JavaBeans-related interface)

  • Microsoft OLE DB Provider for ODBC Drivers (for Open Database Connectivity)

  • Microsoft OLE DB Provider for Oracle (for Oracle databases)

  • Microsoft Jet 3.51 OLE DB Provider (for Jet databases)

  • Microsoft OLE DB Provider for SQL Server (for SQL Server databases)

  • Microsoft OLE DB Provider for Directory Services (provides directory services—that is, logon, administration and replication services—for Windows NT Server networks)

Aside from the ODBC provider, the SQL Server provider is used most often in examples, so we will not do so here. On the PC side, I think that the most interesting OLE DB providers are the Jet provider and the ODBC provider, especially since the latter can be used to connect to such things as Excel spreadsheets and text documents. Accordingly, we will take a look at how to set up connection strings using these two providers.

The Microsoft Jet 3.51 OLE DB Provider

Oddly enough, the MSDN Library (which is now the main source of documentation for Microsoft’s development platforms) does not seem to document the Jet 3.51 OLE DB provider—at least I couldn’t find any documentation on it. However, some experimentation will yield sufficient details to use the provider.

You may be wondering why you would want to use this OLE DB provider to connect to a Jet database when DAO was specifically designed for this purpose and works quite well. This is a fair question. I suppose one answer is that we had better stay current with Microsoft’s technology, or we may find ourselves in trouble later on. Frankly, I wish I had a better answer at this time.

The place to start is with the results of the ListDPs procedure shown earlier for the Jet provider:

CLSID = {dee35060-506b-11cf-b1aa-00aa00b8de95}
InprocServer32 = C:Program FilesCommon Filessystemole dbMSJTOR35.DLL 
OLE DB Provider = Microsoft Jet 3.51 OLE DB Provider 
ProgID = Microsoft.Jet.OLEDB.3.51 
VersionIndependentProgID = Microsoft.Jet.OLEDB

Recall that we can use either the ProgID entry or the OLE DB Provider entry as the value of the Provider property of the Connection object.

The AccessExample procedure in Example 17-5 illustrates a connection to a Jet database.

Example 17-5. The AccessExample procedure
Sub AccessExample(  )

Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection

' Set up connection
Set cn = New ADODB.Connection
cn.Provider = "Microsoft Jet 3.51 OLE DB Provider"
cn.ConnectionString = "Data Source=D:BkAccessIIAccessCode.mdb"
cn.Open

' Get full connection string after opening
Debug.Print "Full connection string: " & cn.ConnectionString

' Get list of 2s
Set rs = cn.OpenSchema(adSchemaTables)
Do While Not rs.EOF
    Debug.Print rs!TABLE_NAME & "  Type: " & rs!TABLE_TYPE
    rs.MoveNext
Loop

rs.Close
cn.Close

End Sub

After declaring and creating a Connection object:

Dim cn As ADODB.Connection
Set cn = New ADODB.Connection

we set the Provider property:

cn.Provider = "Microsoft Jet 3.51 OLE DB Provider"

As for the ConnectionString property, without knowing much about the connection string format, we try specifying just a data source:

cn.ConnectionString = "Data Source=D:BkAccessIIAccessCode.mdb"

Then we open the connection and print the ConnectionString property:

cn.Open
Debug.Print "Full connection string: " & cn.ConnectionString

The resulting output gives us a full connection string, which in this case is:

Provider=Microsoft.Jet.OLEDB.3.51; _
Persist Security Info=False; _
User ID=Admin; _
Data Source=D:BkAccessIIAccessCode.mdb; _
Mode=Share Deny None; _
Extended Properties=";COUNTRY=0;CP=1252;LANGID=0x0409"; _
Locale Identifier=1033; _
Jet OLEDB:System database=""; _
Jet OLEDB:Registry Path=""; _
Jet OLEDB:Database Password=""; _
Jet OLEDB:Global Partial Bulk Ops=2

Much of this connection string, such as the Persist Security Info, is obscure. Fortunately, we don’t seem to need it. Note that the Provider parameter is the ProgID rather than the text description that we used to set this value.

Finally, to test the connection, we also print out a list of all of the tables in the database using the OpenSchema method of the Connection object. The result is:

MSysACEs  Type: SYSTEM TABLE
MSysIMEXColumns  Type: TABLE
MSysIMEXSpecs  Type: TABLE
MSysModules  Type: TABLE
MSysModules2  Type: TABLE
MSysObjects  Type: SYSTEM TABLE
MSysQueries  Type: SYSTEM TABLE
MSysRelationships  Type: SYSTEM TABLE
Names  Type: TABLE
Table1  Type: TABLE

The Microsoft OLE DB Provider for ODBC Drivers

Open Database Connectivity ( ODBC) for short, is an Application Programming Interface (API) designed for connecting to databases of various types. The term database is used here in a very general sense to refer not only to traditional relational databases, such as Access, FoxPro, Oracle, or SQL Server databases, but also to less traditional “databases,” such as delimited text files or Excel worksheets.

Since ODBC is still very commonly used and will be for some time, I have included Appendix C, which describes this technology in some detail. For now, we want to discuss how to connect to an ODBC data source through the OLE DB provider for ODBC. To understand the process completely and create your own connection strings, you must be familiar with ODBC Data Source Names. These are discussed in Appendix C. However, to modify the connection strings for the Excel files and text files that we will discuss later, you don’t really need to know anything about DSNs beyond the following.

The term Data Source Name (DSN) refers not simply to the name of the data source, but to a description of the data source and its accompanying driver, as well as the attributes of a connection between the two. For instance, a DSN includes the name of the data source, the complete path of the data source, the name of the driver, and details about the connection to the data source, such as whether the connection is read-only. As we will see in the Appendix C, there are various types of DSNs. A DSN is created using the ODBC Administrator, which can be activated by clicking on the ODBC icon in the Windows Control Panel. Appendix C discusses how to use this applet.

Again referring to the output of the ListDPs procedure described earlier, we first note that the Provider property of the Connection object can be set to either MSDASQL (or its version-dependent counterpart, MSDASQL.1) or the string "Microsoft Jet 3.51 OLE DB Provider". Also, since this provider is the default, we can simply omit the Provider property altogether.

Fortunately, there is some documentation for the Microsoft OLE DB provider for ODBC, and, equally fortunately, it is quite clearly written, as far as it goes. Here is what the documentation says about the connect string (this is from the Microsoft MSDN Library CD):

Because you can omit the Provider parameter, you can therefore compose an ADO connection string that is identical to an ODBC connection string for the same data source, using the same parameter names (DRIVER=, DATABASE=, DSN=, and so on), values, and syntax as you would when composing an ODBC connection string. You can connect with or without a predefined data source name (DSN) or FileDSN.

Syntax with a DSN or FileDSN:

“[Provider=MSDASQL;] { DSN=name | FileDSN=filename } ; [DATABASE=database;] UID=user; PWD=password”

Syntax without a DSN (DSN-less connection):

“[Provider=MSDASQL;] DRIVER=driver; SERVER=server; DATABASE=database; UID=user; PWD=password”

If you use a DSN or FileDSN, it must be defined through the ODBC Administrator in the Windows Control Panel. As an alternative to setting a DSN, you can specify the ODBC driver (DRIVER=), such as “SQLServer,” the server name (SERVER=), and the database name (DATABASE=).

You can also specify a user account name (UID=), and the password for the user account (PWD=) in the ODBC-specific parameters or in the standard ADO-defined User ID and Password parameters. If you include both the ADO and the ODBC-specific parameters for these values, the ADO parameters take precedence.

Although a DSN definition already specifies a database, you can specify a DATABASE parameter in addition to a DSN to connect to a different database. This also changes the DSN definition to include the specified database. It is a good idea to always include the DATABASE parameter when you use a DSN. This will ensure that you connect to the proper database because another user may have changed the default database parameter since you last checked the DSN definition.

This seems to be saying that when we omit the provider portion of the connection string (which can always be supplied using the Provider property), an OLE DB connection string is identical with an ODBC connection string. Of course, this begs the question: “How do we compose an ODBC connection string?”

The simplest answer is to let Windows do this for us. However, the starting point for this is a DSN that we must create, probably using the ODBC Administrator. The GetODBCConnectString procedure in Example 17-6 will extract a connection string from a DSN. The procedure first uses DAO (yes, DAO) to create an ODBC workspace. Then the OpenConnection method:

Set c = ws.OpenConnection("", dbDriverPrompt, , "ODBC;")

causes Windows to display the ODBC Administrator so we can create a DSN. Once this is done, the procedure prints the complete connection string.

Example 17-6. The GetODBCConnectString procedure
Private Sub GetODBCConnectString(  )

' Create an ODBC workspace and get the connect string for a DSN

Dim db As Database, ws As Workspace, rs As Recordset
Dim cn As Connection

Set ws = CreateWorkspace("NewODBC", "admin", "", dbUseODBC)

' The following causes a prompt for the DSN
Set cn = ws.OpenConnection("", dbDriverPrompt, , "ODBC;")
Debug.Print cn.Connect
cn.Close

End Sub

Actually, there are two types of ODBC connection strings—DSN and DSN-less. Here are examples of the two types of connection strings for a connection to an Excel worksheet and to a text file. These strings were obtained using the GetODBCConnectString procedure:

' Excel DSN-less connection string
ODBC; _
DBQ=D:BkAccessIIConnect.xls; _
DefaultDir=D:kado; _
Driver={Microsoft Excel Driver (*.xls)}; _
DriverId=790; _
FIL=excel 5.0; _
ImplicitCommitSync=Yes; _
MaxBufferSize=512; _
MaxScanRows=8; _
PageTimeout=5; _
ReadOnly=0; _
SafeTransactions=0; _
Threads=3; _
UID=admin; _
UserCommitSync=Yes;

' Excel DSN connection string
ODBC; _
DSN=ConnectExcel; _
DBQ=D:BkAccessIIConnect.xls; _
DefaultDir=D:kado; _
DriverId=790; _
FIL=excel 5.0; _
MaxBufferSize=512; _
PageTimeout=5; _
UID=admin;

' Text file DSN-less connection string
ODBC; _
DefaultDir=D:kado; _
Driver={Microsoft Text Driver (*.txt;*.csv)}; _
DriverId=27; _
Extensions=txt,csv,tab,asc; _
FIL=text; _
ImplicitCommitSync=Yes; _
MaxBufferSize=512; _
MaxScanRows=25; _
PageTimeout=5; _
SafeTransactions=0; _
Threads=3; _
UID=admin; _
UserCommitSync=Yes;

' Text file DSN connection string
ODBC; _
DSN=ConnectText; _
DBQ=D:kado; _
DefaultDir=D:kado; _
DriverId=27; _
FIL=text; _
MaxBufferSize=512; _
PageTimeout=5; _
UID=admin; _

The main difference between the two types of connection strings is that in a DSN connection string, the DSN file is referenced so that ODBC can get information from that file. In a DSN-less string, all required information must be supplied directly. Thus, in many ways DSN-less connection strings are superior since they do not require an external DSN file.

Let me reiterate (lest you become annoyed with me) that we will discuss creating DSNs using the ODBC Administrator in Appendix C. At this point, however, you should just keep the following in mind:

  • If you just want to connect to an Excel spreadsheet or text file, you can modify and use the connection strings in the upcoming examples.

  • If you want to create a connection string for a different ODBC provider, you can use the GetODBCConnectString procedure to get the proper connection string, but for this you will need to use the ODBC Administrator to create a DSN. A discussion of how to do this is given in Appendix C, along with more details on DSNs and ODBC in general. As we will see in the appendix, by creating a File DSN, the GetODBCConnectString procedure will produce a DSN-less connection string!

So let us turn to some actual examples.

Connecting to an Excel workbook

The ExcelExample procedure shown in Example 17-7 illustrates how to connect to an Excel worksheet named MasterTable (shown in Figure 17-6) in the workbook D:BkAccessIIConnect.xls.

A test Excel worksheet
Figure 17-6. A test Excel worksheet

The procedure uses the SQL statement:

"SELECT * FROM [MasterTable$]"

to open a recordset based on this table. (I can’t tell you how long it took me to determine that a dollar sign must be appended to the end of an Excel worksheet name.)

We set the connect string to:

' Connection string
cn.ConnectionString = _
    "DRIVER={Microsoft Excel Driver (*.xls)};DBQ=D:BkAccessIIConnect.xls;"

Note the DBQ parameter. Based on the documentation from Microsoft that I quoted earlier, I first tried to use the parameter name DATABASE, but was rudely rewarded with the message “Operation cancelled” at the line:

cn.Open

(In case you are wondering how I discovered that DBQ was the correct name, I used the ODBC Administrator to create a DSN and inspected the DSN file with a text editor.)

The ExcelExample procedure in Example 17-7 prints the full connection string, which in this case is:

Provider=MSDASQL.1; _
Connect Timeout=15; _
Extended Properties="DBQ=D:BkAccessIIConnect.xls; _
	Driver={Microsoft Excel Driver (*.xls)}; _
	DriverId=790; _
	MaxBufferSize=512; _
	PageTimeout=5;"; _
Locale Identifier=1033

Next, the procedure prints the field names for the Excel worksheet, which are the entries in the first row. (I didn’t know this until I ran this code.) It then prints the remaining rows of the table. Note the use of the GetRows function to grab all of the records in the recordset at once.

Finally, the procedure gathers some support information for future reference.

Example 17-7. The ExcelExample procedure
Sub ExcelExample(  )

Dim r As Integer, f As Integer
Dim vrecs As Variant

Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Dim fld As ADODB.Field

' Set up connection
Set cn = New ADODB.Connection

' Set provider
' Note we can also use the ProgID: "MSDASQL.1", or nothing!
cn.Provider = "Microsoft OLE DB Provider for ODBC Drivers"

' Connection string
cn.ConnectionString = _
    "DRIVER={Microsoft Excel Driver (*.xls)};DBQ=D:BkAccessIIConnect.xls;"

' Open the connection
cn.Open

' Get full connection string after opening
Debug.Print "Full connection string: " & cn.ConnectionString

' Get recordset using rs.open SQL statement
Set rs = New ADODB.Recordset
rs.CursorLocation = adUseClient
rs.Open "SELECT * FROM [MasterTable$]", cn, adOpenDynamic, adLockOptimistic

' Print the field names (from first row)
For Each fld In rs.Fields
    Debug.Print fld.Name,
Next
Debug.Print

' Get the rows all at once
vrecs = rs.GetRows(6)

For r = 0 To UBound(vrecs, 2)
    For f = 0 To UBound(vrecs, 1)
        Debug.Print vrecs(f, r),
    Next
    Debug.Print
Next

' Check support options while we are here
Debug.Print
Debug.Print "Client-Side Dynamic Recordset:"
Debug.Print "adAddNew: " & rs.Supports(adAddNew)
Debug.Print "adBookmark: " & rs.Supports(adBookmark)
Debug.Print "adDelete: " & rs.Supports(adDelete)
Debug.Print "adFind: " & rs.Supports(adFind)
Debug.Print "adUpdate: " & rs.Supports(adUpdate)
Debug.Print "adMovePrevious: " & rs.Supports(adMovePrevious)

rs.Close
cn.Close

End Sub

The output from the support information code is:

Client-Side Dynamic Recordset:
adAddNew: True
adBookmark: True
adDelete: True
adFind: True
adUpdate: True
adMovePrevious: True

This shows that ADO provides pretty good access to an Excel worksheet.

Connecting to a text file

The TextExample procedure, shown in Example 17-8, illustrates how to create a text file and add text to it using the ODBC provider for OLE DB. (Before running this procedure, you will probably want to change the DefaultDir value.)

Example 17-8. The TestExample procedure
Sub TextExample(  )

Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Dim sCS As String
Dim sSQL As String

' Declare new connection
Set cn = New ADODB.Connection

' Form connection string
sCS = "DefaultDir=d:kado;"
sCS = sCS & "Driver={Microsoft Text Driver (*.txt; *.csv)};"
sCS = sCS & "DriverId=27;"
cn.ConnectionString = sCS

cn.Open

' Get full connection string after opening
Debug.Print "Full connection string: " & cn.ConnectionString

' Create a new text file and add a line
On Error Resume Next
cn.Execute "CREATE TABLE [newfile.txt] (FirstName TEXT, LastName TEXT);"

If Err.Number <> 0 And Err.Number <> vbObjectError + 3604 Then
    MsgBox "Error: " & Err.Number & ": " & Err.Description
    Err.Clear
End If

sSQL = "INSERT INTO [newfile.txt] (FirstName, LastName) Values ('steve', 'roman'),"
cn.Execute sSQL

' Open a recordset
Set rs = New ADODB.Recordset
rs.Open "SELECT * FROM NewFile.txt", cn, adOpenDynamic, adLockOptimistic

' Check support options while we are here
Debug.Print
Debug.Print "Client-Side Dynamic Recordset:"
Debug.Print "adAddNew: " & rs.Supports(adAddNew)
Debug.Print "adBookmark: " & rs.Supports(adBookmark)
Debug.Print "adDelete: " & rs.Supports(adDelete)
Debug.Print "adFind: " & rs.Supports(adFind)
Debug.Print "adUpdate: " & rs.Supports(adUpdate)
Debug.Print "adMovePrevious: " & rs.Supports(adMovePrevious)

rs.Close
cn.Close

End Sub

In this case, there is a wrinkle in the connection-string requirements. We seem to need the clause:

DriverId = 27;

in the connection string, even though the driver name is also given. Without the DriverId, we get the confusing error message "Data source name not found and no default driver specified.” As with the Excel example, to figure this out, I created a DSN with the ODBC Administrator and inspected the resulting file. Starting with the entire connection string based on that file, I slowly eliminated entries until I got a minimal working connection string.

Note also that when creating a new text file, we need to deal with the possibility that the file already exists. The line:

On Error Resume Next

tells VBA that if an error occurs, it should simply skip the line that produced the error and execute the next line. Now consider the code that will handle an error:

If Err.Number <> 0 And Err.Number <> vbObjectError + 3604 Then
    MsgBox "Error: " & Err.Number & ": " & Err.Description
    Err.Clear
End If

If we remove the On Error Resume Next line, the second time we run the procedure, we will get the error message in Figure 17-7.

An error message
Figure 17-7. An error message

Now, VBA uses error numbers starting with the constant vbObjectError (which equals &H8004000) to indicate object errors. The error number in Figure 17-7 is thus:

&H8004000 + &H0e14 = vbObjectError + 3604

So, the error-handling code:

If Err.Number <> 0 And Err.Number <> vbObjectError + 3604 Then
    MsgBox "Error: " & Err.Number & ": " & Err.Description
    Exit Sub
End If

looks for errors message other than error number vbObjectError+3604. If it finds such an error, it displays a message and exits. However, if the error is the one shown in Figure 17-7, then the procedure just ignores it. This is what we want, because the next line of code just inserts a line in the existing file.

The full connection string for this text connection is:

Provider=MSDASQL.1; _
Connect Timeout=15; _
Extended Properties="DefaultDir=d:kado; _
...Driver={Microsoft Text Driver (*.txt; *.csv)}; _
...DriverId=27;MaxBufferSize=512;PageTimeout=5;"; _
Locale Identifier=1033

and the support-related output is:

Client-Side Dynamic Recordset:
adAddNew: True
adBookmark: False
adDelete: True
adFind: True
adUpdate: True
adMovePrevious: True

Thus, we even have pretty good access to a text file, but we cannot use bookmarks.

ODBC support

The documentation for the ODBC data provider does include some useful tables that describe which features are available for various recordset types. These tables are reproduced here as Tables 17-3 and 17-4.

Table 17-3. Availability of properties by Recordset

Property

ForwardOnly

Dynamic

Keyset

Static

AbsolutePage

Not available

Not available

Read/write

Read/write

AbsolutePosition

Not available

Not available

Read/write

Read/write

ActiveConnection

Read/write

Read/write

Read/write

Read/write

BOF

Read-only

Read-only

Read-only

Read-only

Bookmark

Not available

Not available

Read/write

Read/write

CacheSize

Read/write

Read/write

Read/write

Read/write

CursorLocation

Read/write

Read/write

Read/write

Read/write

CursorType

Read/write

Read/write

Read/write

Read/write

EditMode

Read-only

Read-only

Read-only

Read-only

EOF

Read-only

Read-only

Read-only

Read-only

Filter

Read/write

Read/write

Read/write

Read/write

LockType

Read/write

Read/write

Read/write

Read/write

MarshalOptions

Read/write

Read/write

Read/write

Read/write

MaxRecords

Read/write

Read/write

Read/write

Read/write

PageCount

Not available

Not available

Read-only

Read-only

PageSize

Read/write

Read/write

Read/write

Read/write

RecordCount

Not available

Not available

Read-only

Read-only

Source

Read/write

Read/write

Read/write

Read/write

State

Read-only

Read-only

Read-only

Read-only

Status

Read-only

Read-only

Read-only

Read-only

Table 17-4. Availability of methods by Recordset

Method

ForwardOnly

Dynamic

Keyset

Static

AddNew

Yes

Yes

Yes

Yes

CancelBatch

Yes

Yes

Yes

Yes

CancelUpdate

Yes

Yes

Yes

Yes

Clone

No

No

Yes

Yes

Close

Yes

Yes

Yes

Yes

Delete

Yes

Yes

Yes

Yes

GetRows

Yes

Yes

Yes

Yes

Move

Yes

Yes

Yes

Yes

MoveFirst

Yes

Yes

Yes

Yes

MoveLast

No

Yes

Yes

Yes

MoveNext

Yes

Yes

Yes

Yes

MovePrevious

No

Yes

Yes

Yes

NextRecordset (except Jet)

Yes

Yes

Yes

Yes

Open

Yes

Yes

Yes

Yes

Requery

Yes

Yes

Yes

Yes

Resync

No

No

Yes

Yes

Supports

Yes

Yes

Yes

Yes

Update

Yes

Yes

Yes

Yes

UpdateBatch

Yes

Yes

Yes

Yes

An Example: Using ADO over the Web

Let us conclude this chapter with a simple real-world illustration of the use of ADO. Many web sites expose data from an underlying database. Now, it is quite easy to save an Access table in the form of an HTML page, using Access’ Export feature. However, the resulting data is static. To generate dynamic data in response to a user’s input, we need to do some programming.

One of my duties is to maintain a web site called The Mathematics Online Bookshelf (http://www.mathbookshelf.com). This site is essentially a frontend for a searchable Jet database of several thousand high-level mathematics books. The user can fill in a search form and click a Search button. All matching records will be returned to the user over the Web. Let’s look at a simplified version of the ADO code used to search the database. (Incidentally, the context of this code is an Active Server Pages (ASP) file, and the scripting language is VBScript. However, you don’t need to know anything about these technologies.)

Figure 17-8 shows a greatly simplified version of the search form. This version allows user input of author, title, and publisher, and the principle is the same for more complicated forms.

A search page
Figure 17-8. A search page

We begin by noting that in VBScript, the Like operator uses a percent sign (%) to represent any string and an underscore ( _ ) to denote any single character. (This is the syntax of regular expressions.)

First, we declare some variables. Since this code is written as VBScript, variables are declared without a type. Note that we include variables that correspond to the values of each search-form control.

' Declare variables
Dim cn, rs, sSQL
Dim author, authorexact, title, titleexact, publisher
Dim connective
Dim cMatches

Then we assign the variables to the control’s values, as returned by the ASP Request object.

' Gather input from search form
author = Request("txtAuthor")
authorexact = Request("optAuthor")
title = Request("txtTitle")
titleexact = Request("optTitle")
publisher = Request("lstPublishers")

Now we open an ADO connection to the database, which is called MobBooks, and declare a recordset variable for later use.

' Open a connection to MobBooks database
Set cn = Server.CreateObject("ADODB.Connection")
Set rs = Server.CreateObject("ADODB.Recordset")

cn.Provider = "Microsoft Jet 3.51 OLE DB Provider"
cn.ConnectionString = "Data Source=" & Server.MapPath("/MobBooks.mdb") & ";Jet OLEDB:
Database Password=""xxxxx"""
cn.Open

Now we can build an SQL statement based on the contents of the search form, as contained in the variables.

We begin by creating a JOIN between the MobBooks and the MobPubs tables. The reason is that the PUB field in the MobBooks table contains abbreviations for the publisher names, but we want to display the full publisher names, which are in the MobPubs table.

' Build SQL statement

' Start with a join between MobBooks and 
' Publishers to pick up Long name of publisher
sSQL = "SELECT MobBooks.*, MobPubs.[LONG NAME] AS Publisher" 
sSQL = sSQL & " FROM MobBooks INNER JOIN MobPubs ON MobBooks.PUB = MobPubs.PUBLISHER"

connective = " WHERE "

' Publisher
If publisher <> "-All Publishers-" Then
   sSQL = sSQL & connective & "([Long Name] = '" & publisher & "')"
   connective = " AND "
End If

' Author
if author <> "" then 
   if authorexact = "exact" then
      sSQL = sSQL & connective & "(AU='" & author & "')"
   else
      sSQL = sSQL & connective & "(AU Like '%" & author & "%')"
   End If
   connective = " AND "
End If

' Title
if title <> "" then 
   if titleexact = "exact" then
      sSQL = sSQL & connective & "(Title='" & title & "')"
   else
      sSQL = sSQL & connective & "(Title Like '%" & title & "%')"
   End If
   connective = " AND "
End If

Next we open the recordset:

' Open recordset
rs.Open sSQL, cn

Now we can write the search results to HTML output, using the Write method of the ASP Response object (the HTML header has already been written):

' Write search results to html output

' First write search form's control values for reference
connective = ""
Response.Write "<font color='Green'>Search Criteria</font><br>"
If title <> "" Then
   Response.Write connective & "<font color='Blue'>Title</font>:" & title
   connective = ";  "
end if
If author <> "" Then
   Response.Write connective & "<font color='Blue'>Author</font>:" & author
   connective = ";  "
End If
If publisher <> "" Then
   Response.Write connective & "<font color='Blue'>Pub</font>:" & publisher
End If
Response.Write "<br>"
connective = ""

' Loop through recordset
cMatches = 0
Do While Not rs.eof

   cMatches = cMatches + 1

   Response.Write "<HR><font color='Green'>" & cMatches & " - " & 
   rs("Title") & "</font>"
   Response.Write "<br>" & rs("Au")

   ' Collect bibliographic data from recordset
   bib = ""
   if rs("Date") <> "" then bib = bib & ", " & rs("Date")
   if rs("ISBN") <> "" then bib = bib & ", " & rs("ISBN")
   if rs("Pages") <> "" then bib = bib & ", " & rs("Pages") & " pp."
   if rs("Price") <> "" then bib = bib & ", $" & rs("Price")

   ' Remove leading comma and space and print it
   bib = "<br>" & mid(bib, 2)
   Response.write bib

   ' Write TOC
   if rs("TOC") <> "" then Response.write 
      "<br><font color='Green' ><i>Contents</i></font>: " & rs("TOC")

   rs.MoveNext
Loop

Response.Write "<HR>"

rs.close
cn.close

That’s it. As you can see, a little ADO programming is all it takes to “publish” an Access database over the Web.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.208.183