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.
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.
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.”
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.
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.
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).
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.
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:
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.
Provides support for saving a Recordset object to a file and restoring a Recordset object from a file.
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.
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).
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.
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!
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.
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.
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 represents a connection to a data store through a data provider.
The main properties of the Connection object are:
Sets the length of time to wait for a response to a command from the data source before issuing a timeout error message.
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.
Sets the length of time to wait for a connection to be made before issuing a timeout error message.
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.
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.
Returns the Errors collection of all Error objects (if any) for the previous command.
Specifies the access mode for the connection and can be set to any one of the following:
Signals that permission has not yet been set or cannot be determined. This is the default.
Is read-only permission.
Is write-only permission.
Is read/write permission.
Prevents other users from opening the connection with read permission.
Prevents other users from opening the connection with write permission.
Prevents other users from opening the connection.
Prevents other users from opening the connection with any permission.
Specifies the data provider. Note that the data provider can alternatively be specified in the ConnectionString property.
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
Returns the ADO version number as a string.
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
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.
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:
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.)
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.
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.
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.
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.
The main properties of the Recordset object are described here:
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
).
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.
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.
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.
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
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.
As discussed earlier, this property specifies the location of the cursor: client-side or server-side.
As discussed earlier, this property specifies the type of cursor: dynamic, keyset, static, or forward-only.
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:
Indicates that no editing operation is in progress.
Indicates that the data in the current record buffer has been modified but has not yet been saved.
Indicates that the AddNew
method has been
invoked and the new data in the current record buffer
has not yet been saved.
Indicates that the current record has been deleted.
This returns the Fields collection for the given recordset. We will discuss Field objects later in the chapter.
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.
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.
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.
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.
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
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.
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"
.
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.
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.
Let us discuss the main properties of the Command object.
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.
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.
Sets or returns the length of time to wait for the command to execute before displaying a timeout error. The default is 30 seconds.
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.
Can be used to assign a name to a command.
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.
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
.
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).
Recordset |
Parameter |
Field |
Connection |
Command |
each have a Properties property that returns a Properties collection. This collection contains a Property 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:
Identifies the property, as in the previous code.
An integer that specifies the data type of the property. It can be one of the values in Table 17-2.
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:
Indicates that the Type value is an array of values.
Indicates that the Type value is a pointer to a value.
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.
A Variant containing the value of the dynamic property.
A Long that describes attributes of the property. It can be a sum of one or more of the following values:
The property is not supported by the data provider.
The user must specify a value for this property before the data source is initialized.
The property is optional.
The property can be read.
The property can be set.
To illustrate, consider the PropertiesExample procedure shown in Example 17-3.
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 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.
Here are the properties of the Field object:
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
.
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.
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.
The field value is writable.
The provider cannot determine if we can write to the field.
The field contains fixed-length data.
The field accepts Null
values.
Null
values
can be read from the field.
The field is a long binary field. Hence, the
AppendChunk
and
GetChunk
methods
are available for this field.
The field contains some type of record ID, such as a record number or unique identifier.
The field contains a time or date stamp used to track updates.
The provider caches field values and subsequent reads are done from the cache.
This is the name of the field. Note that the Name property is read-only for Field objects.
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.
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.
This specifies the data type for the field. The possible values are listed earlier in Table 17-1.
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.
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.
' 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.
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.
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.
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
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.
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.
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.
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.
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.
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.)
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.
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.
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.
Property | ForwardOnly | Dynamic | Keyset | Static |
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 |
Method | ForwardOnly | Dynamic | Keyset | Static |
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 |
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.
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.
18.221.208.183