Microsoft Data Access Technical Articles Overview

Microsoft Data Access SDK provides technical articles to address cross-technology and comprehensive overview topics, and to bring you late-breaking information on DA SDK component technologies. Articles that comprise information supplementary to documentation on component technologies are noted in the listing in this overview.

Note You can also find some of these articles and others not listed here at http://www.microsoft.com/data/.

For information on support for Microsoft data access components, see the Getting Help from Microsoft Technical Support page.

The information contained in these technical articles represents the current view of Microsoft Corporation on the issues discussed as of the date of publication. Because Microsoft must respond to changing market conditions, it should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information presented after the date of publication.

These documents are for information purposes. MICROSOFT MAKES NO WARRANTIES, EXPRESS OR IMPLIED, IN THESE DOCUMENTS.

The names of companies, products, people, characters, and/or data mentioned herein after fictitious and are in no way intended to represent any real individual, company, product, or event, unless otherwise noted.

© 1998 Microsoft Corporation. All rights reserved.

Universal Data Access

  • Manage Data from Myriad Sources with UDA Interfaces

  • OLE DB/ADO: Making Universal Data Access a Reality

  • Universal Data Access Q & A

  • Data Access Technologies

  • Microsoft Strategy for Universal Data Access

Data Access Technologies

  • Designing Efficient Applications for Microsoft SQL Server

  • Improving Performance of the Data Access Components when Running with Internet Information Server4.0

ADO

  • Exploring ActiveX Data Access Objects from an RDO Point of View

  • ADO and SQL Server Developer’s Guide

  • Using ADO with Windows Foundation Classes

OLE DB

  • Data Access for the Masses Through OLE DB

  • OLE DB for the ODBC Programmer

  • OLE DB Sample Provider (supplement to OLE DB documentation)

  • OLE DB Leveling (supplement to OLE DB documentation)

  • Conformance Testing in OLE DB 2.0 (supplement to OLE DB documentation)

OLE DB for Online Analytical Processing (OLAP)

  • OLE DB for OLAP Sample Applications (supplement to PivotTable Service documentation)

ODBC

  • ODBC 3.7 Visual Studio Analyzer Events (supplement to ODBC documentation)


Mapping ADO Methods to OLE DB Interfaces

Use the following information as a guide for conformance testing.

OLE DB Interfaces ADO Requires
IAccessor

IColumnsInfo

IConvertType

IDBCreateSession

IDBInitialize

IDBProperties

IOpenRowset

IRowset

IRowsetInfo

ISourcesRowset (Note: ISourceRowset is implemented in the OLE DB Enumerator provided in the MSDA SDK. It is only requiered if the provider friendly name is used instead of ProgID.)

OLE DB Interfaces ADO Uses, If Available

IColumnsRowset

ICommand

ICommandPrepare

ICommandProperties

ICommandText

ICommandWithParameters

IDBCreateCommand

IErrorInfo

IErrorRecords

IRowsetChange

IRowsetLocate

IRowsetResynch

IRowsetScroll

IRowsetUpdate

IRowsetView

ISQLErrorInfo

IViewChapter

IViewFilter

IViewSort

ITransaction

ITransactionLocal

OLE DB Interfaces Not Used by ADO 2.0

IDBInfo

IDBSchemaRowset

IGetDataSource

IRowsetIdentity

IRowsetNotify

ISessionProperties

ISupportErrorInfo

ITransactionJoin

ITransactionObject

ITransactionOptions

OLE DB Interfaces Used by ADO 2.1

IDBDataSourceAdmin

IIndexDefinition

IRowsetIndex

ITableDefinition

CursorType to Rowset Properties Mapping

adOpenForwardOnly | All cursor types, IRowset, IDBSequentialStream, IColumnsRowset
adOpenKeyset | DBPROP_OWNUPDATEDELETE, DBPROP_OWNINSERT, DBPROP_OTHERUPDATEDELETE, DBPROP_IRowsetLocate DBPROP_CANSCROLLBACKWARDS, DBPROP_CANFETCHBACKWARDS, DBPROP_CANHOLDROWS, DBPROP_IRowsetRefresh, DBPROP_REMOVEDELETED
adOpenStatic | DBPROP_OWNUPDATEDELETE, DBPROP_OWNINSERT, DBPROP_IRowsetLocate DBPROP_CANSCROLLBACKWARDS, DBPROP_CANFETCHBACKWARDS, DBPROP_CANHOLDROWS, DBPROP_IRowsetRefresh
adOpenDynamic | DBPROP_OWNUPDATEDELETE, DBPROP_OWNINSERT, DBPROP_OTHERUPDATEDELETE, DBPROP_OTHERINSERT DBPROP_CANSCROLLBACKWARDS, DBPROP_CANFETCHBACKWARDS, DBPROP_REMOVEDELETED

LockType to Rowset Properties Mapping

adLockReadOnly | None
adLockOptimistic | DBPROP_IRowsetChange, DBPROP_UPDATABILITY, DBPROP_IRowsetUpdate
adLockPessimistic | DBPROP_IRowsetChange, DBPROP_UPDATABILITY, DBPROP_IRowsetUpdate
adLockBatchOptimistic | DBPROP_IRowsetChange, DBPROP_UPDATABILITY, DBPROP_IRowsetUpdate

The following table maps the methods, properties, and collections of ADO objects to the corresponding OLE DB methods called.

NOTE: ADO caches information and defers the call to the OLE DB method.

ADO Object OLE DB Method
Command
    Method
    Execute ICommand::Execute
    Property
    ActiveConnection |
CommandText | ICommandText::SetCommandText
CommandTimeout | ICommandProperties::SetCommandProperties(DBPROP_COMMANDTIMEOUT)
CommandType |
Prepared | ICommandPrepare::Prepare and ICommandPrepare::Unprepare
    Collection
    Parameters get : ICommandWithParameters::GetParameterInfo or DBSCHEMA_PROCEDURE_PARAMETERS schema rowset.
    set : ICommandWithParameters::SetParameterInfo
    Properties IDBProperties::GetPropertyInfo, ICommandProperties::GetProperties and ICommandProperties::SetProperties

Connection

    Method
    BeginTrans | ITransactionLocal::StartTransaction
Close |
CommitTrans | ITransactionLocal::Commit
Execute | ICommand::Execute or IOpenRowset::OpenRowset
Open | IDBInitialize::Initialize and IDBCreateSession::CreateSession
RollBack | ITransactionLocal::Abort
OpenSchema | IDBSchemaRowset::GetRowset
    Property
    Attributes | ITransactionLocal::StartTransaction
CommandTimeout | ICommandProperties::SetCommandProperties(DBPROP_COMMANDTIMEOUT)
ConnectionTimeout | IDBProperties::SetProperties(DBPROP_INIT_TIMEOUT)
DefaultDatabase | IDBProperties::GetProperties(DBPROP_CURRENTCATALOG) and IDBProperties::SetProperties(DBPROP_CURRENTCATALOG)
IsolationLevel | ITransactionLocal::StartTransaction
Mode | IDBProperties::GetProperties(DBPROP_INIT_MODE) and IDBProperties::SetProperties(DBPROP_INIT_MODE)
Provider | ISourcesRowset::GetSourcesRowset
Version |
    Collection
    Errors | IErrorRecords
Properties | IDBProperties::GetPropertyInfo, IDBProperties::GetProperties and IDBProperties::SetProperties

Error

    Method
    None
    Property
    Description | IErrorRecords::GetErrorInfo
HelpContext | IErrorRecords::GetErrorInfo
HelpFile | IErrorRecords::GetErrorInfo
NativeError | IErrorRecords::GetCustomErrorObject and ISQLErrorInfo::GetSQLInfo
Number |
Source | IErrorRecords::GetErrorInfo
SQLState | IErrorRecords::GetCustomErrorObject and ISQLErrorInfo::GetSQLInfo
    Collection
    None

Field

    Method
    AppendChunk | ISequentialStream::Write
GetChunk | ISequentialStream::Read
    Property
    ActualSize | IAccessor::CreateAccessor and IRowset::GetData
Attributes | IColumnInfo::GetColumnInfo
DefinedSize | IColumnInfo::GetColumnInfo
Name | IColumnInfo::GetColumnInfo
NumericScale | IColumnInfo::GetColumnInfo
OriginalValue | IRowsetUpdate::GetOriginalData
Precision | IColumnInfo::GetColumnInfo
Type | IColumnInfo::GetColumnInfo
UnderlyingValue | IRowsetRefresh::GetLastVisibleData or IRowsetRefresh::GetVisibleData
Value | IAccessor::CreateAccessor and IRowset::GetData and IRowset::SetData
    Collection
    Properties IColumnsRowset::GetAvailableColumns and IColumnsRowset::GetColumnsRowset

Parameter

    Method
    AppendChunk
    Property
    Attributes get : ICommandWithParameters::GetParameterInfo or DBSCHEMA_PROCEDURE_PARAMETERS schema rowset.
    set : ICommandWithParameters::SetParameterInfo
    Direction get : ICommandWithParameters::GetParameterInfo or DBSCHEMA_PROCEDURE_PARAMETERS schema rowset.
    set : ICommandWithParameters::SetParameterInfo
    Name get : ICommandWithParameters::GetParameterInfo or DBSCHEMA_PROCEDURE_PARAMETERS schema rowset
    NumericScale get : ICommandWithParameters::GetParameterInfo or DBSCHEMA_PROCEDURE_PARAMETERS schema rowset.
    set : ICommandWithParameters::SetParameterInfo
    Precision get : ICommandWithParameters::GetParameterInfo or DBSCHEMA_PROCEDURE_PARAMETERS schema rowset.
    set : ICommandWithParameters::SetParameterInfo
    Size get : ICommandWithParameters::GetParameterInfo or DBSCHEMA_PROCEDURE_PARAMETERS schema rowset.
    set : ICommandWithParameters::SetParameterInfo
    Type get : ICommandWithParameters::GetParameterInfo or DBSCHEMA_PROCEDURE_PARAMETERS schema rowset.
    set : ICommandWithParameters::SetParameterInfo
    Value IAccessor::CreateAccessor and ICommand::Execute
    Collection
    Properties

Recordset

    Method
    AddNew | IRowsetChange::InsertRow
CancelBatch | IRowsetUpdate::Undo
CancelUpdate | None
Clone | IRowsetLocate
Close | IAccessor::ReleaseAccessor, IRowset::ReleaseRows
Delete | IRowsetChange::DeleteRows
GetRows | IAccessor::CreateAccessor, IRowsetLocate::GetRowsAt, IRowset::GetNextRows and IRowset::GetData
Move | IRowsetLocate::GetRowsAt or IRowset::GetNextRows
MoveFirst | IRowsetLocate::GetRowsAt or IRowset::RestartPosition
MoveLast | IRowsetLocate::GetRowsAt
MoveNext | IRowsetLocate::GetRowsAt or IRowset::GetNextRows
MovePrevious | IRowsetLocate::GetRowsAt or IRowset::GetNextRows
NextRecordset | IMultipleResults::GetResult
Open | IOpenRowset::OpenRowset or ICommand::Execute
Requery | IOpenRowset::OpenRowset or ICommand::Execute
Resync | IRowsetRefresh::RefreshVisibleData
Supports | IRowsetInfo::GetProperties
Update | IRowsetChange::SetData and/or IRowsetUpdate::Update
UpdateBatch | IRowsetUpdate::Update
    Property
    AbsolutePage get : IRowsetScroll::GetApproximatePosition.
    set : IRowsetScroll::GetRowsAtRatio or IRowsetLocate::GetRowsAt
    AbsolutePosition get : IRowsetScroll::GetApproximatePosition.
    set : IRowsetScroll::GetRowsAtRatio or IRowsetLocate::GetRowsAt
    ActiveConnection IDBInitialize::Initialize and IDBCreateSession::CreateSession
    BOF None
    Bookmark IAccessor::CreateAccessor and IRowsetLocate::GetRowsAt
    CacheSize cRows argument to IRowsetLocate::GetRowsAt or IRowset::GetNextRows
    CursorType ICommandProperties::SetProperties
    EditMode IRowsetUpdate::GetRowsStatus
    EOF None
    Filter IRowsetUpdate::GetPendingRows and IRowsetLocate::GetRowsByBookmark OR IRowsetView::CreateView and IRowsetView::OpenViewChapter and IViewFilter::SetFilter
    LockType ICommandProperties::SetProperties
    MaxRecords IOpenRowset::OpenRowset(DBPROP_MAXROWS) or ICommandProperties::SetCommandProperties(DBPROP_MAXROWS)
    PageCount IRowsetScroll::GetApproximatePosition
    PageSize Used for absolute positioning with PageCount and AbsolutePage properties
    RecordCount IRowsetScroll::GetApproximatePosition
    Source
    Status IRowsetUpdate::GetRowStatus
    Collection
    Fields | IColumnInfo::GetColumnInfo
Properties | IRowsetInfo::GetPropertyInfo, IRowsetInfo::GetProperties and IRowsetInfo::SetProperties


Exploring ActiveX Data Objects from an RDO Point of View

Leveraging existing software designs with the new Universal Data Access interface from Microsoft.

William R. Vaughn
Microsoft® Visual Studio™
Enterprise Product Manager

Introduction

No consultant worth his or her salt will tell you to fix something that isn’t broken. This paper is not about "fixing" existing Remote Data Objects (RDO) applications that are doing the job they were designed to do. It is about knowing what benefits can be achieved by using ActiveX® Data Objects (ADO) when it makes sense. ADO adds a number of new choices when you need to access data. Specifically, ADO is designed to better address the special needs of Internet developers, developers needing to access special types of data—in addition to traditional relational sources, and in the process, permit creation of faster, smaller and more efficient applications. With the release of version 1.5, ADO is well on its way to becoming the de facto standard for data access with the Visual Basic® development system and the rest of the Visual Studio development system languages including Visual C++®, Visual J++™, Visual Basic Scripting Edition, Visual InterDev™ and Microsoft® Office applications.

Question: Are developers going have to re-write their RDO code now that ADO is here? The answer to this questions is no—not unless the ADO’s expanded features offer real solutions to problems that RDO can’t solve.

This paper discusses why Microsoft has chosen to create a new all-encompassing data access interface and how your applications can take advantage of its new features. Does ADO replace RDO? No, ADO is implemented alongside of RDO and DAO as both of these established programming interfaces are still supported. Does ADO 1.5 functionality replace RDO 2.0? No, today ADO does not implement everything that RDO supports. However, ADO 2.0 will support a superset of RDO’s functionality.

This information is presented from the point of view of an RDO developer. After a brief outline of how we got to this point, we walk through a number of typical scenarios in RDO and show how to implement the same functionality in ADO. This paper by no means covers all of ADO’s functionality. For example, ADO implements a number of new features that makes it far better suited for code run remotely on an IIS server, thus making development of Microsoft® Transaction Server or Visual Basic Scripting Edition components easier. However, that’s the meat of another paper.

How Did We Get Here?

Over the last six years or so, Microsoft has been driven by customer demand to release an ever-expanding series of data access interfaces. Beginning with the DB-Library interface for Visual Basic ("VBSQL"), and ending with the most recent interface to OLE DB—ADO. Each of these interfaces addressed broadening customer requirements. As Visual Basic applications grew more sophisticated and data-centric, so did the data access interfaces provided:

  • VBSQL – an API-interface to Microsoft® SQL Server™ built around the C-based DB-Library interface. VBSQL provided the high-speed, lightweight interface Visual Basic developers needed to access databases on both Microsoft and Sybase SQL Server. VBSQL cannot access any data source other than a SQL Server—and today VBSQL (and DB-Library) are limited to just Microsoft SQL Server version 6.5 and older. While VBSQL will work with the upcoming version of Microsoft SQL Server, it won’t be upgraded to support Microsoft SQL Server 7.0’s new features.

  • ODBC—a widely accepted standard data access interface designed to access any relational database with an ODBC driver. By placing all DBMS-dependent code in the driver, developers could access a common API interface regardless of the back-end database. This gave developers the flexibility they needed, but often at the price of more complex applications. While the ODBC API had fewer calls than VBSQL (and DB-Library), these calls required more and more complex arguments. Because the ODBC API also did not include an interface layer like VBSQL, Visual Basic-based applications were exposed to the expense and hazards of direct API development. In general, ODBC as an interface has been widely accepted, but only when accessed by an object-based interface like DAO, ODBCDirect, or RDO—or even ADO. When the goal was to create a single application that could access more than one database (for example, Microsoft® Access/ Microsoft® Jet and SQL Server), applications often had to add back-end-specific code to deal with special cases or complex data types. This meant the dream of writing a single application to reference more than one type of DBMS was often frustrated by reality. Over the years, ODBC has evolved to be the most widely accepted means to access relational data sources—and some non-relational data sources. The following diagram shows the basic architecture of an ODBC application.

ODBC Application Architecture

  • DAO—The first object-based interface that exposed the Microsoft Jet database engine to access native Jet databases, a number of popular ISAM databases, and ODBC data sources. DAO was also coupled into the Visual Basic-based Data control which exposed an entirely new paradigm of "bound" or data-aware controls. This made it easier than ever to create data access applications. However, this ease of use also meant that developers had to be extra-mindful of techniques that made scaling more difficult. DAO is one of the most widely used object-based data access interfaces in use today as millions of Visual Basic and Microsoft Access developers have used it to build applications of all kinds. ODBCDirect is a variation on DAO that exposes many RDO functions.

The evolution of object-based data access interfaces.

  • RDO—Remote Data Objects is another object-based data access interface created to provide a tight, lightweight interface to ODBC, while still retaining the ease of development provided by DAO. By patterning its object model after DAO, RDO provided an easy-to-program interface that exposed virtually all of ODBC’s low-level power and flexibility. RDO is somewhat limited in that it did not access Jet or ISAM databases particularly well, and could only access relational databases through existing ODBC drivers. However, RDO has proven to be the interface of choice for a large number of SQL Server, Oracle and other large relational DBMS customers. RDO provides the objects, properties and methods needed to access the more complex aspects of stored procedures and complex result sets.

  • OLE DB—This new low-level object-based interface introduces a "universal" data access paradigm. That is, OLE DB is not restricted to ISAM, Jet, or even relational data sources, but is capable of dealing with any type of data—information—regardless of its format or storage method. OLE DB is not designed to be accessed directly from Visual Basic due to its complex interfaces but ADO exposes virtually all of OLE DB’s functionality—so this is really not a requirement for Visual Basic developers.

  • ADO—ActiveX Data Objects is the object-based interface to OLE DB. By using a DAO-like data access interface, developers can now access an even broader variety of data sources—using both OLE DB service providers and existing ODBC drivers through its OLE DB for ODBC intermediate interface. ADO’s most serious shortcoming at this point is its scope. Currently, ADO only implements part of RDO’s broad range of functionality—but ADO version 2.0 is expected to implement a superset of both RDO and DAO. This paper will outline the current state of ADO development and briefly outline how RDO’s object model maps to ADO.

  • RDS-Remote Data Service ADC has now been integrated with ActiveX Data Objects (ADO) to provide data remoting within the same programming model as ADO. This makes it easier to design, code, and deploy both web-based and LAN-based applications. To clarify the relationship of ADC to ADO, ADC is now known as the Remote Data Service (RDS), a feature of ADO. RDS goes beyond the current generation of web data access tools by allowing clients to update the data they see.

A Closer Look at ADO

Microsoft ActiveX Data Objects (ADO) enable your client applications to access and manipulate data in a database server and other data stores through any OLE DB provider. The primary benefits of ADO are ease of use, high speed, low memory overhead, and a small disk footprint. ADO supports key features for building client/server and web-based applications, including the following:

  • Independently-created objects. Unlike Data Access Objects (DAO) or Remote Data Objects (RDO), you no longer have to navigate through a hierarchy to create objects because most ADO objects can be independently created. For example, you don’t have to open a connection to create a Recordset—you simply have to declare it. This allows you to create and track only the objects you need, and also results in fewer ADO objects and thus a smaller working set. This technology also enables an entirely new data access paradigm—that of dissociate result sets managed independently of connections and specific data sources.

  • Batch updating helps improve performance by locally caching changes to data, then writing them all to the server in a single update.

  • Support for stored procedures with in/out parameters, return values and multiple recordsets.

  • Different cursor types, including the potential for support of back-end–specific cursors.

  • Support for limits on number of returned rows and other query goals for performance tuning.

  • Automatic or manual parameters management for better performance and more control over parameters.

  • Free-threaded objects for efficient web server applications.

Note: While ADO supports these features, all of the underlying providers and drivers called by ADO may not. Check the topic, Using OLE DB Providers with ADO located on www.microsoft.com/data/ado/ as well as the documentation for the underlying providers and drivers to determine what functionality they support. Unless otherwise noted in the language reference topics, all of the ADO 1.5 Objects, Methods, and Properties described in this document are available when used with the Microsoft OLE DB Provider for ODBC and Microsoft SQL Server version 6.5.

Remote Data Service

ADO’s Remote Data Service (RDS) allows data remoting. This means you can move data from a server to a client application or web page, manipulate the data on the client, and return updates to the server in a single round trip. Previously released as Advanced Data Connector 1.0, RDS has been combined with the ADO programming model to simplify client-side data remoting. For more information, see the Remote Data Service documentation.

Installing ADO

As part of the Microsoft Data Access Components (MDAC), ADO and RDS are automatically installed and registered by a host product, such as Microsoft® Internet Information Server. The host product setup program may require that you specifically request the Microsoft Data Access Components in order to have ADO and RDS installed. In any case, you can find ADO and MDAC on the web at http://www.microsoft.com/data/.

ADO is a dual-interface COM type library. The filename is msado15.dll. The program ID (ProgID) is "ADODB." In 2 and 3 tier database applications, ADO code that is to execute on the client uses the ProgID "ADOR" instead.

To use ADO with Microsoft Visual Basic or Microsoft Office, you also need to establish a reference to the ADO type library. Select References from the Project menu, check the box for "Microsoft ActiveX Data Objects 1.5 Library," and then click "OK." ADO objects, methods, and properties will then be accessible through the Visual Basic for Applications(VBA) Object Browser and the IDE Editor.

To use ADO with Microsoft Visual J++, you also need to create a set of class files using the "Java Type Library Wizard". Select the Java Type Library Wizard from the Tools menu, check the box for "Microsoft ActiveX Data Objects 1.5 Library," and then click "OK." The wizard will then create the necessary Java class files for ADO.

The New ADO Object Model

ADO "flattens" the rather complex RDO (or DAO) object model to facilitate understanding and use of the object model. Shown below is a diagram showing the ADO interfaces and how they relate to one another:

ADO Interfaces

Although ADO objects can be created outside the scope of a hierarchy, the objects exist within hierarchical relationships, as shown in the ADO Object Model. Notice that this model seems considerably simpler than RDO’s, so don’t expect a 1:1 correspondence with ADO objects, properties and methods. There will be lots of documentation and helpful hints on how to map the DAO and RDO model to ADO.

There are 7 objects in the ADO object model. In some ways these objects are similar to RDO objects but often combine functionality into a flatter model.

  • Command - Maintains information about a command, such as a query string, parameter definitions, etc. You can execute a command string on a Connection object or a query string as part of opening a Recordset object, without defining a Command object. The Command object is useful where you want to define query parameters, or execute a stored procedure that returns output parameters. The Command object is similar to RDO’s rdoQuery object. It supports a number of new properties used to describe the type and purpose of the query—these help ADO optimize the operation being performed.

  • Connection - Maintains connection information with the data provider. This object is similar to the rdoConnection object in most respects. The connection maintains properties to indicate the cursor type, connect string, query (command) timeout, connection timeout, and other ADO-specific properties such as Default Database and Isolation level (and more).

  • Error - Contains extended error information about an error condition raised by the provider. Since a single statement can generate two or more errors, the Errors collection can contain more than one Error object at a time, all of which result from the same incident.

  • Field - Contains information about a single column of data within a Recordset. The Recordset object features a Fields collection to contain all of its Field objects. This is equivalent to RDO’s rdoColumn object but it has far more information about the field than RDO such as precision and numeric scale. Richer data type support is also available in ADO.

  • Parameter - A single parameter for a parameterized Command. The Command object features a Parameters collection to contain all of its Parameter objects. This is equivalent to RDO’s rdoParameter object but again has more detail than provided in RDO. Like RDO’s rdoParameters collection, ADO’s Parameters object can be created automatically by sending queries to the DBMS. However, you can also build this collection yourself to improve performance at runtime—something RDO does not support.

  • Property - A provider-defined characteristic of an ADO object. There is no RDO equivalent to this object, but DAO has an object something like this. ADO objects have two types of properties: built-in, and dynamic. This is one of ADO’s neatest features in that it lets the ADO service provider author expand ADO to address special interfaces that only this driver supports.
    • Built-in properties are those properties implemented in ADO and immediately available to any new object, using the familiar MyObject.Property syntax. Built-in properties do not appear as Property objects in an object’s Properties collection, so while you can change their values, you cannot modify their characteristics or delete them.

    • Dynamic properties are defined by the underlying data provider, and appear in the Properties collection for the appropriate ADO object. For example, a property specific to the provider may indicate if a Recordset object supports transactions or updating.

These additional properties appear as Property objects in that Recordset object’s Properties collection. Dynamic properties can be referenced only through the collection, using the MyObject.Properties(0) or MyObject.Properties("Name") syntax. You will find that each data provider (might) implement one or more special properties to deal with special provider-specific operations.

  • Recordset - A set of records returned from a query and a cursor into those records. You can open a Recordset (i.e., execute a query) without explicitly opening a Connection object. However, if you do first create a Connection object, you can open multiple Recordset objects on the same connection. Generally, the ADO Recordset is equivalent to RDO’s rdoResultset. Again, the ADO object supports a number of new (or changed) properties and methods. We will be discussing these as we go.

The ADO Object Model from an RDO Perspective

Generally, the ADO object model is flatter (has fewer objects) but more properties, methods and method arguments. For example, there are no equivalents to the rdoEngine or rdoEnvironment objects—these expose the ODBC driver manager and hEnv interfaces. The InfoMessage event supplied by the rdoEngine object is not supported in ADO at this time (nor are any of the events). Nor can you create ODBC data sources from ADO—despite the fact that your interface might be through the ODBC OLE DB service provider—but this is coming in a later version. As we work through the rest of the ADO interface, we will find other dissimilarities. Some of these differences are covered by new approaches to the problem (and are addressed), some were left behind for one good reason or another, and some of which are yet to be implemented.

Migrating to ADO

With the introduction of ADO, developers can create applications that perform all of the fundamental data-access operations that all client/server and multi-tier applications require. ADO 2.0 promises to implement a superset of RDO’s and DAO’s functionality. No, that does not mean that the code you have been writing in RDO and DAO will automatically morph over to equivalent ADO code when the time comes to convert. However, it does mean the problems you have solved with these older object models in the past will be solvable with ADO—and more--ADO implements a number of new data access strategies never before available to address new problems.

When deciding to migrate to ADO, you have to decide if ADO’s additional capabilities are enough to justify converting existing software. Another consideration is how your existing developers can leverage their existing RDO development skills. Although RDO will be available indefinitely, it is always a good idea to consider ADO for applications currently under design.

Because ADO 2.x is expected to exceed RDO’s functionality, ADO work done now can be leveraged even more easily when ADO 2.0 is released. However, if you must use the features available in RDO 2.0 today, be sure to study this paper for code that might not be easy to convert or for programming paradigms that are most like ADO’s. This will make the eventual transition easier.

Which Data Access Interface Is the "Right" One?

To get an in-depth understanding of the issues, see the paper "Choosing the Right VB5 Data Access Interface" on http://www.microsoft.com/vbasic/. This paper concludes that if ADO will address your current needs, use it. Otherwise, for new designs, use RDO for client/server systems and DAO for ISAM or small departmental systems. There are other factors to consider when choosing a data access interface, so this simplistic statement should not be taken without considering the other factors discussed in the aforementioned paper.

Using RDO to Solve Basic Data Access Problems

The following sections discuss how to solve some basic data access problems using RDO. This section is followed by a parallel discussion on how to perform the same operations with ADO. Each of these "problems" addresses a SQL Server database—a variation of the Biblio database installed with Visual Basic 5.0.

RDO: Establishing a Connection

Getting a connection open requires a connect string and a decision or two about how ODBC is to create the connection. Note that a connection is not required by RDO to create an rdoQuery object, but is required to initially create an rdoResultset object.

Dim cn As New rdoConnection
Dim cnB As New rdoConnectionConst ConnectString = "uid=billva;pwd=bv;" _
driver={SQL Server};server=sequel2;" _
database=biblio;dsn=''"

This connect string accesses a specific SQL Server and permits ODBC to open a "DSN-less" connection. Note that this is a typical ODBC connect string with all of the standard arguments.

Private Sub Form_Load()
With cn
cn.Connect = ConnectString
cn.LoginTimeout = 10
cn.CursorDriver = rdUseNone
cn.EstablishConnection rdDriverNoPrompt
End With

Establish a connection specifying the type of cursor driver and login timeout. By default, RDO uses rdUseIfNeeded, which invokes server-side cursors on SQL Server—we override this in our example. We also choose to generate an error if the user-id and password do not match. The second connection performs the client-batch updates. Notice that the connect string used in the first operation.

With cnB
cnB.Connect = ConnectString
cnB.CursorDriver = rdUseClientBatch
cnB.EstablishConnection
End With
End Sub

Handle any errors that occur when the connection is opened. This event fires when the connection operation is completed so we can test to see if it worked and enable any buttons that rely on an open connection.

Private Sub cn_Connect(ByVal ErrorOccurred As Boolean)
If ErrorOccurred Then
MsgBox "Could not open connection", vbCritical
Else
RunOKFrame.Enabled = True
End If
End Sub

RDO: Run a Basic Query

To return a basic result set based on an SQL statement. In this case we execute a restricted query and pass the result set to a control that simply dumps the result to the MSFlexGrid. Building a result set requires use of an open connection.

Private Sub RunButton_Click()
Dim rs As rdoResultset
Set rs = cn.OpenResultset("select * from titles where title like '%h'")
rdoGrid1.ShowData rs
rs.Close
End Sub

RDO: Display a Result Set in the MSFlexGrid

This routine is the ShowData method of a custom ActiveX control used to display data from a result set in an MSFlexGrid. The code sets the grid up based on the rdoColumns property titles and initializes the grid making it ready for the data. Note the use of the OrdinalPosition property to index the result set rdoColumns property. There are two sets of code to extract data from the rdoResultset—one that uses GetClipString, and another that uses the GetRows method to contrast typical approaches to this problem. Note that ADO 1.5 does not initially support the GetClipString method—its support in ADO 2.0 is under consideration.

Public Function ShowData(Resultset As rdoResultset) As Variant
Dim cl As rdoColumn
Static GridSetup As Boolean
Dim MaxL As Integer
Dim rsl As rdoResultset
Dim Rows As Variant
On Error GoTo ShowDataEH
Set rsl = Resultset
If GridSetup Then
Else
FGrid1.Rows = 51
FGrid1.Cols = rsl.rdoColumns.Count
FGrid1.Row = 0
For Each cl In rsl.rdoColumns
FGrid1.Col = cl.OrdinalPosition - 1
FGrid1 = cl.Name
If rsl.rdoColumns(cl.OrdinalPosition - 1).ChunkRequired Then
MaxL = 1
Else
MaxL = rsl.rdoColumns(cl.OrdinalPosition - 1).Size + 4
End If
If MaxL > 20 Then MaxL = 20
FGrid1.ColWidth(FGrid1.Col) = TextWidth(String(MaxL, "n"))
Next cl
GridSetup = True
End If

FGrid1.Rows = 1 'Clear Grid of data (except titles)
FGrid1.Rows = 51
FGrid1.Row = 1
FGrid1.Col = 0
FGrid1.RowSel = FGrid1.Rows - 1
FGrid1.ColSel = FGrid1.Cols - 1
FGrid1.Clip = rsl.GetClipString(50, , , "-")
' With FGrid1
' Rows = rsl.GetRows(FGrid1.Rows)
' For i = 1 To UBound(Rows, 2) ' Loop through resultset returned
' .Row = i
' For j = 0 To UBound(Rows, 1) - 1
' .Col = j
' .Text = "" & Rows(j, i)
' Next j
' Next i
' .Row = 1
' End With
ExitShowData:
FGrid1.RowSel = 1
FGrid1.ColSel = 0
Exit Function

ShowDataEH:
Select Case Err
Case 40022:
FGrid1.Clear
Resume ExitShowData
Case 13
FGrid1.Text = "< >"
Resume Next
Case Else
MsgBox "Could not display data: " & Err & vbCrLf & Error$
Resume ' ExitShowData
End Select

End Function

Using ADO to Solve Basic Data Access Problems

The following discussion walks through code used to perform the same basic operations using ADO. Initially we create a set of stand-alone ADO objects referenced off of the ADODB object. These objects are fleshed out later in the code when we set specific properties to open connections and execute result sets.

ADO: Establishing a Connection

Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
Dim cnB As New ADODB.Connection
Dim Qy As New ADODB.Command

The next line of code creates a connect string—just like RDO’s. In both cases we are using ODBC’s "DSN-less" connection strategy to save time and increase performance. It seems the documentation calls for a somewhat different Connect string that references an ADO "Provider" instead of a "Driver" and does not call for the "dsn=" syntax, but the ODBC connect string used in RDO seems to work fine.

Const ConnectString = "uid=billva;pwd=bv;driver={SQLServer}; _ server=sequel2;database=biblio;dsn=''"

These next declarations set up the variables used throughout the example. Note creation of a variant array to hold the result set.

Dim sql As String
Dim rc As Integer
Dim i As Integer
Dim Changes As Integer
Dim bms() As Variant

Next, we open the ADO connection to our database in the Form_Load event. Note that this code is very similar to RDO except that the constants are prefixed with "ad" instead of "rd". While this is the case in this example, many ADO constants are not the same as their RDO equivalents so you cannot just change the first two letters and expect them to work.

Note that we have to use the ADO Properties collection to deal with the desired prompt behavior—but ADO defaults to "no prompt" which makes more sense anyway, so you should not have to specify the prompting behavior. In RDO, we could simply set the behavior using the OpenConnection argument. In ADO we have to set the Properties ("Prompt") property—an example of this is shown in the code.

Keep in mind that the code in your RDO Connect event is handled differently—probably in an in-line ADO error handler, as ADO does not (yet) support events. This means that you will have to work with situations where the "informational" messages sent back by SQL Server will be discarded and that asynchronous open connection routines will have to be coded using synchronous techniques.

Private Sub Form_Load()
With cn
' Establish DSN-less connection
.ConnectionString = ConnectString
.ConnectionTimeout = 10
.CursorLocation = adUseNone
‘ .Properties("Prompt") = adPromptNever ‘ This is not required in ADO
.Open
End With
With cnB
.ConnectionString = ConnectString
.CursorLocation = adUseClientBatch
.Open
End With
End Sub

ADO: Run a Basic Query

Once the connection is open, we can proceed to running a query. This code is very similar to the code we just executed with RDO. However, in this case we use the new ADO Open method that takes the SQL query and the ADO Connection object as arguments—instead of using the rdoConnection object’s .OpenResultset object. Yes, you can also use the ADO Connection object’s Execute method—just as you could in RDO as long as it didn’t return a rowset.

No you won’t be able to run this query asynchronously in ADO—not until version 2.0. Yes, you can choose to process the query’s resultset asynchronously. That is, ADO will tell the cursor driver to complete population of the result set in the background by setting the "IDBAsynchStatus" property to True. No, ADO will not return control any sooner when this option is set as with RDO asynchronous operations, but the cursor will be populated (in time) without your code having to move to the end of the result set.

Private Sub RunButton_Click()
Dim rs As New ADODB.Recordset
rs.Open "select * from titles where title like '%h'", cn
ADOGrid1.ShowData rs
rs.Close
End Sub

ADO: Display a Result Set in the MSFlexGrid

The following code implements the same ShowData method of a custom ActiveX control adapted from the RDO control. In this case, since we can’t use GetClipString, we use GetRows instead. Since we then have to parse the resulting Variant array ourselves, the routine is noticeably slower. If you have problems getting the GetRows function to work, you will have to convert to a more conservative (read slower) approach that loops through the rows. However, this technique should be avoided as it is far less efficient than either using GetRows or binding directly to a control.

Note how the OrdinalPosition property can no longer be used as an index on the Fields collection to pull out the column titles (as we could in RDO). To deal with this substitute a new integer counter to address the column being worked on. We also had to use the DefinedSize and ActualSize properties to find the TEXT and IMAGE data type fields that would not fit in a column. These new properties make it easier to determine the details of specific field values. I also added additional code to deal with BLOB types when we worked through the data columns.

Public Function ShowData(Resultset As Recordset) As Variant
Dim cl As Field
Static GridSetup As Boolean
Dim MaxL As Integer
Dim Op As Integer
Dim rsl As Recordset
Dim rows As Variant
On Error GoTo ShowDataEH
Set rsl = Resultset
If GridSetup Then
Else
FGrid1.rows = 51
FGrid1.Cols = rsl.Fields.Count
FGrid1.Row = 0
Op = 0
For Each cl In rsl.Fields
FGrid1.Col = Op
FGrid1 = cl.Name
If rsl.Fields(Op).DefinedSize > 255 Then
MaxL = 1
Else
MaxL = rsl.Fields(Op).ActualSize + 4
End If
If MaxL > 20 Then MaxL = 20
FGrid1.ColWidth(FGrid1.Col) = TextWidth(String(MaxL, "n"))
Op = Op + 1
Next cl
GridSetup = True
End If
FGrid1.rows = 1
FGrid1.rows = 51
ReDim rows(FGrid1.Cols, FGrid1.rows)
With FGrid1
rows = rsl.GetRows(adGetRowsRest)
''
'' The following code is used instead of GetRows which did not work
''
''
' i = 1
' Do Until rsl.EOF Or i >= FGrid1.rows
' For j = 0 To rsl.Fields.Count - 1
' If rsl(j).Type = adLongVarBinary _
‘ Or rsl(j).Type = adLongVarChar Then
' rows(j, i) = "<binary>"
' Else
' rows(j, i) = rsl(j)
' End If
' Next j
' i = i + 1
' rsl.MoveNext
' Loop
'
' Take the array filled above and pass it to the grid
'
For i = 1 To UBound(rows, 2) ' Loop through resultset returned
.Row = i
For j = 0 To UBound(rows, 1)
.Col = j
.Text = "" & rows(j, i)
Next j
Next i
.Row = 1
End With
ExitShowData:
FGrid1.RowSel = 1
FGrid1.ColSel = 0
Exit Function
ShowDataEH:
Select Case Err
Case 40022, 3021:
FGrid1.Clear
Resume ExitShowData
Case 13, Is < 0
rows(j, i) = "< >"
Resume 'Next
Case Else
MsgBox "Could not display data: " & Err & vbCrLf & Error$
Resume ' ExitShowData
End Select
End Function

Using RDO to Access More Complex Data

The next challenge is to access data sources that are more complex than simple select queries. In this next series of exercises, we will execute a parameter-driven stored procedure (without the benefit of the UserConnection designer) and execute an optimistic batch update operation.

RDO: Executing a Parameter Query

One of the first of our more challenging tasks is to execute a fairly ordinary SELECT statement that requires parameters. This is done in two phases:

  1. Build a query that expects a parameter, pass in the first parameter and execute the query.

  2. Execute the query again with new parameters.

The code shown below implements this technique. The first time this subroutine is called, RDO is asked to create a new RDO query object. Since the object is appended to the rdoConnection object’s rdoQueries collection, we can re-reference it each time the subroutine is called. Each subsequent time the subroutine is called, the Refresh method is used to re-execute the query. This technique builds a temporary stored procedure (under the covers) that is referenced by the Requery method. This temporary SP is dropped when the connection is closed.

Private Sub ParmQueryButton_Click()
Dim Qy As New rdoQuery
Dim rs As rdoResultset
Static FirstTime As Boolean
If cn.rdoQueries.Count = 0 Then
FirstTime = True
sql = "select * from authors where year_born = ?"
Set Qy = cn.CreateQuery("Pq", sql)
End If
Qy(0) = QueryParam.Text
If FirstTime Then
Set rs = Qy.OpenResultset()
FirstTime = False
Else
rs.Requery
End If
rdoGrid1.ShowData rs
rs.Close
End Sub

RDO: Executing a Parameter-Driven Stored Procedure

Since many, many client/server applications depend heavily on stored procedures (SP), any data access interface worth its salt must be able to execute them quickly and efficiently. Stored procedures pose a number of complex issues to the developer. In some cases, stored procedures require management of OUTPUT and return status values and other, more conventional arguments. In addition, an SP can return several complex result sets including PRINT or RAISERROR statement return values. Question: does it make sense to make your SPs this complex? Well, sometimes, but not always. It might be easier to keep SPs simpler and more modular and use Visual Basic application code to tie them together.

The code samples below show how RDO approaches these problems. To start with we execute a simple parameter-based SP and show the results in our grid. We do make an accommodation or two to the subsequent ADO design, and those are noted. We will be using the same connections established in the earlier examples. We will not be comparing use of the UserConnection designer with ADO, as it is simply not supported at this time. Ideally, this is the most efficient way to execute a parameter-driven SP.

Note that our code requires us to include a correct ODBC "Call" statement. Again, this is not necessary in the UserConnection designer, but is essential here in the RDO code-based approach. We do use the stand-alone rdoQuery object and assign the already open Connection to it. This object can then be used in subsequent calls to handle a parameter query—that’s the next set of code.

Note that the code does not attempt to reference the return status argument. This value is not available until the result set is fully populated—only then does SQL Server return this value.

Private Sub RunSPButton_Click()
Dim Qy As New rdoQuery
Dim rs As rdoResultset
sql = "{? = Call AuthorByYearBorn (?,?)}"
Set Qy.ActiveConnection = cn
Qy.sql = sql
Qy.rdoParameters(0).Direction = rdParamReturnValue
Qy(1) = "1947"
Qy(2) = "1948"
Set rs = Qy.OpenResultset()
rdoGrid1.ShowData rs
ShowRows = rs.RowCount
rs.Close
End Sub

RDO: Executing a Multiple-Result Set Stored Procedure

The next challenge is to execute a query that returns more than one result set. It is not unusual for a stored procedure to return more than a single set of rows or result set that contains results from an action query—as a matter of fact, it’s quite common. You code must deal with each of the result sets individually unless you want to toss out the entire product of your query. This is fairly simple in RDO—you simply use the MoreResults method to step through the result sets one at a time. Each call to MoreResults closes the current result set and moves to the next (if there is one).

Private Sub MultipleRSButton_Click()
sql = "Select * from Authors Where year_born is not null; " _
& "Select * from Authors where year_born is null"
Set rs = cn.OpenResultset(sql)
rdoGrid1.ShowData rs
i = MsgBox("Ready for next results?", vbYesNoCancel)
If i = vbYes Then
If rs.MoreResults Then
rdoGrid1.ShowData rs
End If
End If
End Sub

RDO: Executing Action Queries

In case your application needs to manipulate tables directly, or simply perform a maintenance operation, (like SQL Server’s DBCC functions), you can use the Execute method to run the query directly. In this case we don’t need ODBC (or SQL Server) to create a temporary SP to run the query as we won’t be doing this again. Of course, if this is a regular operation, creating an SP to do it would make sense. Note that we can pick up the rows affected by this query by using the RowsAffected property.

Private Sub ExecuteButton_Click()
sql = "Begin Transaction " _
& " Update Authors " _
& " set Year_Born = 1900 where year_born is null" _
& " rollback transaction"
Screen.MousePointer = vbHourglass
cn.Execute sql, rdExecDirect
ShowRows = cn.RowsAffected
Screen.MousePointer = vbDefault
End Sub

RDO: Executing Optimistic Batch Queries

Our next challenge is to execute a query that can be used to drive a subsequent "optimistic batch update" operation. In this case, we fetch a result set using the ClientBatch cursor library and save the bookmarks for each row fetched. When the user chooses a row in the grid (where the rows are displayed), we ask the user to provide a new value and post that value to the result set. The trick is that these changes are not made against the data until we finally decide to run the BatchUpdate method.

Private Sub BatchOpsButton_Click()
Dim rs As rdoResultset
sql = "Select * from Authors where year_born is null"
cnB.QueryTimeout = 45
Set rs = cnB.OpenResultset(sql, rdOpenKeyset, rdConcurBatch)
rs.MoveLast: rs.MoveFirst
ReDim bms(rs.RowCount + 1) As Variant
Do Until rs.EOF
bms(i) = rs.Bookmark
i = i + 1
rs.MoveNext
Loop
rs.MoveFirst
rdoGrid1.ShowData rs
End Sub

Perform the update operation based on user input

Private Sub rdoGrid1_Click()
Dim rs As rdoResultset
Dim NewValue As Integer
NewValue = InputBox("Enter new age -- 1900 to 1997", "Author Age", "1960")
rs.Bookmark = bms(rdoGrid1.Row)
rs.Edit
rs!Year_Born = NewValue
rs.Update
Changes = Changes + 1
i = MsgBox("Commit all " & Changes & " changes?", vbYesNoCancel)
Select Case i
Case vbYes
rs.BatchUpdate
Changes = 0
Case vbNo
Exit Sub
Case vbCancel
Changes = 0
i = MsgBox("Cancel just this change (Yes) or all " & Changes & " made so far (No)?", _
vbYesNo)
If i = vbYes Then
rs.CancelBatch (True)
Else
rs.CancelBatch
End If
End Select
End Sub

Using ADO to Access More Complex Data

Using the preceding code as a base, the next task is to demonstrate how to perform each of these fundamental operations on ADO. In some cases, as in the first example below, the change is fairly significant. Yes, I expect that some of the code might not be needed, as the properties being set would work with their default settings. However, I include it as it can help performance in some cases—by avoiding unneeded calls to the DBMS for more information.

ADO: Executing a Parameter Query

This code is designed to execute a fairly simple table-access query that accepts a marked parameter. Again, we use the "?" character to indicate where to place the parameter. In this case, however, we do not create an rdoQuery object that is kept in a collection off of the rdoConnection object—we use a stand-alone ADO Command object created (and scoped) earlier. The first time through we setup the Command properties, and each time thereafter, we simply execute the command after having changed the parameter.

ADO gives you a lot of flexibility here—more, in some cases than RDO. By telling ADO everything it needs to know about a query, ADO will not have to execute informational queries against the DBMS to get missing information. This means queries run faster the first time and every time and that they run the way you want them to.

Note that we don’t have to build the ADO Parameters collection in code—it created for us as it is when we use RDO. However, it is possible to do so and doing so can improve performance—but make your application more complex to code. Just make sure that the Command is associated with an open connection so ADO can query the service provider (and the server) for the parameters’ description.

To run the query and create the result set, we use the Execute method against the Command object.

Private Sub ParmQueryButton_Click()
Static FirstTime As Boolean
If Cmd.CommandText = "" Then
Cmd.ActiveConnection = cn
FirstTime = False
With Cmd
.CommandText = "select * from authors where year_born = ?"
.CommandType = adCmdText
.CommandTimeout = 15
End With
‘
‘ The following section of code is not required,
‘ but can make execution faster. It eliminates the need
‘ for ADO to fetch the parameter metrics from the server.
‘
With Parm
.Type = adInteger
.Size = 4
.Direction = adParamInput
.Value = QueryParam.Text
Cmd.Parameters.Append Parm
End With
End If
Cmd.Parameters(0).Value = QueryParam.Text
Set rs = Cmd.Execute()
ADOGrid1.ShowData rs
rs.Close
End Sub

ADO: Executing a Parameter-Driven Stored Procedure

ADO has a lot of flexibility when it comes to executing stored procedures. This also means that it will require more code. As with the previous example, you will still have to build up your own ADODB Parameters collection. Since the UserConnection designer is not online yet for ADO (that’s coming), this has to be done carefully. In this case we are executing a simple two-argument SP "AuthorByYearBorn" that returns a small result set.

Private Sub RunSPButton_Click()
Dim Qy As New ADODB.Command
Dim Parm As New ADODB.Parameter
Dim Parm2 As New ADODB.Parameter
Set Qy.ActiveConnection = cn
Qy.CommandType = adCmdStoredProc
Qy.CommandText = "AuthorByYearBorn"
Parm.Type = adInteger
Parm.Name = "YearHigh"
Parm.Size = 4
Parm.Direction = adParamInput
Parm.Value = 1947
Qy.Parameters.Append Parm
Set Parm2 = New ADODB.Parameter
Parm2.Type = adInteger
Parm2.Name = "YearLow"
Parm2.Size = 4
Parm2.Direction = adParamInput
Parm2.Value = 1948
Qy.Parameters.Append Parm2
Set rs = Qy.Execute(ShowRows)
ADOGrid1.ShowData rs
End Sub

ADO: Executing a Multiple-Result Set Stored Procedure

Our next challenge is the basic multiple-result set SP. As we said before this is not that uncommon, so you should be expected to deal with this type of issue quite frequently. ADO’s approach is very unlike the RDO approach. ADO uses the NextRecordset method which lets you assign the next record set in the batch to an ADO Recordset object. In ADO 1.5, the previous Recordset is automatically closed, but subsequent versions are expected to leave them open until specifically closed. Since you might not know how many result sets will be generated by your query, you must now test Recordset State property to see if it is still open—you no longer have a Boolean return value to test as in RDO.

This new paradigm for handling multiple result sets is certainly different than what and RDO developer is used to working with. Yes, the same Recordset object can be re-used or anther can be created to handle each result set. But in the latter case, you will have to manually close the Recordset object when you are done with it to prevent memory leaks—at least when ADO is upgraded to keep these objects open once you move to the next result set.

Private Sub MultipleRSButton_Click()
Dim rs2 As New ADODB.Recordset
sql = "Select * from Authors Where year_born is not null; " _
& "Select * from Authors where year_born is null"
rs.Open sql, cn
Do
i = MsgBox("Ready for results?", vbYesNoCancel)
If i = vbYes Then
ADOGrid1.ShowData rs
Set rs = rs.NextRecordset
End If
Loop Until rs.State = adStateClosed
End Sub

ADO: Executing Action Queries

When you need to execute an action query, you can take advantage of the Execute method in ADO to do the job. In this case, we have to set a few more properties than we do in RDO. However, these can improve data access performance so ADO does not have to poll the server to determine what to do, or how to handle the query. Note that the new output argument for the Execute method returns the number of rows affected. Generally, you don’t see Visual Basic using arguments passed back to the application—just arguments passed to the object interface.

Private Sub ExecuteButton_Click()
Dim Qy As New ADODB.Command
Dim Rows As Long
sql = "Begin Transaction " _
& " Update Authors " _
& " set Year_Born = 1900 where year_born is null" _
& " rollback transaction"
Qy.ActiveConnection = cn
Qy.ActiveConnection = cn
Qy.CommandText = sql
Qy.CommandType = adCmdText
Qy.Execute Rows
MsgBox Rows & " rows would have been affected", vbInformation
End Sub

RDO: Executing Optimistic Batch Queries

Our last ADO example implements a batch operation. I was able to code this pretty much as I had for RDO and found the ADO coverage of this functionality very comprehensive. Note that the routine used to change the chosen row in the R/W result set did not require starting an "Edit" session. Simply changing the contents of a Field and using the Update method was enough to make the changes to the DB.

Private Sub BatchOpsButton_Click()
sql = "Select * from Authors where year_born is null"
rs.Open sql, cnB, adOpenKeyset, adLockBatchOptimistic
rs.MoveLast: rs.MoveFirst
ReDim bms(rs.RecordCount + 1) As Variant
Do Until rs.EOF
bms(i) = rs.Bookmark
i = i + 1
rs.MoveNext
Loop
rs.MoveFirst
ADOGrid1.ShowData rs
rs.Close
End Sub
Private Sub ADOGrid1_Click()
Dim NewValue As Integer
NewValue = InputBox("Enter new age -- 1900 to 1997", "Author Age", "1960")
rs.Bookmark = bms(ADOGrid1.Row)
rs!Year_Born = NewValue
rs.Update
Changes = Changes + 1
i = MsgBox("Commit all " & Changes & " changes?", vbYesNoCancel)
Select Case i
Case vbYes
rs.BatchUpdate
Changes = 0
Case vbNo
Exit Sub
Case vbCancel
Changes = 0
i = MsgBox("Cancel just this change (Yes) or all " & Changes & " made so far (No)?", _
vbYesNo)
If i = vbYes Then
rs.CancelBatch (True)
Else
rs.CancelBatch
End If
End Select
End Sub

Summary

All, in all an RDO developer will notice a number of new ways to deal with data when using ADO. Most of the standard techniques are covered by ADO functionality Version 1.5, and only in a few cases were we forced to choose alternative paths to get our problems solved. When ADO 2.0 arrives with its richer coverage of RDO and DAO functionality, we should see fewer instances (if any at all) that require radical changes in our development approach.

Last updated: October 23, 1997

© 1997 Microsoft Corporation. All rights reserved. Terms of Use.


ADO and SQL Server Developer's Guide

ADO and SQL Server Developer’s Guide

by Joyce Chen and Richard Patterson

Introduction

Consistently accessing data within the enterprise is a challenge for today’s business applications. ODBC provides the first step toward overcoming this challenge by enabling applications to access relational databases. However, as developers and system architects want to include nonrelational data sources and to work in environments such as the Internet, they encounter the dilemma of either developing their own data-access paradigms or working with application program interfaces (APIs) that are incompatible in the new environments. Microsoft®ActiveX® Data Objects (ADO) along with OLE DB solves this dilemma by providing a single model that works with all data sources in a variety of environments.

ADO provides consistent, high-performance access to data, whether you’re creating a front-end database client or middle-tier business object using an application, tool, language, or even an Internet browser. ADO is the single data interface you need to know for 1- to n-tier client/server and Web-based, data-driven solution development.

This paper introduces ADO and the ADO programming model for application developers who are targeting Microsoft SQL Server. Particular attention is given to taking advantage of SQL Server features with ADO, such as stored procedures and server cursors. The concepts presented in the sections titled “The ADO Object Model” and “Using ADO with Visual Basic®, Visual Basic, Scripting Edition (VBScript), Visual C++®, and Java” are applicable to all ADO programmers.

Sample Application and Code References

The Microsoft Visual Basic OutputParam project is referenced and included with this paper. The OutputParam project contains examples on how to use the ADO object model with SQL Server. OutputParam contains two files:

  • OutputParam.vbp
    Sample Visual Basic file that contains the OutputParam project settings.

  • OutputParam.frm
    Sample Visual Basic/ADO file that contains the form and code for the OutputParam project. This form consists of CommandButtons. Each CommandButton corresponds to code that is referenced in each section of this paper.

ADO Overview

ADO was first introduced as the data access interface in Microsoft® Internet Information Server. ADO is implemented with a small footprint to provide minimal network traffic in key Internet scenarios, and a minimal number of layers between the front-end and data source. ADO is easy to use because it is called using a familiar metaphor: the Automation interface, available from just about any tool and language on the market today. Because of its popularity as an easy-to-use, lightweight interface to all kinds of data, and the growing need for an interface spanning many tools and languages, ADO is being enhanced to combine the best features of, and eventually replace RDO and DAO, the data access interfaces in widest use today. ADO is in many ways similar to RDO and DAO, for example, it uses similar language conventions. ADO provides simpler semantics, which makes it easy to learn for today’s developers.

ADO is designed to be the application-level interface to OLE DB, Microsoft’s newest and most powerful data access paradigm. OLE DB provides high-performance access to any data source. Together ADO and OLE DB form the foundation of the Universal Data Access strategy. OLE DB enables universal access to any data. ADO makes it easy for developers to program. Because ADO is built on top of OLE DB, it benefits from the rich universal data access infrastructure that OLE DB provides.

OLE DB Overview

OLE DB is an open specification designed to build on the success of ODBC by providing an open standard for accessing all kinds of data throughout the enterprise. OLE DB is a core technology supporting universal data access. Whereas ODBC was created to access relational databases, OLE DB is designed for the relational and nonrelational information sources, such as mail stores, text and graphical data for the Web, directory services, and IMS and VSAM data stored in the mainframe. OLE DB components consist of data providers, which expose their data; data consumers, which use data; and service components, which process and transport data (for example, query processors and cursor engines). These components are designed to integrate smoothly to help OLE DB component vendors quickly bring high-quality OLE DB components to market. OLE DB includes a bridge to ODBC to enable continued support for the broad range of ODBC relational database drivers available today.

The OLE DB Providers

There are two types of OLE DB applications: consumers and providers. A consumer can be any application that uses or consumes OLE DB interfaces. For example, a Microsoft Visual C++ application that uses OLE DB interfaces to connect to database server is an OLE DB consumer. The ADO object model that uses OLE DB interfaces is an OLE DB consumer. Any application that uses the ADO object model uses OLE DB interfaces indirectly through the ADO objects.

An OLE DB provider implements OLE DB interfaces; therefore, an OLE DB provider allows consumers to access data in a uniform way through a known set of documented interfaces. In a sense, an OLE DB provider is similar to an ODBC driver that provides a uniform mechanism for accessing relational data. OLE DB providers not only provide a mechanism for relational data but also for nonrelational types of data. Furthermore, OLE DB providers are built on top of OLE Component Object Model (COM) interfaces that allow more flexibility; where a ODBC driver builds on top of a C API specification.

Microsoft OLE DB SDK version 1.1 shipped two OLE DB providers: the ODBC Provider and sample text provider. The sample text provider serves as an example that demonstrates the implementation detail of an OLE DB provider. The ODBC Provider is an OLE DB provider for ODBC drivers. This provider provides mechanism for consumers to use the existing ODBC drivers without having to rush to implement new OLE DB providers to replace existing ODBC drivers. For more information about OLE DB and OLE DB providers, see the OLE DB area at http://www.microsoft.com/data/.

The ODBC Provider

The ODBC Provider maps OLE DB interfaces to ODBC APIs. With the ODBC Provider, OLE DB consumers can connect to a database server through the existing ODBC drivers. A consumer calls an OLE DB interface on the ODBC Provider. The ODBC Provider invokes corresponding ODBC APIs and sends the requests to an ODBC driver.

Because the ODBC Provider allows OLE DB consumers to use existing ODBC drivers, there may be some performance concern about the additional layer of the ODBC Provider on top of the existing ODBC driver manager. The design goal of the ODBC Provider is to implement all the functionality of the ODBC driver manager; therefore, the ODBC driver manager is not needed. However, with the ODBC Provider version 1.1, the ODBC driver manager is still required to support connection pooling with ODBC applications.

The ADO Object Model

The ADO object model defines a collection of programmable objects that can be used in Visual Basic, Visual C++, VBScript, Java, and any platform that supports both COM and Automation. The ADO object model is designed to expose the most commonly used features of OLE DB.

The ADO object model contains seven objects:

  • Connection

  • Command

  • Parameter

  • Recordset

  • Field

  • Property

  • Error

and four collections:

  • Fields

  • Properties

  • Parameters

  • Errors

The illustration shows their relationships.

ADO Object Model

The Properties collection and Property object that are not shown in the illustration are available through the Connection, Recordset, and Command objects. The Properties collection and Property object contain provider properties that can be read-only or read/write.

The Connection, Recordset, and Command objects are the centerpieces of the ADO object model. An ADO application can use the Connection object to establish connections with the database server, the Command object to issue commands, such as queries, updates, and so on to the database, and the Recordset object to view and manipulate the returned data. The command language used with the Command object is dependent on the underlying provider for the database. In the case of relational databases, the command language is generally SQL.

The Command object may be optional if the underlining OLE DB provider does not implement the ICommand interface. Because OLE DB providers can exist on top of relational or nonrelational databases, the traditional SQL statements may not be available in a nonrelational database to query data; therefore, the Command object is not required. If the Command object contains parameters, the parameters information can be viewed or specified through the Parameters collection and the Parameter object. The Parameter object describes parameter information for the Command object.

In ADO versions 1.0 and 1.5, all objects can be created except the Error, Field, and Property objects. The Error collections and Error object can be accessed through the Connection object after a provider error occurs. The Field collections and Field object can be accessed through the Recordset object after data exists in the Recordset object. The metadata information of the Recordset object can be viewed through the Fields collection and the Field object.

The Connection Object

The Connection object allows you to establish connection sessions with data sources. The Connection object provides a mechanism for initializing and establishing the connection, executing queries, and using transactions.

The underlying OLE DB provider used for connecting is not limited to the ODBC Provider; other providers can also be used for connecting. A provider can be specified through the Provider property. If none is specified, MSDASQL is the default ODBC provider used for the connection.

The Open method of the Connection object is used to establish a connection. With the ODBC Provider, an ADO application can use the ODBC connection mechanism to connect to a database server. ODBC allows applications to establish a connection through the ODBC data sources, or by explicitly specifying the data source information (commonly referred to as DSN-Less connection). For more information about DSN-Less connections with databses, such as SQL Server, see “Using ODBC with SQL Server” in the Microsoft SQL Server Developer’s Resource Kit.

Before establishing connections, applications can set up a connection string, connection time-out, default database, and connection attributes. The Connection object also allows you to set up the CommandTimeout property for all the command objects associated with this connection. Query strings can be issued through the Execute method.

Transactions can be controlled through the Connection object. The BeginTrans, CommitTrans, and RollbackTrans methods are provided for using transactions.

The example shows using the ODBC Provider, the default OLE DB provider in ADO, to connect to SQL Server:

Dim Cn As New ADODB.Connection

Cn.ConnectionTimeout = 100
' DSN connection
#Cn.Open "pubs", "sa"
' DSN-Less connection for SQL Server
' Cn.Open "Driver={SQL Server};Server=Server1;Uid=sa;Pwd=;Database=pubs"

Cn.Close

The example first sets the connection time-out value to 100 seconds, and then opens a connection using the pubs ODBC data source that points to a SQL Server. A user ID is required for the SQL Server specified in the pubs data source; therefore, the sa user ID is provided as the second parameter of the Open method. There is no password, so the third parameter is omitted.

In addition to using an ODBC data source, this example also provides a commented line that shows how to connect to SQL Server without an ODBC data source. The SQL Server ODBC driver, {SQL Server}, is used to connect to a SQL Server called Server1. The user ID to connect to Server1 is sa, and there is no password for sa. The default database for this connection is the pubs database.

The example shows using the Provider property to specify an alternative OLE DB provider:

Dim Cn As New ADODB.Connection
Dim rs As ADODB.Recordset

Cn.Provider = "sampprov"
Cn.Open "data source=c:\sdks\oledbsdk\samples\sampclnt\"
Set rs = Cn.Execute("customer.csv")

An OLE DB provider, sampprov, is specified in this example. In addition to the ODBC Provider, OLE DB SDK shipped a sample text provider. The text provider allows an application to retrieve data from a text file. In this example, a connection is established by specifying the data source directory c:\sdks\oledbsdk\samples\sampleclnt\, and the data in the customer.csv file is returned as the result of the Execute method.

The example shows using the BeginTrans and the CommitTrans or RollbackTrans methods:

Dim Cn As New ADODB.Connection
Dim rs As New ADODB.Recordset

' Open connection.
Cn.Open "pubs", "sa"


Conformance Testing in OLE DB 2.0

This paper has four sections:

  • Introduction

  • Building and running the OLE DB interface tests

  • Using the TableDump utility to generate initialization files

  • Building and running the ADO minimal tests

Introduction

The purpose of the conformance tests is to encourage developers to write good, compliant OLE DB providers.

The conformance tests help provider writers confirm that their implementations match the OLE DB specification. Having a common set of tests also encourages consistent behavior across providers.

Comprehensive testing of any OLE DB provider includes many tasks:

  • Interface tests

  • Tests for provider-specific behavior

  • Stress tests

  • Performance tests

  • Multiuser testing

  • Application tests (using the provider to build applications)

  • Testing with a variety of consumers

The conformance tests contribute to the first task (OLE DB interface testing) and to the last task (consumer testing, through ADO). They are not intended to address any other tasks in this list.

The conformance tests are meant to supplement a provider writer’s own testing, not to replace it.

They include test variations for normal usage, as well as some error and boundary cases. They do not check performance, stress scenarios, low-resource conditions, multiuser cases, or provider-specific behavior.

The conformance tests check usage of individual OLE DB interfaces and ADO methods. They do not check application scenarios.

There are three stages in the life cycle of conformance tests: unsupported, proposed, and approved.

Unsupported tests are provided only as examples. They were originally designed to work with the OLE DB Provider for ODBC Drivers, and may not work correctly with other providers.

Proposed tests have been updated to work with a wide range of providers — for example, read-write, read-only, SQL, and non-SQL. After broader exposure, they will move to “approved” category. Microsoft plans to provide web links for providers who pass the approved tests. Other incentives may be used in the future. They are made available now so provider teams can use them to test their providers.

Two sets of conformance tests are included in the Data Access SDK version 2.0: OLE DB interface tests and ADO minimal tests.

The OLE DB interface tests check compliance to standards in the OLE DB Programmer’s Reference. There is at least one test module per OLE DB interface. The tests are grouped into levels, corresponding to the OLE DB interface levels.

OLE DB leveling identifies the critical interfaces and behaviors (properties) required for providers and expected by consumers. If you are unfamiliar with OLE DB leveling, see OLE DB Leveling in this collection of technical articles.

The OLE DB interface tests were originally written to test theMicrosoft OLE DB Provider for ODBC Drivers, which is also included in this SDK. The first set of tests (for the level 0/minimum provider interfaces) has been modified to handle a wide range of providers, and has been moved from Unsupported to Proposed.

In Data Access SDK version 2.0, only the level 0 (minimum) interface tests are supported. Tests for the other interfaces are still available in the Unsupported subdirectory. In future releases, more interface tests will be improved to support generic providers, and will be moved from Unsupported to Proposed.

The ADO minimal tests are a small set of tests, at least one per ADO method, which check that ADO statements work correctly against a provider. These tests act as a “sanity” check that ADO applications should work with that provider. There are tests for each ADO method, including methods used for updates and for transactions. The tests are grouped so that you can run just the level 0 tests, which do not require updatability or transactions; or you can run the full set of ADO tests.

Both the OLE DB and ADO tests use Table Dump generated files to identify provider-specific information, and to handle provider variations (such as read-only versus read-write, SQL versus non-SQL). The TableDump utility generates initialization files needed by the tests. This utility can be found in the Tools subdirectory.

Conformance directory structure:

Conformance

Include

Lib

Tests

Proposed

Unsupported

Tools

ADOPriv

PrivLib

TableDump

Section 2: Building and Running the OLE DB Conformance Tests

OLE DB Level 0 Conformance Tests

There is at least one level 0 test module for each level 0 interface, plus one test module for Data Coercion, and one test module for simple threading cases. In total there are 19 level 0 tests. The names of these tests are abbreviated forms of the interface names.

Name of file | Interface tested
IACCESSR | IAccessor
ICLSFACT | IClassFactory and CoCreateInstance
ICOLINFO | IColumnsInfo
ICNVTTYP | IConvertType
IDBCRSES | IDBCreateSession
IDBINIT | IDBInitialize
IDBPRPTS | IDBProperties
IGETDSO | IGetDataSource
IOPENRW | IOpenRowset
IPERSIST | IPersistFile
IROWSET | IRowset
IROWCHNG | IRowsetChange
IROWDEL | IRowsetChange::DeleteRows
IROWIDEN | IRowsetIdentity
IROWINFO | IRowsetInfo
IROWNEW | IRowsetChange::InsertRow
ISESPRPT | ISessionProperties
THREADS | Threading test (tests free-threaded only, not apartment model)
DATALITE | Data Coercion test

Test Coverage

The level 0 test modules cover each interface in the minimum provider level section of the OLE DB Leveling document, plus Data Coercion and Threading.

The tests are organized into test modules (one per interface). Each test module contains test cases, and each test case contains test variations.

All the OLE DB test modules use Privlib.lib, which is a set of common C++ functions such as table creation, data comparison, and accessor creation.

Getting Started

The OLE DB test sources, include files, and resource files can be found in

InstallDirectory\Conformance\Tests\Proposed\OLEDBTests

Additional include files are in

InstallDirectory\Conformance\Include

Libs are in

InstallDirectory\Conformance\Lib

Source for the test tools (PrivLib and TableDump) can be found in

InstallDirectory\Conformance\Tools\

Local Test Manager (LTM), the tool used to run the conformance tests, can be found in

InstallDirectory\bin\oledb

Building a Tool (TableDump or Privlib) or Test

You should build PrivLib and TableDump before trying to build any of the OLE DB tests.

To build a tool or test, use Open Workspace on the .dsp in Visual C++®.

For PrivLib, the .dsp is in

InstallDirectory\Conformance\Tools\PrivLib

Add all the include directories and all the LIB directories for the version 2.0 SDK to your Visual C++ directory. (To do this in Visual C++, on the Tools menu, click Options, and then click Directories.)

Update your system path to include the following directories at the beginning of the path list:

InstallDirectory\Conformance\Tests\Proposed\OLEDBTests\Include

InstallDirectory\Conformance\Include

InstallDirectory\Include

InstallDirectory\Conformance\Lib

For VC 5 users, build PrivLib using the default target.

For VC 6 users, change the default target to "privlib - Win32 x86 Debug" or "privlib - Win32 (Alpha) axp Debug", and then build.

To build an OLE DB test on an Intel® machine, build the test using the Debug MBCS target. On an Alpha machine, build the tests with the DEBUG target. For VC 5 users, these targets should be the default targets. For VC 6 users, change the default targets, and then build.

Create an initialization file by using the TableDump program found in the Tools directory. (See Section 3 of this paper for more information on the TableDump utility.)

Running the OLE DB Conformance Tests

The Local Test Manager (LTM) is now a standalone exe that does not have to be registered. Once LTM is running, set up your provider by adding a new provider, which means specifying the connection string. The next section contains more information on creating a provider connection string.

Register the tests by going to Test/Register Item and selecting the Test module to register.

Next, click the test module (not just so the test module name is highlighted but the box is checked), click the provider you added, and then click Run.

Test modules and LTM are organized so you can run a test module, a test case, or a test variation. Hightlight the test module name and click the plus sign next to the test module to expose the test cases in that module. Click the plus sign next to a test case to expose the test variations in that test case. Each variation tests a single test scenario. Each variation returns pass, failure, or skipped. Many variations also return the expected HRESULT and the actual HRESULT received if an error occurs.

Provider Connection String

Before running a test in LTM, you must set up a provider and specify a provider connection string. Normally, you should use an initialization file (FILE=filename.out) and specify the connection flags:

CONFLEVEL=CONF_LEVEL_0 | CONF_STRICT;

where

CONFLEVEL=CONF_LEVEL_0 specifies that all level 0 interfaces are supported.

CONFLEVEL=CONF_STRICT specifies that only level 0 interfaces are used when running the tests.

Sample provider strings

Provider: OLE DB Provider for ODBC Drivers

LTM connection string:

DATASOURCE=datasource; USERID=userid; PASSWORD=password;

DATASOURCE identifies the ODBC data source name set up through the ODBC Data Source Administrator.

Provider: SQL Server OLE DB Provider

LTM connection string:

DATASOURCE=server; USERID=userid; PASSWORD=password;

DATASOURCE identifies the name of the server to which you’re connecting.

Provider: Oracle OLE DB Provider

LTM connection string:

DATASOURCE=server; USERID=userid; PASSWORD=password;

DATASOURCE identifies the name of the Oracle server to which you’re connecting.

Provider: OLE DB Simple Provider (OSP)

LTM connection string:

FILE=\filename.ini;

This is normally run with an .ini file because OSP does not support commands.

Provider: Jolt (OLE DB Provider for Microsoft® Jet)

LTM connection string:

DATASOURCE=c:\file.mdb; USERID=Admin;

Debugging a Test

The level 0 tests should run with zero failures.

However, in case of a failure, you can debug by following these steps:

  1. In Visual C++, open any of the test project files in \Msdasdk\Comformance\Tests\Proposed.

  2. On the Settings menu, click Project, and then click the General tab.

  3. Under Executable for debug session, enter the path to LTM.exe, for example:

    c:\msdasdk\bin\oledb\ltm.exe

  4. Open the source code for the test in the Src directory, then set breakpoints.

  5. To start LTM.exe, press F5.

  6. Select the test variations to debug, select the provider, and then start the test.

    You will hit your breakpoint.

Note If you did not build the test DLL, then the .pdb file must be in the same directory as the test, or in a search path.

Section 3: Using the Table Dump Utility to Generate .Ini Files

Trying to develop complete OLE DB conformance tests generic enough to handle the broad range of OLE DB providers is not an easy task. There are many differences, limitations, and issues that are unique to each and every provider.

Some of the major issues are:

  • Providers may have their own provider-specific Initialization properties. Standard initialization properties from the Initialization property group can be specified on the connection string.

  • Some providers are completely read-only.

  • Some providers do not allow table creation.

  • Providers may have their own unique CommandText (if testing Commands).

Just trying to initialize generically to a provider is not trivial. We need more information from the provider. To guarantee data is sent and received correctly we have to know what the data is supposed to be ahead of time. With a provider that doesn’t support Table Creation or modification of data, it’s hard to establish a known set of data.

To solve these problems and still test a wide range of OLE DB providers, we have developed an initialization file that can be used by all conformance tests. This file contains all necessary information to get initialized to the provider, understand the provider’s command syntax if available, confirm that the data retrieved is correct, and verify all other aspects of the provider meeting a particular conformance level.

TableDump creates and obtains all information needed to run for the provider’s level of conformance.

Provider Requirements

To solve the stated problems and still work with the broad range of providers, we currently impose the following requirements.

  • We need to know the initialization properties ahead of time, and all values to get connected.

  • We need to know the command syntax for all statements if you are testing commands.

    Note Since commands are not in level 0, you will not use commands if using CONFLEVEL=CONF_LEVEL_0 | CONF_STRICT; options.

  • We need to obtain all columns and rows of data and place into this file to be able to verify data correctly for all providers.

Data Requirements

For complete conformance testing there are requirements placed upon the table/data provided to the conformance tests:

  • The table must contain a separate column for every possible supported DBTYPE. This assures all types or data are tested and obtained.

  • The table must contain at least 10 rows of data, and should be around 60-100 rows.

  • The data provided in the table should stress the data type, which can be ColumnSize, Precision, or Scale. The data should have at least one MIN value for every data type, and at least one entry with maximum precision for that data type.

Usage

TableDump is a simple command-line utility that takes arguments for all the required information it needs.

Here is a list of current arguments, their values, and meaning.

Note All arguments are uppercase. They must be followed by “=” and end in a semicolon to distinguish them from other arguments.

PROVIDER=ProviderName;

Required Argument. This is the Provider ProgID of the provider to use. TableDump will use ProgIDToCLSID to convert this to a CLSID to obtain an instance of the provider in CoCreateInstance.

TABLENAME=TableName;

Required Argument. This is the TableName to pass to IOpenRowset.

Note Although the argument is called TableName, this does not imply that TableDump only works with relational providers. Here, TableName just indicates the DBID passed to IOpenRowset.

BINDINGTYPE=DBTYPE_WSTR | DBTYPE_STR | DBTYPE_VARIANT;

Optional Argument. Normally this option is not needed. When TableDump creates the Accessor, it needs to bind all columns to a type that can be represented in a file (string format). Because all providers are required to convert all supported types to DBTYPE_WSTR, this should not be needed by a provider that conforms to the spec, since this is the default TableDump will use. Unfortunately, not all providers conform, so we have allowed the user to specify the binding type to a common type that can be converted to STR. If using DBTYPE_VARIANT, it’s the provider’s responsibility to convert all values to VARIANT, and the tool will handle all conversions from VARIANT to string.

DEFAULTQUERY=DefaultQuery;

Optional Argument. Normally this option is not needed. If you want to test command support, this is the default command for all queries used by the tests and PrivLib. All entries in the QUERY section will have this command as the default. You can then modify that section based upon the grammar or command syntax required for your provider.

OUTPUT=OutputFileName;

Optional Argument. This is the output file name for the generated .ini file. You can include the entire path, including file name and extension. TableDump will use whatever is provided, or will default to “TableDump.out” if this option is not specified.

CACHE_AUTHINFO=;

ENCRYPT_PASSWORD=;

INTEGRATED=;

MASK_PASSWRD=;

PASSWORD=;

PERSIST_ENCRYPTED=;

PERSIST_SENSITIVE_AUTHUNFO=;

USERID=;

DATASOURCE=;

HWND=;

IMPERSONATION_LEVEL=;

LCID=;

LOCATION=;

MODE=;

PROMPT=;

PROTECTION_LEVEL=;

PROVIDERSTRING=;

TIMEOUT=;

Optional Arguments. Each one of the above arguments corresponds to a property in the DBPROPSET_DBINIT group (without the preceding DBPROP_*_). To initialize your provider, you need to specify only whatever arguments are required. If none are needed for initialization, then none need to be specified. When using MSDASQL, usually only DATASOURCE, USERID, and PASSWORD are needed to initialize the provider.

Usage With LTM

When creating a provider, specify FILE=filename.ini in the LTM InitString (). When running a test, the test will check the existence of this keyword and will process the .ini file before beginning any test variations.

Format

Currently the format of the .ini is not that important because there is a tool generator. For most providers, the file will not have to be modified. In case manual editing is required, the following gives a brief description of the .ini file’s sections and meanings, along with a sample .ini file.

[INFO:]

This is for all provider initialization and other init information. Currently this will contain:

  • The provider’s name,

  • The table name to use in IOpenRowset and/or the default query to use (For details on when this is used, see DEFAULTQUERY above.)

The following sample is from a provider MSDASQL, the table is Customers, and the initialization properties are DBPROP_INIT_DATASOURCE, DBPROP_AUTH_PASSWORD, and DBPROP_AUTH_USERID:

[INFO] {Provider (Table, <DataSource>, <DefaultQuery>)}
{ PROVIDER=MSDASQL (TABLE=Customers, DATASOURCE= OLE_DB_NWind_Jet}

[QUERY:]

This section is a large list of all the command statements that the test will try to use in the process of testing the provider. TableDump will generate all of these based upon the specified default query when the tool is used. This section may need to be manually edited to make sure that all command statements contain the correct grammar, columns, etc. to obtain the operation. The uppercase enumeration indicates the intended command, and everything between quotes represents the provider-specific syntax of that operation.

This section is only looked at if you’re using the DEFAULTQUERY option (see the option for more details). If the above example specified DEFAULTQUERY=select * from Customers, this section would contain all statements with a default CommandText string within the quotes.

The first SELECT_ALLFROMTABLE represents a command that will get executed to select all the columns from the table, so “select * from Customers” is our provider’s syntax to do that. The next is SELECT_EMPTYROWSET and the text inside quotes is the provider-specific syntax to obtain an empty rowset.

 [QUERY] {SQLEnumIdentifier("Query")}
{SELECT_ALLFROMTBL("select * from Customers ")}
{SELECT_EMPTYROWSET("select * from Customers where 0 = 1")}

[COLUMN:]

This section is a list of all columns found on the specified table or a result from the default query. It contains all the information commonly reported from IColumnsInfo, such as type information, precision, scale, and other info flags to be used when creating and verify data.

[COLUMN] {ColName(iOrdinal, TYPE, ulColumnSize, bPrecision, bScale, dwFlags)}
{CustomerID(1,DBTYPE_STR ,5,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}

[DATA:]

This section is a list of all data found on the specified table or a result from the default query. The format is a 1:1 correspondence to the column section above. The first row of data is for the first row, and the first column refers to the first column in the Column section. All data is represented in the file in {“String”} format, and NULL is represented as {(null)}. When the testing framework reads this file back in, all types will get converted back into the correct type indicated in the Column section. When the column is of type VARIANT the data value is also prefixed with the VT_TYPE indicator for the specified subtype. To represent a VARIANT with subtype VT_I4 and Value 3, TableDump will produce VT_I4(“3), and DBTYPE_VARIANT will be indicated in the Column section. This is for providers that support VARIANT types, or support the possibility of every VARIANT in different rows to be of a different subtype.

 [DATA] {<Type>("data")}
{("ALFKI")},{("Alfreds Futterkiste")},{("Maria Anders")},{("Sales Representative")},{("Obere Str. 57")},{("Berlin")},{(null)},{("12209")},{("Germany")},{("030-0074321")},{("030-0076545")},

Example

Suppose you want to test a provider using the OLE DB conformance tests, and need to generate an .ini file that will be used by all the tests.

Here is the example provider information:

ProvderName: MSDASQL

DataSource: OLE_DB_NWind_Jet

UserID: admin

Password:

Table: Customers

DefaultQuery: select * from Customers

The following information is what’s needed to generate the .ini file :

TableDump.exe PROVIDER=MSDASQL; DATASOURCE= OLE_DB_NWind_Jet; USERID=admin; PASSWORD=; TABLENAME=Customers; DEFAULTQUERY=select * from Customers; OUTPUT=Customers.ini;

This will create the following file, Customers.ini:

[INFO] {Provider (Table, <DefaultQuery>)}
{ PROVIDER=MSDASQL (TABLE=Customers, DEFAULTQUERY="select * from Customers")}

[QUERY] {SQLEnumIdentifier("Query")}
{SELECT_ALLFROMTBL("select * from Customers")}
{SELECT_SEARCHABLE("select * from Customers")}
{SELECT_UPDATEABLE("select * from Customers")}
{SELECT_ABCANDCOLLIST("select * from Customers")}
{SELECT_DISTINCTCOLLISTORDERBY("select * from Customers")}
{SELECT_REVCOLLIST("select * from Customers")}
{SELECT_COLLISTGROUPBY("select * from Customers")}
{SELECT_COLLISTWHERELASTCOLINSELECT("select * from Customers")}
{SELECT_REVCOLLISTFROMVIEW("select * from Customers")}
{SELECT_COUNT("select * from Customers")}
{SELECT_COLLISTSELECTREVCOLLIST("select * from Customers")}
{SELECT_EMPTYROWSET("select * from Customers")}
{SELECT_COLLISTFROMTBL("select * from Customers")}
{SELECT_COLLISTTBLUNIONTBL("select * from Customers")}
{SELECT_COLLISTORDERBYCOLONECOMPUTE("select * from Customers")}
{SELECT_CROSSPRODUCT("select * from Customers")}
{SELECT_LEFTOUTERJOIN("select * from Customers")}
{SELECT_RIGHTOUTERJOIN("select * from Customers")}
{SELECT_FROMTBLWITHPARAMS("select * from Customers")}
{SELECT_CHANGECOLNAME("select * from Customers")}
{SELECT_DUPLICATECOLUMNS("select * from Customers")}
{SELECT_REVERSEDUPLICATECOLUMNS("select * from Customers")}
{SELECT_MAXCOLINQUERY("select * from Customers")}
{SELECT_COMPUTEDCOLLIST("select * from Customers")}
{SELECT_UPDATEABLEALLROWS("select * from Customers")}
{SELECT_ORDERBYNUMERIC("select * from Customers")}
{INSERT_1ROW("select * from Customers")}
{INSERT_ALLWITHPARAMS("select * from Customers")}
{SELECT_ALL_WITH_SEARCHABLE_AND_UPDATEABLE("select * from Customers")}
{SELECT_ALL_WITH_BLOB_AT_END("select * from Customers")}
{NO_QUERY("select * from Customers")}
{SELECT_ALL_WITH_FOR_BROWSE("select * from Customers")}

[COLUMN] {ColName(iOrdinal, TYPE, ulColumnSize, bPrecision, bScale, dwFlags)}
{CustomerID(1,DBTYPE_STR ,5,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{CompanyName(2,DBTYPE_STR ,40,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{ContactName(3,DBTYPE_STR ,30,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{ContactTitle(4,DBTYPE_STR ,30,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{Address(5,DBTYPE_STR ,60,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{City(6,DBTYPE_STR ,15,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{Region(7,DBTYPE_STR ,15,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{PostalCode(8,DBTYPE_STR ,10,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{Country(9,DBTYPE_STR ,15,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{Phone(10,DBTYPE_STR ,24,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}
{Fax(11,DBTYPE_STR ,24,255,255,DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE | DBCOLUMNFLAGS_MAYBENULL)}

[DATA] {<Type>("data")}
{("ALFKI")},{("Alfreds Futterkiste")},{("Maria Anders")},{("Sales Representative")},{("Obere Str. 57")},{("Berlin")},{(null)},{("12209")},{("Germany")},{("030-0074321")},{("030-0076545")},
{("ANATR")},{("Ana Trujillo Emparedados y helados")},{("Ana Trujillo")},{("Owner")},{("Avda. de la Constitución 2222")},{("México D.F.")},{(null)},{("05021")},{("Mexico")},{("(5) 555-4729")},{("(5) 555-3745")},
{("ANTON")},{("Antonio Moreno Taquería")},{("Antonio Moreno")},{("Owner")},{("Mataderos 2312")},{("México D.F.")},{(null)},{("05023")},{("Mexico")},{("(5) 555-3932")},{(null)},
{("AROUT")},{("Around the Horn")},{("Thomas Hardy")},{("Sales Representative")},{("120 Hanover Sq.")},{("London")},{(null)},{("WA1 1DP")},{("UK")},{("(171) 555-7788")},{("(171) 555-6750")},
{("BERGS")},{("Berglunds snabbköp")},{("Christina Berglund")},{("Order Administrator")},{("Berguvsvägen 8")},{("Luleå")},{(null)},{("S-958 22")},{("Sweden")},{("0921-12 34 65")},{("0921-12 34 67")},
{("BLAUS")},{("Blauer See Delikatessen")},{("Hanna Moos")},{("Sales Representative")},{("Forsterstr. 57")},{("Mannheim")},{(null)},{("68306")},{("Germany")},{("0621-08460")},{("0621-08924")},
{("BLONP")},{("Blondel père et fils")},{("Frédérique Citeaux")},{("Marketing Manager")},{("24, place Kléber")},{("Strasbourg")},{(null)},{("67000")},{("France")},{("88.60.15.31")},{("88.60.15.32")},
{("BOLID")},{("Bólido Comidas preparadas")},{("Martín Sommer")},{("Owner")},{("C/ Araquil, 67")},{("Madrid")},{(null)},{("28023")},{("Spain")},{("(91) 555 22 82")},{("(91) 555 91 99")},
{("BONAP")},{("Bon app'")},{("Laurence Lebihan")},{("Owner")},{("12, rue des Bouchers")},{("Marseille")},{(null)},{("13008")},{("France")},{("91.24.45.40")},{("91.24.45.41")},
{("BOTTM")},{("Bottom-Dollar Markets")},{("Elizabeth Lincoln")},{("Accounting Manager")},{("23 Tsawassen Blvd.")},{("Tsawassen")},{("BC")},{("T2F 8M4")},{("Canada")},{("(604) 555-4729")},{("(604) 555-3745")},
{("BSBEV")},{("B's Beverages")},{("Victoria Ashworth")},{("Sales Representative")},{("Fauntleroy Circus")},{("London")},{(null)},{("EC2 5NT")},{("UK")},{("(171) 555-1212")},{(null)},

*The rest is removed for simplicity and saving trees…

Section 4: Building and Running the ADO Minimal Tests

ADO Level 0 Conformance Tests

The mapping of ADO properties and methods to OLE DB interfaces provides the basis for the grouping of ADO properties and methods into the conformance levels.

Before running ADO Level O conformance tests, you must manually modify the .ini file generated by TableDump. (See example in the previous section, “Example.”) Modify the SELECT_EMPTYROWSET line under [QUERY] by adding a query that will yield no results. For example:

{SELECT_EMPTYROWSET("select * from mytable where 1=2")}

If you do not modify this line, end-of-file and beginning-of-file tests will fail.

ADO Update Conformance Tests

These tests are provided to confirm that ADO Update statements will work with compliant updatable providers.

ADO Transaction Conformance Tests

These tests are provided to confirm that ADO Transaction statements will work with compliant providers that support transactions.

Building ADOPriv.dll

Before running the ADO tests, you must build ADOPriv.dll.

  1. To build ADOPriv.dll, use Open Workspace on the InstallDirectory\ Conformance\Tools\ADOPriv\ADOPriv.dsp in Visual C++®.

  2. Add all the include directories and all the LIB directories for the version 2.0 SDK to your Visual C++ directory. (To do this in Visual C++, on the Tools menu, click Options, and then click Directories.)

  3. Update your system path to include the following directories:

    InstallDirectory\Conformance\Tests\Proposed\OLEDBTests\Include

    InstallDirectory\Conformance\Include

    InstallDirectory\Conformance\Lib

  4. Build ADOPriv using the default target.

  5. Copy ADOPriv.dll to

    InstallDirectory\Conformance\Lib

  6. Register it in that directory using Regsrv32.exe

Getting Started

The ADO tests are in

InstallDirectory\Conformance\Tests\Proposed\ADOTests

The test .dll and .reg files are in the ADOTests subdirectory itself. The source is in the SRC subdirectory, in case you need to rebuild the tests.

ADOPriv.dll is in the Tools subdirectory. (This is the wrapper for the PrivLib used by the ADO tests for common functionality.)

  1. Register the ADO tests by executing regsvr32 on each of the ADO test .dll files. For example:
    regsvr32 adolvl0.dll

  2.
Execute each of the .reg files. For example: adolvl0.reg.

    3.

  3.
If you haven’t already done so, create an .ini file using the TableDump program found in the Tools subdirectory.

    4.

  4.
Start LTM by executing LTM.exe. When LTM is running, set up a provider by adding a new provider, which means specifying the connection string. See the next section for more information on creating a provider connection string.

    5.

  5.
Click the test module, click the provider you added, and then click Run.
    6.

Test modules and LTM are set up so you can run a test module, a test case, or a test variation.

  6.
Click the plus sign next to the test module to expose the cases in that module. Click the plus sign next to a test case to expose the variations in that case.
    7.

Each variation tests a single test scenario. Each variation will return pass or failure. In many cases, the variation will also return the expected HRESULT and the actual HRESULT received.

Provider Connection String

ODBC Provider Example:

DATASOURCE=ADOT7;
USERID=adolab;
PASSWORD=sometext;
FILE=C:\testdrop\ wayl7.ini;
DefaultQuery=select * from wayl7;
CURSORLOC=client;CONNECTIONTIMEOUT=500;

Oracle Provider Example:

DATASOURCE=goliath.world;
USERID=ADOLAB1;
PASSWORD=sometext;
FILE=C:\testdrop\ wayl7can.ini;
DefaultQuery=select * from wayl7;
CURSORLOC=server;

Debugging

Your provider should get zero failures when running the Level 0 tests. If it supports updates and transactions, it should also get zero failures when running the Updatability and Transaction tests.

In case there is a failure, the ADO tests can be viewed and stepped through by using Visual Studio™. Simply launch LTM, launch Visual Studio, attach to the TD process, and open the desired Visual Basic® source file.

It is not possible at this time to debug the ADO tests in the Visual Basic environment


Improving Performance of the Data Access Components When Running with Internet Information Server 4.0

November 1997

by Leland Ahlbeck

Introduction

This paper addresses issues that may affect speed when returning data to a Web client running in an Internet Information Server® (IIS) environment. This document focuses on Microsoft® products, but some of the issues raised may relate to other OLE DB data providers or ODBC drivers. Most of the performance testing on these issues used Microsoft® SQL Server™; therefore, the document emphasizes SQL Server.

Terminology

Data provider

A data provider is a source for OLE DB data. Examples of OLE DB data providers include the OLE DB Sample data provider that ships with the Data Access SDK, and the SQL Server data provider. Another example of a data provider is the Microsoft OLE DB data provider for ODBC data sources.

Consumer

A consumer is a user of a data provider. ADO is a consumer of OLE DB data sources.

OLE DB data provider for ODBC data sources

This data provider is effectively a translation layer between OLE DB and ODBC. It enables OLE DB consumers such as ADO to communicate with ODBC data sources.

Middle tier

The group of components that are running on the Web server. Usually, this includes server software and any components that are being used to service Web pages.

How Data Flows from the Database to the Middle Tier

Typically, when a Web server services a request for data on a Web page, many data components are involved. Keep in mind that all the processing is being done on the middle tier, where the end user’s computer is just making the request and receiving the results. Data flow follows this process:

  1. Processing the request for data starts with the ASP or ISAPI DLL. This starts the process and makes the call to the OLE DB data provider.

  2. The OLE DB data provider services the request and performs the query on the database. Or, in the case of the OLE DB data provider for ODBC data sources, a call is made to ODBC.

  3. The server processes the request for data and returns the data to the data provider.

  4. The data provider returns the data to the requesting software—either an ASP or an ISAPI DLL.

  5. The data is formulated into Web page format and presented to the user.

Threading Support

Threading is the ability of the middle tier running on the Web server to communicate to other components in one thread or multiple threads. The chosen threading model can have a large impact on performance. For example, you can choose to communicate through one thread to the server hosting your data, or through multiple threads. If you choose multiple threads, you can provide support for multiple customers simultaneously.

Threading Models Defined

Multiple threading models are supported. This paper discusses only three: apartment threading, free-threading, and objects marked as both. (The default threading model set up by the installation process is the apartment model. This accommodates Microsoft Jet by default because the Microsoft Jet database engine is limited to apartment threading.)

  • In the case of apartment threading, only one thread from each object communicates with the back-end server, but the Web server can support multiple objects entered simultaneously by different threads. Apartment threading is the default threading model, not because of efficiency but because of compatibility with other Microsoft products. Microsoft Jet, the Microsoft Access database engine, supports this threading model only.

  • For free-threaded objects, there are no restrictions on the number of threads that can access an object. Therefore, multiple threads access the same object, which makes calls to the database (as opposed to multiple copies of the object making calls to the database).

  • For objects marked as both, the object can be accessed as either apartment or free-threaded. This offers the most flexibility for your Web server but may not be supported by all back ends.

Different components of the system can have different threading models. In the case of IIS 4.0, the component that can improve database performance is the threading model applied to and supported by the data access components. This could be either an ISAPI DLL or the ADO data access component. In either case, use the most efficient threading model supported by your OLE DB data provider.

Note An exception to this is an ADO application running against SQL Server. You should use the apartment threading model if you will get your data either from the OLE DB data provider for Access databases, or from the Microsoft Jet ODBC database driver by way of the OLE DB data provider for ODBC data sources.

Free Threading and ADO Objects

By default, all ADO objects are marked in the registry as apartment model. If caching of ADO objects off the Application or Session object is required, you must mark the affected object as free-threaded in the registry. To do so, you will need to edit the ADOFre15.reg file. Follow these steps:

  1. Locate the ADOFre15.reg file. If you accepted the default program file location when installing IIS, ADOFre15.reg will be located in C:\Program Files\Common Files\System\ADO.

  2. Make the directory containing the file your current directory and issue the command RegEdit ADOFre15.reg.

  3. Restart your server and the change will take effect.

If you determine you need to switch the ADO threading model back to apartment threading, follow the above instructions but edit the ADOApt15.reg file.

Sharing an ADO Connection object across multiple threads will not necessarily increase performance, and could potentially decrease it. If the provider supports connection pooling (as does running against the SQL Server ODBC 3.5 driver), this is a more efficient mechanism. Also, if you leave a cached connection open between operations, it could be dropped or the link to the server could be broken.

Connection Pooling

Connection pooling, when enabled, sets up a pool of connections to service database connection requests. Enabling this feature allows a middle tier such as IIS to hold on to the open connections and hand them out to Web clients making requests from the database. Keeping these connections open saves time when validating subsequent requests and making the connection to the back-end server. By default, connection pooling is set to On in IIS 4.0 and Off in IIS 3.0; to improve performance, it is recommended that you enable connection pooling.

When connection pooling is enabled, a request for a connection comes to the driver manager and is handled in one of three ways: If there are no connections in the pool, a new connection is created and passed to the user. If connections are available in the pool and the connection properties (User ID, Password, and so on) requested by the client match the connection properties of the pooled connection, the client is given the open connection in the pool. If connections are available but the connection properties do not match, a new connection with the appropriate properties will be created for the client.

When using connection pooling with SQL Server or any database system that limits user logins to a specified number, keep in mind that each connection uses up one of the licensed logins. Depending on your situation, this may or may not be beneficial. The driver manager will hold on to the connection, possibly preventing another user from logging on to the server if you are running. On the other hand, when connection pooling is enabled you may have fewer user connections in the pool servicing more users.

Enabling Connection Pooling

Version 3.0 of IIS

To enable connection pooling using IIS 3.0, follow these steps:

  1. From the Start menu, click Run. In the command line, type RegEdit and click OK.

  2. In the left pane of the Registry Editor, open the following tree (the backslashes indicate subdirectories):
    HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\ W3SVC\ASP\Parameters

  3. Double-click StartConnectionPool in the right pane. This opens an Edit String dialog box. Change the 0 (zero—the value for Off) to 1 (the value for On).

  4. Click OK to save changes.

IIS Version 4.0

ODBC 3.5 now enables connection pooling at two levels: the database driver and the driver manager. This means that Internet Information Server automatically supports connection pooling at the driver-manager level and enables connection pooling for Microsoft SQL Server. Additionally, if you want your ODBC driver to participate in connection pooling, you must set the driver's CPTimeout value in the Windows NT® registry. For more information about setting this value, see the ODBC documentation or visit the Microsoft ODBC Web site at http://www.microsoft.com/odbc. The benefit of the IIS 4.0 structure is the ability to enable connection pooling for some ODBC drivers and disable it for others.

Enabling Connection Pooling from Your Application

You can enable connection pooling from your application by calling the ODBC API. To enable connection pooling, call SQLSetEnvAttr, passing a value for the type of connection pool you want to enable. For more information about the parameters you can pass to enable connection pooling, see the ODBC documentation or visit the Microsoft ODBC Web site at http://www.microsoft.com/odbc/.

Connection Pooling and SQL Server Temporary Stored Procedures

It can be dangerous to create stored procedures to process prepared statements while connection pooling is enabled. Before getting into the dangers of connection pooling and temporary stored procedures, you must first understand when temporary stored procedures are created and, more importantly, when they are deleted.

Creating temporary stored procedures for prepared SQL statements is an option you can configure from the ODBC Data Source Administrator. The default for this setting is On for the SQL Server 2.65 and 3.5 drivers. This means that when an SQL statement is prepared, a temporary stored procedure is created and compiled. When the prepared command is called by ICommand.Execute or through one of the ADO methods, the temporary stored procedure is executed, saving the overhead of parsing and compiling the SQL statement. If used properly, this feature can potentially improve the performance of your application. If your SQL statement will be executed more than twice, or if it contains parameters and will be called multiple times, you should prepare the statement. Keep in mind that preparing an SQL statement carries a price, and that preparation is lost once you disconnect from the database.

When connection pooling is enabled, you must decide when to delete the temporary stored procedures. With the SQL Server 2.65 driver, temporary stored procedures are released when the connection is released. With the SQL Server 3.5 driver, you can choose to delete them either when the connection is released, or when the connection is released and as appropriate while you’re connected.

The danger with temporary stored procedures and connection pooling is that if you are using the default setting, you may run out of room in TempDB, where temporary stored procedures are created and stored. When connection pooling is enabled, a connection is made to the database; but when the client releases it, the connection goes back into the pool. When this happens, the connection is not released and the stored procedures are not deleted from TempDB. You end up with temporary stored procedures in TempDB that are no longer associated with a client and will never be called again.

If you use the SQL Server 2.65 driver, it is advisable to disable the creation of stored procedures on prepare when connection pooling is enabled. With the SQL Server 3.5 driver, you should disable the Create Temporary Stored Procedures option or set it to “disconnect and as appropriate” when running connection pooling. Setting this option to “disconnect and as appropriate” means that the ODBC SQL Server driver will drop the connection when the OLE DB ICommand object that created the temporary stored procedure is released. If your client code is ADO, the stored procedure will be released when the ADODB.Recordset and command object are closed.

If you are using ODBC 3.0 and a connection without DSN, or a file DSN, to connect to your server, you can set the additional connection properties supported by your driver. For a listing of keywords supported by your driver, see the driver-specific documentation. The following ADO Connection example shows how to connect to an SQL Server database without using a DSN. In addition to specifying the server, user ID, and password, it specifies the default database.

Dim cn As ADODB.Connection
Set cn = New ADODB.Connection
cn.ConnectionString ="DRIVER=SQL Server;
SERVER=DAPIPERF03;UID=sa;PWD=;DATABASE=ssNSTL"
cn.Open

Monitoring Connection Pooling

With ODBC 3.5 you can monitor connection pooling using the NT Performance Monitor (PerfMon.exe). Using Performance Monitor, you can watch connection pooling at work. The driver manager will keep statistics on connection pooling, which you can then view through the NT Performance Monitor. For Performance Monitor to track changes in connection pooling, you need to make some additions to the system registry on the IIS server.

Registry Entries for Monitoring Connection Pooling

As with any changes you make to the registry, it is extremely important to back up the registry first. To do this, use the backup facilities provided in the registry editor.

To change the registry to monitor connection pooling, follow these steps:

  1. Save the attached files and copy them to your Windows NT system directory.

  2. In the Windows NT system directory, run this command: RegEdit CPMonitor.reg. This step adds the necessary keys to the registry to track the connection pooling performance counters.

  3. In the Windows NT system directory, run lodctr odbcperf.ini. This step loads the information about the performance counters that you will view with Performance Monitor.

  4. Restart your server.

Connection Pooling Counters and What They Mean

After restarting your server, start Performance Monitor (PerfMon.exe). In the Add Counter dialog box, there will be an entry for ODBC Connection Pooling under the process list box, with six new keys that you can monitor. The naming convention of the keys makes a distinction between hard and soft connections and disconnections. The connection pooling process takes hard actions. The application utilizing connection pooling takes soft actions.

You should see the most activity from the ODBC Soft Connection Counter, which shows you the number of connections that are satisfied by the connection pool. In this case, a live connection is coming from the pool rather than having to make a connection to the database.

Counter | Definition
ODBC Hard Connection Counter per second | The number of actual connections per second that are made to the server. When you first put your environment under heavy load, this counter will go up very quickly. After a few seconds it will drop to zero; this is normal when connection pooling is working. Once the connections to the server have been established, they will be used and placed in the pool for reuse.
ODBC Hard Disconnect Counter per second | The number of hard disconnects per second issued to the server. These are actual connections to the server that are being released by connection pooling.
ODBC Soft Connection Counter per second | Shows far more activity than the Hard Connection Counter. It shows the number of connections made to the database from open connections that were in the pool. Depending on your server load, it would not be uncommon to have 40–60 soft connections per second.
ODBC Soft Disconnection Counter per second | The number of disconnects per second issued by the application. When the application releases or disconnects, the connection is placed back in the pool.
ODBC Current Active Connection Counter | The number of connections in the pool that are currently in use. With some older versions of ODBC shipped with IIS 4.0, this counter does not work. See http://www.microsoft.com/odbc/ for updates to the ODBC driver manager.
ODBC Current Free Connection Counter | Displays the current number of free, live connections available in the pool.

Other Performance Counters

Other performance counters also can be useful for evaluating system performance. The following table details additional performance counters, the objects they are based on, and a description of why they are interesting.

Object | Counter | Description
SQL Server | Max Users | The maximum number of users that have concurrently connected to the SQL Server.
SQL Server | Current Users | The number of current users that are connected to the SQL Server. This number should be very close to the maximum number of hard connections per second from the connection pooling object.
SQL Server | Cache I/O hits | The percentage of data that is being read from the SQL Server cache, opposed to being read from disk. The higher the number, the better, because it is much faster to pull data from cache.
Processor | Processor utilization | Performance key that shows you the server's percentage of utilization.
IIS | Currently executing requests per second | The total number of executing requests per second.
IIS | Currently queued requests | The number of requests that are waiting to be serviced by your server.

Connection Pooling and Dead ODBC Connections

When a connection is severed from the database, it potentially could prevent all applications requesting connections from connecting to the database. If a dead connection were handed to an application as a working connection, the application would fail and the dead connection would be placed back into the connection pool. Once the dead connection was back in the pool, the driver manager would keep handing it to other applications requesting a connection.

In an effort to fix this problem, ODBC released a fix to the driver manager that would use an SQL Server-specific driver attribute to detect a dead connection at disconnect time. If the connection were dead, it would be destroyed rather than being placed back into the pool. This behavior fixed the problem of dead connections.

However, this correction introduced another problem with multithreaded applications that use ODBC. When the server crashes and no longer responds to the connection requests, all the connections are deleted from the pool. The multithreaded application attempts to make a new connection to the server, which does not respond. While the first connection is waiting to time out, the application makes many more connections. Eventually, the ODBC client will be swamped by connection requests. In an effort to detect dead servers, the driver manager will retry the connection to the server only once. If it is successful, the connection will be handed to the application requesting the connection. If the connection attempt fails, the connection is returned to the pool to block future connection attempts.

Dead Server Detection in Detail

There are two ways the driver manager attempts to determine if the server is dead. One technique involves checking the read-only connection attribute SQL_ATTR_CONNECTION_DEAD to determine if the server is working prior to returning the real connection to the connection pool. The second involves a number of ODBC status codes that contain information about network failure. These codes are then used to determine whether the problem was related to network failure or other problems.

The following procedure shows the series of events used by the driver manager to determine if the server is not responding. The process is based on a flag and a reconnect time, which is representative of the amount of time you will wait prior to attempting to reconnect to the server.

  1. The first time a connection is determined to be dead, it is placed back in the pool and marked as dead. The time that a reconnection should be attempted is noted on the connection before it is placed back into the pool.

  2. When the driver manager retrieves a connection from the pool that has been marked as dead, and the reconnect time has not yet been reached, it is immediately returned to the pool and returns an error to the application. This blocks other applications from trying to connect to the server. In this case, an error is returned to the requesting application.

  3. When a connection marked as dead is retrieved from the pool at the reconnect time, the connection is immediately returned to the pool to prevent other applications from trying to connect to the server. The connection to the server is then retried. If the connection is successful, the dead connection is removed from the pool and you allow the pool to function as normal. If the connection to the server fails, you reset the retry time on the dead connection in the pool.

Controlling Dead Server Detection

A new registry setting controls the retry timeout. This registry key allows you to alter the default value of 60 seconds to any time interval. You can find it in the registry under HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\ODBC Connection Pooling\Retry Wait. The key is of data type REG_SZ and is the timeout in seconds.

You can also control this setting from your application by calling the ODBC function ODBCSetTryWaitValue. The function takes a DWORD as an argument and returns TRUE if successful.

Following is a Visual Basic for Applications code sample that demonstrates setting ODBCSetTryWaitValue. In this example the value is set to 30, for a 30-second timeout on a dead connection.

Declare Function ODBCSetTryWaitValue Lib "ODBC32.dll" (ByVal dwTimeOut As Integer) As Boolean
Sub SetServerWait()
    Dim fRetValue As Boolean
    fRetValue = ODBCSetTryWaitValue(30)
End Sub

This setting was added to the Service Pack 1 release, which is available at the Microsoft ODBC Web site at http://www.microsoft.com/odbc/.

Cursor Considerations

When programming your application, you should consider the type of cursor you are requesting from the server. In the case of Microsoft SQL Server, you have a choice; each cursor carries specific functionality, benefits, and limitations. If your application is returning data from the server in a forward-only manner, the most efficient cursor to use will be Default Result Set. The default result set in SQL Server translates to a forward-only, read-only cursor. But in this case, all the query results are pushed from the server to the workstation without any further requests from the workstation. There are some limitations associated with this cursor; see the SQL Server documentation for a more complete description of this cursor type. Coding your application so it opens a dynamic, keyset, or static cursor only when necessary to update or delete records will improve the overall performance of the application. You should give special consideration to cases where you are planning to use a dynamic cursor, because it is by far the most expensive cursor you can request from a performance standpoint. In many cases, a keyset cursor would provide adequate functionality and optimal performance.

Cursor Summary

To get optimal performance from your application, you need to select the appropriate cursor for your needs. Each cursor contains a specific set of functionality, so choosing your cursor carefully will ensure that you aren't paying a performance price for functionality you do not need. The following table describes cursor types available from SQL Server and other ODBC data source back-end servers. For specific information about which cursors are supported by your back-end server, see your server documentation.

Cursor Type | Description
Default Result Set (SQL Server-specific) | A special forward-only/read-only cursor available only with Microsoft SQL Server. When the server is in this mode, it doesn't wait for the client to request the data before providing it. Once the cursor has been opened, the server loads the wire with data; the only overhead involves pulling the data off the wire.
Forward-only | Cursor movement can only be made one row at a time in a forward direction (from the top to the bottom of the result set).
Keyset | Membership is fixed, but the data is up-to-date with the underlying database. In other words, new rows added by users will not appear in the recordset, but updates to the records will become visible. All types of movement are supported. For your cursor creation to be successful, you will need an index build on your table.
Dynamic | Data is up-to-date at all times. All types of movement are supported. Depending on the provider, bookmarks may be supported.
Static | Effectively provides a snapshot of the data, but depending on the provider may also allow the user to see and perform inserts and updates to the recordset. Inserts and updates from other users will not be dynamically added to the result set. All types of movement are supported, but providers may or may not support bookmarks.

Setting Cursors under ADO, and OLE DB Provider for ODBC Drivers and ODBC

The following table details the settings required for different clients to achieve the specified cursor server side type. This information was tested against SQL Server. If you are using a different database server, check to see which cursor types it supports.

Cursor Type | ADO | OLE DB Provider for ODBC
Default Result Set* | No action required | Set properties: None
ForwardOnly-ReadOnly | Set the cursor location property on the connection object to adUseServer and the cursor type to adOpenForwardOnly on the execute | Set properties: -DBPROP_OTHERINSERT = VARIANT_FALSE -DBPROP_OTHERUPDATEDELETE = VARIANT_FALSE -DBPROP_OWNINSERT = VARIANT_FALSE -DBPROP_OWNUPDATEDELETE = VARIANT_FALSE
Dynamic | Set the cursor location property on the connection object to adUseServer and the cursor type to adOpenDynamic on the execute | Set properties: -DBPROP_OTHERINSERT = VARIANT_TRUE -DBPROP_OTHERUPDATEDELETE = VARIANT_TRUE -DBPROP_OWNINSERT = VARIANT_TRUE -DBPROP_OWNUPDATEDELETE = VARIANT_TRUE
Keyset | Set the cursor location property on the connection object to adUseServer and the cursor type to adOpenKeyset on the execute | Set properties: -DBPROP_OTHERINSERT = VARIANT_FALSE -DBPROP_OTHERUPDATEDELETE = VARIANT_TRUE -DBPROP_OWNINSERT = VARIANT_TRUE -DBPROP_OWNUPDATEDELETE = VARIANT_TRUE
Static | Set the cursor location property on the connection object to adUseServer and the cursor type to adOpenKeyset on the execute | Set properties: -DBPROP_OTHERINSERT = VARIANT_FALSE -DBPROP_OTHERUPDATEDELETE = VARIANT_FALSE -DBPROP_OWNINSERT = VARIANT_TRUE -DBPROP_OWNUPDATEDELETE = VARIANT_TRUE

*When setting the cursor, to get the default result set you need to be aware that rowset size has an effect on the cursor you set. In order to get the default result set, you need to set the rowset size to 1 before executing the SQL statement. For efficiency when returning data, set the rowset size to a higher value after executing the SQL statement. If you are making your call from ADO, you do not need to be concerned with this setting, because the OLE DB provider for ODBC will make the appropriate settings for you.

The following code sample demonstrates how to open a default result set cursor using ADO:

Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
cn.Open
Set rs = New ADODB.Recordset
rs.Open "SELECT * FROM Lots_O_Rows”,cn

The following code example shows how to request a dynamic cursor from SQL Server. Notice the setting for rs.CacheSize, which controls the ODBC rowset size:

Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
cn.Open
Set rs = New ADODB.Recordset
rs.CacheSize = 100
rs.Open "SELECT * FROM Lots_O_Rows”, cn, adOpenDynamic, adLockBatchOptimistic

Cursors and Supported Properties

The following table details the properties the cursor will exhibit, providing the concurrency is set to adLockBatchOptimistic and the table the cursor is open against contains an index. To test whether a property is available for the selected cursor, call the rs.Supports function. For more information about the Supports function or the properties listed below, see the ADO documentation.

Supports Option | adOpenForwardOnly | adOpenDynamic | adOpenKeyset | adOpenStatic
adAddNew | True | True | True | True
adApproxPosition | False | False | False | False
adBookmark | False | False | True | True
adDelete | True | True | True | True
adHoldRecords | False | False | True | True
adMovePrevious | False | True | True | True
adResync | False | False | True | True
adUpdate | True | True | True | True
adUpdateBatch | True | True | True | True

On a table with an index, opened with adLockBatchOptimistic, the following CursorTypes are applicable as indicated per property.

Method | adOpenForwardOnly | adOpenDynamic | adOpenKeyset | adOpenStatic
AbsolutePage | Not Applicable | Not Applicable | Write Only | Write Only
AbsolutePosition | Not Applicable | Not Applicable | Write Only | Write Only
ActiveConnection | Read Write | Read Write | Read Write | Read Write
BOF | Read Only | Read Only | Read Only | Read Only
Bookmark | Not Applicable | Not Applicable | Read Write | Read Write
CacheSize | 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
MaxRecords | Read Write | Read Write | Read Write | Read Write
PageCount | Not Applicable | Not Applicable | Read Only | Read Only
PageSize | Read Write | Read Write | Read Write | Read Write
RecordCount | Not Applicable | Not Applicable | Read Only | Read Only
Source | Read Write | Read Write | Read Write | Read Write
Status | Read Only | Read Only | Read Only | Read Only

On a table with an index, opened with the concurrency option set to adLockBatchOptimistic, the following CursorTypes are applicable as indicated per method.

Method | adOpenForwardOnly | adOpenDynamic | adOpenKeyset | adOpenStatic
AddNew | Yes | Yes | Yes | Yes
CancelUpdate | Yes | Yes | Yes | Yes
CancelBatch | 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 | No | Yes | Yes
MovePrevious | No | Yes | Yes | Yes
Open | Yes | Yes | Yes | Yes
Requery | Yes | Yes | Yes | Yes
Resync | No | No | Yes | Yes
Supports | Yes | Yes | Yes | Yes
Update | Yes | Yes | Yes | Yes
UpdateBatch | Yes | Yes | Yes | Yes

Additional Notes

  1. Although you can request batch updating with a forward-only and dynamic cursor, you cannot update more than one row at a time. Users should use keyset or static cursors with batch updating. If you are using SQL Server as your back-end server, the only cursor type that will do true batch updating is keyset.

IRowsetScroll is not implemented in the OLE DB provider for ODBC drivers; thus the AbsolutePosition and AbsolutePage properties are write-only. Furthermore, these properties require bookmarks and will not be supported by SQL Server when requesting a dynamic cursor.

QBU Cursors

QBU cursors are high-breed cursors that support updates via query-based operations. Consequently, you can request a forward-only/read-only QBU cursor from the OLE DB data provider. This gives you a forward-only/read-only cursor that supports updates to the result set, but you will not be able to see the updates unless you close your rowset and reopen it. OLE DB actually takes your update to the rowset and formulates an SQL statement, creating a new command object and executing the SQL statement against your database. The problem with this technique is that when the OLE DB code formulates the SQL statement, it might use all of its column values to formulate the WHERE clause of the statement, depending on your database design and the version of the ODBC driver you are running.

If your application is doing just a few updates to the database, it is advisable to use a forward-only/read-only QBU cursor. This is more efficient than using a dynamic cursor that would directly support your updates to the database. This cursor type's biggest limitation is that you will not be able to see your updates to the database until you close and reopen your recordset. To determine whether this is more efficient, you should run the application both ways with a timer.

Scrolling Backward

In this area, performance improved greatly with ODBC 3.5 drivers and version 1.5 of the OLE DB provider for ODBC data sources. Unfortunately, big improvements did not occur across all cursor types. Static cursors have shown the most improvements in scrolling backward. These performance improvements have made it possible to implement bookmark technology underneath the ODBC SQLScrollFetch interface. The OLE DB provider for ODBC drivers makes calls to SQLScrollFetch using bookmarks to fetch backward. If you are connecting to SQL Server and need to scroll backward, the keyset cursor would be your best choice.

Network Issues

SQL Server Network Libraries

When you install the SQL Server ODBC driver on a computer, the only network library usually installed is Named Pipes; for performance reasons, you should install additional libraries. The most direct way to get the additional network libraries is to install the SQL Server client utilities, which will install the additional network libraries. After installation, they are available through the ODBC Data Source Administrator. The network library with the best performance is TCP/IP.

Configuring Network Libraries

You can configure each DSN to work with a different network library. When you set up the DSN you can optionally choose the network library that you want the DSN to use. By default the SQL Server DSN will be configured to use Named Pipes. If you are using the SQL Server 2.65 driver, the network library is listed on the DSN configuration screen. Enter the DLL name of the network library you want to use. The process for the SQL Server 3.5 driver is simpler: On the second screen of the DSN Wizard, click the Client Configuration command button. Choose the Network Library tab from the SQL Server client configuration utility screen. Then you will see a list box of the network libraries that are currently installed on your computer. Be sure you have set up your SQL Server to support the network library you have chosen. Once you have set up a library for one connection, it will become the default for all subsequent connections, as well as any DSN-less connections that your application may use.

Another point about network libraries: After configuring the client and server computers to use a network library other than Named Pipes, you should not remove Named Pipe support from the server. The SQL Server Enterprise Manager uses this protocol to communicate with the server. Removing it will prevent SQL Server Enterprise Manager from communicating with your server.

Networking Bandwidth and Protocols

Many larger corporate networks run multiple protocols to accommodate different generations of network expansion. Some consideration should be given to the interaction of these protocols. For example, some servers running IPX will continually announce themselves with a broadcast message. This message leads to additional network traffic that may have an adverse effect on system performance.

TCP/IP currently provides the best performance. However, multiple network protocols are necessary for some corporate environments; eliminating other protocols and just running TCP/IP may not be practical. With this in mind, it might be possible to isolate the Web server and database server from the other protocols by not routing those protocols to the portion of your network that contains the IIS components. This would create a somewhat isolated network environment for your Web-based applications. Eliminating this additional network traffic may have a positive impact on overall performance.


Data Access Technologies

February 1998

By Robert Green

Introduction

Microsoft offers an array of technologies for accessing data sources from your applications. These include:

  • OLE DB and Microsoft® ActiveX® Data Objects (ADO) for all types of data

  • Open Database Connectivity (ODBC) for relational data

  • Remote Data Objects (RDO) for ODBC data

  • Data Access Objects (DAO) for local data (Microsoft Jet database engine)

  • DAO for ODBC data (ODBCDirect)

This paper puts the various technologies in perspective, reviews their capabilities, and clarifies the role of each. This will give you the background and knowledge to choose the right technologies and interfaces to use, both today and tomorrow, in your database solutions.

Universal Data Access

Universal Data Access is Microsoft’s strategy for providing access to information across the enterprise. Today, companies building database solutions face a number of challenges as they seek to gain maximum business advantage from the data and information distributed throughout their corporations. Through OLE DB and ADO, Universal Data Access provides high-performance access to a variety of information sources, including relational and non-relational sources, and an easy-to-use programming interface that is tool- and language-independent. These technologies enable corporations to integrate diverse data sources, create easy-to-maintain solutions, and use their choice of the best tools, applications, and platform services.

Universal Data Access does not require companies to move data into a single data store, which is expensive and time-consuming, nor does it require that they commit to a single vendor’s products. Universal Data Access is based on open industry specifications with broad industry support, and works with all major established database platforms. Universal Data Access is an evolutionary step from today’s standard interfaces, including ODBC, RDO, and DAO; and extends the functionality of these well-known and well-tested technologies.

Universal Data Access is based on the ability of OLE DB to access data of all types, and it relies on ADO to provide the programming model that application developers will use.

Universal Data Access Architecture

OLE DB

More and more often, businesses are discovering the need to build solutions that span desktop, mid-range, mainframe, and Internet technologies. The variety of data stores that are present in any business today, and the many ways in which they are accessed, create challenges to developing applications that can bridge both the new and the old technologies. As a result, new requirements emerge for middleware that enables a new class of applications to be easily built and managed.

OLE DB is Microsoft’s strategic low-level interface to all kinds of data throughout the enterprise. OLE DB is an open specification designed to build on the success of ODBC by providing an open standard for accessing all kinds of data. Whereas ODBC was created to access relational databases, OLE DB is designed for relational and non-relational information sources, including mainframe ISAM/VSAM and hierarchical databases; email and file system stores; text, graphical, and geographical data; custom business objects; and more.

OLE DB components consist of data providers, which expose their data; data consumers, which use data; and service components, which process and transport data (for example, query processors and cursor engines). These components are designed to integrate smoothly to help OLE DB component vendors bring high quality OLE DB components to market quickly. OLE DB includes a bridge to ODBC to enable continued support for the broad range of ODBC relational database drivers available today.

OLE DB is a set of COM (Component Object Model) interfaces that provide applications with uniform access to data stored in diverse information sources, regardless of location or type. OLE DB is a developing industry standard for data access to and manipulation of both SQL and non-SQL data sources.

OLE DB Architecture

OLE DB goes beyond simple data access by partitioning the functionality of a traditional relational database into logical components. There are three main categories of components built on the OLE DB architecture: data consumers, data providers, and service components.

Data consumers are applications that need access to a broad range of data. These include development tools, languages, and personal productivity tools. An application becomes ODBC-enabled by using the ODBC API to talk to data. Similarly, an application becomes OLE DB-enabled by using the OLE DB API to talk to data. Microsoft is actively encouraging a broad set of tool vendors to write to the OLE DB specification and in the near future all of our development tools will be able to access data via OLE DB.

Data providers make their data available for consuming. They may do this by natively supporting OLE DB or they may rely on additional OLE DB data providers. If a data provider is a native OLE DB provider, an application can talk to it directly, via OLE DB. There is no need for additional drivers or software. Contrast this with ODBC, where an ODBC driver is always needed for an application to talk to the data.

A data provider that does not natively support OLE DB relies on an intermediary in the same way that ODBC data sources rely on ODBC drivers. Data providers develop this intermediary software with the OLE DB SDK. This is important because it means that you can get to data through OLE DB without moving it and without waiting for the data source to be rewritten. To get to Microsoft® Exchange data today, all you would need is a MAPI data provider. You wouldn’t need to wait for a future version of Exchange.

The OLE DB Provider for ODBC enables applications to use OLE DB to talk to relational data via ODBC. This means that you can use OLE DB today to get to all of the same data you currently use ODBC to access. The OLE DB Provider for ODBC ensures that you can continue writing high performance database applications with existing ODBC technologies and drivers.

OLE DB provides a base level data access functionality: the managing of a tabular rowset. In other words, a provider must be able to represent data in rows and columns. Service components provide additional functionality such as query processing or cursor engines. A query processor allows SQL queries to be constructed and run against the data source. A cursor engine provides scrolling capabilities for data sources that don't support scrolling.

So for example, if you wanted to query Microsoft® SQL Server™ data you would not need a service component because SQL Server has both a query processor and cursor engine. However, to query a Microsoft® Internet Information Server log file you would either build or buy a component that provided querying capabilities for text files.

ADO (ActiveX Data Objects)

ADO is Microsoft’s strategic, high-level interface to all kinds of data. ADO provides consistent, high-performance access to data, whether you’re creating a front-end database client or middle-tier business object using an application, tool, language, or even an Internet browser. ADO is the single data interface you need to know for 1- to n-tier client/server and Web-based data-driven solution development.

ADO is designed as an easy-to-use application level interface to Microsoft’s newest and most powerful data access paradigm, OLE DB. OLE DB provides high-performance access to any data source, including relational and non-relational databases, email and file systems, text and graphics, custom business objects, and more. ADO is implemented with a small footprint, minimal network traffic in key Internet scenarios, and a minimal number of layers between the front-end and data source—all to provide a lightweight, high-performance interface. ADO is easy to use because it is called using a familiar metaphor – the Automation interface, available from just about any tool and language on the market today. And since ADO was designed to combine the best features of, and eventually replace RDO and DAO, it uses similar conventions with simplified semantics to make it easy to learn for today’s developers.

ADO ships with the following products:

  • Internet Information Server 4.0 (ADO is the standard way to get data into Active Server Pages)

  • Microsoft® Internet Explorer 4.0

  • The 6.0 versions of Microsoft® Visual Studio™, Microsoft® Visual Basic®, Microsoft® Visual InterDev™ and Microsoft® Visual C++®

  • The Microsoft Data Access Components, the Data Access SDK, and the OLE DB SDK (all available from the Microsoft Web site)

Since ADO is a COM-based component, any application or language capable of working with COM objects can use it. ADO is built around a set of "core" functions that all data sources are expected to implement. ADO is not specifically designed for relational or ISAM databases — but as an object interface to any data source.

ADO serves much the same purpose as DAO and RDO, that is, to give you an easy-to-use object model for accessing data. ADO allows you to access data via OLE DB and therefore is a way to programmatically work with all types of data.

Developers who have used DAO or RDO will recognize the ADO interfaces and will be able to work with them very quickly. ADO is targeted as the natural evolution of the DAO and RDO interfaces. The current version of ADO provides most, but not all, of RDO’s functionality, ADO will eventually provide a superset of RDO functionality and provide a far more sophisticated interface — in addition to an easier programming model. The goal of ADO is to become the Microsoft standard for high-level data access to all data sources in a language and platform neutral manner.

ADO Features

ADO provides the ability to perform data querying and manipulation, that is, SELECT, INSERT, UPDATE, and DELETE statements for all types of data. To access relational data, ADO can use OLE DB directly or it can use the OLE DB Provider for ODBC. To access other types of data, ADO will typically rely on the existence of a data provider. In the same manner that you can get to any relational data if you have the appropriate ODBC driver, you can use ADO to get to any data if you have the appropriate OLE DB provider.

Additional ADO features include:

  • Free threading — Free threading allows you to build very efficient and scalable Web server applications. This means that ADO can be used by multiple clients — each with its own thread — and the threads won’t interfere with each other.

  • Client side cursor engine — Based on technology in Visual FoxPro, query results are retrieved and stored in a cursor in memory on the client. Changes are made to a copy of the data stored in a second cursor. All changes are batched and sent to the server in one trip, reducing network traffic and improving performance.

  • Asynchronous queries — Running a query asynchronously allows control to be returned to the application before the query is completed. This would allow you to give the user the ability to continue or cancel a query that takes a long time to run.

  • Data shaping — This feature is new in ADO 2.0 and provides a way to model complex data relationships, such as one to many or many to many, to create sophisticated data analysis and reporting applications.

  • Disconnected Recordset objects — If the provider supports it, you can create a recordset and then disconnect from the data source. Later on you could reconnect to either refresh the data or upload changes. This reduces network traffic and improves performance.

  • Recordset remoting — Allows users to retrieve data and manipulate it remotely, then update the source. For more information, see the Remote Data Service section below.

  • Events — ADO 2.0 exposes a list of events that can be used to control all aspects of the connection, query, and row-access process. This provides finer control over all aspects of the data access operation and eliminates the need to poll while waiting for completion of asynchronous operations. In addition, this enables debugging using the Visual Studio Enterprise Debugger.

  • Commands as connection methods — If a command is associated with a connection, then the connection automatically acquires a method with the same name. This makes it easier to call stored procedures and queries and to pass parameters to them.

There are currently no data definition capabilities in ADO. As mentioned earlier, ADO today provides a subset of the functionality in RDO. However, it will eventually provide a superset.

Remote Data Service

The Microsoft® Remote Data Service (RDS), formerly known as the Advanced Data Connector (ADC), is your source for distributed data access through the Web. RDS is a feature of ADO and makes full-featured, data-centric Web applications a reality by combining data manipulation of retrieved data, efficient client-side caching, and support for data-aware ActiveX controls with an elegant and powerful programming model.

RDS goes beyond the current generation of Web data access tools by allowing clients to update the data they see. Using drop-in ActiveX data controls, such as grids, lists, and combo boxes, developers can deploy sophisticated user interfaces that allow end users to view and change data with a minimum of programming. End users are no longer restricted to staring at a static HTML results table. With RDS, they can now alter, add, and delete data they have queried and retrieved. In addition, all changes are buffered locally, and can be submitted to the server for inspection, processing, and storage in the database.

The benefits of traditional client/server technology have migrated to the Web, and the read-only, static client is a thing of the past. By providing a local data cache, the end user is now able to navigate through large data sets without costly server round trips.

RDS provides the ability to invoke remote objects over HTTP and DCOM, enabling programmers to develop distributed Web applications that effectively partition application logic between Microsoft® Visual Basic®, Scripting Edition code on the client and server objects. Automation objects written in Visual Basic can expose services to client-side applications, while protecting business logic and data from distribution. Developers are no longer restricted to choosing between "thin" or "fat" clients and servers. They can make an informed choice and partition their data and business logic accordingly.

ODBC (Open Database Connectivity)

ODBC is a common application programming interface (API) for SQL data sources and provides a common interface for accessing heterogeneous SQL databases. ODBC is based on SQL as a standard for accessing data. ODBC’s consistent interface provides maximum interoperability: a single application can access different database management systems (DBMS) through a common set of code.

This enables a developer to build and distribute a client/server application without targeting a specific DBMS or having to know specific details of various back-end data sources. When an application needs to get data from a data source, the application sends a SQL statement to the ODBC Driver Manager, which then loads the ODBC driver required to talk to the data. The driver then translates the SQL sent by the application into the SQL used by the DBMS and sends it to the back-end database. The DBMS retrieves the data and passes it back to the application via the driver and the Driver Manager.

Developers can code directly to the ODBC API by declaring various functions and then using them to connect, send SQL statements, retrieve results, get errors, disconnect, and so on. This is well documented and has been a very popular way of writing Visual Basic client/server applications. However, it is fairly difficult and involves a lot of code. Because of this, ADO, RDO, and DAO are used more often to access ODBC data from applications.

ODBC has been the data access standard since 1992 and has played a very important role in enabling client/server applications. There are over 170 ODBC drivers available. With well-written drivers, ODBC performance is excellent. ODBC in the short and medium term is the best way to access a broad range of relational data due to the high number of available drivers.

Microsoft Data Access Components

The Microsoft Data Access Components consist of the latest versions of ADO, OLE DB, and ODBC, which are released, documented, and supported together. They are the key technologies that enable Universal Data Access. They provide easy-to-use, high-performance access to all types of data throughout the enterprise. Developers creating client/server and Internet/intranet-based data-driven solutions use these components to easily integrate information from a variety of sources, both relational (SQL) and non-relational.

Microsoft Data Access Components 2.0 includes:

  • ActiveX Data Objects (ADO) 2.0

  • Remote Data Service (RDS) 2.0

  • OLE DB Provider for ODBC 2.0

  • OLE DB Simple Provider Toolkit

  • OLE DB Data Link API

  • OLE DB for OLAP

  • ODBC Driver Manager

  • ODBC Drivers for
    • Microsoft SQL Server

    • Oracle

    • Microsoft Jet

    • Microsoft® Visual FoxPro®
  • OLE DB Providers for
    • Microsoft SQL Server

    • Oracle

    • Microsoft Jet
  • Microsoft Jet Engine

RDO (Remote Data Objects)

RDO provides an object model for accessing ODBC data. The RDO programming model is similar to the DAO model, except that it is aimed at SQL databases rather than at ISAM data and therefore more emphasis is placed on stored procedures and taking advantage of server functionality.

RDO was invented to give Visual Basic developers the ability to access ODBC data without having to code to the ODBC API. It is a thin layer over the ODBC API and provides all the functionality of ODBC in an easy-to-use object model.

RDO is designed to take advantage of intelligent database servers that use sophisticated query engines, such as SQL Server and Oracle. It ships with the Enterprise Editions of Visual Basic, Visual C++, and Microsoft Visual Studio.

RDO Features

RDO provides all the expected data manipulation features plus a host of additional features:

  • Multiple result sets — RDO supports using a single query that returns multiple result sets. For example, you might fill multiple list boxes with a single query, reducing network traffic and improving performance.

  • Server-side cursors — Using cursors on the server rather than on the workstation can often improve performance and reduce network load.

  • Asynchronous processing — RDO provides an asynchronous query option that allows control to be returned to the application while a query is still running.

  • ClientBatch cursor library and optimistic batch updates — RDO allows you to dissociate result sets from the connection and work on them independently. When it comes time to update the rows, you can re-associate and issue a batch update to post the changes.

  • Queries as methods — To better support stored procedures, RDO supports the ability to declare stored procedures and parameterized queries as methods of a connection object. This makes it easier to call stored procedures and to pass parameters to them.

  • Events — RDO exposes a list of events that can be used to control all aspects of the connection, query, and row-access process. This provides finer control over all aspects of the data access operation and eliminates the need to poll while waiting for completion of asynchronous operations.

  • ODBC handles — If you need more flexibility or control than is available in the RDO object model you can directly access the ODBC environment, connection, and statement handles. This allows you to code using the ODBC API.

DAO (Data Access Objects)

DAO enables you to use a programming language to access and manipulate data in local or remote databases, and to manage databases, their objects, and their structure.

DAO provides for data access via an object model, rather than via a collection of function calls in a DLL. This makes it much easier and more understandable to make database calls. In addition, DAO hides various plumbing issues from the developer. The developer uses the objects and DAO manages the connections, result sets, and so on.

DAO supports two different database environments, or "workspaces:"

  • Microsoft Jet workspaces allow you to access data in Microsoft Jet databases, Microsoft Jet-connected ODBC databases, and installable ISAM data sources, such as FoxPro®, Paradox or Lotus 1-2-3.

  • ODBCDirect workspaces allow you to access database servers through ODBC, without loading the Microsoft Jet database engine.

It is important to understand that ODBCDirect is part of DAO 3.5. It is an extension to DAO, not a separate entity. ODBCDirect actually uses RDO to get to ODBC data.

DAO 3.5 ships with Microsoft® Access 97, Visual Basic 5.0 or later, Microsoft® Office 97, and Visual C++ 5.0 or later.

DAO Features (Microsoft Jet)

Microsoft Jet offers a great deal of functionality, and if you use a Microsoft Jet workspace you have programmatic access to that functionality. Features available with Microsoft Jet workspaces include:

  • Data manipulation — The ability to read and write data (SELECT, INSERT, UPDATE, DELETE).

  • Heterogeneous joins — With Microsoft Jet, you can perform a join across data sources. For instance you can join a SQL Server table to an Access table.

  • Data definition — In Microsoft Jet workspaces there are objects for tables, fields, and indexes, allowing you to add and delete these through DAO, rather than sending statements such as CREATE TABLE to the back end.

  • Replication— Microsoft Jet provides database replication, which allows the developer to make special copies, called replicas, of a database so that users at different locations can all work on their own copies at the same time and share, or synchronize, their changes.

DAO Features (ODBCDirect)

Using a Microsoft Jet workspace to access ODBC data requires not only loading the Microsoft Jet database engine but also using it as a middleman between the application and the data. ODBCDirect workspaces allow you to access database servers through ODBC, without loading the Microsoft Jet database engine. This provides better performance and also requires less memory (except in Access, where Microsoft Jet is always loaded), at the expense of some functionality, such as heterogeneous joins.

ODBCDirect provides all the expected data manipulation plus additional features, all of which are also found in RDO (this is not surprising since ODBCDirect uses RDO!). These additional features include:

  • Multiple recordsets

  • Server-side cursors

  • Asynchronous processing

  • Input/output parameters and return values

  • ClientBatch cursor library

  • Optimistic batch updates

For a discussion of these features see the RDO Features section earlier in the paper. Note that queries as methods and events are supported in RDO but not in ODBCDirect.

Which Data Access Technology Should You Use Today?

So now you know the various options you have for accessing data today. This paper has covered ADO, ODBC, RDO, DAO with Microsoft Jet workspaces, and DAO with ODBCDirect workspaces. Which should you use today?

ADO is now the standard data access language for Microsoft tools. The current versions of Internet Information Server, Internet Explorer, Visual Basic, Visual InterDev, Visual C++, and Visual J++, have all been written to use ADO as their primary data access language. The next release of Microsoft Office will do the same.

Among the many benefits of ADO is a common language for accessing data. No matter what tool you are using, you can use the same code to query and manipulate data. This allows for much greater and easier code reuse across applications than was possible in the past.

Therefore, if you are starting an application today you should use ADO, unless there are features you need today that are not available in ADO but are available in one or the alternative technologies. However, be aware that the goal of ADO is to superset DAO and RDO.

If you are using DAO or RDO you should still think about how you would move over to ADO when it supersedes these. That way, when the time comes, you will have an easier job migrating to ADO.

Use DAO if…

  • You need to access Microsoft Jet or ISAM data, or you want to take advantage of Microsoft Jet features such as compacting and repairing databases, replication, or DDL through the objects.

  • You have an existing application that uses DAO with Microsoft Jet, and you want to convert the application to use ODBC data, while achieving better performance.

Use RDO if…

  • You are accessing ODBC data and you want the most functionality. ADO today has most of the features of RDO, but not all of them. Remember that in the future ADO will superset RDO.

  • You are developing in Visual Basic and you want to take advantage of features such as the Connection Designer.

Feature Comparison

To help make that decision of which interface to use and also to determine if ADO meets your needs today, the following table presents a list of major features found in either ADO, DAO, or RDO.

ADO, DAO, RDO Feature Comparison

Feature | ADO 2.0 | DAO 3.5 | RDO 2.0
Connect Asynchronously | X |  | X
Run Queries Asynchronously | X | X | X
Batch Updates and Error Handling | X | X | X
Disconnected Recordsets | X |  | X
Events | X |  | X
Integration with Data Environment | X |  |
Integration with Data binding in Visual Basic 6.0 | X |  |
Integration with Visual Data Tools | X |  |
Integration with Visual Basic/Visual C++ Transact-SQL Debugger |  |  | X
Integration with Visual Basic Connection Query Designer |  |  | X
Data Shaping | X |  |
Persistent Recordsets | X |  |
Distributed Transactions | X |  | X
Threadsafe | X | X | X
Free-Threaded | X |  |
In/Out/Return Value Parameters | X | X | X
Independently-created objects | X | X | X
MaxRows Property on Queries | X | X | X
Queries As Methods | X |  | X
Return Multiple Recordsets | X | X | X
Efficient Microsoft Jet Database Access | X | X |
Compatibility from Microsoft Jet to SQL Server |  | X |


Designing Efficient Applications for Microsoft SQL Server

Designing Efficient Applications
for Microsoft SQL Server

by Peter Hussey

Introduction

Designing a Microsoft® SQL Server application for optimal performance can seem a daunting challenge. There are so many choices to make—development tools, database design, application structure, query design, choice of interface—and the “right” choices in each of these areas depend on your unique application requirements and on the skills your development team brings to the project. But you don’t have time and resources to try out several different approaches to see which one works best for your situation. You don’t even have time to read up on all of the different options to make a fully informed decision. So how do you get started?

The good news is that there are many paths to great performing SQL Server applications. You can take any one of a number of different approaches and end up with an application that performs as well as you need it to. Even so, knowing a few basic principles and trade-offs of SQL Server development will help you tremendously as you begin. This paper conveys these basic principles and trade-offs. If you are developing a new SQL Server application or redesigning an existing one, this paper is for you.

One of the first decisions you need to make is where to locate the running code. Should most of the work be done on the client? In the database? On a separate server? Wrapped up in this question is a choice of development tools. Tools such as language compilers or interpreters, debuggers, and run-time deployment platforms often restrict the development model choices. Therefore, you should address decisions about tools and the development model at the same time. This paper introduces the logical three-tier development model and then describes four physical implementations of that model. This is followed by a discussion of database access styles, choosing and optimizing your interface, and tips for leveraging built-in SQL Server features.

Three-Tier Application Development

Logical Three-Tier Model

The logical three-tier model divides an application into three logical components.

Logical Three-Tier Model

  • Data services
    These services join records and maintain database integrity—for example, constraints on valid values for a customer number and an enforced foreign-key relationship between the customer table and the orders table.

  • Business services
    These services apply business rules and logic—for example, adding a customer order and checking a customer’s credit availability.

  • Presentation services
    These services establish the user interface and handle user input—for example, code to display available part numbers and orders for a selected customer.

When deploying an application, there are many ways you can arrange these three logical layers on physical machines. The following sections describe four physical implementations of the logical three-tier model:

  • Physical two-tier implementation with FAT clients

  • Physical two-tier implementation with a FAT server

  • Physical three-tier implementation

  • Internet implementation

Physical Two-Tier Implementation with FAT Clients

A common method for deploying an application is a physical two-tier implementation with FAT clients, where the business logic and presentation services all run on the client. In this implementation, the server acts only as a SQL Server database. Most applications written today using the Microsoft Visual Basic® or PowerBuilder programming systems are examples of this model.

Physical Two-Tier Implementation

A new option in this implementation is the ability to do OLE packaging of business rules for improved reuse. For example, using Visual Basic version 4.0 or later you can code business rules into an OLE object that you can call from another Visual Basic application. This allows you to physically separate business rules from your presentation logic in the code base. If both the user interface application and the business object run at the client, it is still a physical two-tier implementation. Separating the code, however, makes it easy to move to the physical three-tier implementation described later in this paper.

A primary advantage of this FAT client implementation is that the tools that support it are powerful and well established. As of this writing, PowerBuilder is in its fifth major version, and Visual Basic is about to be released in its fifth major version. A disadvantage of this implementation is that deploying the business services at the client generally means more network traffic because the data has to be moved to the client to make the decisions coded in the business logic. On the other hand, the client computer is a good place to store “state” information associated with the user, such as the primary key of the record the user is currently viewing.

Physical Two-Tier Implementation with a FAT Server

In a physical two-tier implementation with a FAT server, business logic and presentation services are deployed from the server database. In this implementation, business logic is generally written as stored procedures and triggers within the database. For example, in the TPC-C benchmarks published for Microsoft SQL Server, the core transaction logic is coded as Transact-SQL stored procedures in the server. Many internally-developed corporate applications also make extensive use of stored procedure logic. Microsoft uses this implementation to handle internal business functions, such as customer information tracking.

Physical Two-Tier Implementation

The major new development in this implementation is the availability of a Transact-SQL debugger. This debugger is integrated into the Enterprise Editions of both Microsoft® Visual C++® version 4.2 and later and Microsoft® Visual Basic® version 5.0. This debugger makes it possible to step through Transact-SQL code, set breakpoints, and view local variables.

The major advantage of this FAT server implementation is performance. The business logic runs in the same process space as the data access code and is tightly integrated into the data searching engine of SQL Server. This means data does not have to be moved or copied before it is operated on, which results in minimal network traffic and the fewest possible network roundtrips between client and server. The published TPC-C benchmarks from Microsoft Corporation and other major database vendors all use this implementation. In the SQL Server TPC-C implementation, each of the five measured transactions is performed in a single roundtrip from client to server because all of the logic of the transaction takes place in a Transact-SQL stored procedure.

The main disadvantage of this implementation is that it limits your choice of development tools. Stored procedures are written in the language supported by the database. SQL Server supports calls from the server to code written in languages other than Transact-SQL, but this option adds complexity and is generally not as efficient as the same functionality written in Transact-SQL.

Physical Three-tier Implementation

The physical three-tier implementation is one of many implementations of the logical three-tier model. Commonly referred to as the “three-tier model,” it is often incorrectly thought of as the only physical implementation of the logical three-tier model. In this implementation, business logic runs in a separate process that can be configured to run on the same server or a different server from the server the database is running on. The key distinction of the physical three-tier implementation is that there is a cross-process boundary, if not a cross-computer boundary, between data services and business services, and another cross-process or cross-computer boundary between business services and presentation services. SAP’s R/3 application suite is a physical three-tier implementation, as are many of the large financial and line-of-business packages from other vendors. Transaction processing monitor products such as Encina or Tuxedo also use this implementation.

Physical Three-Tier Implementation

A major new option for using this implementation is Microsoft® Transaction Server. Transaction Server can host business services written in any language that can produce OLE objects. Transaction Server manages the middle layer and provides many of the run-time services that would otherwise have to be built for a physical three-tier implementation. For example, Transaction Server provides a mechanism for reusing object instances among multiple users.

The physical three-tier implementation offers advantages of database independence. Most physical three-tier implementations access several databases. These applications generally treat databases as standardized SQL engines and make limited use of database-specific features.

Some variations of the physical three-tier implementations also offer language independence. Microsoft Transaction Server, for example, supports any language that can produce OLE/COM in-process objects, including Visual C++, Visual Basic, and Micro Focus COBOL. Any of these languages can be used to write business logic that is then hosted at run time by the Transaction Server. SAP’s application, on the other hand, does not offer language independence—all application code developed in R/3 is written in their language called Advanced Business Application Programming (ABAP).

In some cases, the physical three-tier implementation is more scalable than other physical implementations. If the business logic code consumes a great deal of processor time or physical memory, it can be advantageous to locate those business processes on one or more servers separate from the database to avoid contention for resources. This potential scalability gain is offset by the additional cost of moving data across the network to the middle-tier servers, so it is not a win in all cases. Physical three-tier applications can also potentially access partitioned databases on multiple computers, giving an additional dimension of scalability. Partitioning the database, however, introduces enormous complexities into the application and is not a widespread practice in the industry today.

A disadvantage of the physical three-tier implementation is that it tends to require more management. Also, while the physical three-tier implementation can offer the capability to employ more physical computers on an application, it generally does not offer as compelling a price/performance ratio as an application whose logic is implemented in stored procedures.

Internet Implementation

The Internet has introduced a new twist on the logical three-tier model: the ability to split the presentation services onto a browser client and a Web server. The Web server is actually responsible for formatting the pages that the user sees. The browser is responsible for displaying these pages and downloading additional code they may need. Between the Web server and the database, the choices remain the same for locating the business services logic.

A common Internet implementation is to run both business and presentation services at the Web server. In some products, the business logic can run in the Web server’s process space, thus avoiding the overhead of crossing an additional process boundary. An example of a product that uses this implementation for database applications is Microsoft Internet Database Connector (IDC), which is part of the Microsoft® Internet Information Server (IIS) in the Microsoft® Windows NT® operating system. IDC connects to any ODBC data source, including SQL Server, retrieves data, and formats the data into an HTML page that is sent immediately to a browser client.

Internet Implementation

There are many newly released products that support Internet implementations of database applications. For example, IIS version 3.0 allows developers to write business and presentation services in Microsoft® Visual Basic®, Scripting Edition, and includes the ability to load and invoke an Automation object. Also, Microsoft® ActiveX® controls offer a way to run more of the presentation services and possibly the business services from the browser client. These extensions to Internet technologies give more flexibility for where you can deploy the logical three tiers of a database application written for browser clients.

One key advantage of Internet implementations is that anybody who has a browser client can access these applications. With little or no additional development effort, an application can be accessed simultaneously from the Microsoft Windows® operating system version 3.1, Windows 95, Windows NT, Apple Macintosh, OS/2, and UNIX clients. All of the client functionality required is provided by standard Web browsers. Ease of management is another key advantage of an Internet implementation. In an Internet application, an update to the Web server automatically updates all clients. Managing Web page code at a few servers is easier than managing application versions at many clients.

The basic Internet implementation today (for example, using IIS and IDC and putting business services at that Web server layer) is not a high volume online transaction processing (OLTP) solution. But it is important to note that the application implementations discussed in this paper can be mixed to combine their advantages. For example, an implementation that uses an application’s business services in stored procedures and that handles presentation services at the Web server can be very efficient. In fact, Microsoft’s latest TPC-C benchmarks use IIS to handle browser clients, as opposed to using the alternatives.So an Internet-style application can be used for high volume OLTP if business services are executed as stored procedures in the database.

Choosing an Implementation

Your application requirements will determine which physical implementation of the logical three-tier model you choose. The key requirements to consider are:

  • Performance and scalability

    If your throughput requirements are high and optimum price/performance is the goal, an implementation that uses business logic in stored procedures may be called for.

    If your business services are resource-intensive and the ability to apply many servers to the application is the goal, a physical three-tier implementation may be best.

    On the other hand, PC hardware has become so powerful and cost-effective that your application performance requirements can be satisfied easily by any one of these implementations.

  • Client platform and access

    If a variety of client platforms must have access to your application, an Internet implementation is compelling.

  • Developer skills, especially skills in a particular language.

    If you have developer skills or existing code in a particular language, the cost of choosing an implementation supported by that language is significantly lower.

  • Administration

    Different implementations require different administrative overhead.

  • Database and/or tool independence

    Some implementations require an application to be oriented to a specific database or language.

All of these considerations affect the decision of how to physically implement a three-tier application. There is no one correct answer—the best course of action is to thoroughly understand the alternatives and the trade-offs before choosing an implementation.

When you choose a development tool and decide how to implement the logical three-tier model, you must also choose the interface by which your client application will communicate with SQL Server. The best interface to use depends on the development language and the type of application under development. The choices fall into three categories:

  • Call-level interfaces

  • Object interfaces

  • Embedded SQL

Choosing an Appropriate Interface

This section provides an examination of the interface choices and recommendations for using each interface.

Call-Level Interfaces

A call-level interface offers a set of function calls or APIs that enable client applications to interact with a server database. Call-level interfaces usually use parameters specified as pointers to data input and output buffers owned by the application. Because of this reliance on pointers, call-level interfaces are almost always used from the C/C++ language. With some mapping code, these interfaces can be called from languages that lack pointer support, such as Visual Basic, but usually developers in these languages are more comfortable and productive using an object interface.

SQL Server offers two call-level interfaces:

  • Open Database Connectivity (ODBC)—an industry-standard, call-level interface

  • DB-Library—the original call-level interface that is specific to SQL Server

At a functional level, ODBC and DB-Library are similar interfaces. They both offer function calls to perform tasks such as opening a connection to SQL Server, executing an SQL statement, and retrieving data and metadata from SQL Server. They also have similar performance characteristics. While many database vendors support ODBC by using a mapping layer on top of their proprietary interface, this is not true for Microsoft SQL Server. For SQL Server, these two APIs are implemented at the same logical layer in the software architecture; both are “native” interfaces for SQL Server. (In fact, all current SQL Server TPC-C benchmarks are published using the ODBC interface, which is strong evidence of the performance of SQL Server’s ODBC implementation.) Both APIs offer full access to the same feature sets, with minor exceptions. In particular, ODBC does not impose a generic, lowest-common-denominator approach to building applications. An ODBC application can be written to be compatible with all databases, or tuned for SQL Server implementation, or anywhere in between.

ODBC is the recommended interface and offers the following advantages over DB-Library:

  • ODBC is easier to learn.

    DB-Library uses different API sets for similar functions that are implemented differently, such as retrieving data using a default result set (dbsqlexec, dbresults, dbnextrows) versus a server cursor (dbcursoropen, dbcursorfetch). ODBC implements these similar functions using the same APIs (SQLExecute, SQLExtendedFetch) and a simple statement option to distinguish a default result set from a server cursor. Because of these special-purpose function calls, DB-Library has many more APIs to learn than ODBC (150 versus 50).

  • The ODBC driver uses the performance features of SQL Server automatically.

    For example, SQL Server stored procedures can be executed using an efficient procedure call network format. DB-Library uses a separate set of APIs to send requests in the network format. ODBC uses the same APIs used for sending non-stored procedure requests and looks for the standard ODBC “call” syntax to trigger the use of this efficient network format.

  • ODBC is an industry-standard interface.

    The code and skills used building a SQL Server application on ODBC can be leveraged to build applications for almost any other SQL database. Of course, the code that uses SQL Server features that are not implemented in other ODBC drivers may have to be isolated in a common code base, but this is usually a small portion of the code.

These advantages present a strong case for developing new call-level applications using ODBC. If a company has existing DB-Library applications, there is no need to rewrite them to ODBC unless they are being revised to take advantage of ODBC features. DB-Library applications have excellent performance and will continue to be supported by Microsoft SQL Server for some time. DB-Library, however, will not generally receive feature enhancements in future releases of SQL Server.

Object Interfaces

Object interfaces offer a model of database programming “objects” (such as connections, SQL statements, and result sets) that can be created by your application and used to send and retrieve data from the database. You can use the objects by calling methods defined for the object (such as Execute) and by setting or getting properties on the object (such as CursorType).

Object interfaces vary widely in their level of abstraction, exposure of database features, and performance characteristics. They are also usually restricted to specific programming languages. Microsoft offers several object interfaces with overlapping functionality, including OLE DB, ActiveX Data Objects (ADO), Remote Data Objects (RDO), and Data Access Objects (DAO). Other vendors of database programming tools such as PowerBuilder or SQL Windows offer their own object interfaces as part of their tools.

With so many choices, it is difficult to sort out the features and decide which is best for your application. This paper does not compare Microsoft’s development tools and object interfaces with third-party tools and their associated object models. Such a comparison is complex and highly dependent on factors such as programmer skills, application requirements, and vendor support. This paper does, however, present the major object interfaces provided by Microsoft Corporation and give some recommendations based on development language and application requirements.

OLE DB and ADO

In 1996, Microsoft released the OLE DB Software Development Kit (SDK), establishing a new Component Object Model (COM) interface to tabular (row and column) data providers. Like other parts of OLE, OLE DB defines an object hierarchy and a set of defined interfaces, each of which has defined methods for manipulating data. An implementation of an interface must support all of the methods defined for the interface, so that an application that uses (“consumes”) the interface can rely on those methods being fully supported in that implementation. OLE DB abstracts the concept of tabular data, so that SQL and non-SQL data sources can expose common interfaces for data retrieval and manipulation. This allows a number of different data sources to expose common OLE DB interfaces.

To access SQL data, you can view OLE DB as an object version of the ODBC API: a standard, high performance interface to a wide variety of data sources. In fact, an important implementation of OLE DB that ships with the OLE DB SDK supports ODBC data sources. This implementation, known as the ODBC Provider, makes any ODBC data source accessible to OLE DB consumers. The ODBC Provider provides performance and feature support comparable to using the ODBC call-level interface to these same data sources. The SQL Server ODBC driver has been thoroughly tuned and tested to work with the ODBC Provider. Thus, the ODBC Provider can be an excellent alternative for applications that need an OLE COM interface to general SQL Server data.

OLE DB offers some powerful capabilities, such as notifications, interface sharing, and OLE objects as column types, that make it possible to build data-aware OLE components that are difficult to build using the ODBC interface. So if you are building data-aware OLE components to be used by other applications, OLE DB is likely to be your best interface choice. Likewise, if you have your own data source to expose programmatic access to, you should look at OLE DB. For more details about the capabilities of OLE DB, see the white paper, “OLE DB for the ODBC Programmer.”

Unlike most object interfaces to SQL Server, however, OLE DB does not make programming any easier than using a call-level interface. OLE DB uses pointer data types extensively, which makes it directly accessible only from C/C++. Writing an application to OLE DB also requires a great deal of interface creation and release code that is unrelated to the task of working with application data. Fortunately, the OLE DB SDK also includes a higher-level object interface called ActiveX Data Objects (ADO) that addresses these concerns.

ADO offers a higher-level object interface on top of OLE DB that avoids much of the programming work without sacrificing functionality or performance. ADO is a pointerless object interface that supports Automation, which makes it callable from both C/C++ and from scripting languages like Microsoft® JScript development software or Visual Basic Script. For these reasons, ADO is a more appropriate interface for most business applications, including SQL Server, that need access to OLE DB data sources.

ADO is the recommended and supported data interface for Internet applications written with the Microsoft® Visual InterDev Web development system. These applications call ADO from Active Server Pages (ASPs), incorporating program logic written in Visual Basic Script or JScript. The OLE DB SDK also includes instructions for how to call ADO from Java or C/C++ programs that are authored outside of Visual InterDev. The ADO and OLE DB components shipped in the OLE DB SDK can be freely redistributed with applications that use them.

RDO and DAO

Microsoft Visual Basic ships with two object interfaces to data:

  • Remote Data Objects (RDO)

  • Data Access Objects (DAO)

RDO is an object interface that is closely tied to ODBC and optimized for accessing server databases. It exposes nearly all the functionality available in the SQL Server ODBC driver and makes this functionality easily accessible to Visual Basic programs. RDO supports binding visual controls, such as list boxes and grids, directly to SQL Server data, greatly reducing the amount of code that must be written to display data on the screen. RDO also makes the ODBC call-level interface “handles” available so that a Visual Basic program can use any features in an ODBC driver that were not incorporated in the RDO object model. Even with these many features, RDO imposes little performance overhead on top of ODBC and has a relatively small memory footprint. For these reasons, RDO makes an excellent choice for SQL Server access from Visual Basic programs.

The DAO interface is closely tied to and optimized for the Jet local database, which ships in Visual Basic and Microsoft Access. It offers similar syntax and many of the same features as RDO for getting directly to SQL Server data. For applications that only access SQL Server data, however, DAO offers less functionality and has a larger memory footprint, so it is not the recommended interface. But DAO offers features that RDO does not, such as efficient access to local Jet data and the ability to join data from multiple data sources in a single query. If your application requires these unique features, DAO is the right choice. Conversely, if your application does not involve local Jet data or heterogeneous queries, stick with RDO.

Microsoft plans to develop ADO into the successor of both RDO and DAO. Over time, ADO will become a superset of most of the unique functionality and supported data sources of both of these interfaces. At that point, ADO will be incorporated into Visual Basic and become the primary object interface to data for Visual Basic programs. All three interfaces have a similar object hierarchy and syntax, so migrating applications in the future will not be difficult. In the meantime, ADO is callable from the Visual Basic development environment, but this is not an officially supported or documented configuration as of this writing.

SQL-DMO

SQL Distributed Management Objects (SQL-DMO) is a specialized object interface for SQL Server administration tasks. It has object properties and methods to support operations such as creating devices and databases, establishing replication relationships, scheduling housekeeping tasks, and defining alert actions on errors reported by SQL Server. SQL Enterprise Manager is layered on top of SQL-DMO, which means that almost anything you can do in the user interface can also be done programmatically. SQL-DMO supports OLE Automation interfaces, which makes it accessible from interpreted languages such as Visual Basic, as well as C/C++.

SQL-DMO is not intended to be a general-purpose interface for application data. Most applications, however, need to accomplish some database administration tasks as part of their setup and maintenance modules. These portions of your application can be programmed using standard Transact-SQL statements and stored procedures or through calls to SQL-DMO. In most cases the SQL-DMO method is easier to write because the object syntax is more consistent and handles tasks like repeating an operation for each object in a database much more concisely. The scheduled tasks and alerts objects of SQL-DMO can be an effective tool for making your application self-managing through automatic maintenance tasks and predefined responses to error conditions. A self-managing server will keep your users happy.

Embedded SQL

Embedded SQL is an ANSI-standard programming interface in which SQL statements, delineated by EXEC SQL tags, are incorporated into the source code of an application. The source code is input to a precompiler, which identifies the SQL blocks and replaces them with the appropriate low-level function calls for communicating with the database. An Embedded SQL precompiler for SQL Server is currently available for programs written in C in the form of a toolkit that ships on the Microsoft Developer Network (MSDN™) level 2. This precompiler technology has also been licensed to Micro Focus, who offers it as a toolkit for Cobol programmers. Both precompilers have passed the NIST tests for ANSI-standard compliance with SQL Server version 6.5.

Embedded SQL offers a familiar programming model for developers of applications for other databases such as Oracle or DB2. For Cobol programmers, Embedded SQL is also the most commonly supported database interface of any kind and is an excellent solution for accessing SQL Server. For applications written in C, however, Embedded SQL is somewhat slower than the call-level interfaces and doesn’t allow you to take advantage of specific SQL Server features and performance optimizations. The primary design goal for Embedded SQL for C is to follow the strict ANSI standard for maximum portability of applications. Embedded SQL for C is useful if you are porting an application from another database and have a large code base that would be difficult to adapt to ODBC. For these applications, Embedded SQL for C will perform adequately but may not offer optimal performance and control.

Optimizing Your Interface

Regardless of which interface you choose, you need to learn how to use it effectively. Any interface can be used in ways that create unnecessary overhead and degrade performance. To understand how to get the best performance out of your chosen interface, first look for any technical documentation available on optimizing client/server performance for that interface. Programmers using ODBC or RDO, for example, should read the white paper, “Using ODBC with Microsoft SQL Server.” Much of the advice in this paper, such as when and how to use prepared statements and stored procedures, applies to OLE DB and ADO applications as well.

Use SQL Trace, which ships with SQL Server 6.5, to examine the client/server traffic generated by your application. What you find may surprise you. Many commands being sent to the server may not be generated by your application but instead are sent by the interface you are using. Often these extra commands can be avoided by changing a property or an option in the interface and without affecting the functionality of the application.

For example, the ODBC driver sends a SET TEXTSIZE statement to the server every time a statement handle has a nondefault value for SQL_MAX_LENGTH statement option. If your application does not need to limit the maximum length of returned column data, you can avoid unnecessary overhead by leaving that statement option at its default value. SQL Trace can also point to other opportunities for optimization, such as statements that can be batched or transactions that can be committed sooner. If you can optimize the client/server traffic identified by SQL Trace, you are a long way toward optimizing your overall application.

Database Access Styles

Another key aspect of database application design is how the application code interacts with the database. Some applications treat the database simply as a storage place for records. The application itself performs most of the operations on the data, such as filtering, aggregating, or matching records. Other applications treat the database as a data management engine, performing all of these data operations at the server. The first style of database access is common in programs written to use an indexed sequential access method (ISAM) database. The second style of database access is more appropriate for a program interacting with a relational database like SQL Server. This section compares these two styles of database access.

ISAM-style Database Access

Many applications are originally developed on an ISAM database and are later moved to a relational database. Usually porting to a relational database supports better data sharing with other applications or fits customer database standards. The first port of such an ISAM-style application often substitutes a relational database table for the equivalent ISAM file and leaves the bulk of the application code unchanged. SQL Server will work for these ISAM-style applications, but the application performs much better if it is adapted to take full advantage of relational-style database access and asks the server to do the jobs it is capable of doing.

An ISAM-style Application Example

An example illustrates why an ISAM-style application does not perform optimally on SQL Server. This application example involves three tables: a master customer table, a detail orders table, and a summary invoice table. The program task is to extract the appropriate record for customer “Viki Parrott,” find the customer’s current orders, and then aggregate the order totals for that customer into the invoice summary table. An ISAM-style application to accomplish this task is coded as follows:

  1. The client sends a BEGIN TRANSACTION request to the server.

  2. The client requests that the server open a cursor on the customer table (A) for records with the last name >= 'Parrott'.

  3. The client fetches one or more customer records until the correct record is found.

  4. Having found the customer ID, the client program asks the server to open a cursor on the orders table (B) for orders matching the customer ID.

  5. The client fetches all matching order records for the customer.

  6. The client calculates the aggregate of those detailed records.

  7. The client sends a command to update the invoices table (C) with the calculated aggregate.

  8. The client sends a COMMIT TRANSACTION statement.

An ISAM-Style Application

Performance Problems of ISAM-style Database Access

Why doesn’t the ISAM-style application perform well? The problems with this style of coding are:

  • Excessive network traffic.

    The ISAM-style moves data unnecessarily across the network. This example application moved the master and detail records to the client to calculate an aggregate, a task that could have been done at the server. More importantly, this example required at least seven roundtrips between client and server to accomplish a simple task. Network roundtrips are usually the number one factor for poor performance in a client/server application, an even greater factor than the amount of data transferred between server and client.

  • Excessive use of cursors.

    Cursors are a useful tool in relational databases; however, it is almost always more expensive to use a cursor than to use set-oriented SQL to accomplish a task. In set-oriented SQL, the client application tells the server to update the set of records that meet specified criteria. The server figures out how to accomplish the update as a single unit of work. When updating through a cursor, the client application requires the server to maintain row locks or version information for every row, just in case the client asks to update the row after it has been fetched. Also, using a cursor implies that the server is maintaining client “state” information such as the user’s current rowset at the server, usually in temporary storage. Maintaining this state for a large number of clients is an expensive use of server resources. A better strategy with a relational database is for the client application to get in and out quickly, maintaining no client state at the server between calls. Set-oriented SQL supports this strategy.

  • Unnecessarily long transactions.

    The ISAM-style application example uses a “conversational” style of communicating with the server that involves many network roundtrips within a single transaction. The effect is that transactions get longer in duration. Transactions handled this way can span seconds or minutes or even tens of minutes between the BEGIN TRANSACTION and the COMMIT TRANSACTION statements. These long-running transactions can be fine for a single user, but they scale poorly to multiple users. To support transactional consistency, the database must hold locks on shared resources from the time they are first acquired within the transaction until the transaction commits. If other users need access to the same resources, they must wait. As individual transactions get longer, the queue and other users waiting for locks gets longer and system throughput decreases. Long transactions also increase the chances of a deadlock, which occurs when two or more users are simultaneously waiting on locks held by each other.

Relational-Style Database Access

The problems that result from running an ISAM-style application on SQL Server stem from its failure to take advantage of the capabilities of SQL Server. In contrast, an application that is well designed for SQL Server:

  • Uses SQL effectively.

  • Minimizes network roundtrips to the server during a transaction.

  • Uses stored procedures.

Incorporates the essential indexing and configuration steps for SQL Server.

These characteristics don’t have to be applied with an all-or-nothing approach. They can be incorporated into an application over time.

Structured Query Language

Structured query language (SQL) is the language of Microsoft SQL Server. It makes sense that application developers learn to “speak” this language fluently if they want their applications to communicate effectively with the server. By using SQL effectively, an application can ask the server to perform tasks rather than requiring application code and processing cycles to achieve the same result. More importantly, effective use of SQL can minimize the amount of data that must be read from and written to disk devices at the server. Finally, effective use of SQL can minimize the amount of data shipped to and from SQL Server across the network. Saving disk I/O and network I/O are the most important factors for improving application performance.

Database Design

Using SQL effectively begins with a normalized database design. Normalization is the process of removing redundancies from the data. When converting from an ISAM-style application, normalization often involves breaking data in a single file into two or more logical tables in a relational database. SQL queries then recombine the table data by using relational join operations. By avoiding the need to update the same data in multiple places, normalization improves the efficiency of an application and reduces the opportunities for introducing errors due to inconsistent data.

There are sometimes trade-offs to normalization: A database that is used primarily for decision support (as opposed to update-intensive transaction processing) may not have redundant updates and may be more understandable and efficient for queries if the design is not fully normalized. Nevertheless, unnormalized data is a more common design problem in SQL Server applications than over-normalized data. Starting with a normalized design and then selectively denormalizing tables for specific reasons is a good strategy.

Whatever the database design, you should take advantage of the features in SQL Server to automatically maintain the integrity of the data. The CREATE TABLE statement supports data integrity:

  • CHECK constraints ensure that column values are valid.

  • DEFAULT and NOT NULL constraints avoid the complexities (and opportunities for hidden application bugs) caused by missing column values.

  • PRIMARY KEY and UNIQUE constraints enforce the uniqueness of rows (and implicitly create an index to do so).

  • FOREIGN KEY constraints ensure that rows in dependent tables always have a matching master record.

  • IDENTITY columns efficiently generate unique row identifiers.

  • TIMESTAMP columns ensure efficient concurrency checking between multiple user updates.

  • User-defined data types ensure consistency of column definitions across the database.

By taking advantage of these CREATE TABLE options, you can make the data rules visible to all users of the database, rather than hide them in application logic. These server-enforced rules help avoid errors in the data that can arise from incomplete enforcement of integrity rules by the application itself. Using these facilities also ensures that data integrity is enforced as efficiently as possible.

Efficient Data Retrieval

One of the capabilities of SQL is its ability to filter data at the server so that only the minimum data required is returned to the client. Using these facilities minimizes expensive network traffic between the server and client. This means that WHERE clauses must be restrictive enough to get only the data that is required by the application.

It is always more efficient to filter data at the server than to send it to the client and filter it in the application. This also applies to columns requested from the server. An application that issues a SELECT * FROM... statement requires the server to return all column data to the client, whether or not the client application has bound these columns for use in program variables. Selecting only the necessary columns by name will avoid unnecessary network traffic. It will also make your application more robust in the event of table definition changes, because newly added columns won’t be returned to the client application.

Beyond the SQL syntax itself, performance also depends on how your application requests a result set from the server. In an application using ODBC, the “how” is determined by the statement options set prior to executing a SELECT. When you leave the statement options at default values, SQL Server sends the result set the most efficient way. SQL Server assumes that your application will fetch all the rows from a default result set immediately. Therefore, your application must buffer any rows that are not used immediately but may be needed later. This buffering requirement makes it especially important for you to specify (by using SQL) only the data you need.

It may seem economical to request a default result set and fetch rows only as your application logic or your application user needs them, but this is false economy. Unfetched rows from a default result set can tie up your connection to the server, blocking other work in the same transaction. Still worse, unfetched rows from a default result set can cause SQL Server to hold locks at the server, possibly preventing other users from updating. This concurrency problem may not show up in small-scale testing, but it can appear later when the application is deployed. The lesson here is simple—immediately fetch all rows from a default result set.

Some applications cannot buffer all the data they request of the server. For example, an application that queries a large table and allows the user to specify the selection criteria may return no rows or millions of rows. The user is unlikely to want to see millions of rows. Instead, the user is more likely to re-execute the query with narrower selection criteria. In this case, fetching and buffering millions of rows only to have them thrown away by the user would be a waste of time and resources.

For applications like these, SQL Server offers server cursors that allow an application to fetch a small subset or block of rows from an arbitrarily large result set. If the user wants to see other records from the same result set, a server cursor allows the application to fetch any other block of rows from the result set, including the next n rows, the previous n rows, or n rows starting at a certain row number in the result set. SQL Server does the work to fulfill each block fetch request only as needed, and SQL Server does not normally hold locks between block fetches on server cursors. Server cursors also allow an application to do a positioned update or delete of a fetched row without having to figure out the source table and primary key of the row. If the row data changes between the time it is fetched and the time the update is requested, SQL Server detects the problem and prevents a lost update.

All of these features of server cursors come at a cost. If all the results from a given SELECT statement are going to be used in your application, a server cursor is always going to be more expensive than a default result set. A default result set always requires just one roundtrip between client and server, whereas each call to fetch a block of rows from a server cursor results in a roundtrip. Moreover, some types of server cursors (those declared as dynamic) are restricted to using unique indexes only, while other types (keyset and static cursors) make heavy use of temporary storage at the server. For these reasons, only use server cursors where your application needs their features. If a particular task requests a single row by primary key, use a default result set. If another task requires an unpredictably large or updatable result set, use a server cursor and fetch rows in reasonably sized blocks (for example, one screen of rows at a time).

Advanced Features

Structured query language (SQL) goes far beyond the basics of creating tables, inserting records, and retrieving data from tables. Your application will perform better if it requests the server to perform a task with a more sophisticated SQL statement than if it performs the same task using simple SQL statements combined with application logic. Therefore, learning the full capabilities of SQL is a wise investment of your time. For SQL Server, these capabilities include:

  • An UPDATE statement that can modify rows in one table based on related data in other tables.

  • Rich JOIN capabilities, including INNER, CROSS, LEFT OUTER, RIGHT OUTER, and FULL OUTER joins, as well as combinations of these join types.

  • Subqueries that can be used in any expression context.

  • A CASE expression that provides a powerful conditional logic capability in an ANSI-standard syntax.

  • Sophisticated aggregation functions, including the CUBE and ROLLUP operators to produce multidimensional aggregates in decision support applications.

An ISAM-style application will use few or none of these advanced SQL capabilities and, as a result, will be less efficient than an application designed to take advantage of them when performing equivalent tasks.

Short Transactions

A primary goal of using SQL appropriately is to reduce the amount of data transferred between server and client. Reducing the amount of data transferred will usually reduce the time it takes to accomplish a logical task or transaction. As discussed earlier, long transactions may work fine for a single user, but they can cause concurrency problems and scale poorly for multiple users. Techniques you can use to reduce transaction duration include:

  • Commit transactional changes as soon as possible within the requirements of the application.

    Applications often perform large batch jobs, such as month-end summary calculations, as a single unit of work (and thus one transaction). With many of these applications, individual steps of the job can be committed without compromising database consistency. Committing changes as quickly as possible means that locks are released as quickly as possible. Because SQL Server uses page locks for most operations, releasing locks quickly minimizes their impact on other users who may need access to rows on the same data pages.

  • Take advantage of SQL Server statement batches.

    Statement batches are a way of sending multiple SQL statements to the server at one time, thereby reducing the number of roundtrips to the server. If the statement batch contains multiple SELECT statements, the server will return multiple result sets to the client in a single data stream.

  • Use parameter arrays for repeated operations.

    The ODBC SQLParamOptions function allows multiple parameter sets for a single SQL statement to be sent to the server in a batch, again reducing the number of roundtrips.

  • Use SQL Trace to diagnose application traffic.

    SQL Trace can monitor, filter, and record all of the calls sent from client applications to the server. It will often reveal unexpected application overhead due to unnecessary calls to the server. SQL Trace can also reveal opportunities for batching statements that are currently being sent separately to the server.

Stored Procedures

All well-designed SQL Server applications should use stored procedures. This is true whether or not the application’s business logic is written into stored procedures. Even standard SQL statements with no business logic component gain a performance advantage when “packaged” as stored procedures with parameters.

When they are created, stored procedures go through the parsing and normalization steps that SQL Server performs on any SQL statement. The stored procedure is then saved to disk in the parsed, normalized form of a query tree. At first execution, SQL Server retrieves the saved query tree and optimizes the execution of the SQL based on the values of parameters passed in and the available statistics of the referenced tables and their indexes. On subsequent executions of the same stored procedure by any user, SQL Server looks to the procedure cache first to see if a matching optimized execution plan is available for use with a new set of parameters. SQL statements compiled into stored procedures can thus save a significant amount of processing at execution time.

Stored Procedures

Another advantage of stored procedures is that client execution requests use the network more efficiently than equivalent SQL statements sent to the server. For example, suppose an application needs to insert a large binary value into an image data field. To send the data in an INSERT statement, the application must convert the binary value to a character string (doubling its size) and then send it to the server. The server then converts the value back into a binary format for storage in the image column. In contrast, the application can create a stored procedure of the form:

CREATE PROCEDURE P(@p1 image) AS INSERT T VALUES (@p1)

When the client application requests an execution of procedure P, the image parameter value will stay in its native binary format all the way to the server, thereby saving processing time and network traffic.

SQL Server stored procedures can provide even greater performance gains when they include business services logic. As described earlier, this “FAT server” implementation yields outstanding performance because it moves the processing to the data, rather than moving the data to the processing. Nevertheless, using stored procedures is not restricted to the FAT server development model. Stored procedures that encapsulate predefined SQL statements provide performance benefits in any implementation. Furthermore, you can easily configure the SQL Server ODBC driver to create and use temporary stored procedures automatically, any time your application calls the SQLPrepare API. These temporary stored procedures are not (currently) as efficient as permanent stored procedures created by the application itself, but they can yield good performance gains when an application executes the same prepared SQL statement multiple times. If you write an application to use the ODBC prepare/execute combination, you enable it to selectively take advantage of the current automatic stored procedure option as well as future enhanced versions of this option.

Indexes and Configuration

Indexing and configuring a SQL Server database for optimal performance is usually considered a database administrator’s job, not a development task. However, as a developer, you should know the fundamentals of SQL Server indexes and configuration so that:

  • Database designs incorporate clustered indexes in the original creation scripts.

  • Development computers execute test scenarios as efficiently as possible.

  • Your application can automatically configure the database as part of the installation process, reducing the complexity for the end user.

There are books, self-study courses, and classroom courses that teach SQL Server configuration and tuning in great depth. The following sections, however, cover basic configuration steps that are often overlooked. If an application is well written and these configuration guidelines are followed, SQL Server will perform optimally. Additional tuning can yield small performance gains, but there probably won’t be configuration changes that will result in dramatic performance boosts.

Indexes

SQL Server has two types of indexes: clustered and nonclustered. A clustered index keeps the data rows of a table physically stored in index order. Because there is only one physical order to the rows in a table, there can be a maximum of one clustered index per table. All other indexes on a table are nonclustered. A clustered index is usually the most efficient method to finding a row or set of rows in a table, so choosing the column(s) by which to define the clustered index is an important part of application tuning. But if you don’t know what columns to use, don’t leave off a clustered index entirely. In SQL Server version 6.5, it is rarely a benefit to have no clustered index on a table. For an application in which the query patterns are not well known, you can define the clustered index on the primary key columns. Subsequent analysis of some tables may suggest that the most common queries perform better with the clustered index on a nonkey column such as a foreign key column, in which case the default could be overridden for those tables only.

An index includes distribution statistics about the data values in the index. The SQL Server cost-based query optimizer uses these statistics to determine if a particular index will be useful to solve a query. When a large number of rows are added or changed after an index is defined on a table, these statistics will be inaccurate until you run an UPDATE STATISTICS statement. This can mislead SQL Server into using a poor index or a table scan to resolve a query, which can cause poor query performance and reduced concurrency. An update operation that uses a table scan instead of an index, for example, will lock the entire table for the duration of the transaction.

It is therefore important to update statistics regularly on the active tables in your application. You should also learn how to use the SHOWPLAN feature of SQL Server. It reports information such as the optimizer’s index selections and join orderings, and shows you whether the indexes are being used for your application’s queries. Every unused nonclustered index slows down inserts, updates, and deletes on a table, so make sure that you are using only the indexes you need.

Data Distribution

The axiom of SQL Server disk management is that spreading the database out over multiple physical disk drives improves performance. In almost all cases, the log for a database should be placed on a separate physical disk from the data portion of the database. The data portion of the database should be “striped” across multiple physical devices to achieve maximum throughput. Striping is easy to configure with the disk management facilities built into the Microsoft Windows NT operating system. For the operating system to spread out the data randomly with striping usually yields equivalent performance to placing database tables and indexes on specific devices, and at a fraction of the effort. Windows NT also offers disk configurations (RAID levels) that can protect a database against the failure of a hard disk. All SQL Server databases should take advantage of these capabilities.

Memory Configuration

SQL Server uses memory for data cache and procedure cache, among other things. Using data or a procedure from cache is an order of magnitude faster than reading it from disk. Therefore, allowing SQL Server to use as much of the physical memory of the server computer as practical will yield better performance. On the other hand, assigning too much memory to SQL Server can conflict with other applications on the server. This can cause the Windows NT operating system to start paging real memory out to disk, resulting in worse performance for SQL Server. SQL Server memory usage is not automatically configured at installation. You must manually configure SQL Server memory usage based on published guidelines, and check it with performance monitoring tools. The procedure is simple, and it is essential to achieving optimal throughput on a computer.

A Relational-Style Application Example

The following SQL application performs the same task as the previous ISAM-style example. This time, however, the application task has been rewritten to use the principles of relation-style database access. The application task now uses a stored procedure, P, that takes one ID parameter. Procedure P is created as follows:

CREATE PROCEDURE P (@p1 int) AS
UPDATE C SET c1 =
      (select sum(B.qty)
      from A, B
      where A.pk=C.id
      and A.fk=B.pk)
   WHERE C.id = @id

A Relational-Style Application

This procedure updates the summary table C for a passed-in customer ID, using the detailed customer and order records for that customer ID from tables A and B. This stored procedure uses only ANSI-standard SQL; it does not involve writing business logic in Transact-SQL. Yet it reduces the transaction to a single roundtrip to the server; data stays at the server, rather than moving unnecessarily across the network. Because the action is an atomic SQL statement, the procedure doesn’t even need to wrap the statement in a BEGIN TRANSACTION/COMMIT TRANSACTION pair—the statement is automatically its own transaction. This transaction will be significantly shorter in duration than the equivalent ISAM-style transaction. The user of this SQL application is going to have less impact on other users because the application holds locks for much shorter duration.

Tips for Leveraging Other SQL Server Features

As you develop your SQL Server application, you look for ways to take advantage of features and code that already exist, rather than reinventing these features yourself. Consider the following possibilities:

  • If your application involves multiple SQL Servers, there is a good chance that some data movement between servers will be required. Use the built-in replication facilities of SQL Server to accomplish this data movement. Replication can be set up and monitored through SQL-DMO programming. Replication can play an especially important role in partitioned applications that are designed to scale by adding more SQL Servers.

  • If your application needs to notify users when a particular change occurs in the database, check out the SQL Mail feature. SQL Mail provides a mechanism to send and receive e-mail directly from SQL Server. For example, when inventory falls below a certain threshold, a trigger can automatically run SQL Mail to send an e-mail message to the purchasing department. Conversely, you can use e-mail as the input vehicle for data that is destined for a SQL Server database. E-mail messages can be sent to a mailbox set up for SQL Server, and a SQL Mail task can check the mailbox regularly to automatically insert the data from the messages it finds and reply with a confirmation message.

  • If your application must publish existing SQL Server data to the Internet, look at the SQL Server Web Assistant. The Web Assistant and its associated stored procedures can create and maintain Web pages containing results from SQL Server queries and links to other SQL Server data. These pages can be refreshed on a scheduled or triggered basis as needed, giving you complete control over the database load generated by Web access to your server.

  • If your application needs to take advantage of functions that are available in Windows NT but not available in Transact-SQL, it makes sense to call these functions directly from SQL Server by using extended stored procedures or OLE Automation stored procedures. With extended stored procedures, you can create custom functions in a C DLL that can be executed from SQL Server just like any other stored procedure. These custom functions can in turn call any function available in Windows NT, pass parameters, and return data to SQL Server.

  • OLE Automation stored procedures provide a mechanism for loading and calling methods on any OLE Automation object that exists on the server computer. Since so many tools are capable of creating OLE Automation objects, this capability opens up a wide range of possibilities for extending SQL Server’s functionality. For example, Visual Basic includes many financial functions, such as Net Present Value, that are not offered in Transact-SQL. Visual Basic Enterprise Edition makes it easy to package these functions as methods on your own OLE Automation object. You can then call these methods from SQL Server stored procedures and triggers by using the OLE Automation stored procedure feature.

Conclusion

This paper has pointed out some of the most important principles and trade-offs of SQL Server development. For many decisions, there is no one correct choice. In these cases, know the trade-offs and make an informed decision. For other decisions, consider the themes that crop up again and again in this discussion:

  • Keep your transactions short.

  • Minimize the number of roundtrips to the server.

  • Take advantage of SQL features.

  • Use stored procedures.

Follow these simple guidelines, and you will achieve most of your application’s performance potential without a lot of work.

Finding More Information

This section describes some other sources for information about designing for SQL Server.

SQL Server Home Page

Every SQL Server developer should get to know and use what’s available on the SQL Server home page of the World Wide Web: http://www.microsoft.com/sql. This page is updated regularly and includes product and service pack announcements, benchmark results, white papers, partner information, and much more. The SQL Server home page also includes links to the following areas of specific interest to developers:

  • Technical Support (http://www.microsoft.com/sql/techsupp.htm)

    Includes a Support Newsgroup and the Knowledge Base, a searchable catalog of articles on SQL Server development tips and pitfalls. When you are stuck on a SQL Server problem, the Knowledge Base is usually the best place to look for an answer.

  • SQL Server Developer’s Conference (http://www.microsoft.com/sql/devcon/default.htm)

    Developer’s Conferences are a means for the SQL Server product team to communicate the latest and greatest information on how to most efficiently develop SQL Server applications and to get feedback about what you would like to see in the product. As of this writing, this Web page contains the slides and demonstration code from all the presentations given at the September 1996 conference in Seattle. For example, you can learn how to create a “multiple master” application using SQL Server replication and how to install and configure a self-managing server using script-driven setup and SQL-DMO.

White Papers

White papers published by Microsoft Corporation give detailed guidance to SQL Server developers. Several of these papers are relevant to the issues discussed in this paper:

  • “Using ODBC with Microsoft SQL Server”

    Discusses how to get optimal performance from the SQL Server ODBC driver. The advice in this white paper applies to developers using any interface that runs on top of the ODBC driver, including RDO, OLE DB (The ODBC Provider), and ADO.

  • “OLE DB for the ODBC Programmer”

    Describes the features of OLE DB and how to build applications using this interface in terms that are familiar to ODBC programmers.

  • “Migrating Btrieve Applications to Microsoft SQL Server”

    Focuses on the specific issues involved with migrating an ISAM-style application to SQL Server. It uses an example Btrieve application to illustrate the process, but many of the principles it discusses apply to any ISAM-style code that is rewritten for SQL Server.

  • “Migrating Sybase Applications to Microsoft SQL Server”

    Describes the minor changes needed to migrate a Sybase application to SQL Server.

Microsoft Press Books

Two books published by Microsoft Press® are of particular interest to SQL Server developers: Inside SQL Server by Ron Soukup (available in 1997) and Inside ODBC by Kyle Geiger. Both of these books give in-depth advice based on the authors’ years of experience.

Other Microsoft Products

The best place to get information about the many Microsoft products that work with SQL Server is Microsoft’s home page at http://www.microsoft.com. In particular, check out the following products and technologies:

  • Microsoft Visual InterDev is an environment for building SQL Server applications for the Internet and intranets.

  • Microsoft Transaction Server provides a powerful framework for building physical three-tier applications using any programming tool that can create OLE objects.

  • Microsoft Visual C/C++ Enterprise Edition includes a Transact-SQL editor and debugger that can greatly simplify the process of creating stored procedures and triggers.

  • Microsoft Visual Basic Enterprise Edition is a rapid-development environment for building Windows NT and Windows 95-based applications, including high-performance SQL Server applications developed with RDO.


Manage Data from Myriad Sources with the Universal Data Access Interfaces

By Stephen Rauch

September 1997

Universal Data Access is a set of common interfaces that anyone can use to represent data kept in relational databases, spreadsheets, project and document containers, VSAM, email, or file systems. The data stores simply expose a common data access model to the data.

This article assumes you’re familiar with database programming and OLE

Stephen Rauch is a Technical Evangelist in the Developer Relations Group at Microsoft. He can be reached at srauch@microsoft.com.

If you've ever developed applications that access data from data stores, you have been exposed to a bowl of alphabet soup: ODBC, RDO, DAO, and so on. Many other APIs are used to access things like telephony, email, and file systems. They're all related to getting some kind of information—data.

The Microsoft data access strategy called Universal Data Access provides access to all types of data through a single data access model. Let's explore Universal Data Access and how the alphabet soup fits into the Microsoft strategy. First, let's take a look at a problem both users and developers face when accessing information.

What's the Problem?

The data you use to make business decisions every day is a lot more than just data stored in a relational database. It typically includes personal data in spreadsheets, project management applications, email, mainframe data such as VSAM and AS/400, computed data calculated on-the-fly, and ISAM data. Add to that information available in directory services, documents available on your hard drive or on the network, and the wealth of data in myriad forms on the Internet. Each of these types of data needs to be taken into account when making decisions, yet each is in a different format, stored in a variety of containers, and accessed in a fundamentally different way.

Suppose that I am the project manager responsible for delivering a prototype of the new space shuttle. To prepare for a status meeting, I need to collect information from a variety of sources. Microsoft® Project does a great job of managing scheduling information, so I have a number of project files that describe who is allocated to what tasks, whether they are ahead or behind schedule, and so forth.

Reviewing the project I see that the team responsible for the propulsion unit has continuously added resources to the project but is continuing to fall behind. After identifying the tasks that are causing the team to slip, I do a search on the technical documents describing those tasks. From the information obtained in the technical documents, I do a search of my email folders to find all the related mail that discusses the project and the potential cause for it falling behind. I find a message that refers to a delay in receiving some critical parts for the thruster. The millions of parts that make up the shuttle are inventoried in a SQL Server™ database, so I search for the subset of parts used for the thruster, then compare that to my invoices kept in an Access database. After finding the invoice for the missing parts, I do a Web search for information about the supplying manufacturer and discover that they were involved in a takeover two months before the invoice due date. Further searching on the Web reveals two other companies that manufacture the same components.

Now that I've collected all the information from various resources, discovered the reason for the delay, and struck a quick deal with one of the other manufacturers, I want to make some changes. Let's start by updating all of the parts from the old manufacturer to the new supplier. To make this change, I need to update information in my inventory database, the project specification, and outstanding invoices. I have to make these changes to all sources of data that are related to the project in a safe way. I don't want the changes to be applied in two places and fail in the other two. I need the same type of transactional semantics I get in a single data store even when the data is located in different containers.

Figure 1 Project Tracking Application

So how might this work? I would probably have a project tracking application to display and analyze project information (see Figure 1). The project tracking application would use OLE automation to get to the high-level project information stored in the Microsoft Project files. Then I'd use file system interfaces to search for documents on my hard drive, MAPI to get information from the Microsoft® Exchange data store, ODBC to get information from the SQL Server, and some HTTP search protocol to find information on the Internet.

In this case, the project tracking application utilizes at least five separate APIs to access the different types of data. Some of the interfaces are COM interfaces and some of them are not; some of them expose Automation interfaces and some are procedural. In addition, transactional control does not exist, so I cannot safely make any changes to all of the information within one transaction.

What all of this means is that the developers who write the project tracking application will have to become experts in a multitude of data access methods. Is there a better way? Well, one solution is to put all of the different types of data into a single relational data store. That is, take the SQL data store and incorporate the project data, email data, directory data, and information from the Internet and move it into a single vendor's relational database. This approach solves the problem of having a single API that also gives you transactional control—but it has a number of compromises.

First, it requires moving huge amounts of data. Second, you typically don't own much of the data you need, such as the wealth of information on the Internet. The next problem with moving the data into a single data store is that tools today don't look for or store data into a single relational data store. Email applications, project management tools, spreadsheets, and other tools look into their own data stores for their data. You'd have to rewrite your tools to access the data in the single data store or duplicate the data to the single data store from where it naturally lives—then you run into problems with synchronizing the data. Another problem is that one size does not fit all. The way that relational databases deal with data is not the best way to deal with all types of data. Data (structured, semi-structured, and unstructured) is stored in different containers based on how the data is used, viewed, managed, and accessed. Finally, the performance and query requirements are going to differ by data type depending on what it is you are trying to do.

Universal Data Access

If you can't have all of your data in a single data store, what's the alternative? The Microsoft alternative is called Universal Data Access. With Universal Data Access, you get to data in different data stores through a common set of interfaces, regardless of where the data resides. Your application speaks to a common set of interfaces that generalize the concept of data. Microsoft examined what all types of data have in common, such as how you navigate, represent, bind, use, and share that data with other components. The result is a set of common interfaces that anyone can use to represent data kept in relational databases, spreadsheets, project and document containers, VSAM, email, and file systems. The data stores simply expose common interfaces—a common data access model—to the data.

The Microsoft Universal Data Access strategy is based on OLE DB. OLE DB is a set of low-level C/C++ interfaces designed to efficiently build database components. They allow an individual data store to easily expose its native functionality without having to make the data look like relational data if it isn't, and also allow generic, reusable database components to augment the functionality of simpler providers when needed. Because they are low-level interfaces that deal with pointers, structures, and explicit memory handling for optimizing how data is exposed and shared between components, they are not suitable for calling directly from Microsoft® Visual Basic® or Java. You can think of OLE DB as being to all types of data what ODBC is to SQL data.

Active Data Objects (ADO) is an automation server that exposes a high-level set of database programming objects to the Visual Basic, Java, or C++ programmer on top of an OLE DB data source. Because it is a dual interface—it has no pointers, structures, explicit memory management, and so on—it is suitable for calling from scripting languages such as Visual Basic, Scripting Edition (JScript), and Microsoft® JScript™. Active Server Page (ASP) programmers use ADO methods to access and manipulate data stored in OLE DB data stores. Using ASP, programmers write code on the server to access the data and build an HTML page, which is then shipped back to the client. All of the intelligence and code for accessing and manipulating the data exists on the Internet server. Centralizing this logic on the Internet server and reducing the components downloaded to the client are ideal for many data access scenarios. Figure 2 depicts how the project tracking application discussed earlier may utilize ADO to access data from a variety of data sources.

Figure 2 Accessing Data with ADO

Microsoft ships a set of components collectively known as the Advanced Data Connector (ADC) that provide remoting of an OLE DB data object across the Internet or intranet. This allows the client to retrieve actual data rather than a textual HTML page. This is important if the client wants to use data as something other than formatted text. The client can manipulate the local data through ADO or OLE DB directly, and ADC 1.0 supports hosting standard Visual Basic-style data-bound controls into Microsoft® Internet Explorer (IE), which consume the remoted OLE DB data. The components shipped today as ADC will eventually become a core set of components that allow ADO or OLE DB applications to talk to remote data exactly as they would talk to local data. The fact that these remote data services are invoked is invisible to both the data consumer and the data provider. Let's take a look at each of the technologies in more detail.

OLE DB

In the July 1996 issue of Microsoft Systems Journal I covered build M6.1 of the OLE DB specification (see "Talk to Any Database the COM Way Using the OLE DB Interface"). A lot has happened to the specification since I covered it last year; version 1.0 was released last fall and followed closely by version 1.1. You can retrieve the specification from http://www.microsoft.com/oledb/ or install it from MSDN™ or the Microsoft® Visual Studio™ CDs.

In order to meet its goal of providing data access to all types of data in a COM environment, OLE DB is designed as a component technology. Data sources expose the COM interfaces that reflect their functionality, and common components can be built on top of those interfaces to expose more robust data models. OLE DB achieves this componentization by identifying common characteristics between different data providers and services, and by defining common interfaces to expose those characteristics.

An OLE DB provider exposes OLE DB interfaces over some type of data. Examples of providers include everything from a full SQL DBMS to an ISAM file, text file, or data stream. Obviously, these data providers have different functionality, and it's important not to limit that functionality. But at the same time it's not reasonable to expect all providers that expose simple tabular data to implement a full-blown query engine. Instead of looking at the differences between providers, OLE DB looks at the similarities and defines common interfaces used to expose those similarities. At a minimum, a simple OLE DB provider must implement and support the object types and functionality listed in Figure 3.

Figure 3 Provider Object Types and Functionality

The DataSource object represents a connection to a data store, such as a file or DBMS, through which consumers can operate on the data in the data store. Through the DataSource object, a consumer establishes a connection with the data store. Sessions provide a context for transactions that can be implicitly or explicitly transacted. A DataSource object can support multiple sessions within a single connection. Through the Session object, a consumer can open a Rowset of data. Rowsets expose data in tabular format.

Additional object types include enumerators, which search for available data sources. If a consumer is not customized for a specific data source, it would use an enumerator to search for a data source to use. A transactions object type is used when committing or aborting nested transactions. Command objects execute a textual command such as a SQL statement. The final object type is called errors; errors can be created on any interface on any OLE DB object type and can contain additional information about an error.

By implementing this simple set of objects and minimum set of behaviors, an OLE DB provider is capable of providing a very generic set of functionality to consumers. The functionality will probably be boring, but a generic consumer will be able to display data from the data store.

Well, it's great to have a common way to represent data, but if the data you are talking to has additional functionality such as being able to execute a SQL query, you want a common way to access that extended functionality. So the OLE DB specification, as well as specifying the base interfaces and objects that everyone implements, defines some interesting common extensions to expose additional functionality that specific data stores might have. The key concept to OLE DB is componentization. Rather than say that you must support everything or you can't support anything, Microsoft split the interfaces into groups: some are supported by everyone, and some are extensions for different areas, such as SQL data, ISAM data, and so on.

Componentization lets an OLE DB provider expose exactly its native functionality; then a separate component can be added on to implement the additional incremental functionality required by a consumer. For example, say you implemented just the minimum set of object types and behavior described above in your OLE DB provider. You don't support querying, sorting, or filtering; you just allow consumers to read through your data. Now let's say that a consumer wants to execute a SQL query on data exposed by your OLE DB provider. Instead of your having to write that code, a service component that will execute a query over anyone's simple data can be utilized to query your data. Now when a consumer wants to read through your data, they can talk directly to your OLE DB provider. When the consumer wants to do a query, the query processor can be invoked without the consumer's knowledge to add that incremental functionality. So what you have is a world of cooperating components, where every component natively exposes and consumes OLE DB interfaces, and individual database components can be incrementally added on to a native data store in order to add functionality.

If you have interesting data that you want everyone to have access to through a standard set of interfaces, you should write an OLE DB provider. An example of an OLE DB provider includes OLE DB for the Active Directory. Active Directory Service Interfaces (ADSI) abstracts the capabilities of directory services from different network providers into a single set of interfaces for managing network resources. ADSI makes it easy for the administrator and ISV to access and manipulate LDAP version 2, NetWare 3 and 4, and Windows NT® directories. ADSI includes an OLE DB provider for rich querying of the directory namespaces from OLE DB directly or through ADO. Throughout this article I am going to describe how your data can be accessed and utilized by other technologies that are part of the Microsoft Universal Data Access strategy.

The majority of consumers are going to write to ADO because it is easier to use, provides high performance, and exposes a rich set of functionality. Certain consumers may decide to write to OLE DB interfaces. Again, OLE DB is designed to make these cooperating database components work well together so you have ultimate control in things like memory allocation, sharing memory, freeing memory, using other people's memory, and so forth. Write to OLE DB using C++ for ultimate performance and functionality.

ADO

I covered OLE DB very briefly, but I can't do justice to such an important technology in the space I have available. The one thing to remember is that OLE DB is the foundation. OLE DB consumers consume OLE DB interfaces and interact with OLE DB service components and OLE DB providers. ADO is an OLE DB consumer.

ADO looks very similar to Data Access Objects (DAO) and Remote Data Objects (RDO) because Microsoft took the best features of both models and put them together in a common programming model. The most important points I can make here are that ADO is easy to use, language-independent, and provides extensible interfaces for programmatic access to all types of data.

Since ADO consumes OLE DB interfaces, whose sole purpose is to expose native functionality from diverse types of data stores, you can get to all types of data through ADO. ADO was designed as a set of interfaces that would be common for any high-level data access library that is going to supply a common programming model. And it really is easy to use. Language-independent means that ADO is an automation server that supports dual interfaces. Dual interfaces means that ADO supports an IDispatch as well as a VTBL interface, so that applications written in Visual Basic, C++, and Java can bind directly to the VTBL interface and get fast performance. Scripting languages can use the same objects through IDispatch.

All objects in ADO (see Figure 4) can be instantiated on their own, except the Error and Field objects. The hierarchy of objects found in previous models like DAO and RDO is de-emphasized in the ADO model to allow greater flexibility in reusing objects in different contexts. For example, you can create a Command object, associate and execute it against one connection, then associate it with a different connection and re-execute it. This approach also paves the way for you to create specialized objects (defined at design time) and temporary, unattached recordsets.

Figure 4 ADO Object Model

Imagine a specialized Recordset object that's made to handle any results returned from a data store. It can basically determine, through metadata calls, all of the column information and all the columns that are there, build a recordset, and hand it to you. But those metadata calls can take a little time during runtime. This is where ADO comes in. Because objects in ADO can be instantiated on their own, you can use it to do things like design a Recordset object that is specialized for a certain query against your back-end database at design time. Thus, at design time a tool can capture information about the metadata of all of the columns, and at runtime a Recordset can be opened and immediately bind and start fetching, eliminating metadata calls during runtime.

The Connection object represents a unique session with a data source. In the case of a client/server database system, it may be equivalent to an actual network connection to the server. It's probably the object that you start with when working with any database management system. It allows for things like customization of connection options (such as your isolation level) and starting and ending transactions. The Connection object also allows you to execute commands such as a query or SQL statement. To execute any kind of command, you use the Execute method of the Connection object. If the command returns rows, a default Recordset object is created and returned.

The Command object is analogous to QueryDef in the DAO model and the prepared statement in the RDO model. It represents a command (also known as a query or statement) that can be processed by the data source. You use Command objects to obtain records and create a Recordset object, to execute a bulk operation, or to manipulate the structure of a database. Depending on the capabilities of the OLE DB provider, it can also handle parameters. The Command interface is actually optional in ADO since some OLE DB providers will not implement command execution functionality. Note that the Command data type is not a required object type to implement a generic OLE DB provider. The Command object is implemented in ADO; it doesn't do anything if it's communicating with an OLE DB provider that does not support command functionality.

Commands can be simple SQL statements (or some other language the OLE DB provider recognizes) or calls to stored procedures in the database. Commands can then be executed using the Command's Execute method, or you can create a Recordset object and associate it with the Command object when opening a cursor.

The Command object includes a collection of Parameter objects. If the provider can support commands with parameters, the parameters collection will contain one Parameter object for each parameter in the command. Unlike in DAO and RDO, you can create Parameter objects and explicitly add them to the parameters collection. This allows you to use well-known parameterized commands so you can avoid the sometimes very expensive operation of having the OLE DB provider populate the parameters collection automatically based on the retrieval of information from system catalogs.

Parameter objects represent parameters associated with parameterized queries, or the in/out arguments or return values of stored procedures on a Command object. The Parameter object is intended as a sort of parameter marker in a command for when you are working with things like output arguments or a command that you want to prepare. Command objects have a prepared property because, if you are going to execute something a whole bunch of times, even if you are just changing part of the where clause, it's better for you to create a Command object and prepare it. The OLE DB provider will make sure that it is very efficient.

The Recordset object is your cursor of data. The big difference in ADO is that the Recordset can be used as a data buffer. Traditionally, a Recordset was a way of issuing a query, getting a resultset, and reiterating over the resultset. With ADO, once you have the results, there is no reason why you can't treat it as a data buffer, separating it from the connection to the database, and passing it around safely to other applications or within different areas in an application. This means that the Recordset object manages changes to the recordset, buffering rows that have changed and those that have not, maintaining the old values with the new values. When you're ready to commit the changes, you can re-associate the Recordset with a Connection and use the automated method call UpdateBatch. UpdateBatch packages, inserts, and deletes the updates and sends them to the data store.

If you're more of a stored procedure type of person and don't like the idea of exposing your base tables, you can still work with the enhancements made to the Recordset because Microsoft exposed varying types of status, such as which rows have changed and which rows haven't. When an application hands a data buffer back to you, you can take a look through it and call your own stored procedure calls. You get a nice way of encapsulating a set of data and moving it around your system (regardless if it is distributed, across a network, or around a bunch of DLLs) without having to worry about connections or exposing your data directly.

Additional changes to the Recordset object include improvements for removing unnecessary things, adding optional arguments that reduce the number of lines of code for common scenarios, and changing defaults that don't make sense in today's technologies.

The Field object represents a column in a recordset that you can use to obtain values, modify values, and learn about column metadata.

The Error object contains details about data access errors pertaining to a single operation involving ADO. For those instances where an OLE DB provider returns more than one error, ADO will raise the most descriptive error as a runtime error in Visual Basic or an OLE exception.

Using the Connection Object

The code below shows how to use the Connection object:

Dim con As New Connection

con.Open "dsn", "usr", "pwd"
con.Execute "<sql>"

The first line is how you would typically instantiate a Connection object in Visual Basic. If you are using a scripting language, just replace it with a CreateObject call. In the ADO model you will notice Open and Close metaphors for objects that have an Open and Close state like a Connection object. To open a connection to a data source, just call the Open method and pass a data source name, user, and password as separate arguments. To execute a random statement, use the Execute method.

Those of you who remember DAO and RDO will remember there used to be a separate execute from OpenRecordSet, and you always had to know if you were going to get results or not to use one or the other. In the ADO model, Microsoft cleaned that up. All you really need is one Execute method—you don't need to know which one or whether you are going to get results or not. If there are results returned from the query, the Execute method will return a Recordset object. If there are no results returned from the query, it will return nothing. Microsoft has eliminated a lot of the bulk from the model.

Using the Recordset Object

The Recordset object is just as easy to do a simple Open on. Notice that, instead of using the Connection object and opening the Recordset, I am actually creating the Recordset by itself.

 Dim rs As New Recordset

rs.MaxRows = 100
rs.Open "<sql>", "<con.string>", adOpenKeyset

While Not rs.EOF
·
·
·
      rs.MoveNext
WEnd

In the earlier models, you had to go through a hierarchy of objects and set certain properties of a cursor before the cursor was open and before the query was executed. For example, MaxRows had to be set before the query was executed because the data source needed to know when to chop off the results. In the older model, you had to have a tree of instances and you had to put those things on higher-level objects, even though they didn't belong there. There was always confusion as to which object inherited from which, and which one was used when you didn't set one. What Microsoft has done in the ADO model is eliminate the problem by placing the properties affecting the recordset and the cursor on the Recordset object. Now you can do things like set MaxRows = 100, open the database, or execute a statement by just calling the Open method on the Recordset, just as you call Open on the Connection object.

In the above code sample I show the Open statement taking some optional arguments. These arguments are optional because there are properties on the Recordset that are equivalent to them. What Microsoft did is give you some shorthand. Using the shorthand is a definite performance gain, especially when used with scripting languages. In this example, I am passing in a generic SQL statement, a source that is represented as a Connection object, and a constant informing the Recordset object that when it is opened I want to use a Keyset cursor.

Using the Command Object

Like the Recordset object, the Command object can be instantiated without having to go through a hierarchy of objects. You can associate it with any Connection object. Commands live on their own, so you can use a Command with a Connection object and execute over here, then associate the Command with another Connection object and execute it over there. The Command object is not tied to one Connection object for its entire lifetime and it can travel with different connections.
In the following code sample, I have set the ActiveConnection property to a connection string; again, it could be set to a Connection object as well. Then I set the CommandText, which is just a SQL string if I am accessing a SQL database. Remember, since OLE DB providers can be built to provide data from nonrelational data stores, the CommandText can be any text that an OLE DB provider can understand.

Dim cmd As New Command

cmd.ActiveConnection = "<con.string>"
cmd.CommandText = "<sql>"

cmd.Parameters(0) = <value>
cmd.Execute

cmd.Parameters(0) = <value>
cmd.Execute

As the example shows, if I had a parameter marker in the SQL statement, I would set the parameters by using this type of syntax, just like QueryDefs and prepared statements. Next I would set its value, and finally call Execute. Parameter objects are also a way to capture output parameters and can be marked as a direction of I/O. If the Parameter object is an output argument and the Command object is a stored procedure call, the result would be in the Parameter object.

That's it for code. For more information on ADO you can visit http://www.microsoft.com/ado/. ADO is also distributed with ASP for use with the Microsoft® Internet Information Server (IIS) and the OLE DB Software Development Kit.

What About ODBC?

Now that you have a brief understanding of OLE DB and ADO and how they work with each other, let's move on to ODBC. A lot of data is accessible from ODBC that I don't want to forget in this Universal Data Access model. It is important to understand ODBC's critical role in the world of Universal Data Access. ODBC is the standard interface to SQL relational data. There is an ANSI/ISO specification that is an extension to ANSI SQL92, ISO's SQL specification for a Call-Level Interface based on ODBC. In fact, the new ODBC 3.0 is a fully compliant superset of that international standard.

There is a great deal of incentive for relational database programmers to write to ODBC. But ODBC does require that your data look like a relational database, so it's not always the best way to expose data. ODBC's greatest strength is that it closely follows the embedded SQL model. So if you have a SQL relational database, ODBC is almost trivial for you to implement. If you don't have a SQL database, it can be very difficult to write an ODBC driver to your data because you basically have to write a relational engine on top of your data.

To integrate ODBC into the Universal Data Access strategy, Microsoft built an OLE DB-ODBC provider. What the ODBC provider does is ensure that anybody's existing ODBC data works well in the world of Universal Data Access. In other words, it implements the OLE DB object types that share component data objects on top of any ODBC driver. It does so in a very efficient manner; you can access the data through OLE DB with the same performance that you would get if you accessed it through ODBC.

Without losing your investment in ODBC, you can communicate with ODBC data stores directly through OLE DB interfaces or ADO since ADO consumes OLE DB interfaces.

Leveraging OLE DB and ADO

Before moving on to ADC, let's take a look at how OLE DB and ADO would be used in an existing application. Today, your typical monolithic application contains a number of different things. It generally contains a user interface and a set of business rules that govern how to access different types of data. It might have a client/cursor component in the application if it's talking to SQL data (and that SQL data doesn't support going backwards but your user interface does). The application may have a query processor for talking to a type of data that does not have a query processor in it. It probably has a number of different proprietary interfaces to get to various different data stores. In short, the application is hard to write, maintain, and extend due to the number of proprietary interfaces needed and the code necessary to fill in functionality not provided by the different types of data stores.

Suppose that the application is getting some data from an ISAM data store and some data from a SQL database. It has two different proprietary interfaces: one is the Btrieve interface and the other is OCI to talk to Oracle. I have a monolithic application that has multiple interfaces tied to specific APIs. If I want to point this application at Microsoft SQL Server, I have to add another interface. If the development group writing the application was smart enough to use ODBC to get to Oracle and Microsoft SQL Server, then at least I have one interface to get to relational databases, but I still need to add a new interface to get to, say, spreadsheet data.

The first step is to replace those proprietary APIs with ADO. Because ADO will consume any OLE DB provider, once I write to it I can get to not just ISAM and SQL data, but also things like HTML pages, email data, spreadsheet data, log file data, and a host of other information through a single interface. I can access new types of data without having to add anything to the application. The application is simpler to write because I only have to learn one interface, and it is more versatile because it can talk to more types of data.

What I'm left with is an application that has a user interface and business rules. It still has the client cursor component because it is still talking to some types of data, like SQL data, that may not be able to support scrolling backwards, and the application still has a query processor for doing queries over HTML or email data.

The next step is to add a query processor, which in this case is a common OLE DB service component. When the application talks to a data provider that supports querying, such as a relational database, the application can talk directly to that SQL data through ADO. When the application talks to ISAM data, which doesn't have a SQL query processor, it still talks through ADO, but a query processor is invoked on the application's behalf in order to implement those queries. That's important for a couple of reasons. Putting that query processor anywhere else would mean that every application or every data store would have to write a query processor, which doesn't make sense—there only needs to be one. Also, it allows different query processors to be inserted to support different query syntaxes or functionality.

So now the application has a user interface, business rules, and a client cursor for when it talks to the relational data. Again, one client cursor can be written as a service component that can talk to data regardless of where it comes from, and can be invoked only when it is needed. It can also do all of the cursoring for any application that wants it. The application still talks to ADO; it just has less code in it.

Figure 5 Even business rules can be a component

Now the application has just a user interface and business rules. Why not go for broke and take the business rules out of the application? Business rules generally don't vary from application to application. Rather than implement similar business rules in each application, I should be able to implement them as components that sit and watch the data and make sure that the ways I use the data are correct regardless of the application. Again, the model of OLE DB is that those business rules don't have to sit between the application and the data. They can sit off to the side and, through notifications that OLE DB supports in order to coordinate different users, it can let the business rules component know when something has happened. Ultimately I get an application that looks very similar to Figure 5.

Remoting Data

The Internet imposed a new problem for accessing data. In a typical client/server model you have an application called the client, which talks to a database on a server. Typically you have a persistent connection between the two that works like this: you make a connection, query to retrieve metadata, prepare a statement, provide some parameters, execute it, and read through five or six rows. Typically the client permits the user to update, insert, and delete new rows of information. All this time, the server maintains the client's cursors and state. Well, that worked great up until a couple of years ago when somebody got this idea to make the Internet popular.

The problem with the Internet, of course, is that it is a fundamentally disconnected model. When you have hundreds of thousands of people accessing your Web site, you don't want to keep state for those hundreds of thousands of users that may never complete the operations they start. You need a model in which your client can continue working with data even though your server has forgotten about the client. You need the server to be able to process the request and then forget about the client. Well, it turns out that the Rowset defined by OLE DB works very well. It's a self-contained object that can live on the server or the client.

In the case of working with data on the client, what you need is a copy of the data. As a result, there are two models for accessing data across the Internet. One model uses scripting on the server with ASP. ASP solves an important problem by allowing a Web server to intelligently interact with data and build HTML for the client dynamically. In many cases, this is the model that you want to use. The second model uses client-side scripting for when you want to provide the client with the capability to manipulate and update data.

Microsoft today ships a set of components known collectively as ADC that provides the infrastructure for moving data from the server to the client, allowing developers to create data-centric applications within ActiveX®-enabled browsers such as Internet Explorer. ADC erects a framework through a set of components that provides the remoting of an OLE DB Rowset object through HTTP or DCOM on corporate intranets and over the Internet.

ADC 1.0 provides Visual Basic-like data binding for ActiveX data-aware controls within IE. This means that you can take those data-bound controls that you are used to using in Visual Basic and build data-centric applications for the Internet or intranet. ADC also enables application partitioning with server objects. A generic mechanism for creating business objects on the server is provided with ADC so that you can encapsulate data and business rules on the server away from your clients.

ADC also provides a transparent caching architecture. One thing that is prevalent today on the Internet is displaying data in an HTML document. It's cached locally but it's static, and the data that you have isn't really live. You can work on it and then later send it back and say "update it," but it is not interactive like traditional client/server applications. By transparently caching data locally at the client side, ADC minimizes round-trips to the server. ADC brings the metadata and the data to the client so that a user can work on it at the client. When the client application is finished updating the data, ADC will worry about the plumbing, getting the data to the server, and ensuring data integrity through optimistic concurrence. Let's take a look at the architecture and see how ADC utilizes ADO and OLE DB.

The components shipped today as ADC will become part of a generic remoting architecture that will allow ADO and OLE DB consumers to access remote data exactly as they would talk to local data. The fact that these remote data services are invoked is invisible to both the data consumer and the data provider. In the meantime, it's important to understand how the components that make up ADC work together with OLE DB providers and ADO consumers to provide data remoting.

In an ADC implementation, assume the data space is partitioned into two spaces, the client space and the server space. The client space is the domain of the user. Within this space, the client interacts with the data within Internet Explorer. The server space is where the business logic resides. This is where you can separate and hide from the user what is really happening with the data and how the data is being packaged. The server space also contains the OLE DB providers and any OLE DB service components that expose data from various data sources.

In the client space is the Advanced Data Control, a data source control compatible with IE, as well as a data binding manager for IE-based applications. It allows for Visual Basic-style data access programming within VBScript and HTML Web pages. The Advanced Data Control is a nonvisual drop-in control that is embedded in Web pages to enable database access and updates to Web applications using the ADC framework. It is this control that data-aware controls, like those found in Visual Basic, bind to.

Another part of the ADC framework is an Advanced Data Factory Proxy. It is used in the framework as a proxy for making requests to the Advanced Data Factory on the server. It basically packages up the method requests and places those requests over HTTP to IIS, which fulfills the requests via the Advanced Data Factory.

In this scenario, there are two very similar stacks, one on each side. One of the reasons there are two stacks is that Microsoft wants to provide a consistent paradigm within the architecture that will work today, and that will also work in the future as more OLE DB providers are written. If you are used to programming in ADO, you can build objects in the server spaces that use ADO, or you can use scripting languages on the client space and access ADO Recordsets on the client.

One of the key components in the stack is the Virtual Table Manager (VTM). The VTM is an in-memory relational data cache exposing OLE DB interfaces for data access and manipulation. The VTM supports state marshaling of its contents through IMarshal and IPersistStream interfaces among multiple server tiers while providing client-side, disconnected cursor models over its cache elements. It also maintains relational data, client updates, and record status information. The VTM consumes as well as exposes OLE DB interfaces.

The Advanced Data Internet Server API (ADISAPI) is responsible for breaking up the MIME packets that come through IIS and invoking the methods on the Advanced Data Factory. The Advanced Data Factory is the actual object that will instantiate ADO, create a connection object, perform the query, and receive the resultset. One of the things I want to mention is that the Advanced Data Factory is just a simple business object. Its sole purpose in life is to accept a query, perform the query, return a resultset, receive data, or perform updates. You don't have to be tied to the Advanced Data Factory; you can create your own business objects to handle things like data validation and hiding.

ADO is the data access API. Once you write to ADO, you can take advantage of writing with it on both the client and the server. The primary difference between the server and the client version of ADO today is that, since the Advanced Data Control handles making the query requests to the server, the client's version supports just the Recordset object. In the next version of ADO, you will be able to use exactly the same model for accessing data locally or on a server, and the ADC components will be invoked for you under the covers. For more information about ADC you should visit http://www.microsoft.com/adc/.

What Should I Do Now?

I've described the Microsoft vision for Universal Data Access, the problems that it solves, and a brief overview of the technologies that are part of the Universal Data Access vision. But what should you do now? What about the rest of the data access APIs? Well, the current version of ADO does not provide all of the functionality of RDO (it doesn't have the rich automatic batch cursor model), nor does it include all of the Jet-specific extensions of DAO (such as compaction). This year, ADO will be extended to support a superset of RDO and DAO functionality.

If you are writing new code to access data in Visual Basic today, take a look at the functionality of ADO. If it does everything that you need, use it. If you are accessing ODBC data and require RDO-specific features, then use RDO. RDO will continue to be supported in the future, and Microsoft will have tools available to help programmers migrate to ADO.

Special thanks to Michael Pizzo for his help with this article.

From the September 1997 issue of Microsoft Systems Journal.


Microsoft OLE DB for OLAP Sample Applications

January 1998

Abstract

This document provides information for Microsoft® OLE DB for OLAP Sample Applications.

Overview

The sample OLAP applications included with the OLE DB for OLAP SDK are all client applications. These samples are generic and should work with any multidimensional data provider exposing OLE DB for OLAP interfaces. A sample OLAP multidimensional database (BobsVideo.cub) is included with these samples and accessed via the Microsoft PivotTable Service provider installed with the SDK components.

BobsVideo.cub is a prebuilt cube representing the store sales for a fictitious video store. The source relational data this cube was built from using the Microsoft PivotTable Service provider is also included as VideoStore.mdb for reference.

Microsoft OLEDB for OLAP Sample Applications

OLEDB4OLAPdemo

This console application submits schema rowset commands and MDX statements to an OLAP cube database. The path and name of the cube database file as well as the query file must be specified on the command line. The application opens a connection to the specified cube using the MSOLAP Sample OLAP provider. The query file must be a text file and can be edited with any text editor such as Notepad. Each line contains either a schema rowset command or a MDX statement. The file is opened by the application and each line is submitted to the MSOLAP provider. The output from these commands is logged to a text file. The output file is created using the query file's name with a ".log" extension. This log file is generated in the same directory in which the original cube file exists.

MDX Statements begin with the word SELECT, while schema commands begin with the word SCHEMA. Schema rowset commands starting with the word "Schema" are followed by several parameters delimited by spaces. Parameters are specified with the syntax: Identifier=Value. If the Value contains spaces, it must be enclosed with quotes. The first parameter specifies the desired schema rowset (Cubes, Dimensions, Levels, etc.). Below is an example of parameters specifying the desired restrictions for the sample data cube (BobsVideo.cub).

SCHEMA Rowset=Dimensions Cube_Name="Bobs Video Store" Dimension_Name="Customer Location"

A query file named "BobsVideo.mdx" is included with OLEDB4OLAPdemo. This file contains schema rowset commands and MDX statements pertaining to the sample cube.

Note: For the set of possible restriction identifiers for each schema rowset, please refer to the "Session Object" section in the OLE DB for OLAP Programmer’s Reference. An explanation of MDX syntax can be found in this reference as well.

OLEDB4OLAPdemo usage is as follows:

C:> OleDb4Olap  "Cube File Name"  "Query File Name"

"Cube File Name" is the name and path of the cube database file and "Query File Name" is the name and path of the file containing the schema rowset commands and/or MDX query statements.

DatasetViewer

This windows application is a generic OLE DB client. The DatasetViewer consists of MDI child windows. Each child represents a connection to an OLE DB provider. The child window contains three parts: At the top is a query window where SQL or MDX statements can be entered; A rowset list view where rowset data s displayed; At the bottom is a list box containing information on all OLE DB interface calls made by the application.

It is possible to connect to any OLE DB provider including an OLE DB for OLAP provider. The "Full Connect" dialog enumerates all OLE DB providers presently installed and registered. The sample OLAP provider, MSOLAP, appears in the drop down list of enumerated providers.

To connect to the sample cube database:

  1. Select MSOLAP from the provider list.

  2. Enter the name and path to the BobsVideo.cub file in the Location edit box.

The Location edit box corresponds to the DBPROP_INIT_LOCATION property for the selected provider.

Once the connection has been established, it is possible to browse the OLAP schema information by selecting the Session menu's IDBSchemaRowset->GetRowset menu item. The IDBSchemaRowset dialog provides a drop down list of all the schema rowsets provided by the currently connected data source. Once a schema rowset has been selected from this list, it is possible to restrict the rowset by specifying any, or several, of the restriction options appearing in the dialog. After clicking the OK button, the schema rowset data is displayed in the rowset list view.

To enter an MDX query statement:

  1. Type the query into the query window.

  2. Select either of the Command's menu ICommand->ExecuteRowset or ICommand->ExecuteDataset menu items.

The ExecuteRowset menu will create a flattened dataset and display the rows in the Rowset list view. The ExecuteDataset will create a dataset.

To display the dataset:

  1. Select one of the menu items from the Dataset menu. If the IMDDataset->GetAxisRowset menu item is selected; it provides a dialog to choose the axis from a drop down list of axes that pertain to the MDX query.

  2. Select the OK button to display the axis rowset in the rowset list view.

If the IMDDataset->GetDataSet menu item is selected; it will display the cube data in the rowset list view as a linear list of cube cells. The data and the cell ordinal are displayed for each cell in the list view.

If the IMDDataset->GetAxisInfo menu item is selected; a dialog containing the MDAXISINFO data returned by the OLAP provider is displayed.

MDXSample

This generic OLE DB for OLAP client application, written in Visual Basic®, uses ADO MD to obtain schema information and query a multidimensional data source. The application consists of a query window to enter MDX queries and a schema window where schema information is presented in a tree view as well as a dataset grid where query data is displayed in a pivot table.

To connect to the sample OLAP provider:

  1. Specify the MSOLAP provider in the initial connect dialog.

  2. Enter the name and path to the sample cube database (BobsVideo.cub).

To enter or edit a sample query:

  1. Use Open from the File menu to open the MDXQuery.mdx sample query file.

  2. Select a sample query from the Queries box on the toolbar.

MDXSamples.mdx is a text file which contains several MDX queries designed for the sample cube database file, BobsVideo.cub. New queries can be entered and saved as well.

Fully commented Visual Basic source code for the MDX Sample Application can be viewed by opening the MDXSampl.vbp project file in Visual Basic. The frmMain.frm or Main.frm file contains sample code. A search on "ADO" provides specific information and comments on how ADO MD is used by this application. These commented code examples are useful for creating a new MDX querying application.


OLE DB for the ODBC Programmer

by Michael Pizzo and Jeff Cochran

Introduction

This paper presents an introduction to OLE DB programming using the Microsoft® OLE DB Software Developer’s Kit (SDK). The goal of this paper is to aid Open Database Connectivity (ODBC) programmers in understanding and using OLE DB. It discusses the relationship of ODBC calls to corresponding OLE DB methods and the related programming issues. Basic OLE DB concepts are discussed, whereas advanced OLE DB features, such as notifications, IPersist * objects, transaction objects, and coordinated transactions, are not covered.

OLE DB and ODBC are application programming interfaces (APIs) designed to provide access to a wide range of data sources. A data source consists of the data, its associated database management system (DBMS), the platform on which the DBMS exists, and the network used to access that platform.

ODBC is designed to provide access primarily to SQL data in a multiplatform environment. OLE DB is designed to provide access to all types of data in an OLE Component Object Model (COM) environment. OLE DB includes the SQL functionality defined in ODBC but also defines interfaces suitable for gaining access to data other than SQL data.

OLE facilitates application integration by defining a set of standard interfaces, groupings of semantically related functions through which one application accesses the services of another. Interfaces are the binary standard for component object interaction. Each interface contains a set of functions that define a contract between the object implementing the interface and the client using it.

OLE DB is designed using OLE COM; for more information about COM, see the Microsoft OLE 2 Programmer’s Reference, Volume 1. Each interface consists of a set of related methods. The full functionality of OLE DB is factored into a number of different interfaces. Each data provider will implement some subset of these interfaces.

OLE and OLE DB

Some of the key OLE concepts that you should be aware of include:

  • Interface factoring

    An interface in OLE is a set of related methods. A single OLE object may support multiple interfaces at the same time. The consumer of an object can move between any interfaces supported on that object by calling QueryInterface (see the following bullet, “Interface negotiation”). If an object supports an interface, it supports all of the methods within that interface. Thus, once the consumer has determined that a particular interface is supported, it understands how to interact with the object. New interfaces that augment the functionality already supported by the existing interfaces can be added later, but methods can never be added or removed from existing interfaces.

    Because an object must support either all or none of the methods within an interface, interfaces are generally factored according to functionality. For example, if an object supports reading data and does or does not support writing data, the methods for reading data and those for writing data would appear in two different interfaces. Only the objects that supported writing data would support the interface containing the methods for writing data.

    OLE DB makes extensive use of interface factoring. Individually supportable functionality, such as different levels of scrollability in a result set or command preparation and parameter support, are factored into different interfaces. Each object has one or more required interfaces that encapsulate base functionality for that object and that can expose extended functionality by implementing one or more optional interfaces. The OLE DB consumer can determine what extended functionality the provider supports by querying for these optional interfaces.

  • Interface negotiation

    IUnknown is implemented by all component objects. All other interfaces inherit, directly or indirectly, from this interface. It has three methods: QueryInterface, AddRef, and Release. Each interface is identified by a globally unique identifier (GUID) by which it is known at compile time. To determine if an object supports a particular interface, the client calls QueryInterface on that object. If an object supports the requested interface, QueryInterface returns a pointer to the interface. The interface identifier (IID) allows the client to dynamically determine, by way of a call to IUnknown::QueryInterface, the capabilities of other objects and to get the pointers to needed interfaces. Every interface that is obtained directly (by calls to QueryInterface) or indirectly (by calls to a method that returns an interface) must be released by calling the Release method of that object.

  • Reference counting

    Reference counts are kept on each instance of a pointer to an interface that is derived from IUnknown. This ensures that the object is not destroyed before all references to it are released.

  • Memory management

    OLE uses two kinds of memory: local application task memory and shared memory. All task memory allocated by the OLE libraries and by the object handlers is allocated using either an application-supplied allocator or the default allocator provided by OLE.

    The standard memory management model in COM requires that the callee allocates and the caller frees. OLE DB generally follows this model, except for certain cases where performance can be gained by the caller allocating and reusing the same piece of memory, or in some special cases, by the callee giving the caller pointers to callee-owned data. In this case, the caller is not allowed to write to or free the memory.

  • Unicode

    All COM interface methods pass Unicode rather than ANSI strings. OLE DB follows this convention, except for getting and setting ANSI data that resides in tables.

Components of OLE DB

OLE DB providers can be classified as data providers and service providers. A data provider is one that owns data and exposes it in a tabular form. Some examples are relational database systems and spreadsheets. A service provider is any OLE DB component that does not own the data but encapsulates some service by producing and consuming data through OLE DB interfaces. Examples are query processors and transaction managers.

OLE DB Objects

The following illustration shows the core object model of OLE DB.

OLE DB Object Model

  • Data Source Object

    The data source object is the initial object returned from an enumerator (see the bullet, “Enumerator Object,” that follows), generated by binding a file moniker or other moniker to a data source, or instantiated by calling the OLE function CoCreateInstance with a given OLE DB data provider’s unique class identifier (CLSID). It encapsulates the functionality of the ODBC environment as well as the connection and informational properties of the ODBC connection.

  • Session Object

    A session object defines the scope of a transaction and generates rowsets from the data source. If the provider supports commands, the session also acts as a command factory. The data source object can also support interfaces for describing schema information and for creating tables and indexes for providers that support that functionality. Along with the data source object, the session encapsulates the functionality of the ODBC connection. Calling IDBCreateSession::CreateSession creates a session from the data source object. There can be multiple sessions associated with a data source object.

  • Command Object

    If a provider supports building and executing queries, it exposes a command object. A command object is generated from a session object. It is used to specify, prepare, and execute a DML query or DDL definition and associated properties. The command encapsulates the general functionality of an ODBC statement in an unexecuted state. There may be multiple commands associated with a single session.

  • Rowset Object

    A rowset object is a shared data object that represents tabular data, such as a result set returned by executing a query. Minimally, rowsets can be generated by calling IOpenRowset::OpenRowset on the session. All providers are required to support this minimal functionality. If the provider supports commands, rowsets are used to represent the results of row-returning queries. There are a number of other methods in OLE DB, such as the schema functions, that return information in the form of a rowset. A rowset encapsulates the general functionality of an ODBC statement in the executed state. There may be multiple rowsets associated with a single session or command.

Other OLE DB Objects

The following objects are also defined in OLE DB. They provide recursive data source enumeration, enhanced transaction control, and extended error retrieval.

  • Enumerator Object

    Enumerator Objects list the data sources and enumerators visible to that enumerator. This is similar to the information provided by SQLDataSources, except that the information can be recursive.

  • Transaction Object

    In addition to supporting ITransactionLocal on the session, providers that support transactions can optionally support the creation of a transaction object. Transaction objects provide more advanced transaction functionality, such as the registration of transaction notifications.

  • Error Object

    In addition to the return codes and status information returned by each method in OLE DB, providers can optionally expose an OLE DB error object for extended error information, such as a description of the error or the appropriate SQLSTATE. This is similar to the information returned by SQLError or SQLGetDiagRec1.

1 Unless explicitly stated otherwise, ODBC 3.0 functions and function names are used throughout. For more information on mapping ODBC 1.x and 2.x to their ODBC 3.0 equivalents, see the ODBC 3.0 documentation.

Basic OLE DB

This section describes the basic concepts and procedures defined in OLE DB, such as initializing the environment, locating and connecting to a data source, executing a command, and retrieving data from a rowset.

Application Flow

The application flow in OLE DB is similar to the application flow in ODBC. In both cases, the application:

  • Initializes the environment.

  • Connects to a data source.

  • Creates and executes a command.

  • Processes results, if any.

  • Cleans up.

A typical OLE DB consumer may look like the following code example. For the complete source code listing, see Appendix A. The individual routines in this sample are described in more detail in the following sections. The flow of control is as follows:

  1. Initialize OLE.

  2. Connect to a data source object.

  3. Create and execute a command.

  4. Process the results.

  5. Release objects and uninitialize OLE.
/********************************************************************
* General OLE DB application main()
********************************************************************/
#define UNICODE
#define _UNICODE
#define DBINITCONSTANTS // Initialize OLE constants...
#define INITGUID    // ...once in each app.

#include <windows.h>

#include <oledb.h>  // OLE DB include files
#include <oledberr.h>
#include <msdaguid.h> // ODBC provider include files
#include <msdasql.h>

// Global task memory allocator
IMalloc*    g_pIMalloc = NULL;

int main()
  {
  IDBInitialize*  pIDBInitialize = NULL;
  IRowset*    pIRowset = NULL;

  // Init OLE and set up the DLLs; see "Initializing the
  // Environment."
  CoInitialize(NULL);

  // Get the task memory allocator.
  if (FAILED(CoGetMalloc(MEMCTX_TASK, &g_pIMalloc)))
    goto EXIT;

  // Connect to the data source; see "Initializing a Data Source."
  if (FAILED(myInitDSO(&pIDBInitialize)))
    goto EXIT;

  // Get a session, set and execute a command; see "Getting a
  // Session and Executing a Command."
  if (FAILED(myCommand(pIDBInitialize, &pIRowset)))
    goto EXIT;

  // Retrieve data from rowset; see "OLE DB Rowsets."
  myGetData(pIRowset);

EXIT:
  // Clean up and disconnect.
  if (pIRowset != NULL)
    pIRowset->Release();

  if (pIDBInitialize != NULL)
    {
    if (FAILED(pIDBInitialize->Uninitialize()))
    {
    // Uninitialize is not required, but it will fail if an
    // interface has not been released. We can use it for
    // debugging.
    DumpError("Someone forgot to release something!");
    }
    pIDBInitialize->Release();
    }

  if (g_pIMalloc != NULL)
    g_pIMalloc->Release();

  CoUninitialize();

  return (0);
  }

Initializing the Environment

In ODBC, the application generally dynamically links to the ODBC Driver Manager (Odbc32.dll). The Driver Manager loads and directs calls to the appropriate driver.

In OLE DB, initialization of the environment is achieved by a call to OleInitialize, which initializes the OLE library. This is shown in the preceding code example. After the OLE library is initialized, the proper data provider is loaded by the system according to its class ID, and calls are made directly to the provider.

Initializing a Data Source

The data source object exposes the IDBInitialize and IDBProperties interfaces that contain the methods to connect to a data source. The authentication information such as user ID, password, and the name of the data source are specified as properties of the data source object by calling IDBProperties::SetProperties. The method IDBInitialize::Initialize uses the specified properties to connect to the data source.

In ODBC, establishing a connection involves the following steps:

  1. Call SQLAllocHandle to allocate a connection handle.

  2. Build a connection string containing keywords for authentication information, such as user ID, password, and the name of the data source.

  3. Call SQLDriverConnect providing the connection string and other information, such as level of prompting and the application’s window handle where appropriate.

In OLE DB, the equivalent functionality is achieved by the following steps:

  1. Build an array of property structures describing the authentication information, such as user ID, password, and the name of the data source, as well as the level of prompting and the application’s window handle when appropriate.

  2. Call IDBProperties::SetProperties to set initialization properties. (For more information about properties, see the following section, “Getting and Setting Properties”.)

  3. Call IDBInitialize::Initialize to initialize the data source object.

As can be seen, the model in OLE DB is similar to the model in ODBC. The primary differences are:

  • OLE DB uses a well-defined set of property structures to represent the initialization and connection information, rather than building/parsing keywords within a connection string.

  • The set of property structures is used for all of the initialization/connection information.

  • The set of available initialization properties can be queried through IDBProperties::GetPropertyInfo.

  • Rather than returning a connection string, the user can simply request the current set of initialization property values.

  • Setting the initialization properties is separate from actually initializing (connecting) the data source. This allows the consumer to set, persist, and retrieve connection information without initializing (connecting) the data source.

The following code example shows a routine that initializes a data source object. The general flow of control is:

  1. Create an instance of the OLE DB Provider (in this case, the ODBC Provider).

  2. Set the initialization properties.

  3. Call Initialize to initialize the data source object, using the supplied properties.
/********************************************************************
* Initialize the data source.
********************************************************************/
HRESULT myInitDSO
  (
  IDBInitialize** ppIDBInitialize   // [out]
  )
  {
  // Create an instance of the MSDASQL (ODBC) provider.
  CoCreateInstance(CLSID_MSDASQL, NULL, CLSCTX_INPROC_SERVER,
    IID_IDBInitialize, (void**)ppIDBInitialize);

  if (*ppIDBInitialize == NULL)
    {
    return (E_FAIL);
    }

  // See "Getting and Setting Properties."
  if (FAILED(mySetInitProps(*ppIDBInitialize)))
    {
    return (E_FAIL);
    }

  if (FAILED((*ppIDBInitialize)->Initialize()))
    {
    DumpError("IDBInitialze->Initialize failed.");
    return (E_FAIL);
    }

  return (NOERROR);
  }

Getting and Setting Properties

Properties are used in OLE DB to specify options, such as initialization information on the data source object or supported properties of a rowset, as well as to discover properties of certain objects, such as the updatability of a rowset.

Properties in OLE DB are similar to the environment, connection, and statement attributes in ODBC, with the following exceptions:

  • In OLE DB, the provider can be queried for a list of all supported properties.

  • In OLE DB, properties are grouped into “Property Groups.” Property groups are identified by a GUID. This allows third parties to define properties within their own property group, rather than trying to reserve ranges within a single set of attribute values.

  • Instead of setting and retrieving properties individually, multiple properties can be set or retrieved from multiple groups in a single call. This is done by building an array of property sets, where each property set contains an array of property structures from a single property group.

OLE DB consumers can:

  • Enumerate, set, and retrieve properties on a data source object using IDBProperties.

  • Set or retrieve properties on a session using ISessionProperties.

  • Set or retrieve properties on a command using ICommandProperties.

  • Retrieve properties and information about a rowset using IRowsetInfo.

The following table shows the property groups in OLE DB and their GUIDs.

Property group | Property group identifier (GUID)
Column | DBPROPFLAGS_COLUMN
Data Source | DBPROPFLAGS_DATASOURCE
Data Source Creation | DBPROPFLAGS_DATASOURCECREATE
Data Source Information | DBPROPFLAGS_DATASOURCEINFO
Data Source Initialization | DBPROPFLAGS_DBINIT
Index | DBPROPFLAGS_INDEX
Rowset | DBPROPFLAGS_ROWSET
Session | DBPROPFLAGS_SESSION
Table | DBPROPFLAGS_TABLE

The following structure contains an array of values of properties from a single property set:

typedef struct  tagDBPROPSET
  {
  DBPROP __RPC_FAR* rgProperties;  // Pointer to an array of
             // DBPROP structures.
  ULONG     cProperties;   // Count of properties
             // (DBPROPS) in the array.
  GUID      guidPropertySet; // A GUID that identifies the
             // property set to which the
             // properties belong.
  } DBPROPSET;

The following structure contains information about a single property:

typedef struct  tagDBPROP
  {
  DBPROPID    dwPropertyID;   // ID of property within a
               // property set.
  DBPROPOPTIONS   dwOptions;   // Property is required?
               // Optional?
  DBPROPSTATUS  dwStatus;   // Status returned by the
               // provider indicating success
               // or failure in setting or
               // getting the property.
               // Enumerated values are:
               //  DBPROPSTATUS_OK
               //  DBPROPSTATUS_NOTSUPPORTED
               //  DBPROPSTATUS_BADVALUE
               //  DBPROPSTATUS_BADOPTION
               //  DBPROPSTATUS_BADCOLUMN
               //  DBPROPSTATUS_NOTALLSETTABLE
               //  DBPROPSTATUS_NOTSET
               //  DBPROPSTATUS_NOTSETTABLE
               //  DBPROPSTATUS_CONFLICTING
  DBID      colid;      // Optional, ordinal column
               // property applies to. If the
               // property applies to all
               // columns, colid should be set
               // to DB_NULLID.
  VARIANT     vValue;      // Value of the property.
  } DBPROP;

The following code example shows how an application sets initialization properties on a data source object. The code sets four properties within a single property group. The general flow of control is:

  1. Allocate an array of property structures.

  2. Allocate an array of a single property set.

  3. Initialize common property elements for the properties.

  4. Fill in the following properties:
    • Level of desired prompting (similar to DriverCompletion argument in SQLDriverConnect)

    • Data source name (similar to DSN= element of the ODBC connection string)

    • User name (similar to the UID= element of the ODBC connection string)

    • Password (similar to the PWD= element of the ODBC connection string)
  5. Set the property set to the array of properties and specify that the properties are from the initialization property group.

  6. Get the IDBProperties interface.

  7. Call SetProperties on the interface.
/********************************************************************
* Set initialization properties on a data source.
********************************************************************/
HRESULT mySetInitProps
  (
  IDBInitialize*  pIDBInitialize   // [in]
  )
  {
  const ULONG   nProps = 4;
  IDBProperties*  pIDBProperties;
  DBPROP    InitProperties[nProps];
  DBPROPSET   rgInitPropSet;
  HRESULT   hr;

  // Initialize common property options.
  for (ULONG i = 0; i < nProps; i++ )
    {
    VariantInit(&InitProperties[i].vValue);
    InitProperties[i].dwOptions = DBPROPOPTIONS_REQUIRED;
    InitProperties[i].colid = DB_NULLID;
    }

  // Level of prompting that will be done to complete the
  // connection process.
  InitProperties[0].dwPropertyID = DBPROP_INIT_PROMPT;
  InitProperties[0].vValue.vt = VT_I2;
  InitProperties[0].vValue.iVal = DBPROMPT_NOPROMPT;

  // Data source name--see the sample source code included with the
  // OLE DB SDK.
  InitProperties[1].dwPropertyID = DBPROP_INIT_DATASOURCE;
  InitProperties[1].vValue.vt = VT_BSTR;
  InitProperties[1].vValue.bstrVal =
    SysAllocString(OLESTR("OLE_DB_NWind_Jet"));

  // User ID
  InitProperties[2].dwPropertyID = DBPROP_AUTH_USERID;
  InitProperties[2].vValue.vt = VT_BSTR;
  InitProperties[2].vValue.bstrVal = SysAllocString(OLESTR(""));

  // Password
  InitProperties[3].dwPropertyID = DBPROP_AUTH_PASSWORD;
  InitProperties[3].vValue.vt = VT_BSTR;
  InitProperties[3].vValue.bstrVal = SysAllocString(OLESTR(""));

  rgInitPropSet.guidPropertySet = DBPROPSET_DBINIT;
  rgInitPropSet.cProperties = nProps;
  rgInitPropSet.rgProperties = InitProperties;

  // Set initialization properties.
  pIDBInitialize->QueryInterface(IID_IDBProperties, (void**)
    &pIDBProperties);
  hr = pIDBProperties->SetProperties(1, &rgInitPropSet);

  SysFreeString(InitProperties[1].vValue.bstrVal);
  SysFreeString(InitProperties[2].vValue.bstrVal);
  SysFreeString(InitProperties[3].vValue.bstrVal);

  pIDBProperties->Release();

  if (FAILED(hr))
    {
    DumpError("Set properties failed.");
    }

  return (hr);
  }

Getting a Session and Executing a Command

The data source object exposes the IDBCreateSession interface through which a session object can be created. A session defines transaction scope and acts mainly as a command generator by supporting the IDBCreateCommand interface. Commands contain a DML query or a DDL definition. The execution of a row-returning command yields a rowset object.

The session object in OLE DB is similar to the ODBC connection handle. However, the connection handle in ODBC is used for establishing connections as well as scoping transactions, so the application must allocate and connect a separate connection handle for each concurrent transaction. In OLE DB you can have multiple session objects on one initialized data source object, which means you can have multiple concurrent transactions without having to make multiple connections (where necessary, the provider makes additional connections using the connection information provided in the initialization of the data source object).

The command object in OLE DB is similar to the ODBC statement handle in the unexecuted state. Like the ODBC connection handle, which can have several statement handles, a session object can have several command objects.

An ODBC application performs the following steps to execute a command:

  1. Calls SQLAllocHandle to allocate a statement.

  2. Calls SQLSetStmtAttr to set various attributes that affect how the command is executed (such as query time-out) and how the cursor is opened (such as scrollability, updatability, and so on).

  3. Calls SQLPrepare if it wants to prepare the statement for repeated execution.

  4. Calls SQLExecute or SQLExecDirect to execute the query.

To use a command, an OLE DB consumer performs these steps:

  1. Calls IDBCreateCommand to create a command.

  2. Calls ICommandProperties::SetProperties to set various attributes that affect how the command is executed (such as query time-out), as well as requesting properties to be supported by the resulting rowset. Typical properties include scrollability, updatability, the number of active row handles a consumer can hold at one time, sensitivity to changes outside the rowset, and so on.

  3. Calls ICommandText::SetCommandText to specify the command text, along with a GUID representing the command’s dialect. The standard dialect for ANSI SQL commands is DBGUID_DBSQL.

  4. Calls ICommandPrepare::Prepare if it wants to prepare the query for repeated execution.

  5. Calls ICommandText::Execute to execute the command.

The following code example shows setting and executing a command, and retrieving a rowset. The general flow of control is:

  1. Obtain the IDBCreateSession interface.

  2. Call CreateSession to create a session object that scopes the transaction boundaries within the current connection.

  3. Call CreateCommand to create a command object within the transaction.

  4. Call SetCommandText to set the command text.

  5. Call Execute to execute the command.

  6. Release the command object.

  7. Return the rowset object.
/********************************************************************
* Execute a command, retrieve a rowset interface pointer.
********************************************************************/
HRESULT myCommand
  (
  IDBInitialize*  pIDBInitialize, // [in]
  IRowset**   ppIRowset   // [out]
  )
  {
  IDBCreateSession* pIDBCreateSession;
  IDBCreateCommand* pIDBCreateCommand;
  IRowset*    pIRowset;
  ICommandText*   pICommandText;
  LPCTSTR     wSQLString = OLESTR("SELECT CompanyName,
                 City, Phone, Fax")
             OLESTR(" FROM Customers")
             OLESTR(" ORDER BY CompanyName,
                 City");
  LONG      cRowsAffected;
  HRESULT     hr;

  // Get the DB session object.
  if (FAILED(pIDBInitialize->QueryInterface(IID_IDBCreateSession,
    (void**) &pIDBCreateSession)))
    {
    DumpError("Session initialization failed.");
    return (E_FAIL);
    }

  // Create the session, getting an interface for command creation.
  hr = pIDBCreateSession->CreateSession(NULL, IID_IDBCreateCommand,
    (IUnknown**) &pIDBCreateCommand);
  pIDBCreateSession->Release();
  if (FAILED(hr))
    {
    DumpError("Create session failed.");
    return (hr);
    }

  // Create the command object.
  hr = pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText,
    (IUnknown**) &pICommandText);
  if (FAILED(hr))
    {
    DumpError("Create command failed.");
    return (hr);
    }
  pIDBCreateCommand->Release();

  // The command requires the actual text as well as an indicator
  // of its language and dialect.
  pICommandText->SetCommandText(DBGUID_DBSQL, wSQLString);

  // Execute the command.
  hr = pICommandText->Execute(NULL, IID_IRowset, NULL,
   &cRowsAffected, (IUnknown**) &pIRowset);
  if (FAILED(hr))
    {
    DumpError("Command execution failed.");
    }
  pICommandText->Release();

  *ppIRowset = pIRowset;
  return (hr);
  }
OLE DB Rowsets

A rowset provides a standard way to work with a multiset of rows where each row contains one or more columns of data. This provides a standard way for all OLE DB data providers to expose data in tabular form.

A rowset can be obtained in several ways. The first method, supported by all providers, uses the IOpenRowset interface. IOpenRowset provides for the simple case of retrieving all the data from the table. This is equivalent to executing SELECT * FROM table. The second method, supported by SQL providers, involves creating and executing a command to get a rowset that meets specific criteria, such as SELECT * FROM table WHERE condition. The ICommandProperties interface on the command can be used to specify the interfaces and properties that must be supported by the rowsets returned by ICommand::Execute.

Conceptually, rowsets are similar to result sets in ODBC; however, their implementation is different.

In ODBC, when the application calls SQLFetch or SQLGetData to retrieve data, the data is read from the database into the application’s memory. At that point, the application owns the data; neither the ODBC driver nor other components have access to that data. This model works fine provided that:

  • The application is the only client of the data.

  • A single component provides all of the functionality needed by the application.

  • The application is in an environment in which it can maintain a connection to the database that keeps the state of the application’s position within the data, concurrency information for data retrieved, and so on.

OLE DB rowsets are not only designed to provide a high performance solution for the preceding scenarios but are also designed so that:

  • Multiple controls can work together on the same data. For example, a grid control and a chart control can model the same results: If the grid control updates the data, the chart control gets a notification and can redraw the graph accordingly.

  • Multiple components can work together to add incremental functionality to a single instance of the data. The data does not have to be copied to multiple components, and method calls don’t have to go through a hierarchy of components. Each component can operate directly on the data.

  • The application can work in a disconnected environment with a stand-alone data object that maintains state, concurrency, and other information.

The OLE DB rowset basically takes the memory buffer out of the application and puts it in a stand-alone, shared data object. Rather than the application buffering data in its own memory, components access data in this shared memory through high-performance binding descriptions known as accessors. Because, in many cases, the application merely retrieves pointers to existing data and multiple components can access the same copy of the data, data access can often be faster than copying the data into the application’s own buffers. The rowset object also allows multiple components to coordinate their activities on this shared data object through notifications, and because the components are all sharing the same data, they can immediately see the changes made by other components. The rowset object exposes bookmarks, which let each individual component keep track of its own position, and the rowset object keeps track of concurrency information for deferred updating and optimistic concurrency control.

Retrieving Data from a Rowset

The main differences between how data is retrieved in ODBC and how data is retrieved in OLE DB are a direct result of the differences between the application-owned data model of ODBC and the shared-data model of OLE DB.

The most basic rowset in OLE DB exposes the following interfaces:

  • IColumnsInfo. Provides information about the columns of the rowset (metadata). This is similar to SQLDescribeCol in ODBC.

  • IRowsetInfo. Provides information about the rowset. This is similar to SQLGetStmtAttr in ODBC.

  • IAccessor. Permits definition of groups of column bindings describing the way in which tabular data is bound to program variables. This is similar to the bindings specified through descriptors by calling SQLSetDescField in ODBC version 3.0.

  • IRowset. Contains methods for iterating through the rows in the rowset sequentially. The methods in IRowset are similar to SQLFetch in ODBC.

Retrieving Rows

The provider manages the row buffers on behalf of the consumer. Rows are fetched from the data source into this row buffer using methods such as IRowset::GetNextRows, IRowsetLocate::GetRowsAt, and IRowsetLocate::GetRowsByBookmark. These methods are similar to SQLExtendedFetch in ODBC, except that instead of reading the data into the applications buffers, these functions read the data into the shared data object and return row handles (hRows) to the fetched data.

The consumer accesses the data from these row handles using IRowset::GetData. IRowset::GetData takes an accessor that maps fields of the row to and/or from fields in a structure on the consumer side. The types of the fields in the consumer’s structure are indicated by the bindings in the accessor, and IRowset::GetData makes any necessary conversions between the buffered data and the consumer’s data structure. If GetData encounters an error, it sets the status value for the column to the appropriate error.

To retrieve a row of data from the result set in ODBC, the application:

  1. Calls SQLBindCol to bind the columns of the result set to storage locations, if not already done.

  2. Calls SQLFetch to move to the next row and retrieve data for all bound columns.

  3. Calls SQLGetData to retrieve data from unbound columns.

In OLE DB, the consumer performs the following functions to retrieve data:

  1. Calls IAccessor::CreateAccessor to specify binding information if not already done.

  2. Calls IRowset::GetNextRows to retrieve the next set of row handles.

  3. Calls IRowset::GetData to retrieve the data from the row handles according to the bindings specified by the accessor.

IRowset::GetData is similar to SQLGetData in ODBC, except that IRowset::GetData:

  • Can be called for any held row handle, not just the current row.

  • Allows the retrieval of multiple columns in a single call.

  • Can return an interface to a live OLE object, not just a binary large object (BLOB).

  • Can be used to return a pointer to data within the provider rather than a copy of the data.

  • Returns an interface to a stream over the object rather than retrieve long data through multiple calls to SQLGetData.

The following code example demonstrates data retrieval in OLE DB. The general flow of control is:

  1. Get a description of the rowset.

  2. Build binding structures based on the description.

  3. Obtain the IAccessor interface on the rowset object.

  4. Call CreateAccessor to create an accessor.

  5. Call GetNextRows to retrieve the next set of row handles.

  6. Call GetData to access the data for each row.

  7. Release the set of retrieved row handles.

  8. Repeat steps 4 through 6 until all the data has been retrieved.

  9. Release the accessor.
/********************************************************************
* Retrieve data from a rowset.
********************************************************************/
void myGetData
  (
  IRowset*  pIRowset  // [in]
  )
  {
  ULONG     nCols;
  DBCOLUMNINFO* pColumnsInfo = NULL;
  OLECHAR*    pColumnStrings = NULL;
  ULONG     nCol;
  ULONG     cRowsObtained;    // Count of rows
                // obtained
  ULONG     iRow;       // Row count
  HROW    rghRows[NUMROWS_CHUNK]; // Row handles
  HROW*     pRows = &rghRows[0];  // Pointer to the row
                // handles
  IAccessor*  pIAccessor;     // Pointer to the
                // accessor
  HACCESSOR   hAccessor;      // Accessor handle
  DBBINDSTATUS* pDBBindStatus = NULL;
  DBBINDING*  pDBBindings = NULL;
  char*     pRowValues;

  // Get the description of the rowset for use in binding structure
  // creation; see "Describing Query Results."
  if (FAILED(myGetColumnsInfo(pIRowset, &nCols, &pColumnsInfo,
    &pColumnStrings)))
    {
    return;
    }

  // Create the binding structures; see "Elements of an Accessor."
  myCreateDBBindings(nCols, pColumnsInfo, &pDBBindings,
    &pRowValues);
  pDBBindStatus = new DBBINDSTATUS[nCols];

  // Create the accessor; see "Creating Accessors."
  pIRowset->QueryInterface(IID_IAccessor, (void**) &pIAccessor);
  pIAccessor->CreateAccessor(
    DBACCESSOR_ROWDATA,// Accessor will be used to retrieve row
         // data
    nCols,     // Number of columns being bound
    pDBBindings,   // Structure containing bind info
    0,       // Not used for row accessors
    &hAccessor,    // Returned accessor handle
    pDBBindStatus  // Information about binding validity
    );
  // Process all the rows, NUMROWS_CHUNK rows at a time.
  while (TRUE)
    {
    pIRowset->GetNextRows(
    0,      // Reserved
    0,      // cRowsToSkip
    NUMROWS_CHUNK,  // cRowsDesired
    &cRowsObtained,   // cRowsObtained
    &pRows );     // Filled in w/ row handles.

    // All done; there are no more rows left to get.
    if (cRowsObtained == 0)
    break;

    // Loop over rows obtained, getting data for each.
    for (iRow=0; iRow < cRowsObtained; iRow++)
    {
    pIRowset->GetData(rghRows[iRow], hAccessor, pRowValues);
    for (nCol = 0; nCol < nCols; nCol++)
      {
      wprintf(OLESTR("%s%s:"), pColumnsInfo[nCol].pwszName,
        wcslen(pColumnsInfo[nCol].pwszName) > 10 ?
        OLESTR("\t") : OLESTR("\t\t"));
      printf("\t%s\n",
        &pRowValues[pDBBindings[nCol].obValue]);
      }
    printf("\n");
    }

    // Release row handles.
    pIRowset->ReleaseRows(cRowsObtained, rghRows, NULL, NULL,
    NULL);
    }  // End while

  // Release the accessor.
  pIAccessor->ReleaseAccessor(hAccessor, NULL);
  pIAccessor->Release();

  delete [] pDBBindings;
  delete [] pDBBindStatus;

  g_pIMalloc->Free( pColumnsInfo );
  g_pIMalloc->Free( pColumnStrings );

  return;
  }

Describing Query Results

In ODBC, to describe the results of a query, an application:

  • Calls SQLNumResultCols to find out the number of columns in the result set.

  • Calls SQLDescribeCol or SQLColAttribute to describe each column in the result set.

In OLE DB, to describe the results of a query, an application:

  • Calls IColumnsInfo or IColumnsRowset to describe the columns in the rowset.

In OLE DB, the consumer can also call IRowsetInfo to get information about properties supported on the rowset. This is similar to calling SQLGetStmtAttr on an executed statement handle in ODBC.

The following code example shows getting the metadata from the result set. Note that the ColumnsInfo interface passes off ownership of both the DBCOLUMNINFO structure array and the OLECHAR string that holds strings that are part of the rowset’s metadata. These strings are freed using the task memory allocation interface, IMalloc.

/********************************************************************
* Get the characteristics of the rowset (the ColumnsInfo interface).
********************************************************************/
HRESULT myGetColumnsInfo
  (
  IRowset*    pIRowset,    // [in]
  ULONG*    pnCols,    // [out]
  DBCOLUMNINFO**  ppColumnsInfo, // [out]
  OLECHAR**   ppColumnStrings  // [out]
  )
  {
  IColumnsInfo* pIColumnsInfo;
  HRESULT   hr;

  if (FAILED(pIRowset->QueryInterface(IID_IColumnsInfo, (void**)
   &pIColumnsInfo)))
    {
    DumpError("Query rowset interface for IColumnsInfo failed");
    return (E_FAIL);
    }

  hr = pIColumnsInfo->GetColumnInfo(pnCols, ppColumnsInfo,
   ppColumnStrings);
  if (FAILED(hr))
    {
    DumpError("GetColumnInfo failed.");
    *pnCols = 0;
    }

  pIColumnsInfo->Release();
  return (hr);
  }

OLE DB Accessors

An accessor contains the binding information for one or more columns or parameters. The bindings supplied by the caller describe a data structure with offsets for each value. The consumer is responsible for correctly calculating the offsets. The consumer specifies the desired data type within the structure and the provider agrees to the binding if the conversion is legal and supported. The accessors are created on the rowset or command and their implementation is specific to the provider. They contain information or code to pack and unpack rows held by the provider. The consumer uses them like handles on the access functions of the row or column. Consumers can use more than one accessor, or a single accessor can define the mapping between a structure and multiple columns or parameters.

Accessors in OLE DB are similar to descriptor handles in ODBC. Each represents a reusable, extensible set of binding information for multiple columns of a result set or parameters to a statement. Multiple accessors can be created for a single rowset, just as multiple descriptors can be used on a single result set. Accessors can be created on a command object so that they can be shared between multiple rowsets the same way that descriptors can be shared between multiple result sets (hStmts).

Creating Accessors

In ODBC version 3.0, applications can create reusable descriptor handles that specify extendable binding information for a set of columns. To use a descriptor, the ODBC application:

  • Calls SQLAllocHandle to allocate a descriptor handle.

  • Calls SQLSetDescField for each piece of binding information on each column.

In OLE DB, consumers create reusable accessor handles that specify binding information for a set of columns. To use an accessor, the consumer:

  • Creates an array of binding structures, each describing the binding information for a single column.

  • Calls CreateAccessor to create an accessor handle using the binding information.

The main difference between descriptors and accessors is that SQLSetDescField is called multiple times to set individual properties for each column represented by the descriptor, while all of the binding information for an accessor is built into an array of binding structures that is passed in a single call to CreateAccessor. This is partially addressed in ODBC by the presence of “concise” functions, such as SQLBindCol and SQLSetDescRec, which allow the application to specify the most common subset of binding information in a single call per column.

Elements of an Accessor

The section of memory bound to a column or parameter must have at least one and can have up to three of the following parts:

  • Value. The actual column or parameter value.

  • Length. The actual length of the column or parameter data stored in the value part, in bytes.

  • Status. This serves a dual purpose. First, it indicates if the value is NULL. Second, it is used to return information about whether the value was successfully passed to or returned from the provider.

It is possible to have columns that are deferred. For a deferred column, the provider is not required to retrieve data until IRowset::GetData is called for that column or, if the column contains an OLE object, until a method used to access the object is called. The advantage of deferred columns is that data retrieval can be postponed until the data is needed, which is very beneficial when the data is large.

The following structure defines the information specified for each column bound by an accessor.

typedef struct tagDBBINDING
  {
  ULONG iOrdinal;         // Ordinal of column or
               // parameter to bind.
  ULONG obValue;         // Offset in consumer's buffer
               // for value.
  ULONG obLength;         // Offset in consumer's buffer
               // for length of value.
  ULONG obStatus;         // Offset in consumer's buffer
               // for status of operation.
  ITypeInfo __RPC_FAR* pTypeInfo;   // Reserved, should be NULL.
  DBOBJECT __RPC_FAR* pObject;   // Pointer to object structure.
               // Structure describes access
               // to OLE objects in the
               // column.
  DBBINDEXT __RPC_FAR* pBindExt;   // Reserved, should be NULL.
  DBPART dwPart;         // Enumerated parts to bind
               // (value/length/status).
  DBMEMOWNER dwMemOwner;      // Memory is owned by consumer?
               // Provider?
  DBPARAMIO eParamIO;      // Parameter type
               // (input/output/not a
               // parameter).
  ULONG cbMaxLen;         // Size of consumer's value
               // buffer if memory is consumer
               // owned.
  DWORD dwFlags;         // Reserved, should be 0.
  DBTYPE wType;         // Data type indicator.
  BYTE bPrecision;         // Precision for
               // numeric/decimal data types.
  BYTE bScale;         // Scale for numeric/decimal
               // data types.
  } DBBINDING;

The provider returns column or output parameter data to the consumer and also sets the status value of each column or output parameter.

Status value | Meaning
DBSTATUS_S_OK | Column/parameter value set/retrieved successfully.
DBSTATUS_S_ISNULL | Column/parameter value is the NULL value.
DBSTATUS_S_TRUNCATED | Column/parameter truncated.
DBSTATUS_S_DEFAULT | The provider should use the parameter’s default value.
DBSTATUS_E_BADACCESSOR | The accessor used to read/write the value was invalid.
DBSTATUS_E_CANTCONVERT | The accessor specified an invalid conversion.
DBSTATUS_E_CANTCREATE | The provider could not create a storage object for large data.
DBSTATUS_E_DATAOVERFLOW | The conversion failed due to a data overflow.
DBSTATUS_E_INTEGRITYVIOLATION | The data value violated an integrity constraint.
DBSTATUS_E_PERMISSIONDENIED | The data value could not be set due to insufficient permissions.
DBSTATUS_E_SCHEMAVIOLATION | The data value violated the Schema for the column.
DBSTATUS_E_SIGNMISMATCH | The data value had the incorrect sign.
DBSTATUS_E_UNAVAILABLE | The data was not available.

The following code example shows traversing a DBCOLUMNINFO structure array obtained from a rowset and creating a set of bindings based on that information. An accessor is created from the binding structure array in the previous code sample.

/********************************************************************
* Create binding structures from column information. Binding
* structures will be used to create an accessor that allows row value
* retrieval.
********************************************************************/
void myCreateDBBindings
  (
  ULONG nCols,       // [in]
  DBCOLUMNINFO* pColumnsInfo,  // [in]
  DBBINDING** ppDBBindings,  // [out]
  char** ppRowValues     // [out]
  )
  {
  ULONG   nCol;
  ULONG   cbRow = 0;
  DBBINDING*  pDBBindings;
  char*   pRowValues;

  pDBBindings = new DBBINDING[nCols];

  for (nCol = 0; nCol < nCols; nCol++)
    {
    pDBBindings[nCol].iOrdinal = nCol+1;
    pDBBindings[nCol].obValue = cbRow;
    pDBBindings[nCol].obLength = 0;
    pDBBindings[nCol].obStatus = 0;
    pDBBindings[nCol].pTypeInfo = NULL;
    pDBBindings[nCol].pObject = NULL;
    pDBBindings[nCol].pBindExt = NULL;
    pDBBindings[nCol].dwPart = DBPART_VALUE;
    pDBBindings[nCol].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
    pDBBindings[nCol].eParamIO = DBPARAMIO_NOTPARAM;
    pDBBindings[nCol].cbMaxLen = pColumnsInfo[nCol].ulColumnSize;
    pDBBindings[nCol].dwFlags = 0;
    pDBBindings[nCol].wType = pColumnsInfo[nCol].wType;
    pDBBindings[nCol].bPrecision = pColumnsInfo[nCol].bPrecision;
    pDBBindings[nCol].bScale = pColumnsInfo[nCol].bScale;

    cbRow += pDBBindings[nCol].cbMaxLen;
    }

  pRowValues = new char[cbRow];

  *ppDBBindings = pDBBindings;
  *ppRowValues = pRowValues;

  return;
  }

Advanced Rowset Topics

OLE DB rowset interfaces and properties expose database cursor and bookmark support to the developer. In addition, OLE DB exposes support for client use of database resources such as scrollbar interface support.

The OLE DB cursor model is derived from that implemented in ODBC and includes support for cursor type, cursor scrolling capability, and transaction isolation levels.

Block Cursors

OLE DB supports block cursors by specifying the number of rows desired in the IRowset::GetNextRows method. In ODBC, a block cursor is defined by specifying the rowset size using the SQL_ATTR_ROW_ARRAY_SIZE attribute in a call to SQLSetStmtAttr prior to calling SQLFetchScroll.

Sequential Rowsets

Sequential rowsets do not support locating specific rows, or fetching rows already retrieved. GetNextRows can retrieve one or more rows, but the rows must all be released before another GetNextRows call is permitted. This restriction is known as strict sequential cursor. This is similar to the forward-only cursor supported by ODBC. A more flexible model, relaxed sequential, allows prior rows to be held, but GetNextRows is still sequential, and the consumer cannot revisit earlier parts of the rowset. The relaxed sequential model also allows changes and updates to rows that have already been passed but have been held. Sequential rowsets do not implement the IRowsetLocate interface.

GetNextRows is positional; that is, it fetches rows sequentially, at all times keeping track of the previous position. It can move forward, or backward if the rowset supports reversible direction. IRowset::RestartPosition repositions the next fetch position to the start of the rowset. GetNextRows keeps track of the next fetch position so that a sequence of calls to this method with no skips and no change of direction reads the entire rowset. This position is not connected to or disturbed by any other method that gets rows. Rowsets that support the property DBPROP_CANFETCHBACKWARDS can take a negative number for the count of requested rows. If the count of requested rows is negative, then the fetch direction is backwards. GetNextRows increases the reference count of each row for which it returns a handle. ReleaseRows must be called to release the handles that have been retrieved.

GetNextRows is the same as calling SQLFetch in ODBC with no columns bound, except that it can move forward or backward relative to the current position and can return multiple row handles.

Scrollable Rowsets

Scrollable rowsets support IRowsetLocate. IRowsetLocate is equivalent to ODBC SQLFetchScroll. Scrollable rowsets are not positional; the consumer uses bookmarks to fetch relative to a previous position.

IRowsetLocate supports the following scrolling options:

  • Forward or backward

  • To the beginning or end of the rowset

  • Relative to a specific row

Bookmarks

A bookmark is a value that the consumer can use to quickly and easily reposition to a row in the result set without incurring the overhead of holding on to the row. Bookmarks are valid only during the lifetime of the rowset to which they refer. An application retrieves a bookmark as column zero, in the same way it retrieves data from any other column in the rowset.

Bookmark columns have the DBCOLUMNFLAG_ISBOOKMARK flag set in their column information. A bookmark may have a data type indicator of DBTYPE_I4 or DBTYPE_STR.

OLE DB provides some standard bookmarks:

  • DBBMK_INVALID. The bookmark is invalid. This is generally used for initializing variables.

  • DBBMK_FIRST. Indicates the first row of the rowset.

  • DBBMK_LAST. Indicates the last row of the rowset.

A bookmark becomes invalid if the row it points to is deleted, if it is based on a primary key and some of those key values were changed, or if the row it points to is no longer in the keyset. The validity of bookmarks after a transaction has been committed or aborted depends on the property DBPROP_COMMIT_PRESERVE. If this is set to true, then bookmarks remain valid; otherwise, they do not.

Scrollbar Support

IRowsetScroll is the interface for moving to approximate locations in a moveable rowset. This method can be used for cases where precise positioning is not critical. IRowsetScroll supports the following interfaces:

  • IRowsetScroll::GetApproximatePosition. Gets the position of the row corresponding to the specified bookmark.

  • IRowsetScroll::GetRowsAtRatio. Gets rows starting from a fractional position in the rowset.

Cursor Types

OLE DB support for scrollable cursors is provided and determined by the values of the rowset properties.

Property | Meaning
DBPROP_CANSCROLLBACKWARDS | The rowset can return to previously read rows.
DBPROP_OWNINSERT | The rowset can see its own inserts.
DBPROP_OTHERINSERT | The rowset can see inserts made outside of the rowset.
DBPROP_OWNUPDATEDELETE | The rowset can see its own changes and deletions.
DBPROP_OTHERUPDATEDELETE | The rowset can see changes and deletions made outside of the rowset.

These options are used to specify the characteristics of the static, keyset, and dynamic cursors defined in ODBC as follows:

  • Static cursor

    In a static cursor, the membership, ordering, and values of the rowset are fixed after the rowset is opened. Rows updated, deleted, or inserted after the rowset is opened are not visible to the rowset until the command is reexecuted.

    To obtain a static cursor, the application sets the properties:

    • DBPROP_CANSCROLLBACKWARDS to VARIANT_TRUE

    • DBPROP_OTHERINSERT to VARIANT_FALSE

    • DBPROP_OTHERUPDATEDELETE to VARIANT_FALSE

    In ODBC, this is equivalent to specifying SQL_CURSOR_STATIC for the SQL_ATTR_CURSOR_TYPE attribute in a call to SQLSetStmtAttr.

  • Keyset-driven cursor

    In a keyset-driven cursor, the membership and ordering of rows in the rowset are fixed after the rowset is opened. However, values within the rows can change after the rowset is opened, including the entire row that is being deleted. Updates to a row are visible the next time the row is fetched, but rows inserted after the rowset is opened are not visible to the rowset until the command is reexecuted.

    To obtain a keyset-driven cursor, the application sets the properties:

    • DBPROP_CANSCROLLBACKWARDS to VARIANT_TRUE

    • DBPROP_OTHERINSERT to VARIANT_FALSE

    • DBPROP_OTHERUPDATEDELETE to VARIANT_TRUE

    In ODBC, this is equivalent to specifying SQL_CURSOR_KEYSET_DRIVEN for the SQL_ATTR_CURSOR_TYPE attribute in a call to SQLSetStmtAttr.

  • Dynamic cursor

    In a dynamic cursor, the membership, ordering, and values of the rowset can change after the rowset is opened. The row updated, deleted, or inserted after the rowset is opened is visible to the rowset the next time the row is fetched.

    To obtain a dynamic cursor, the application sets the properties:

    • DBPROP_CANSCROLLBACKWARDS to VARIANT_TRUE

    • DBPROP_OTHERINSERT to VARIANT_TRUE

    • DBPROP_OTHERUPDATEDELETE to VARIANT_TRUE

    In ODBC, this is equivalent to specifying SQL_CURSOR_DYNAMIC for the SQL_ATTR_CURSOR_TYPE attribute in the call to SQLSetStmtAttr.

  • Cursor sensitivity

    If the rowset property DBPROP_OWNINSERT is set to VARIANT_TRUE, the rowset can see its own inserts; if the rowset property DBPROP_OWNUPDATEDELETE is set to VARIANT_TRUE, the rowset can see its own updates and deletes. These are equivalent to the presence of the SQL_CASE_SENSITIVITY_ADDITIONS bit and a combination of the SQL_CASE_SENSITIVITY_UPDATES and SQL_CASE_SENSITIVITY_DELETIONS bits that are returned in the ODBC SQL_STATIC_CURSOR_ATTRIBUTES2 SQLGetInfo request.

Handling Long Data Types

To a rowset, a BLOB or OLE storage object is a large sequence of uninterpreted bytes that a consumer stores in a column. It is the consumer’s responsibility to interpret this sequence of bytes. BLOBs and OLE storage objects are categorized as:

  • BLOB. Uninterpreted sequence of bytes. The type indicator of a BLOB column is DBTYPE_BYTES, DBTYPE_STR, or DBTYPE_WSTR.

  • IPersist *Object. An OLE object supporting either IPersistStream, IPersistStreamInit, or IPersistStorage. The type indicator is DBTYPE_UNKNOWN.

A consumer can determine what types of BLOBs, OLE storage objects, and other types of OLE objects that a provider supports by calling IDBProperties with the DBPROP_OLEOBJECTS property.

BLOBs as Long Data

If the entire BLOB can be held in memory, it is treated as long in-memory data. To read the BLOB data, the consumer binds the column with type identifier DBTYPE_BYTES, DBTYPE_STR, or DBTYPE_WSTR, and calls IRowset::GetData with an accessor containing this binding. The provider then returns the entire contents of the BLOB to the consumer.

BLOBs as Storage Objects

If a BLOB is too large to hold in memory, the consumer manipulates it through the ISequentialStream storage interface. The rows in the rowset are containers of the storage objects.

On retrieval, BLOB columns are deferred by default. Their data is not retrieved and storage objects are not created until GetData is called. In particular, methods that retrieve rows, such as GetNextRows, do not return data for BLOB columns in the data cache.

A storage object created by the provider remains valid until one of the following occurs:

  • The consumer calls IRowset::ReleaseRows to release the row containing the storage object, or IRowset::Release to release the rowset containing the row.

  • The consumer calls ITransaction::Commit or ITransaction::Abort, and the retaining flag is not set.

It is the consumer’s responsibility to release the storage object, even if the containing row has been released.

Accessing BLOB Data with Storage Objects

To bind to a BLOB data as a storage object, a consumer creates an accessor that includes a binding to the BLOB column. The consumer:

  1. Sets the dwType element of the DBBINDING structure for the BLOB column to DBTYPE_IUNKNOWN.

  2. Sets the iid element of the DBOBJECT structure in the binding to IID_ISequentialStream.

  3. Sets the dwFlags element of the DBOBJECT structure in the binding.

To read data from a BLOB column using a storage object, the consumer:

  1. Creates an accessor that includes a binding for the column.

  2. Calls IRowset::GetData with this accessor. The provider creates a storage object over the BLOB’s data and returns a pointer to the requested storage interface (ISequentialStream) on this object.

  3. Calls methods on the storage interface to read the BLOB’s data (ISequentialStream::Read).

If the consumer calls GetData, GetVisibleData, or GetOriginalData multiple times for the BLOB column, the provider returns distinct pointers to storage interfaces on each call. This is similar to opening a file a number of times and returning a different file handle each time. It is the consumer’s responsibility to call Release on each of these storage interfaces.

For example, the following code example binds to a BLOB column and uses ISequentialStream::Read to read the data. For the complete source code listing, see Appendix B. The general flow of control is:

  1. Create a binding structure to retrieve the ISequentialStream interface from an OLE storage object.

  2. Obtain the IAccessor interface.

  3. Call CreateAccessor to create the accessor.

  4. Call GetNextRows to retrieve the row handles.

  5. Call GetData to retrieve the storage object from a row.

  6. Call ISequentialStream to read the data from the stream.

  7. Repeat steps 4, 5, and 6 to retrieve new storage objects and get the data.
/********************************************************************
* Retrieve data from an ODBC LONG_VARCHAR column (Notes in
* Employees).
********************************************************************/
void myGetBLOBData
  (
  IRowset*    pIRowset         // [in]
  )
  {
  DBOBJECT    ObjectStruct;      // For binding, retrieve
                  // an object pointer.
  DBBINDING   rgBinding[1];      // Bind a single column.

  IAccessor*  pIAccessor = NULL;   // Accessor creation
  HACCESSOR   hAccessor = NULL;
  ULONG     ulErrorBinding;

  void*     pData;         // Bound consumer buffer
  HROW    rghRows[1];
  HROW*     pRows = &rghRows[0];
  ULONG     cRows;

  char    szNotes[BLOCK_SIZE + 1];   // Text data from
                  // "Notes"
  ULONG     cbRead;         // Count of bytes read

  // Set up the object structure for accessor creation. Ask the
  // provider to return an ISequentialStream interface for reading.
  ObjectStruct.dwFlags = STGM_READ;
  ObjectStruct.iid = IID_ISequentialStream;

  // Set up the binding structure for the accessor.
  rgBinding[0].iOrdinal = 1;      // Only one column
  rgBinding[0].obValue  = 0;      // Offset to data
  rgBinding[0].obLength = 0;      // Ignore length
  rgBinding[0].obStatus = sizeof(IUnknown*);  // Offset to status
  rgBinding[0].pTypeInfo = NULL;      // Reserved
  rgBinding[0].pObject  = &ObjectStruct;  // Our interface
                // request
  rgBinding[0].pBindExt = NULL;     // Reserved
  rgBinding[0].dwPart = DBPART_VALUE |  // Get both VALUE...
            DBPART_STATUS;  // ...and STATUS
                   // parts.
  rgBinding[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
  rgBinding[0].eParamIO = DBPARAMIO_NOTPARAM;
  rgBinding[0].cbMaxLen = 0;      // Not applicable
  rgBinding[0].dwFlags  = 0;      // Reserved
  rgBinding[0].wType = DBTYPE_IUNKNOWN;   // Type
                // DBTYPE_IUNKNOWN
  rgBinding[0].bPrecision = 0;      // Not applicable
  rgBinding[0].bScale = 0;        // Not applicable

  // Get the accessor interface and create the accessor.
  pIRowset->QueryInterface(IID_IAccessor, (void**) &pIAccessor);

  if (FAILED(pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 1,
    rgBinding, sizeof(IUnknown*) + sizeof(ULONG), &hAccessor,
    &ulErrorBinding)))
    {
    DumpError("CreateAccessor failed.");
    return;
    }

  // Allocate memory for the returned pointer and the status
  // field. The first sizeof(IUnknown*) bytes are for the pointer
  // to the object; the next sizeof(ULONG) bytes are for the
  // status.
  pData = new BYTE[sizeof(IUnknown*) + sizeof(ULONG)];

   while (TRUE)
    {
    // Get the next row.
    if (FAILED(pIRowset->GetNextRows(NULL, 0, 1, &cRows,
      &pRows)))
    {
    DumpError("GetNextRows failed.\n");
    break;
    }

    if (cRows == 0)
    {
    break;
    }

    // Get the row data, the pointer to an ISequentialStream*.
    if (FAILED(pIRowset->GetData(*pRows, hAccessor, pData)))
    {
    DumpError("GetData failed.\n");
    break;
    }

    // Read and process BLOCK_SIZE bytes at a time.
    if ((ULONG)((BYTE*)pData)[rgBinding[0].obStatus] ==
      DBSTATUS_S_ISNULL)
    {
    // Process NULL data.
    printf("<null>");
    }
    else if ((ULONG)((BYTE*)pData)[rgBinding[0].obStatus] ==
      DBSTATUS_S_OK)
    {
    do
      {
      (*((ISequentialStream**) pData))->Read(szNotes,
      BLOCK_SIZE, &cbRead);
      if (cbRead > 0)
        {
        // Process data.
        szNotes[cbRead] = (char) NULL;
        printf(szNotes);
        }
      }
    while (cbRead >= BLOCK_SIZE);

    (*((ISequentialStream**) pData))->Release();

    printf("\n\n");
    }

    pIRowset->ReleaseRows(cRows, pRows, NULL, NULL, NULL);
    }

  // Clean up.
  pIAccessor->ReleaseAccessor(hAccessor, NULL);
  pIAccessor->Release();

  delete [] pData;
  }

To write data to a BLOB column using a storage object, the consumer first creates an accessor that includes a binding for the column and then:

  1. Calls IRowset::GetData with the accessor that binds the BLOB column. The provider creates a storage object over the BLOB’s data and returns a pointer to the requested storage interface (ISequentialStream) on this object.

  2. Calls a method on the storage interface to write data (ISequentialStream::Write).

    OLE’s structured storage model supports both transacted and direct modes. In transacted mode, all changes are buffered, and the buffered changes are persisted or discarded only when an explicit commit or abort request is done. In direct mode, every change is followed by an automatic commit. If the storage object is transacted (that is, the STGM_TRANSACTED flag is set in the dwFlags element of the DBOBJECT structure in the binding), the storage object does not publish the changes to the containing rowset until the consumer calls Commit on the storage interface. If the storage object is not transacted (that is, the STGM_DIRECT flag is set), the storage object publishes the changes to the containing rowset when the consumer calls a method on the storage interface to write the changes.

Alternately, the consumer calls IRowsetChange::SetData or IRowsetChange::InsertRow with the accessor that binds the BLOB column, passing a pointer to a storage interface on a separate storage object.

Data Manipulation

Data manipulation in OLE DB can be executed using SQL queries in commands or using the IRowsetChange interface.

IRowsetChange allows the consumer to:

  • Update columns of a row by calling IRowsetChange::SetData.

  • Delete a row or rows by calling IRowsetChange::DeleteRows.

  • Insert a new row by calling IRowsetChange::InsertRow.

Delayed Updates

OLE DB supports delayed updates. With delayed updates, changes made to the rowset are not transmitted to the data source until IRowsetUpdate::Update is called.

A rowset can be in delayed or immediate update mode, depending on whether it exposes the IRowsetUpdate interface. The consumer specifies whether IRowsetUpdate should be supported prior to opening the rowset.

If the IRowsetUpdate interface is not included on the rowset, then the rowset is said to be in immediate update mode, and the changes are immediately transmitted to the data source. If IRowsetUpdate is present, then the changes are not transmitted to the data source until IRowsetUpdate::Update is called.

For rowsets in delayed update mode, IRowsetUpdate allows the consumer to:

  • Retrieve the initial value read for the row by calling IRowsetUpdate::GetOriginalData.

  • Get a list of pending changes by calling IRowsetUpdate::GetPendingRows.

  • Get the pending status of a given set of rows by calling IRowsetUpdate::GetRowStatus.

  • Undo some, or all, of the pending changes by calling IRowsetUpdate::Undo.

  • Commit some, or all, of the changes to the data source by calling IRowsetUpdate::Update.

Prepared Commands

If a command is to be executed multiple times, it is often more efficient to prepare it. Command preparation tells the query processor to save the execution plan so that it doesn’t have to be rebuilt for each execution.

The ICommandPrepare interface supports methods for preparing a command. Calling ICommandPrepare::Prepare in OLE DB is equivalent to calling SQLPrepare in ODBC.

Commands with Parameters

Parameters are used to insert scalar values into a command at execute time. Parameters are generally used in conjunction with a prepared command so that the command can be executed multiple times, each time with a different value.

To specify parameters in ODBC, an application uses SQLBindParameter and SQLParamData in the following order:

  1. Calls SQLBindParameter for each parameter to specify the parameter type and bind buffers for the parameters’ values.

  2. Calls SQLSetStmtAttr if multiple values are to be specified for each set of parameters.

  3. Places the values in appropriate buffers.

  4. Calls SQLExecute or SQLExecDirect.

To specify parameters in OLE DB, an application uses the ICommandWithParameters interface in the following order:

  1. Creates an accessor describing the binding information for the set of parameters.

  2. Calls ICommandWithParameters::SetParameterInfo to specify the types of the parameters.

  3. Calls ICommand::Execute to execute the command, passing a structure containing the accessor, number of parameter sets, and a pointer to data for the parameters.

Binding Parameters

The consumer specifies parameter descriptions by setting information in the DBPARAMBINDINFO structure passed to ICommandWithParameters::SetParameterInfo. This is similar to the type, precision, and scale information specified in SQLBindParameter in ODBC.

The DBPARAMBINDINFO structure is:

typedef struct tagDBPARAMBINDINFO
  {
  LPOLESTR  pwszDataSourceType;   // Data type name (OLESTR)
  LPOLESTR  pwszName;      // Name of the parameter
  ULONG   ulParamSize;   // Maximum length of data
               // accepted
  DBPARAMFLAGS  dwFlags;   // Input/output/signed/nullable/object
  BYTE    bPrecision;   // Precision for numeric data
            // types
  BYTE    bScale;   // Scale for numeric data types
  } DBPARAMBINDINFO;

The ICommand::Execute method takes a pointer to the DBPARAMS structure as an argument. This structure includes a pointer to the data as well as an accessor that describes the layout of the data. Bindings described in the accessor are similar to the bindings specified in SQLBindParameter in ODBC. OLE DB allows the specification of multiple sets of parameters in a single call by specifying the number of sets of parameters in the cParamSets element of the DBPARAMS structure. This is similar to calling SQLSetStmtAttr in ODBC.

The DBPARAMS structure is:

typedef struct tagDBPARAMS
  {
  void __RPC_FAR*  pData;      // Data, array containing
               // parameter values
  ULONG    cParamSets;   // Count of sets of parameter
               // values in the data array
  HACCESSOR    hAccessor;   // Handle of parameter
               // describing accessor
  } DBPARAMS;

The following code example shows parameter passing in OLE DB. For the complete source code listing, see Appendix C. The general flow of control is:

  1. Create bindings describing the parameters.

  2. Obtain the IDBCreateSession interface.

  3. Call CreateSession to create a session object that scopes the transaction boundaries within the current connection.

  4. Call CreateCommand to create a command object within the transaction.

  5. Call SetCommandText to set the command text.

  6. Obtain the ICommandWithParameters interface on the command object.

  7. Call SetParameterInfo to specify the parameter information.

  8. Prepare the command.

  9. Create a parameter accessor.

  10. Build a structure containing the parameter information.

  11. Call Execute, providing the parameter accessor and parameter
    information, to execute the command.

  12. Release the command object.
/********************************************************************
*  Execute a prepared INSERT statement with parameters.
********************************************************************/
HRESULT myInsertWithParameters
  (
  IDBInitialize*  pIDBInitialize
  )
  {
  IDBCreateSession* pIDBCreateSession;
  IDBCreateCommand* pIDBCreateCommand;
  ICommandText*   pICommandText;
  ICommandPrepare*  pICommandPrepare;
  ICommandWithParameters* pICmdWithParams;
  IAccessor*    pIAccessor;
  WCHAR     wSQLString[] =
      OLESTR("insert into Customers (CustomerID,
        CompanyName, ContactName,")
      OLESTR(" ContactTitle, Address, City, Region,
        PostalCode, Country,")
      OLESTR(" Phone, Fax)")
      OLESTR(" values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
  DBPARAMS    Params;
  long      cRowsAffected;
  HACCESSOR     hParamAccessor;

  NEWCUSTOMERDATA   aNewCustomers[] =
    {
    "YOUME",
    "You and Me Grocers",
    "William Smith",
    "General Manager",
    "383 15th Ave. N.",
    "New York",
    "NY",
    "10018",
    "USA",
    "(212) 555-8847",
    "(212) 555-9988",

    "YORBL",
    "Yorbalinda's",
    "Mary Jones",
    "Owner",
    "22 Sunny Vale Rd.",
    "San Diego",
    "CA",
    "93122",
    "USA",
    "(605) 555-4322",
    "(605) 555-4323"
    };
  NEWCUSTOMER   NewCustomer;

  ULONG     nParams = 11;
  DBPARAMBINDINFO   rgParamBindInfo[] =
    {
    OLESTR("DBTYPE_CHAR"),  OLESTR("CustomerID"),  5,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("CompanyName"),  40,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("ContactName"),  30,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("ContactTitle"), 30,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("Address"),  60,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("City"),   15,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("Region"),   15,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("PostalCode"), 10,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("Country"),  15,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("Phone"),    24,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("FAX"),    24,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    };
  ULONG     rgParamOrdinals[] =
           {1,2,3,4,5,6,7,8,9,10,11};

  // Get the session.
  pIDBInitialize->QueryInterface(IID_IDBCreateSession,
    (void**)&pIDBCreateSession);
  pIDBCreateSession->CreateSession(NULL, IID_IDBCreateCommand,
    (IUnknown**) &pIDBCreateCommand);
  pIDBCreateSession->Release();

  // Create the command.
  pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText,
    (IUnknown**) &pICommandText);
  pIDBCreateCommand->Release();

  // The command requires the actual text as well as an indicator
  // of its language.
  pICommandText->SetCommandText(DBGUID_DBSQL, wSQLString);

  // Set parameter information.
  pICommandText->QueryInterface(IID_ICommandWithParameters,
    (void**)&pICmdWithParams);
  pICmdWithParams->SetParameterInfo(nParams, rgParamOrdinals,
    rgParamBindInfo);
  pICmdWithParams->Release();

  // Prepare the command.
  pICommandText->QueryInterface(IID_ICommandPrepare,
    (void**)&pICommandPrepare);
  if (FAILED(pICommandPrepare->Prepare(0)))
    {
    pICommandPrepare->Release();
    pICommandText->Release();
    return (E_FAIL);
    }
  pICommandPrepare->Release();

  // Create parameter accessors.
  if (FAILED(myCreateParamAccessor(pICommandText, &hParamAccessor,
    &pIAccessor)))
    {
    pICommandText->Release();
    return (E_FAIL);
    }

  Params.pData = &NewCustomer;  // pData is the buffer pointer
  Params.cParamSets = 1;    // Number of sets of parameters
  Params.hAccessor = hParamAccessor;// Accessor to the parameters

  // Specify the parameter information.
  for (UINT nCust = 0; nCust < 2; nCust++)
    {
    strcpy(NewCustomer.acCustomerID,
     aNewCustomers[nCust].szCustID);
    strcpy(NewCustomer.acCompanyName,
     aNewCustomers[nCust].szCompanyName);
    strcpy(NewCustomer.acContactName,
     aNewCustomers[nCust].szContactName);
    strcpy(NewCustomer.acContactTitle,
     aNewCustomers[nCust].szContactTitle);
    strcpy(NewCustomer.acAddress,
     aNewCustomers[nCust].szAddress);
    strcpy(NewCustomer.acCity, aNewCustomers[nCust].szCity);
    strcpy(NewCustomer.acRegion, aNewCustomers[nCust].szRegion);
    strcpy(NewCustomer.acPostalCode,
     aNewCustomers[nCust].szPostalCode);
    strcpy(NewCustomer.acCountry,
     aNewCustomers[nCust].szCountry);
    strcpy(NewCustomer.acPhone, aNewCustomers[nCust].szPhone);
    strcpy(NewCustomer.acFAX, aNewCustomers[nCust].szFAX);

    // Execute the command.
    pICommandText->Execute(NULL, IID_NULL, &Params,
     &cRowsAffected, NULL);

    printf("%ld rows inserted.\n", cRowsAffected);
    }

  pIAccessor->ReleaseAccessor(hParamAccessor, NULL);
  pIAccessor->Release();
  pICommandText->Release();

  return (NOERROR);
  }

Creating Parameter Accessors

Input parameter data is read from, and output parameter data is written to, the specified locations within Params.pData according to the bindings specified by the accessor. An array of parameter sets can be passed in pParamData. cParamSets indicates the number of elements of the array.

The following code example shows parameter binding for the NEWCUSTOMER structure used in the previous example. The general flow of control is:

  1. Specify the common binding information for all the parameters.

  2. Specify the specific binding information for each parameter.

  3. Call CreateAccessor to create the parameter accessor.

  4. Return the accessor handle along with the interface used to create it (so that it can be freed later).
/********************************************************************
*  Create parameter accessor.
********************************************************************/
HRESULT myCreateParamAccessor
  (
  ICommand* pICmd,  // [in]
  HACCESSOR*  phAccessor, // [out]
  IAccessor** ppIAccessor // [out]
  )
  {
  IAccessor*  pIAccessor;
  HACCESSOR   hAccessor;
  const ULONG   nParams = 11;
  DBBINDING   Bindings[nParams];
  DBBINDSTATUS  rgStatus[nParams]; // Returns information for
             // individual binding
             // validity.
  HRESULT   hr;

  ULONG     acbLengths[] = {5, 40, 30, 30, 60, 15, 15, 10,
            15, 24, 24};

  for (ULONG i = 0; i < nParams; i++)
    {
    Bindings[i].iOrdinal = i + 1;
    Bindings[i].obLength = 0;
    Bindings[i].obStatus = 0;
    Bindings[i].pTypeInfo = NULL;
    Bindings[i].pObject = NULL;
    Bindings[i].pBindExt = NULL;
    Bindings[i].dwPart = DBPART_VALUE;
    Bindings[i].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
    Bindings[i].eParamIO = DBPARAMIO_INPUT;
    Bindings[i].cbMaxLen = acbLengths[i];
    Bindings[i].dwFlags = 0;
    Bindings[i].wType = DBTYPE_STR;
    Bindings[i].bPrecision = 0;
    Bindings[i].bScale = 0;
    }

  Bindings[0].obValue = offsetof(NEWCUSTOMER, acCustomerID);
  Bindings[1].obValue = offsetof(NEWCUSTOMER, acCompanyName);
  Bindings[2].obValue = offsetof(NEWCUSTOMER, acContactName);
  Bindings[3].obValue = offsetof(NEWCUSTOMER, acContactTitle);
  Bindings[4].obValue = offsetof(NEWCUSTOMER, acAddress);
  Bindings[5].obValue = offsetof(NEWCUSTOMER, acCity);
  Bindings[6].obValue = offsetof(NEWCUSTOMER, acRegion);
  Bindings[7].obValue = offsetof(NEWCUSTOMER, acPostalCode);
  Bindings[8].obValue = offsetof(NEWCUSTOMER, acCountry);
  Bindings[9].obValue = offsetof(NEWCUSTOMER, acPhone);
  Bindings[10].obValue = offsetof(NEWCUSTOMER, acFAX);

  pICmd->QueryInterface(IID_IAccessor, (void**)&pIAccessor);

  hr = pIAccessor->CreateAccessor(
    DBACCESSOR_PARAMETERDATA,   // Accessor that will be used
               // to specify parameter data
    nParams,         // Number of parameters being
               // bound
    Bindings,         // Structure containing bind
               // information
    sizeof(NEWCUSTOMER),      // Size of parameter structure
    &hAccessor,         // Returned accessor handle
    rgStatus         // Information about binding
               // validity
    );

  if (FAILED(hr))
    {
    DumpError("Parameter accessor creation failed.");
    }
  else
    {
    *ppIAccessor = pIAccessor;
    *phAccessor = hAccessor;
    }

  return (hr);
  }

Advanced OLE DB Topics

This section describes advanced OLE DB topics, such as data source location, catalog and schema functions, transactions, error handling, and data types.

Locating a Data Source

In ODBC, the application generally calls SQLDataSources to enumerate the different ODBC data sources installed on the computer. The application can also call SQLDrivers to enumerate the specific drivers and connect to them directly, without a data source. ODBC version 3.0 defines the concept of a file DSN, which is a data source definition that lives as a file in the file system.

In OLE DB, consumers can browse for data sources using an enumerator. Enumerators provide a recursive, hierarchical model for browsing data sources. This model is similar to browsing for files in the file system, where a folder is represented by an enumerator and a file is represented by a data source. Just as folders can enumerate other folders as well as files, an enumerator can enumerate other enumerators as well as data sources.

A root enumerator is provided as part of the OLE DB SDK that generates a rowset of available data providers and enumerators. Binding to a data source returns an uninitialized data source object for that data source.

Once created, a data source object can be persisted as a file in the file system. Binding a file moniker to one of these persisted data sources generates an uninitialized data source object with a particular state, such as a data source name or location. Persisted data sources are equivalent to file DSNs in ODBC version 3.0. The consumer must still initialize the data source object after it is loaded.

Catalog and Schema Functions

Schema information in OLE DB is retrieved using rowsets in the same manner as other types of data. The schema information specified in OLE DB is based on the ANSI SQL concepts of catalog and schema. A schema is a collection of database objects that are owned or have been created by a particular user, and a catalog contains one or more schemas.

The IDBSchemaRowset interface provides a mechanism to the consumer for retrieving information about the database without knowing its structure. Schema rowsets are identified by GUIDs. Each schema GUID has a set of restrictions that can be specified to reduce the result set to the desired information. For example, to get a list of columns in a particular table, the schema name, database name, and table name can be specified as restrictions. The order and data type of these restrictions are fixed and are listed in the Microsoft OLE DB Programmer’s Reference. It is not necessary to specify all the restrictions; however, the unspecified restrictions must be set to type DBTYPE_EMPTY or VT_EMPTY.

The IDBSchemaRowset interface has two methods:

  • IDBSchemaRowset::GetRowset. Returns one of the standard schema information rowsets.

  • IDBSchemaRowset::GetSchema. Returns a list of schema information views accessible through the GetRowset method.

The IDBSchemaRowset interface encapsulates the functionality of the catalog functions in ODBC, such as SQLTables, SQLColumns, SQLProcedures, SQLProcedureColumns, SQLStatistics, SQLPrimaryKeys, SQLForeignKeys, and SQLGetTypeInfo.

Transactions

A session can be inside or outside a transaction at any point in time. When a session is created, it is outside a transaction, and all work done under the scope of that session is immediately committed on each method call. This is referred to as the autocommit or implicit commit mode.

If the provider supports transactions, the session supports the ITransactionLocal interface. Calling ITransactionLocal::StartTransaction begins a transaction on the session. ITransactionLocal inherits from the ITransaction interface, which supports the Commit and Abort methods. When a session enters a transaction, all work done by the session, its command and rowsets, is part of that transaction.

The following code example shows calling commit or abort on a session:

ITransactionLocal*  pITransactionlocal;

pIDBCreateCommand->QueryInterface(IID_ITransactionLocal,
  (void**) &pITransactionlocal);
pITransactionlocal->StartTransaction(
  ISOLATIONLEVEL_READCOMMITTED, 0, NULL, NULL);

...    // Do some work...setting bSave for the following.

if (bSave) // For commit
  pITransactionlocal->Commit(FALSE, XACTTC_SYNC_PHASEONE, 0);
else   // For abort
  pITransactionlocal->Abort(NULL, FALSE, FALSE); // For abort

pITransactionlocal->Release();

Transactions have two important properties: retention and preservation. Retention indicates whether another transaction is implicitly started on committing or aborting the transaction. Preservation indicates whether the rowset capabilities are preserved on committing or aborting the transaction. Committing or aborting a transaction with the fRetention flag set to TRUE will implicitly start another transaction. After a commit or abort, the full functionality of the rowset is preserved or is zombied, depending on whether the rowset properties DBPROP_COMMITPRESERVE and DBPROP_ABORTPRESERVE are set to TRUE or FALSE. A zombied rowset is an object whose functionality is virtually lost. It supports only IUnknown operations and releasing row handles. A preserved rowset has all its capabilities intact.

Isolation Levels

OLE DB defines the following isolation levels. These are similar to the isolation levels in ODBC.

  • Read Uncommitted. The values in a record being accessed can change at any time.

  • Read Committed. The values in a record do not change from the time the record is read until it is rewritten or passed over.

  • Repeatable Read. The relationship between values in different records doesn’t change while the transaction is executing.

  • Serializable. The existence of reference records does not change while the transaction is executing.

Error Handling

Methods return error information in two ways. The code returned by a method indicates the overall success or failure of a method while error objects provide detailed information. Success and warning return codes begin with S_ or DB_S_ and indicate that the method completed successfully. If a single warning condition occurs, the method returns the return code for that condition. If multiple warning conditions occur, a hierarchy is defined to determine which code is returned. Error return codes begin with E_ or DB_E_ and indicate that the method failed completely. Error objects in OLE DB are an extension of the error objects in Automation.

  • The ISupportErrorInfo interface, defined by Automation, determines whether an object can return OLE DB error objects and which interfaces on that object can return OLE DB error objects.

  • The IErrorInfo interface, defined by Automation, returns the error message, the name of the component and the GUID of the interface in which the error occurred, and the name and topic of the help file that applies to the error.

  • The IErrorLookup interface is exposed by a provider supporting OLE DB error objects. The provider creates an error lookup service and the lookup service exposes the IErrorLookup interface. This service is used by IErrorRecords and IErrorInfo interfaces.

  • The IErrorRecords interface is the main interface through which OLE DB error objects are accessed.

  • The ISQLErrorInfo interface is used to return the SQLSTATE and native error. All ODBC-related providers can expose this interface.

Data Types

OLE DB uses standard OLE- and Windows-based data types, and uses DBTYPE as a type indicator. A type indicator denotes the data type of a piece of memory. A type indicator is used to describe data types in metadata, method calls, and data and parameter binding.

Type indicators | Physical type
DBTYPE_EMPTY |
DBTYPE_NULL |
DBTYPE_RESERVED |
DBTYPE_I1 | signed char
DBTYPE_I2 | SHORT
DBTYPE_I4 | LONG
DBTYPE_I8 | LARGE_INTEGER
DBTYPE_UI1 | BYTE
DBTYPE_UI2 | unsigned short
DBTYPE_UI4 | unsigned int
DBTYPE_UI8 | ULARGE_INTEGER
DBTYPE_R4 | float
DBTYPE_R8 | double
DBTYPE_CY | LARGE_INTEGER
DBTYPE_DECIMAL | DBDECIMAL
DBTYPE_NUMERIC | DBNUMERIC
DBTYPE_DATE | DATE
DBTYPE_BOOL | VARIANT_BOOL
DBTYPE_BYTES | BYTE[cbMaxlen]
DBTYPE_BSTR | BSTR
DBTYPE_STR | char[cbMaxLen]
DBTYPE_WSTR | wchar_t[cbMaxLen]
DBTYPE_VARIANT | VARIANT
DBTYPE_IDISPATCH | IDispatch *
DBTYPE_IUNKOWN | IUnknown *
DBTYPE_GUID | GUID
DBTYPE_ERROR | SCODE
DBTYPE_BYREF | void *
DBTYPE_ARRAY | SAFEARRAY *
DBTYPE_VECTOR | DBVECTOR
DBTYPE_UDT | undefined
DBTYPE_DBDATE | DBDATE
DBTYPE_DBTIME | DBTIME
DBTYPE_TIMESTAMP | DBTIMESTAMP

Each OLE DB provider chooses a default type indicator for each of its native types and documents them in the PROVIDER_TYPES schema rowset. Providers are required to transfer data in the C type that corresponds to that type indicator and to convert data to and from WCHAR, if such a conversion is defined in the conversion tables. For the conversion table and guidelines, see Microsoft OLE DB Programmer’s Reference, Appendix A.

ODBC Attributes and Corresponding OLE DB Properties

OLE DB and ODBC are different interfaces. A one-to-one correspondence between the properties of one interface and the attributes of the other does not exist. OLE DB contains a rich definition of property sets and interface method parameters that provide services significantly different from those provided by ODBC. For more information, see the OLE DB documentation of schema rowsets, property sets, and methods.

The following tables show ODBC attributes and their OLE DB property equivalents. Where a one-to-one mapping cannot be made easily, the OLE DB property set and property columns are left blank.

Column Attributes

ODBC developers use SQLDescribeCol, SQLColAttribute, or SQLGetDescField to retrieve a given attribute. The interfaces and retrieval methods in OLE DB are also shown.

ODBC attribute 		OLE DB property set, schema rowset,
or method
OLE DB property
SQL_DESC_AUTO_
UNIQUE_VALUE 		IColumnsRowset::GetColumnsRowset DBCOLUMN_
ISAUTOINCREMENT
SQL_DESC_BASE_
COLUMN_NAME 		IColumnsRowset::GetColumnsRowset DBCOLUMN_
BASECOLUMNNAME
SQL_DESC_BASE_
TABLE_NAME 		IColumnsRowset::GetColumnsRowset DBCOLUMN_
BASETABLENAME
SQL_DESC_CASE_
SENSITIVE 		IColumnsRowset::GetColumnsRowset DBCOLUMN_
ISCASESENSITIVE
SQL_DESC_CATALOG_
NAME 		IColumnsRowset::GetColumnsRowset DBCOLUMN_
BASECATALOGNAME
SQL_DESC_CONCISE_
TYPE
SQL_DESC_COUNT | IColumnsInfo::GetColumnInfo | The value returned in the pcColumns argument
SQL_DESC_DISPLAY_SIZE
SQL_DESC_FIXED_
PREC_SCALE 		IDBSchemaRowset::GetRowset DBSCHEMA_PROVIDER_TYPES rowset restricted by DBCOLUMN_TYPE property from IColumnsRowset::GetColumnsRowset FIXED_PREC_SCALE column
SQL_DESC_LABEL | IColumnsRowset::GetColumnsRowset | DBCOLUMN_NAME
SQL_DESC_LENGTH | IColumnsInfo::GetColumnInfo | Derived from ulColumnSize
SQL_DESC_LITERAL_
PREFIX 		IDBSchemaRowset::GetRowset DBSCHEMA_PROVIDER_TYPES rowset restricted by DBCOLUMN_TYPE property from IColumnsRowset::GetColumnsRowset LITERAL_PREFIX column
SQL_DESC_LITERAL_
SUFFIX 		IDBSchemaRowset::GetRowset DBSCHEMA_PROVIDER_TYPES rowset restricted by DBCOLUMN_TYPE property from IColumnsRowset::GetColumnsRowset LITERAL_SUFFIX column
SQL_DESC_LOCAL_
TYPE_NAME 		IDBSchemaRowset::GetRowset DBSCHEMA_PROVIDER_TYPES rowset restricted by DBCOLUMN_TYPE property from IColumnsRowset::GetColumnsRowset LOCAL_TYPE_NAME column
SQL_DESC_NAME | IColumnsRowset::GetColumnsRowset | DBCOLUMN_NAME
SQL_DESC_NULLABLE IColumnsRowset::GetColumnsRowset
IColumnsInfo::GetColumnInfo 		DBCOLUMN_FLAGS or dwFlags flag DBCOLUMNFLAGS_
ISNULLABLE
SQL_DESC_NUM_PREX_
RADIX
SQL_DESC_OCTET_
LENGTH 		IColumnsRowset::GetColumnsRowset DBCOLUMN_
OCTETLENGTH
SQL_DESC_PRECISION IColumnsRowset::GetColumnsRowset
IColumnsInfo::GetColumnInfo 		DBCOLUMN_
PRECISION or bPrecision
SQL_DESC_SCALE IColumnsRowset::GetColumnsRowset
IColumnsInfo::GetColumnInfo 		DBCOLUMN_SCALE or bScale
SQL_DESC_SCHEMA_
NAME 		IColumnsRowset::GetColumnsRowset DBCOLUMN_
BASESCHEMANAME
SQL_DESC_SEARCHABLE IColumnsRowset::GetColumnsRowset DBCOLUMN_
ISSEARCHABLE
SQL_DESC_TABLE_NAME IColumnsRowset::GetColumnsRowset DBCOLUMN_
BASETABLENAME
SQL_DESC_TYPE IColumnsRowset::GetColumnsRowset
IColumnsInfo::GetColumnInfo 		DBCOLUMN_TYPE or wType
SQL_DESC_TYPE_NAME | IDBSchemaRowset::GetRowset DBSCHEMA_PROVIDER_TYPES rowset restricted by DBCOLUMN_TYPE property from IColumnsRowset::GetColumnsRowset | TYPE_NAME column
SQL_DESC_UNNAMED
SQL_DESC_UNSIGNED IDBSchemaRowset::GetRowset DBSCHEMA_PROVIDER_TYPES rowset restricted by DBCOLUMN_TYPE property from IColumnsRowset::GetColumnsRowset UNSIGNED_
ATTRIBUTE column
SQL_DESC_UPDATABLE IColumnsRowset::GetColumnsRowset
IColumnsInfo::GetColumnInfo 		DBCOLUMN_FLAGS or dwFlags flag DBCOLUMNFLAGS_
WRITE

Connection Attributes

ODBC connection attributes that control the behavior of the ODBC drivers, such as SQL_ATTR_ASYNC_ENABLE, are not included. OLE DB database and schema properties are indicated.

ODBC attribute | OLE DB property set, schema rowset, or method | OLE DB property or flag
SQL_ATTR_ACCESS_MODE | IDBProperties::SetProperties | DBPROP_ DATASOURCEREADONLY
SQL_ATTR_AUTOCOMMIT |  | Autocommit mode is on if the session is outside of a transaction
SQL_ATTR_CONNECTION_ TIMEOUT |  |
SQL_ATTR_CURRENT_ CATALOG | IDBProperties::SetProperties | DBPROP_ CURRENTCATALOG
SQL_ATTR_LOGIN_ TIMEOUT | IDBProperties::SetProperties | DBPROP_INIT_TIMEOUT
SQL_ATTR_QUIET_MODE | IDBProperties::SetProperties | DBPROP_INIT_HWND
SQL_ATTR_TXN_ ISOLATION | ITransactionLocal::StartTransaction | isoLevel

Statement Attributes

OLE DB command properties and rowset methods are indicated.

ODBC statement attributes that control driver behaviors, such as SQL_ATTR_NOSCAN, are not included in the table. Statement attributes that are used to set bound parameter and column behaviors are also not included. OLE DB uses the DBBINDING structure together with data accessors to control arrays of parameters and bound columns. For more information, see “Getting and Setting Data” in the OLE DB documentation.

ODBC attribute 		OLE DB property set, schema rowset,
or method
OLE DB property or flag
SQL_ATTR_ASYNC_
ENABLE 		Multiple concurrent operations are performed by different threads
SQL_ATTR_
CONCURRENCY 		ITransactionLocal::StartTransaction
ISessionProperties::SetProperties
DBPROPSET_SESSION 		isoLevel value for the StartTransaction interface, DBPROP_SESS_
AUTOCOMMIT_ISOLEVELS when set for auto commit mode on the session
SQL_ATTR_CURSOR_
SCROLLABLE 		ICommandProperties::SetProperties
DBPROPSET_ROWSET 		DBPROP_
CANSCROLLBACKWARDS
SQL_ATTR_CURSOR_
SENSITIVITY 		ICommandProperties::SetProperties
DBPROPSET_ROWSET 		DBPROP_OTHERINSERT and DBPROP_
OTHERUPDATEDELETE
SQL_ATTR_CURSOR_
TYPE 		ICommandProperties::SetProperties
DBPROPSET_ROWSET 		DBPROP_CANSCROLL-BACKWARDS, DBPROP_OTHER-INSERT, and DBPROP_
OTHERUPDATEDELETE
SQL_ATTR_KEYSET_
SIZE
SQL_ATTR_MAX_
LENGTH
SQL_ATTR_MAX_ROWS ICommandProperties::SetProperties
DBPROPSET_ROWSET 		DBPROP_MAXROWS
SQL_ATTR_QUERY_
TIMEOUT 		ICommandProperties::SetProperties
DBPROPSET_ROWSET 		DBPROP_
COMMANDTIMEOUT
SQL_ATTR_RETRIEVE_
DATA 		IRowset::GetData Data is not retrieved until GetData method is called on the rowset
SQL_ATTR_ROW_
ARRAY_SIZE 		IRowset::GetNextRows cRows value
SQL_ATTR_ROW_
NUMBER

SQLGetInfo

SQLGetInfo returns information about a specific ODBC data source. OLE DB offers provider property sets and other entry points to expose provider-specific behaviors.

OLE DB allows providers to define additional property sets. For example, the ODBC provider shipped with the OLE DB SDK defines the DBPROPSET_PROVIDERROWSET and DBPROPSET_PROVIDERDATASOURCEINFO property sets, and some SQLGetInfo InfoType parameters are supported through these provider-specific property sets.

In the following table, SQLGetInfo InfoType parameters that have no meaning in OLE DB have been eliminated (for example, SQL_DM_VER, which returns the ODBC Driver Manager version number).

Where a one-to-one match does not exist, a tip is provided to help you discover the information. For example, SQLGetInfo InfoType SQL_ALTER_DOMAIN has no direct support in OLE DB. However, you can use the provider’s IDBInfo::GetKeywords method to determine whether or not the provider recognizes the SQL ALTER statement.

SQLGetInfo InfoType value | OLE DB property set, schema rowset, or method | OLE DB property or flag
SQL_ACCESSIBLE_ PROCEDURES |  |
SQL_ACCESSIBLE_TABLES |  |
SQL_ACTIVE_ ENVIRONMENTS |  |
SQL_AGGREGATE_ FUNCTIONS | IDBInfo::GetKeywords |
SQL_ALTER_DOMAIN | IDBInfo::GetKeywords |
SQL_ALTER_TABLE | IDBInfo::GetKeywords |
SQL_ASYNC_MODE |  |
SQL_BATCH_ROW_ COUNT | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ MULTIPLEPARAMSETS
SQL_BATCH_SUPPORT |  |
SQL_BOOKMARK_ PERSISTENCE |  | Bookmarks are valid for the lifetime of the rowset to which they apply
SQL_CATALOG_LOCATION | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ CATALOGLOCATION
SQL_CATALOG_NAME | IDBInfo::GetLiteralInfo | DBLITERAL_CATALOG_ NAME
SQL_CATALOG_NAME_ SEPARATOR | IDBInfo::GetLiteralInfo | DBLITERAL_CATALOG_ SEPARATOR
SQL_CATALOG_TERM | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_CATALOGTERM
SQL_CATALOG_USAGE | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_CATALOGUSAGE
SQL_COLLATION_SEQ | IDBSchemaRowset::GetRowset COLLATIONS | COLLATION_NAME column
SQL_COLUMN_ALIAS |  |
SQL_CONCAT_NULL_ BEHAVIOR | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ CONCATNULLBEHAVIOR
SQL_CONVERT (all types) | IConvertType::CanConvertType |
SQL_CONVERT_ FUNCTIONS | IDBInfo::GetKeywords |
SQL_CORRELATION_NAME |  |
SQL_CREATE_ASSERTION | IDBInfo::GetKeywords |
SQL_CREATE_ CHARACTER_SET | IDBInfo::GetKeywords |
SQL_CREATE_COLLATION | IDBInfo::GetKeywords |
SQL_CREATE_DOMAIN | IDBInfo::GetKeywords |
SQL_CREATE_SCHEMA | IDBInfo::GetKeywords |
SQL_CREATE_TABLE | IDBInfo::GetKeywords |
SQL_CREATE_ TRANSLATION | IDBInfo::GetKeywords |
SQL_CREATE_VIEW | IDBInfo::GetKeywords |
SQL_CURSOR_COMMIT_ BEHAVIOR | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_PREPARE-COMMITBEHAVIOR DBPROP_ COMMITPRESERVE
SQL_CURSOR_ROLLBACK_ BEHAVIOR | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_PREPARE-ABORTTBEHAVIOR DBPROP_ ABORTPRESERVE
SQL_CURSOR_SENSITIVITY |  |
SQL_DATA_SOURCE_NAME | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ DATASOURCENAME
SQL_DATA_SOURCE_ READ_ONLY | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ DATASOURCEREADONLY
SQL_DATABASE_NAME | IDBProperties::GetProperties DBPROPSET_DATASOURCE | DBPROP_ CURRENTCATALOG
SQL_DATETIME_LITERALS |  |
SQL_DBMS_NAME | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_DBMSNAME
SQL_DBMS_VER | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_DBMSVER
SQL_DDL_INDEX | IDBInfo::GetKeywords |
SQL_DEFAULT_TXN_ ISOLATION |  | Transaction isolation is explicitly specified
SQL_DESCRIBE_ PARAMETER |  |
SQL_DRIVER_NAME | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_PROVIDERNAME
SQL_DRIVER_ODBC_VER | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ PROVIDEROLEDBVER
SQL_DRIVER_VER | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_PROVIDERVER
SQL_DROP_ASSERTION | IDBInfo::GetKeywords |
SQL_DROP_CHARACTER_ SET | IDBInfo::GetKeywords |
SQL_DROP_COLLATION | IDBInfo::GetKeywords |
SQL_DROP_DOMAIN | IDBInfo::GetKeywords |
SQL_DROP_SCHEMA | IDBInfo::GetKeywords |
SQL_DROP_TABLE | IDBInfo::GetKeywords |
SQL_DROP_TRANSLATION | IDBInfo::GetKeywords |
SQL_DROP_VIEW | IDBInfo::GetKeywords |
SQL_DYNAMIC_CURSOR_ ATTRIBUTES1 | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_ CANSCROLLBACKWARDS, DBPROP_OTHERINSERT, DBPROP_ OTHERUPDATEDELETE, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_DYNAMIC_CURSOR_ ATTRIBUTES2 | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_ CANSCROLLBACKWARDS, DBPROP_OTHERINSERT, DBPROP_ OTHERUPDATEDELETE, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_EXPRESSIONS_IN_ ORDERBY |  |
SQL_FETCH_DIRECTION | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_ CANSCROLLBACKWARDS
SQL_FILE_USAGE |  |
SQL_FORWARD_ONLY_ CURSOR_ATTRIBUTES1 | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_OTHERINSERT, DBPROP_ OTHERUPDATEDELETE, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_FORWARD_ONLY_ CURSOR_ATTRIBUTES2 | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_OTHERINSERT, DBPROP_ OTHERUPDATEDELETE, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_GETDATA_ EXTENSIONS |  |
SQL_GROUP_BY | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_GROUPBY
SQL_IDENTIFIER_CASE | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_IDENTIFIERCASE
SQL_IDENTIFIER_QUOTE_ CHAR | IDBInfo::GetLiteralInfo | DBLITERAL_QUOTE
SQL_INDEX_KEYWORDS | IDBInfo::GetKeywords |
SQL_INFO_SCHEMA_VIEWS |  |
SQL_INSERT_STATEMENT |  |
SQL_INTEGRITY |  |
SQL_KEYSET_CURSOR_ ATTRIBUTES1 | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_ CANSCROLLBACKWARDS, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_KEYSET_CURSOR_ ATTRIBUTES2 | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_ CANSCROLLBACKWARDS, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_KEYWORDS | IDBInfo::GetKeywords |
SQL_LIKE_ESCAPE_ CLAUSE | IDBInfo::GetLiteralInfo | DBLITERAL_ESCAPE_ PERCENT, DBLITERAL_ ESCAPE_UNDERSCORE
SQL_MAX_ASYNC_ CONCURRENT_ STATEMENTS |  |
SQL_MAX_BINARY_ LITERAL_LEN | IDBInfo::GetLiteralInfo | DBLITERAL_BINARY_ LITERAL
SQL_MAX_CATALOG_ NAME_LEN | IDBInfo::GetLiteralInfo | DBLITERAL_CATALOG_ NAME
SQL_MAX_CHAR_ LITERAL_LEN | IDBInfo::GetLiteralInfo | DBLITERAL_CHAR_LITERAL
SQL_MAX_COLUMN_ NAME_LEN | IDBInfo::GetLiteralInfo | DBLITERAL_COLUMN_ NAME
SQL_MAX_COLUMNS_ IN_GROUP_BY |  |
SQL_MAX_COLUMNS_ IN_INDEX |  |
SQL_MAX_COLUMNS_ IN_ORDER_BY |  |
SQL_MAX_COLUMNS_ IN_SELECT |  |
SQL_MAX_COLUMNS_ IN_TABLE |  |
SQL_MAX_CONCURRENT_ ACTIVITIES |  |
SQL_MAX_CURSOR_NAME_ LEN |  |
SQL_MAX_DRIVER_ CONNECTIONS | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ACTIVESESSIONS
SQL_MAX_IDENTIFIER_ LEN |  |
SQL_MAX_INDEX_SIZE | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_MAXINDEXSIZE
SQL_MAX_PROCEDURE_ NAME_LEN | IDBInfo::GetLiteralInfo | DBLITERAL_PROCEDURE_ NAME
SQL_MAX_ROW_SIZE | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_MAXROWSIZE
SQL_MAX_ROW_SIZE_ INCLUDES_LONG | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ MAXROWSIZEINCLUDES-BLOB
SQL_MAX_STATEMENT_ LEN | IDBInfo::GetLiteralInfo | DBLITERAL_TEXT_ COMMAND
SQL_MAX_TABLE_NAME_ LEN | IDBInfo::GetLiteralInfo | DBLITERAL_TABLE_NAME
SQL_MAX_TABLES_IN_ SELECT | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ MAXTABLESINSELECT
SQL_MAX_USER_NAME_ LEN | IDBInfo::GetLiteralInfo | DBLITERAL_USER_NAME
SQL_MULT_RESULT_SETS | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ MULTIPLERESULTS
SQL_MULTIPLE_ACTIVE_ TXN |  | Provider is responsible for spawning connections, if necessary, to provide multiple transactions from one DSO
SQL_NEED_LONG_DATA_ LEN |  |
SQL_NON_NULLABLE_ COLUMNS | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ COLUMNDEFINITION
SQL_NULL_COLLATION | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_NULLCOLLATION
SQL_NUMERIC_FUNCTIONS |  |
SQL_OJ_CAPABILITIES |  |
SQL_ORDER_BY_ COLUMNS_IN_SELECT | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ ORDERBYCOLUMNSIN-SELECT
SQL_OUTER_JOINS |  |
SQL_OWNER_TERM | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_SCHEMATERM
SQL_OWNER_USAGE | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_SCHEMAUSAGE
SQL_PARAM_ARRAY_ ROW_COUNTS |  |
SQL_PARAM_ARRAY_ SELECTS |  |
SQL_PROCEDURE_TERM | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ PROCEDURETERM
SQL_PROCEDURES | IDBSchemaRowset::GetSchemas returns GUID for PROCEDURES schema rowset |
SQL_QUOTED_IDENTIFIER_ CASE | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ QUOTEDIDENTIFIERCASE
SQL_ROW_UPDATES | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_IRowsetResynch
SQL_SCHEMA_TERM | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_SCHEMATERM
SQL_SCHEMA_USAGE | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_SCHEMAUSAGE
SQL_SCROLL_OPTIONS | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_ CANSCROLLBACKWARDS, DBPROP_OTHERINSERT, DBPROP_ OTHERUPDATEDELETE, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_SEARCH_PATTERN_ ESCAPE |  |
SQL_SERVER_NAME | IDBProperties::GetProperties DBPROPSET_INIT | DBPROP_INIT_LOCATION
SQL_SPECIAL_ CHARACTERS | IDBInfo::GetLiteralInfo | Value of pwszInvalidChars when inquiring about identifiers, and so on
SQL_STATIC_CURSOR_ ATTRIBUTES1 | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_ CANSCROLLBACKWARDS, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_STATIC_CURSOR_ ATTRIBUTES2 | ICommandProperties:: GetProperties DBPROPSET_ROWSET | DBPROP_ CANSCROLLBACKWARDS, DBPROP_OWNINSERT, DBPROP_ OWNUPDATEDELETE
SQL_STRING_FUNCTIONS |  |
SQL_SUBQUERIES | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_SUBQUERIES
SQL_SYSTEM_FUNCTIONS |  |
SQL_TABLE_TERM | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_TABLETERM
SQL_TIMEDATE_ADD_ INTERVALS |  |
SQL_TIMEDATE_DIFF_ INTERVALS |  |
SQL_TIMEDATE_ FUNCTIONS |  |
SQL_TXN_CAPABLE | IUnknown::QueryInterface on session for IID_ITransactionLocal |
SQL_TXN_ISOLATION_ OPTION | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_ SUPPORTED-TXNISORETAIN
SQL_UNION |  |
SQL_USER_NAME | IDBProperties::GetProperties DBPROPSET_DATASOURCEINFO | DBPROP_USERNAME

The following interfaces also support setting and getting of property information:

  • ICommandProperties. Registers with the command the properties from the Rowset Property Group. The consumer can specify whether these properties specify optional or required support on the returned rowset.

  • IRowsetInfo::GetProperties. Returns the current settings of all properties supported by the rowset. The value returned by this method can be different from the value returned by ICommandProperties::GetProperties for a property. This is because the provider’s ability to support the property might be affected by the current transaction or current query.

Mapping ODBC Functions to OLE DB Methods

Because a direct mapping between ODBC functions and OLE DB methods does not exist in all cases, only similar functionality is provided. When more than one method can be used to achieve the same thing, the methods are separated with the word “or”; for example, MethodA or MethodB. When more than one method must be called in sequence, the methods are numbered; for example, 1. MethodA, 2. MethodB, and so on.

ODBC function | OLE DB method | Remarks/property
SQLAllocHandle Environment Connection Descriptor Statement | OleInitialize CoCreateInstance IAccessor::CreateAccessor IDBCreateCommand::CreateCommand |
SQLBindCol | IAccessor::CreateAccessor |
SQLBindParameter | IAccessor::CreateAccessor |
SQLBrowseConnect | ISourcesRowset::GetSourcesRowset |
SQLBulkOperations Insertions Deletions Updates Retrieve by bookmark | IRowsetChange::InsertRows IRowsetChange::DeleteRows IRowsetChange::SetData IRowsetLocate::GetRowsByBookmark | If IRowsetUpdate is requested, all operations are batched until IRowsetUpdate::Update is called
SQLCancel | ICommand::Cancel |
SQLCloseCursor | IRowset::Release |
SQLColAttribute | IColumnsInfo::GetColumnInfo or IColumnsRowset::GetColumnsRowset |
SQLColumnPrivileges | IDBSchemaRowset::GetRowset | DBSCHEMA_ COLUMN_PRIVILEGES
SQLColumns | IDBSchemaRowset::GetRowset | DBSCHEMA_COLUMNS
SQLConnect | 1. IDBProperties::SetProperties 2. IDBInitialize::Initialize |
SQLDataSources | ISourcesRowset::GetSourcesRowset |
SQLDescribeCol | IColumnsInfo::GetColumnInfo |
SQLDescribeParam | ICommandWithParameters:: GetParameterInfo |
SQLDisconnect | IDBInitialize::Uninitialize |
SQLDriverConnect | 1. IDBProperties::SetProperties 2. IDBInitialize::Initialize |
SQLDrivers | ISourcesRowset::GetSourcesRowset |
SQLEndTran | ITransaction::Commit or ITransaction::Abort |
SQLError | 1. GetErrorInfo 2. IErrorRecords 3. IErrorInfo |
SQLExecDirect | ICommand::Execute |
SQLExecute | ICommand::Execute |
SQLFetch | 1. IRowset::GetNextRows, IRowsetLocate::GetRowsAt, or IRowsetLocate::GetRowsByBookmark 2. IRowset::GetData |
SQLFetchScroll | 1. IRowset::GetNextRows, IRowsetLocate::GetRowsAt, or IRowsetLocate::GetRowsByBookmark 2. IRowset::GetData |
SQLForeignKeys | IDBSchemaRowset::GetRowset | DBSCHEMA_ FOREIGN_KEYS
SQLFreeHandle Environment Connection Descriptor Statement | OleUninitialize IDBInitialize::Release IAccessor::ReleaseAccessor IRowset::Release |
SQLFreeStmt SQL_DROP SQL_CLOSE SQL_UNBIND SQL_RESET_ PARAMS | ICommand::Release IRowset::Release IAccessor::ReleaseAccessor IAccessor::ReleaseAccessor |
SQLGetConnectAttr | IDBProperties::GetProperties |
SQLGetCursorName | Not applicable | OLE DB performs updates through update methods, as opposed to positioned cursor operations
SQLGetData | IRowset::GetData (for all data except BLOBs) or ILockBytes, ISequentialStream, IStorage, or IStream (for BLOB data) |
SQLGetDescField | IAccessor::GetBindings | Elements within binding structure
SQLGetDescRec | IAccessor::GetBindings | Elements within binding structure
SQLDiagField | 1. GetErrorInfo 2. IErrorRecords 3. IErrorInfo |
SQLDiagRec | 1. GetErrorInfo 2. IErrorRecords 3. IErrorInfo |
SQLGetEnvAttr | IDBProperties::GetProperties |
SQLGetFunctions | IUnknown::QueryInterface |
SQLGetInfo | IDBProperties::GetProperties |
SQLGetStmtAttr | ICommandProperties::GetProperties or IRowsetInfo::GetProperties |
SQLGetTypeInfo | IDBSchemaRowset::GetRowset | DBSCHEMA_ PROVIDER_TYPES
SQLMoreResults | IMultipleResults |
SQLNativeSql | ICommandText::GetCommandText | With NULL as pguidDialect
SQLNumParams | ICommandWithParams:: GetParameterInfo |
SQLNumResultCols | IColumnsInfo::GetColumnInfo |
SQLParamData | Not applicable |
SQLPrepare | ICommand::Prepare |
SQLPrimaryKeys | IDBSchemaRowset::GetRowset | DBSCHEMA_ PRIMARY_KEYS
SQLProcedureColumns | IDBSchemaRowset::GetRowset | DBSCHEMA_ PROCEDURE_PARAMETERS, DBSCHEMA_ PROCEDURE_COLUMNS
SQLProcedures | IDBSchemaRowset::GetRowset | DBSCHEMA_PROCEDURES
SQLPutData | IRowsetChange::SetData (for all data except BLOBs) or ISequentialStream (for BLOB data) |
SQLRowCount | cRowsAffected in ICommand::Execute |
SQLSetConnectAttr | IDBProperties::SetProperties |
SQLSetCursorName | Not applicable | OLE DB performs updates through update methods, as opposed to positioned cursor operations
SQLSetDescField | IAccessor::CreateAccessor | Elements within binding structure
SQLSetDescRec | IAccessor::CreateAccessor | Elements within binding structure
SQLSetEnvAttr | IDBProperties::SetProperties |
SQLSetParam | ICommandWithParameters::SetParameterInfo |
SQLSetPos SQL_POSITION SQL_REFRESH SQL_UPDATE SQL_DELETE SQL_ADD | Not applicable IRowset::GetData or IRowsetResynch IRowsetChange::SetData IRowsetChange::Delete IRowsetChange::InsertRow IRowsetUpdate::Update |
SQLSetStmtAttr | ICommandProperties::SetProperties |
SQLSpecialColumns | IColumnsInfo::GetColumnInfo | DBCOLUMNFLAGS_ ISROWID and DBCOLUMNFLAGS_ ISROWVER
SQLStatistics | IDBSchemaRowset::GetRowset | DBSCHEMA_STATISTICS
SQLTablePrivileges | IDBSchemaRowset::GetRowset | DBSCHEMA_TABLE_ PRIVILEGES
SQLTables | IDBSchemaRowset::GetRowset | DBSCHEMA_TABLES
SQLTransact | ITransaction::Abort and ITransaction::Commit |

Conclusion

ODBC and OLE DB each provide a rich application programming interface for the database developer. OLE and the component object model present the ODBC developer with an entirely new way of writing. The ODBC developer, familiar with procedural programming, must become comfortable with the world of OLE objects and the interface negotiation required to manipulate those objects.

In addition, OLE DB often presents a new implementation of familiar development concepts like tabular data structures. The ODBC developer will need to rethink application structure to make effective use of OLE DB’s sessions, property sets, rowsets, and accessors.

Becoming comfortable with OLE objects is a first step toward successful OLE application development. In this paper, tasks typically executed by an ODBC application are implemented through OLE DB object creation and manipulation. The information presented builds on the ODBC database developer’s existing knowledge to smooth the introduction to this new and different technology.

Finding More Information

For more information about OLE DB and ODBC, see the following publications:

Brockschmidt, Kraig. Inside OLE. Microsoft Press®, 1995.

Geiger, Kyle. Inside ODBC. Microsoft Press, 1995.

Microsoft ODBC 3.0 Programmer’s Reference and SDK Guide. Microsoft Press, 1997.

Microsoft OLE 2 Programmer’s Reference. Volume 1. Microsoft Press, 1994.

Microsoft OLE DB Programmer’s Reference.

Appendix A

The following is a complete source code listing for the example presented in “Basic OLE DB.” The example selects the CompanyName, City, Phone, and FAX columns from the Customers table of the Access Northwind sample database. The database is shipped with the OLE DB SDK and is installed as the ODBC data source OLE_DB_NWind_Jet.

To build the file using Microsoft® Visual C++® 4.0 or later:

  1. Create a new console application.

  2. Copy the following code to a new .cpp file.

  3. Ensure that your build directory settings reference the OLE DB SDK include directory.

  4. Alter the build, link settings to include linking to Oledb.lib.
/********************************************************************
* OLE DB
********************************************************************/
#define UNICODE
#define _UNICODE
#define DBINITCONSTANTS // Initialize OLE constants...
#define INITGUID    // ...once in each app

#include <windows.h>
#include <stdio.h>

#include <oledb.h>  // OLE DB include files
#include <oledberr.h>
#include <msdaguid.h> // ODBC provider include files
#include <msdasql.h>

// Macros--number of row identifiers to retrieve
#define NUMROWS_CHUNK     35

// Prototypes
HRESULT myInitDSO(IDBInitialize** ppIDBI);
HRESULT mySetInitProps(IDBInitialize* pIDBInitialize);
HRESULT myCommand(IDBInitialize* pIDBI, IRowset** ppIRowset);
void  myGetData(IRowset* pIRowset);
void  DumpError(LPSTR lpStr);
HRESULT myGetColumnsInfo(IRowset* pIRowset, ULONG* pnCols,
       DBCOLUMNINFO** ppColumnsInfo, OLECHAR** ppColumnStrings);
void  myCreateDBBindings(ULONG nCols, DBCOLUMNINFO* pColumnsInfo,
       DBBINDING** ppDBBindings, char** ppRowValues);

// Global task memory allocator
IMalloc*    g_pIMalloc = NULL;

/********************************************************************
* General OLE DB application main()
********************************************************************/
int main()
  {
  IDBInitialize*  pIDBInitialize = NULL;
  IRowset*    pIRowset = NULL;

  // Init OLE and set up the DLLs
  CoInitialize(NULL);

  // Get the task memory allocator.
  if (FAILED(CoGetMalloc(MEMCTX_TASK, &g_pIMalloc)))
    goto EXIT;

 // Connect to the data source.
  if (FAILED(myInitDSO(&pIDBInitialize)))
    goto EXIT;

  // Get a session, set and execute a command.
  if (FAILED(myCommand(pIDBInitialize, &pIRowset)))
    goto EXIT;

  // Retrieve data from rowset.
  myGetData(pIRowset);

EXIT:
  // Clean up and disconnect.
  if (pIRowset != NULL)
    pIRowset->Release();

  if (pIDBInitialize != NULL)
    {
    if (FAILED(pIDBInitialize->Uninitialize()))
    {
    // Uninitialize is not required, but it will fail if an
    // interface has not been released; we can use it for
    // debugging.
    DumpError("Someone forgot to release something!");
    }
    pIDBInitialize->Release();
    }

  if (g_pIMalloc != NULL)
    g_pIMalloc->Release();

  CoUninitialize();

  return (0);
  }

/********************************************************************
* Initialize the data source.
********************************************************************/
HRESULT myInitDSO
  (
  IDBInitialize** ppIDBInitialize  // [out]
  )
  {
  // Create an instance of the MSDASQL (ODBC) provider.
  CoCreateInstance(CLSID_MSDASQL, NULL, CLSCTX_INPROC_SERVER,
    IID_IDBInitialize, (void**)ppIDBInitialize);

  if (*ppIDBInitialize == NULL)
    {
    return (E_FAIL);
    }

  if (FAILED(mySetInitProps(*ppIDBInitialize)))
    {
    return (E_FAIL);
    }

  if (FAILED((*ppIDBInitialize)->Initialize()))
    {
    DumpError("IDBInitialze->Initialize failed.");
    return (E_FAIL);
    }

  return (NOERROR);
  }

/********************************************************************
* Set initialization properties on a data source.
********************************************************************/
HRESULT mySetInitProps
  (
  IDBInitialize*  pIDBInitialize  // [in]
  )
  {
  const ULONG   nProps = 4;
  IDBProperties*  pIDBProperties;
  DBPROP    InitProperties[nProps];
  DBPROPSET   rgInitPropSet;
  HRESULT   hr;

  // Initialize common property options.
  for (ULONG i = 0; i < nProps; i++ )
    {
    VariantInit(&InitProperties[i].vValue);
    InitProperties[i].dwOptions = DBPROPOPTIONS_REQUIRED;
    InitProperties[i].colid = DB_NULLID;
    }

  // Level of prompting that will be done to complete the
  // connection process
  InitProperties[0].dwPropertyID = DBPROP_INIT_PROMPT;
  InitProperties[0].vValue.vt = VT_I2;
  InitProperties[0].vValue.iVal = DBPROMPT_NOPROMPT;

  // Data source name--see the sample source included with the OLE
  // DB SDK.
  InitProperties[1].dwPropertyID = DBPROP_INIT_DATASOURCE;
  InitProperties[1].vValue.vt = VT_BSTR;
  InitProperties[1].vValue.bstrVal =
    SysAllocString(OLESTR("OLE_DB_NWind_Jet"));

  // User ID
  InitProperties[2].dwPropertyID = DBPROP_AUTH_USERID;
  InitProperties[2].vValue.vt = VT_BSTR;
  InitProperties[2].vValue.bstrVal = SysAllocString(OLESTR(""));

  // Password
  InitProperties[3].dwPropertyID = DBPROP_AUTH_PASSWORD;
  InitProperties[3].vValue.vt = VT_BSTR;
  InitProperties[3].vValue.bstrVal = SysAllocString(OLESTR(""));

  rgInitPropSet.guidPropertySet = DBPROPSET_DBINIT;
  rgInitPropSet.cProperties = nProps;
  rgInitPropSet.rgProperties = InitProperties;

  // Set initialization properties.
  pIDBInitialize->QueryInterface(IID_IDBProperties, (void**)
    &pIDBProperties);
  hr = pIDBProperties->SetProperties(1, &rgInitPropSet);

  SysFreeString(InitProperties[1].vValue.bstrVal);
  SysFreeString(InitProperties[2].vValue.bstrVal);
  SysFreeString(InitProperties[3].vValue.bstrVal);

  pIDBProperties->Release();

  if (FAILED(hr))
    {
    DumpError("Set properties failed.");
    }

  return (hr);
  }

/********************************************************************
* Execute a command, retrieve a rowset interface pointer.
********************************************************************/
HRESULT myCommand
  (
  IDBInitialize*  pIDBInitialize, // [in]
  IRowset**   ppIRowset   // [out]
  )
  {
  IDBCreateSession* pIDBCreateSession;
  IDBCreateCommand* pIDBCreateCommand;
  IRowset*    pIRowset;
  ICommandText*   pICommandText;
  LPCTSTR     wSQLString = OLESTR("SELECT CompanyName,
               City, Phone, Fax")
             OLESTR(" FROM Customers")
             OLESTR(" ORDER BY CompanyName,
               City");
  LONG      cRowsAffected;
  HRESULT     hr;

  // Get the DB session object.
  if (FAILED(pIDBInitialize->QueryInterface(IID_IDBCreateSession,
    (void**) &pIDBCreateSession)))
    {
    DumpError("Session initialization failed.");
    return (E_FAIL);
    }

  // Create the session, getting an interface for command creation.
  hr = pIDBCreateSession->CreateSession(NULL, IID_IDBCreateCommand,
    (IUnknown**) &pIDBCreateCommand);
  pIDBCreateSession->Release();
  if (FAILED(hr))
    {
    DumpError("Create session failed.");
    return (hr);
    }

  // Create the command object.
  hr = pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText,
    (IUnknown**) &pICommandText);
  if (FAILED(hr))
    {
    DumpError("Create command failed.");
    return (hr);
    }
  pIDBCreateCommand->Release();

  // The command requires the actual text as well as an indicator
  // of its language and dialect.
  pICommandText->SetCommandText(DBGUID_DBSQL, wSQLString);

  // Execute the command.
  hr = pICommandText->Execute(NULL, IID_IRowset, NULL,
   &cRowsAffected, (IUnknown**) &pIRowset);
  if (FAILED(hr))
    {
    DumpError("Command execution failed.");
    }
  pICommandText->Release();

  *ppIRowset = pIRowset;
  return (hr);
  }

/********************************************************************
* Get the characteristics of the rowset (the ColumnsInfo interface).
********************************************************************/
HRESULT myGetColumnsInfo
  (
  IRowset*    pIRowset,    // [in]
  ULONG*    pnCols,    // [out]
  DBCOLUMNINFO**  ppColumnsInfo, // [out]
  OLECHAR**   ppColumnStrings  // [out]
  )
  {
  IColumnsInfo* pIColumnsInfo;
  HRESULT   hr;

  if (FAILED(pIRowset->QueryInterface(IID_IColumnsInfo, (void**) &pIColumnsInfo)))
    {
    DumpError("Query rowset interface for IColumnsInfo failed");
    return (E_FAIL);
    }

  hr = pIColumnsInfo->GetColumnInfo(pnCols, ppColumnsInfo, ppColumnStrings);
  if (FAILED(hr))
    {
    DumpError("GetColumnInfo failed.");
    *pnCols = 0;
    }

  pIColumnsInfo->Release();
  return (hr);
  }

/********************************************************************
* Create binding structures from column information. Binding
* structures will be used to create an accessor that allows row value
* retrieval.
********************************************************************/
void myCreateDBBindings
  (
  ULONG nCols,       // [in]
  DBCOLUMNINFO* pColumnsInfo,  // [in]
  DBBINDING** ppDBBindings,  // [out]
  char** ppRowValues     // [out]
  )
  {
  ULONG   nCol;
  ULONG   cbRow = 0;
  DBBINDING*  pDBBindings;
  char*   pRowValues;

  pDBBindings = new DBBINDING[nCols];

  for (nCol = 0; nCol < nCols; nCol++)
    {
    pDBBindings[nCol].iOrdinal = nCol+1;
    pDBBindings[nCol].obValue = cbRow;
    pDBBindings[nCol].obLength = 0;
    pDBBindings[nCol].obStatus = 0;
    pDBBindings[nCol].pTypeInfo = NULL;
    pDBBindings[nCol].pObject = NULL;
    pDBBindings[nCol].pBindExt = NULL;
    pDBBindings[nCol].dwPart = DBPART_VALUE;
    pDBBindings[nCol].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
    pDBBindings[nCol].eParamIO = DBPARAMIO_NOTPARAM;
    pDBBindings[nCol].cbMaxLen = pColumnsInfo[nCol].ulColumnSize;
    pDBBindings[nCol].dwFlags = 0;
    pDBBindings[nCol].wType = pColumnsInfo[nCol].wType;
    pDBBindings[nCol].bPrecision = pColumnsInfo[nCol].bPrecision;
    pDBBindings[nCol].bScale = pColumnsInfo[nCol].bScale;

    cbRow += pDBBindings[nCol].cbMaxLen;
    }

  pRowValues = new char[cbRow];

  *ppDBBindings = pDBBindings;
  *ppRowValues = pRowValues;

  return;
  }

/********************************************************************
* Retrieve data from a rowset.
********************************************************************/
void myGetData
  (
  IRowset*  pIRowset  // [in]
  )
  {
  ULONG     nCols;
  DBCOLUMNINFO* pColumnsInfo = NULL;
  OLECHAR*    pColumnStrings = NULL;
  ULONG     nCol;
  ULONG     cRowsObtained;   // Number of rows obtained
  ULONG     iRow;      // Row count
  HROW    rghRows[NUMROWS_CHUNK];// Row handles
  HROW*     pRows = &rghRows[0]; // Pointer to the row
               // handles
  IAccessor*  pIAccessor;    // Pointer to the accessor
  HACCESSOR   hAccessor;     // Accessor handle
  DBBINDSTATUS* pDBBindStatus = NULL;
  DBBINDING*  pDBBindings = NULL;
  char*     pRowValues;

  // Get the description of the rowset for use in binding structure
  // creation.
  if (FAILED(myGetColumnsInfo(pIRowset, &nCols, &pColumnsInfo,
    &pColumnStrings)))
    {
    return;
    }

  // Create the binding structures.
  myCreateDBBindings(nCols, pColumnsInfo, &pDBBindings,
    &pRowValues);
  pDBBindStatus = new DBBINDSTATUS[nCols];

  // Create the accessor.
  pIRowset->QueryInterface(IID_IAccessor, (void**) &pIAccessor);
  pIAccessor->CreateAccessor(
    DBACCESSOR_ROWDATA, // Accessor will be used to retrieve row
          // data.
    nCols,      // Number of columns being bound
    pDBBindings,    // Structure containing bind info
    0,      // Not used for row accessors
    &hAccessor,   // Returned accessor handle
    pDBBindStatus   // Information about binding validity
    );

  // Process all the rows, NUMROWS_CHUNK rows at a time
  while (TRUE)
    {
    pIRowset->GetNextRows(
    0,      // Reserved
    0,      // cRowsToSkip
    NUMROWS_CHUNK,  // cRowsDesired
    &cRowsObtained,   // cRowsObtained
    &pRows );     // Filled in w/ row handles.

    // All done; there are no more rows left to get.
    if (cRowsObtained == 0)
    break;

    // Loop over rows obtained, getting data for each.
    for (iRow=0; iRow < cRowsObtained; iRow++)
    {
    pIRowset->GetData(rghRows[iRow], hAccessor, pRowValues);
    for (nCol = 0; nCol < nCols; nCol++)
      {
      wprintf(OLESTR("%s%s:"), pColumnsInfo[nCol].pwszName,
        wcslen(pColumnsInfo[nCol].pwszName) > 10 ?
        OLESTR("\t") :
        OLESTR("\t\t"));
      printf("\t%s\n",
        &pRowValues[pDBBindings[nCol].obValue]);
      }
    printf("\n");
    }

    // Release row handles.
    pIRowset->ReleaseRows(cRowsObtained, rghRows, NULL, NULL,
     NULL);
    }  // End while

  // Release the accessor.
  pIAccessor->ReleaseAccessor(hAccessor, NULL);
  pIAccessor->Release();

  delete [] pDBBindings;
  delete [] pDBBindStatus;

  g_pIMalloc->Free( pColumnsInfo );
  g_pIMalloc->Free( pColumnStrings );

  return;
  }

/********************************************************************
* Dump an error to the console.
********************************************************************/
void DumpError(LPSTR lpStr)
  {
  printf(lpStr);
  printf("\n");
  }

Appendix B

The following is a complete source code listing for the example presented in “Handling Long Data Types.” The example selects the Notes column (LONG_VARCHAR) from the Employees table of the Access Northwind sample database. The database is shipped with the OLE DB SDK and is installed as the ODBC data source OLE_DB_NWind_Jet.

To build the file using Microsoft Visual C++ 4.0 or later:

  1. Create a new console application.

  2. Copy the following code to a new .cpp file.

  3. Ensure that your build directory settings reference the OLE DB SDK include directory.

  4. Alter the build link settings to include linking to Oledb.lib.
/********************************************************************
* Using ISequentialStream to retrieve LONG_VARCHAR/ LONG_VARBINARY
* (BLOB) data
********************************************************************/
#define UNICODE
#define _UNICODE
#define DBINITCONSTANTS
#define INITGUID

#include <windows.h>
#include <stdio.h>

#include <oledb.h>  // OLE DB include files
#include <oledberr.h>
#include <msdasql.h>

#define  BLOCK_SIZE   250

// Prototypes
HRESULT myInitDSO(IDBInitialize** ppIDBI);
HRESULT mySetInitProps(IDBInitialize*  pIDBInitialize);
HRESULT myCommand(IDBInitialize* pIDBI, IRowset** ppIRowset);
void  myGetBLOBData(IRowset* pIRowset);
void  DumpError(LPSTR lpStr);

IMalloc*    g_pIMalloc = NULL;

/********************************************************************
* OLE DB application main()
********************************************************************/
int main()
  {
  IDBInitialize*  pIDBInitialize = NULL;
  IRowset*    pIRowset = NULL;

  //Init OLE and set up the DLLs.
  CoInitialize(NULL);

  // Get the task memory allocator.
  if (FAILED(CoGetMalloc(MEMCTX_TASK, &g_pIMalloc)))
    goto EXIT;

  // Connect to the data source.
  if (FAILED(myInitDSO(&pIDBInitialize)))
    goto EXIT;

  // Get a session, set and execute a command.
  if (FAILED(myCommand(pIDBInitialize, &pIRowset)))
    goto EXIT;

  myGetBLOBData(pIRowset);

EXIT:
  if (pIRowset != NULL)
    pIRowset->Release();

  if (pIDBInitialize != NULL)
    {
    if (FAILED(pIDBInitialize->Uninitialize()))
    {
    // Uninitialize is not required, but it will fail if an
    // interface has not been released. We can use it for
    // debugging.
    DumpError("Someone forgot to release something!");
    }
    pIDBInitialize->Release();
    }

  if (g_pIMalloc != NULL)
    g_pIMalloc->Release();

  CoUninitialize();

  return (0);
  }

/********************************************************************
* Initialize the data source.
********************************************************************/
HRESULT myInitDSO
  (
  IDBInitialize** ppIDBInitialize   // [out]
  )
  {
  // Create an instance of the MSDASQL (ODBC) provider.
  CoCreateInstance(CLSID_MSDASQL, NULL, CLSCTX_INPROC_SERVER,
    IID_IDBInitialize, (void**)ppIDBInitialize);

  if (*ppIDBInitialize == NULL)
    {
    return (E_FAIL);
    }

  if (FAILED(mySetInitProps(*ppIDBInitialize)))
    {
    return (E_FAIL);
    }

  if (FAILED((*ppIDBInitialize)->Initialize()))
    {
    DumpError("IDBInitialze->Initialize failed.");
    return (E_FAIL);
    }

  return (NOERROR);
  }

/********************************************************************
* Set initialization properties on a data source.
********************************************************************/
HRESULT mySetInitProps
  (
  IDBInitialize*  pIDBInitialize   // [in]
  )
  {
  const ULONG   nProps = 4;
  IDBProperties*  pIDBProperties;
  DBPROP    InitProperties[nProps];
  DBPROPSET   rgInitPropSet;
  HRESULT   hr;

  // Initialize common property options.
  for (ULONG i = 0; i < nProps; i++ )
    {
    VariantInit(&InitProperties[i].vValue);
    InitProperties[i].dwOptions = DBPROPOPTIONS_REQUIRED;
    InitProperties[i].colid = DB_NULLID;
    }

  // Level of prompting that will be done to complete the
  // connection process.
  InitProperties[0].dwPropertyID = DBPROP_INIT_PROMPT;
  InitProperties[0].vValue.vt = VT_I2;
  InitProperties[0].vValue.iVal = DBPROMPT_NOPROMPT;

  // Data source name--see the sample source included with the OLE
  // DB SDK.
  InitProperties[1].dwPropertyID = DBPROP_INIT_DATASOURCE;
  InitProperties[1].vValue.vt = VT_BSTR;
  InitProperties[1].vValue.bstrVal =
    SysAllocString(OLESTR("OLE_DB_NWind_Jet"));

  // User ID
  InitProperties[2].dwPropertyID = DBPROP_AUTH_USERID;
  InitProperties[2].vValue.vt = VT_BSTR;
  InitProperties[2].vValue.bstrVal = SysAllocString(OLESTR(""));

  // Password
  InitProperties[3].dwPropertyID = DBPROP_AUTH_PASSWORD;
  InitProperties[3].vValue.vt = VT_BSTR;
  InitProperties[3].vValue.bstrVal = SysAllocString(OLESTR(""));

  rgInitPropSet.guidPropertySet = DBPROPSET_DBINIT;
  rgInitPropSet.cProperties = nProps;
  rgInitPropSet.rgProperties = InitProperties;

  // Set initialization properties.
  pIDBInitialize->QueryInterface(IID_IDBProperties, (void**)
   &pIDBProperties);
  hr = pIDBProperties->SetProperties(1, &rgInitPropSet);

  SysFreeString(InitProperties[1].vValue.bstrVal);
  SysFreeString(InitProperties[2].vValue.bstrVal);
  SysFreeString(InitProperties[3].vValue.bstrVal);

  pIDBProperties->Release();

  if (FAILED(hr))
    {
    DumpError("Set properties failed.");
    }

  return (hr);
  }

/********************************************************************
* Execute a command selecting Notes from Employees.
********************************************************************/
HRESULT myCommand
  (
  IDBInitialize*  pIDBInitialize, // [in]
  IRowset**   ppIRowset   // [out]
  )
  {
   IDBCreateSession* pIDBCreateSession;
   IDBCreateCommand* pIDBCreateCommand;
   IRowset*    pIRowset;
   ICommandText*   pICommandText;
  LPCTSTR     wSQLString = OLESTR("SELECT Notes ")
             OLESTR("FROM Employees");
  LONG      cRowsAffected;
  HRESULT     hr;

  // Set for failure.
  *ppIRowset = NULL;

  // Get the DB session object.
  hr = pIDBInitialize->QueryInterface(IID_IDBCreateSession,
    (void**) &pIDBCreateSession);
  if (FAILED(hr))
    {
    DumpError("Session initialization failed.");
    return (hr);
    }

  // Create the session.
  hr = pIDBCreateSession->CreateSession(NULL, IID_IDBCreateCommand,
    (IUnknown**) &pIDBCreateCommand);
  pIDBCreateSession->Release();
  if (FAILED(hr))
    {
    DumpError("Create session failed.");
    return (hr);
    }

  // Create the command object.
  hr = pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText,
    (IUnknown**) &pICommandText);
  if (FAILED(hr))
    {
    DumpError("Create command failed.");
    return (hr);
    }
  pIDBCreateCommand->Release();

  // The command requires the actual text as well as an indicator
  // of its language and dialect.
   pICommandText->SetCommandText(DBGUID_DBSQL, wSQLString);

  // Execute the command.
  hr = pICommandText->Execute(NULL, IID_IRowset, NULL,
   &cRowsAffected, (IUnknown**) &pIRowset);
  if (FAILED(hr))
    {
    DumpError("Command execution failed.");
    }

  pICommandText->Release();

  *ppIRowset = pIRowset;
  return (hr);
  }

/********************************************************************
* Retrieve data from an ODBC LONG_VARCHAR column (Notes in
* Employees).
********************************************************************/
void myGetBLOBData
  (
  IRowset*    pIRowset  // [in]
  )
  {
  DBOBJECT    ObjectStruct;   // For binding, retrieve an
               // object pointer.
  DBBINDING   rgBinding[1];   // Bind a single column.

  IAccessor*  pIAccessor = NULL;   // Accessor creation
  HACCESSOR   hAccessor = NULL;
  ULONG     ulErrorBinding;

  void*     pData;       // Bound consumer buffer
  HROW    rghRows[1];
  HROW*     pRows = &rghRows[0];
  ULONG     cRows;

  char    szNotes[BLOCK_SIZE + 1];// Text data from "Notes"
  ULONG     cbRead;       // Count of bytes read

  // Set up the object structure for accessor creation. Ask the
  // provider to return an ISequentialStream interface for reading.
  ObjectStruct.dwFlags = STGM_READ;
  ObjectStruct.iid = IID_ISequentialStream;

  // Set up the binding struct for the accessor.
  rgBinding[0].iOrdinal = 1;      // Only one column
  rgBinding[0].obValue  = 0;      // Offset to data
  rgBinding[0].obLength = 0;      // Ignore length
  rgBinding[0].obStatus = sizeof(IUnknown*);  // Offset to status
  rgBinding[0].pTypeInfo = NULL;      // Reserved
  rgBinding[0].pObject  = &ObjectStruct;  // Our interface
                // request
  rgBinding[0].pBindExt = NULL;     // Reserved
  rgBinding[0].dwPart = DBPART_VALUE |  // Get both VALUE
            DBPART_STATUS;  // and STATUS
                   // parts.
  rgBinding[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
  rgBinding[0].eParamIO = DBPARAMIO_NOTPARAM;
  rgBinding[0].cbMaxLen = 0;      // Not applicable
  rgBinding[0].dwFlags  = 0;      // Reserved
  rgBinding[0].wType = DBTYPE_IUNKNOWN;   // Type
                // DBTYPE_IUNKNOWN
  rgBinding[0].bPrecision = 0;      // Not applicable
  rgBinding[0].bScale = 0;        // Not applicable

  // Get the accessor interface and create the accessor.
  pIRowset->QueryInterface(IID_IAccessor, (void**) &pIAccessor);

  if (FAILED(pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 1,
    rgBinding, sizeof(IUnknown*) + sizeof(ULONG), &hAccessor,
    &ulErrorBinding)))
    {
    DumpError("CreateAccessor failed.");
    return;
    }

  // Allocate memory for the returned pointer and the status field.
  // The first sizeof(IUnknown*) bytes are for the pointer to the
  // object; the next sizeof(ULONG) bytes are for the status.
  pData = new BYTE[sizeof(IUnknown*) + sizeof(ULONG)];

  while (TRUE)
    {
    // Get the next row.
    if (FAILED(pIRowset->GetNextRows(NULL, 0, 1, &cRows,
       &pRows)))
    {
    DumpError("GetNextRows failed.\n");
    break;
    }

    if (cRows == 0)
    {
    break;
    }

    // Get the row data, the pointer to an ISequentialStream*.
    if (FAILED(pIRowset->GetData(*pRows, hAccessor, pData)))
    {
    DumpError("GetData failed.\n");
    break;
    }

    // Read and process BLOCK_SIZE bytes at a time.
    if ((ULONG)((BYTE*)pData)[rgBinding[0].obStatus] ==
      DBSTATUS_S_ISNULL)
    {
    // Process NULL data.
    printf("<null>");
    }
    else if ((ULONG)((BYTE*)pData)[rgBinding[0].obStatus] ==
      DBSTATUS_S_OK)
    {
    do
      {
      (*((ISequentialStream**) pData))->Read(szNotes,
      BLOCK_SIZE, &cbRead);
      if (cbRead > 0)
        {
        // process data
        szNotes[cbRead] = (char) NULL;
        printf(szNotes);
        }
      }
    while (cbRead >= BLOCK_SIZE);

    (*((ISequentialStream**) pData))->Release();

    printf("\n\n");
    }

    pIRowset->ReleaseRows(cRows, pRows, NULL, NULL, NULL);
    }

  // Clean up.
  pIAccessor->ReleaseAccessor(hAccessor, NULL);
  pIAccessor->Release();

  delete [] pData;
  }

/********************************************************************
* Dump an error to the console.
********************************************************************/
void DumpError(LPSTR lpStr)
  {
  printf(lpStr);
  printf("\n");
  }

Appendix C

The following is a complete source code listing for the example presented in “Data Manipulation.” The example inserts two new rows into the Customers table of the Access Northwind sample database. The database is shipped with the OLE DB SDK and is installed as the ODBC data source OLE_DB_NWind_Jet.

To build the file using Microsoft Visual C++ 4.0 or later:

  1. Create a new console application.

  2. Copy the following code to a new .cpp file.

  3. Ensure that your build directory settings reference the OLE DB SDK include directory.

  4. Alter the build, link settings to include linking to Oledb.lib.
/********************************************************************
* Parameterized execution in OLE DB
********************************************************************/
#define UNICODE
#define _UNICODE
#define DBINITCONSTANTS
#define INITGUID

#include <windows.h>
#include <stdio.h>
#include <stddef.h>   // For offset of macro
#include <cguid.h>  // IID_NULL

#include <oledb.h>  // OLE DB include files
#include <oledberr.h>
#include <msdaguid.h>
#include <msdasql.h>

// Types--new customer struct
typedef struct tagNEWCUSTOMER
  {
  char  acCustomerID[6];
  char  acCompanyName[40];
  char  acContactName[30];
  char  acContactTitle[30];
  char  acAddress[60];
  char  acCity[15];
  char  acRegion[15];
  char  acPostalCode[10];
  char  acCountry[15];
  char  acPhone[24];
  char  acFAX[24];
  } NEWCUSTOMER;

// Types--new customer data struct
typedef struct tagNEWCUSTOMERDATA
  {
  PSTR    szCustID;
  PSTR    szCompanyName;
  PSTR    szContactName;
  PSTR    szContactTitle;
  PSTR    szAddress;
  PSTR    szCity;
  PSTR    szRegion;
  PSTR    szPostalCode;
  PSTR    szCountry;
  PSTR    szPhone;
  PSTR    szFAX;
  } NEWCUSTOMERDATA;

// Prototypes
HRESULT myInitDSO(IDBInitialize** ppIDBI);
HRESULT mySetInitProps(IDBInitialize* pIDBInitialize);
HRESULT myCreateParamAccessor(ICommand* pICmd, HACCESSOR* phAccessor,
    IAccessor** ppIAccessor);
HRESULT myInsertWithParameters(IDBInitialize* pIDBInitialize);
void  myDelete(IDBInitialize* pIDBInitialize);
void  DumpError(LPSTR lpStr);

IMalloc*    g_pIMalloc = NULL;

/********************************************************************
* main()--control flow
********************************************************************/
int main()
  {
  IDBInitialize*  pIDBInitialize = NULL;
  IRowset*    pIRowset = NULL;
  HRESULT   hr;

  // Init OLE and set up the DLLs.
  CoInitialize(NULL);

  // Get the task memory allocator.
  if (FAILED(CoGetMalloc(MEMCTX_TASK, &g_pIMalloc)))
    goto EXIT;

  if (FAILED(myInitDSO(&pIDBInitialize)))
    goto EXIT;

  // Execute a prepared statement with parameters.
  if (FAILED(myInsertWithParameters(pIDBInitialize)))
    goto EXIT;

  // Delete rows just added.
  myDelete(pIDBInitialize);

EXIT:
  // Clean up and disconnect.
  if (pIRowset != NULL)
    pIRowset->Release();

  if (pIDBInitialize != NULL)
    {
    hr = pIDBInitialize->Uninitialize();
    pIDBInitialize->Release();
    }

  if (g_pIMalloc != NULL)
    g_pIMalloc->Release();

  CoUninitialize();

  return (0);
  }

/********************************************************************
*  Execute a prepared INSERT statement with parameters.
********************************************************************/
HRESULT myInsertWithParameters
  (
  IDBInitialize*  pIDBInitialize
  )
  {
  IDBCreateSession* pIDBCreateSession;
  IDBCreateCommand* pIDBCreateCommand;
  ICommandText*   pICommandText;
  ICommandPrepare*  pICommandPrepare;
  ICommandWithParameters* pICmdWithParams;
  IAccessor*    pIAccessor;
  WCHAR     wSQLString[] =
      OLESTR("insert into Customers (CustomerID,
        CompanyName, ContactName,")
      OLESTR(" ContactTitle, Address, City, Region,
        PostalCode, Country,")
      OLESTR(" Phone, Fax)")
      OLESTR(" values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
  DBPARAMS    Params;
  long      cRowsAffected;
  HACCESSOR     hParamAccessor;

  NEWCUSTOMERDATA   aNewCustomers[] =
    {
    "YOUME",
    "You and Me Grocers",
    "William Smith",
    "General Manager",
    "383 15th Ave. N.",
    "New York",
    "NY",
    "10018",
    "USA",
    "(212) 555-8847",
    "(212) 555-9988",

    "YORBL",
    "Yorbalinda's",
    "Mary Jones",
    "Owner",
    "22 Sunny Vale Rd.",
    "San Diego",
    "CA",
    "93122",
    "USA",
    "(605) 555-4322",
    "(605) 555-4323"
    };
  NEWCUSTOMER   NewCustomer;

  ULONG     nParams = 11;
  DBPARAMBINDINFO   rgParamBindInfo[] =
    {
    OLESTR("DBTYPE_CHAR"),  OLESTR("CustomerID"),  5,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("CompanyName"),  40,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("ContactName"),  30,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("ContactTitle"), 30,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("Address"),  60,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("City"),   15,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("Region"),   15,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("PostalCode"), 10,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("Country"),  15,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("Phone"),    24,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    OLESTR("DBTYPE_VARCHAR"), OLESTR("FAX"),    24,
     DBPARAMFLAGS_ISINPUT, 0, 0,
    };
  ULONG     rgParamOrdinals[] =
          {1,2,3,4,5,6,7,8,9,10,11};
  // Get the session.
  pIDBInitialize->QueryInterface(IID_IDBCreateSession,
    (void**)&pIDBCreateSession);
  pIDBCreateSession->CreateSession(NULL, IID_IDBCreateCommand,
    (IUnknown**) &pIDBCreateCommand);
  pIDBCreateSession->Release();

  // Create the command.
  pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText,
    (IUnknown**) &pICommandText);
  pIDBCreateCommand->Release();

  // The command requires the actual text as well as an indicator
  // of its language.
  pICommandText->SetCommandText(DBGUID_DBSQL, wSQLString);

  // Set parameter information.
  pICommandText->QueryInterface(IID_ICommandWithParameters,
    (void**)&pICmdWithParams);
  pICmdWithParams->SetParameterInfo(nParams, rgParamOrdinals,
    rgParamBindInfo);
  pICmdWithParams->Release();

  // Prepare the command.
  pICommandText->QueryInterface(IID_ICommandPrepare, (void**)&pICommandPrepare);
  if (FAILED(pICommandPrepare->Prepare(0)))
    {
    pICommandPrepare->Release();
    pICommandText->Release();
    return (E_FAIL);
    }
  pICommandPrepare->Release();

  // Create parameter accessors.
  if (FAILED(myCreateParamAccessor(pICommandText, &hParamAccessor,
    &pIAccessor)))
    {
    pICommandText->Release();
    return (E_FAIL);
    }

  Params.pData = &NewCustomer; // pData is the buffer pointer
  Params.cParamSets = 1;   // Number of sets of parameters
  Params.hAccessor = hParamAccessor; // Accessor to the parameters

  // Specify the parameter information.
  for (UINT nCust = 0; nCust < 2; nCust++)
    {
    strcpy(NewCustomer.acCustomerID,
     aNewCustomers[nCust].szCustID);
    strcpy(NewCustomer.acCompanyName,
     aNewCustomers[nCust].szCompanyName);
    strcpy(NewCustomer.acContactName,
     aNewCustomers[nCust].szContactName);
    strcpy(NewCustomer.acContactTitle,
     aNewCustomers[nCust].szContactTitle);
    strcpy(NewCustomer.acAddress,
     aNewCustomers[nCust].szAddress);
    strcpy(NewCustomer.acCity, aNewCustomers[nCust].szCity);
    strcpy(NewCustomer.acRegion, aNewCustomers[nCust].szRegion);
    strcpy(NewCustomer.acPostalCode,
     aNewCustomers[nCust].szPostalCode);
    strcpy(NewCustomer.acCountry,
     aNewCustomers[nCust].szCountry);
    strcpy(NewCustomer.acPhone, aNewCustomers[nCust].szPhone);
    strcpy(NewCustomer.acFAX, aNewCustomers[nCust].szFAX);

    // Execute the command.
    pICommandText->Execute(NULL, IID_NULL, &Params,
     &cRowsAffected, NULL);
    printf("%ld rows inserted.\n", cRowsAffected);
    }

  pIAccessor->ReleaseAccessor(hParamAccessor, NULL);
  pIAccessor->Release();
  pICommandText->Release();

  return (NOERROR);
  }

/********************************************************************
*  Delete rows just added using simple execution.
********************************************************************/
void myDelete
  (
  IDBInitialize*  pIDBInitialize
  )
  {
  IDBCreateSession* pIDBCreateSession;
  IDBCreateCommand* pIDBCreateCommand;
  ICommandText*   pICommandText;
  WCHAR     wSQLDelete1[] =
        OLESTR("delete from Customers where CustomerID = 'YOYOM'");
  WCHAR     wSQLDelete2[] =
        OLESTR("delete from Customers where CustomerID = 'YORBL'");
  long      cRowsAffected;

  // Get the session.
  pIDBInitialize->QueryInterface(IID_IDBCreateSession, (void**)&pIDBCreateSession);
  pIDBCreateSession->CreateSession(NULL, IID_IDBCreateCommand,
    (IUnknown**) &pIDBCreateCommand);
  pIDBCreateSession->Release();

  // Create the command.
  pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**) &pICommandText);
  pIDBCreateCommand->Release();

  // Set the command text for first delete statement and execute
  // the command.
  pICommandText->SetCommandText(DBGUID_DBSQL, wSQLDelete1);
  pICommandText->Execute(NULL, IID_NULL, NULL, &cRowsAffected, NULL);

  printf("%ld rows deleted.\n", cRowsAffected);

  // Do it again.
  pICommandText->SetCommandText(DBGUID_DBSQL, wSQLDelete2);
  pICommandText->Execute(NULL, IID_NULL, NULL, &cRowsAffected, NULL);

  printf("%ld rows deleted.\n", cRowsAffected);

  pICommandText->Release();

  return;
  }

/********************************************************************
*  Create parameter accessor.
********************************************************************/
HRESULT myCreateParamAccessor
  (
  ICommand* pICmd,  // [in]
  HACCESSOR*  phAccessor, // [out]
  IAccessor** ppIAccessor // [out]
  )
  {
  IAccessor*  pIAccessor;
  HACCESSOR   hAccessor;
  const ULONG   nParams = 11;
  DBBINDING   Bindings[nParams];
  DBBINDSTATUS  rgStatus[nParams]; // Returns information for
             // individual binding
             // validity.
  HRESULT   hr;

  ULONG     acbLengths[] = {5, 40, 30, 30, 60, 15, 15, 10,
             15, 24, 24};

  for (ULONG i = 0; i < nParams; i++)
    {
    Bindings[i].iOrdinal = i + 1;
    Bindings[i].obLength = 0;
    Bindings[i].obStatus = 0;
    Bindings[i].pTypeInfo = NULL;
    Bindings[i].pObject = NULL;
    Bindings[i].pBindExt = NULL;
    Bindings[i].dwPart = DBPART_VALUE;
    Bindings[i].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
    Bindings[i].eParamIO = DBPARAMIO_INPUT;
    Bindings[i].cbMaxLen = acbLengths[i];
    Bindings[i].dwFlags = 0;
    Bindings[i].wType = DBTYPE_STR;
    Bindings[i].bPrecision = 0;
    Bindings[i].bScale = 0;
    }

  Bindings[0].obValue = offsetof(NEWCUSTOMER, acCustomerID);
  Bindings[1].obValue = offsetof(NEWCUSTOMER, acCompanyName);
  Bindings[2].obValue = offsetof(NEWCUSTOMER, acContactName);
  Bindings[3].obValue = offsetof(NEWCUSTOMER, acContactTitle);
  Bindings[4].obValue = offsetof(NEWCUSTOMER, acAddress);
  Bindings[5].obValue = offsetof(NEWCUSTOMER, acCity);
  Bindings[6].obValue = offsetof(NEWCUSTOMER, acRegion);
  Bindings[7].obValue = offsetof(NEWCUSTOMER, acPostalCode);
  Bindings[8].obValue = offsetof(NEWCUSTOMER, acCountry);
  Bindings[9].obValue = offsetof(NEWCUSTOMER, acPhone);
  Bindings[10].obValue = offsetof(NEWCUSTOMER, acFAX);

  pICmd->QueryInterface(IID_IAccessor, (void**)&pIAccessor);

  hr = pIAccessor->CreateAccessor(
    DBACCESSOR_PARAMETERDATA,   // Accessor that will be used
               // to specify parameter data
    nParams,         // Number of parameters being
               // bound
    Bindings,         // Structure containing bind
               // information
    sizeof(NEWCUSTOMER),      // Size of parameter structure
    &hAccessor,         // Returned accessor handle
    rgStatus         // Information about binding
               // validity
    );

  if (FAILED(hr))
    {
    DumpError("Parameter accessor creation failed.");
    }
  else
    {
    *ppIAccessor = pIAccessor;
    *phAccessor = hAccessor;
    }

  return (hr);
  }

/********************************************************************
* Initialize the Data Source.
********************************************************************/
HRESULT myInitDSO
  (
  IDBInitialize** ppIDBInitialize // [out]
  )
  {
  // Create an instance of the MSDASQL (ODBC) provider.
  CoCreateInstance(CLSID_MSDASQL, NULL, CLSCTX_INPROC_SERVER,
    IID_IDBInitialize, (void**)ppIDBInitialize);

  if (*ppIDBInitialize == NULL)
    {
    return (E_FAIL);
    }

  if (FAILED(mySetInitProps(*ppIDBInitialize)))
    {
    return (E_FAIL);
    }

  if (FAILED((*ppIDBInitialize)->Initialize()))
    {
    DumpError("IDBInitialze->Initialize failed.");
    return (E_FAIL);
    }

  return (NOERROR);
  }

/********************************************************************
* Set initialization properties on a data source.
*********************************************************************
HRESULT mySetInitProps
  (
  IDBInitialize*  pIDBInitialize  // [in]
  )
  {
  const ULONG   nProps = 4;
  IDBProperties*  pIDBProperties;
  DBPROP    InitProperties[nProps];
  DBPROPSET   rgInitPropSet;
  HRESULT   hr;

  // Initialize common property options.
  for (ULONG i = 0; i < nProps; i++ )
    {
    VariantInit(&InitProperties[i].vValue);
    InitProperties[i].dwOptions = DBPROPOPTIONS_REQUIRED;
    InitProperties[i].colid = DB_NULLID;
    }

  // Level of prompting that will be done to complete the
  // connection process.
  InitProperties[0].dwPropertyID = DBPROP_INIT_PROMPT;
  InitProperties[0].vValue.vt = VT_I2;
  InitProperties[0].vValue.iVal = DBPROMPT_NOPROMPT;

  // Data source name--see the sample source included with the OLE
  // DB SDK.
  InitProperties[1].dwPropertyID = DBPROP_INIT_DATASOURCE;
  InitProperties[1].vValue.vt = VT_BSTR;
  InitProperties[1].vValue.bstrVal =
    SysAllocString(OLESTR("OLE_DB_NWind_Jet"));

  // User ID
  InitProperties[2].dwPropertyID = DBPROP_AUTH_USERID;
  InitProperties[2].vValue.vt = VT_BSTR;
  InitProperties[2].vValue.bstrVal = SysAllocString(OLESTR(""));

  // Password
  InitProperties[3].dwPropertyID = DBPROP_AUTH_PASSWORD;
  InitProperties[3].vValue.vt = VT_BSTR;
  InitProperties[3].vValue.bstrVal = SysAllocString(OLESTR(""));

  rgInitPropSet.guidPropertySet = DBPROPSET_DBINIT;
  rgInitPropSet.cProperties = nProps;
  rgInitPropSet.rgProperties = InitProperties;

  // Set initialization properties.
  pIDBInitialize->QueryInterface(IID_IDBProperties, (void**)
    &pIDBProperties);
  hr = pIDBProperties->SetProperties(1, &rgInitPropSet);

  SysFreeString(InitProperties[1].vValue.bstrVal);
  SysFreeString(InitProperties[2].vValue.bstrVal);
  SysFreeString(InitProperties[3].vValue.bstrVal);

  pIDBProperties->Release();

  if (FAILED(hr))
    {
    DumpError("Set properties failed.");
    }

  return (hr);
  }

/********************************************************************
* Dump an error to the console.
********************************************************************/
void DumpError(LPSTR lpStr)
  {
  printf(lpStr);
  printf("\n");
  }


OLE DB Leveling

Choosing the Right Interfaces

Abstract

OLE DB defines a comprehensive set of interfaces for accessing a diverse range of data types, located in a variety of data stores. While the OLE DB Programmer's Reference can be intimidating in size, no one provider is expected to support all of the functionality defined in the reference. Providers only expose the interfaces that reflect the natural functionality of their data store. Providers all support a base level of functionality. Above that base level, common service components provide interoperability through generic implementations of extensions such as scrolling or notifications if the provider doesn't support them.

Introduction — OLE DB as a Component Technology

To meet its goal of providing data access to all types of data in a COM (Component Object Model) environment, OLE DB is designed as a component technology. In OLE DB, data sources expose the interfaces that reflect their functionality, and common components can be built on top of those interfaces to expose more robust data models. To define a component architecture, OLE DB identifies common characteristics between different data providers and services, and defines common interfaces to expose those characteristics. So, for example, while a rowset may be obtained through a number of very different mechanisms, the end result is still a rowset, with well-defined interfaces, methods and characteristics. With OLE DB, navigating the result of a complex multi-table join is no different than navigating the results of an opened text file containing tabular data. Defining common interfaces in this manner allows components to more efficiently augment the individual data provider's native functionality.

An OLE DB provider exposes OLE DB interfaces over some type of data. OLE DB providers include everything from a full SQL DBMS to a text file or data stream. Obviously these data providers have different functionality, and it's important not to limit that functionality. But at the same time it's not reasonable to expect all providers that expose simple tabular data to implement a full-blown query engine as well.

Once the base functionality is defined, the next step is to view the additional functionality as incremental additions to this base functionality. Thus, the more sophisticated providers can expose these advanced features in addition to the base level interfaces. Furthermore, individual service components can be built to implement these features on top of the simpler providers.

Consumers

Developers writing OLE DB consumers can choose their level of interoperability with OLE DB providers. Consumers may be written to consume a specific provider, in which case they are designed to be aware of the functionality of the provider. Or they may be written to consume generic providers. In order to consume generic providers, the consumer may do one of the following:

  • Consume a minimum set of functionality and work with all OLE DB providers.

  • Consume a higher level of functionality and query the provider for support of extended functionality.

  • Consume a higher level of functionality and invoke service components to implement missing functionality, where such service components are available.

Base Consumer Functionality

The following table describes the minimum level of functionality that a consumer can expect to be supported when talking to any OLE DB provider. The table is broken into three columns:

  • Object – Lists the OLE DB object that supports the interface or behavior.

  • Base Interfaces – Lists the functionality available over any OLE DB provider.

  • Updatable – Lists the minimum functionality available over any OLE DB provider that supports updating data. Providers not supporting this complete set of functionality are considered read-only providers.

Table 1 – Base Consumer Interfaces

Object | Base interfaces | Updatable
DataSource | IConnectionPointContainer for: - IDBAsynchNotify |
 | IDatasource1 |
 | IDBAsynchStatus2 |
 | IDBCreateSession |
 | IDBInitialize |
 | IDBProperties |
 | IPersist |
 | ISupportErrorInfo |
DataSource Behavior | DBPROP_INIT_PROMPT3 |
Session | IGetDataSource |
 | IOpenRowset |
 | ISessionProperties |
 | ISupportErrorInfo |
Rowset | IAccessor | IRowsetChange
 | IColumnsInfo |
 | IConnectionPointContainer for: - IDBAsynchNotify - IRowsetNotify |
 | IConvertType |
 | IDBAsynchStatus2 |
 | IRowset |
 | IRowsetFind |
 | IRowsetIdentity |
 | IRowsetInfo |
 | IRowsetLocate |
 | IRowPosition1 |
 | IRowsetScroll |
 | ISupportErrorInfo |
Rowset Behavior | DBPROP_ CANHOLDROWS | DBPROP_ REMOVEDELETED
 | DBPROP_ CANFETCHBACKWARDS | DBPROP_ OWNUPDATEDELETE
 | DBPROP_ CANSCROLLBACKWARDS | DBPROP_ OWNINSERT

1These interfaces are always supported by common components in the SDK. They are never implemented directly by providers.

2Support for IDBAsynchStatus does not require that the provider support any operations asynchronously. In the non-asynch case, the provider would simply block when creating or initializing the object, and return a completion status whenever GetStatus was called.

3DBPROP_INIT_PROMPT is required if any initialization properties are required in order to connect to the provider. If calling Initialize() succeeds on a freshly created Datasource, the provider does not need to support this property.

Providers

Providers support the native functionality of the data which they expose. However, in order to be consumed by generic consumers, providers must be able to support at least the minimum base consumer functionality defined in the previous table. You can develop a provider to support the minimum level of functionality in one of three ways:

  • If your data is simple, cached data, use the OLE DB Provider Writer's Toolkit to implement a provider in Microsoft® Visual Basic®, Microsoft® Visual J++™, C, or C++.

  • Implement the minimum provider functionality defined in section 2.1 directly in C or C++, and rely upon service components to implement the additional functionality required for the base consumer functionality.

  • Implement at least the functionality defined in both section 2.1 and section 2.2 directly in C or C++.

In addition, providers may expose interfaces for the extended functionality described in the next section.

All providers must be either apartment or free threaded, and must support aggregation of the DataSource, Session, and Rowset. If applicable, providers must support the aggregation of the Command and View objects.

Minimum Provider Functionality

It's important for provider writers to implement the full set of interfaces that apply to their particular type of data. At a minimum, the provider must implement the interfaces and behavior listed in Table 2 to be considered a generic OLE DB provider. Providers implementing the minimum provider functionality can rely on common service components available in the SDK to implement the base consumer functionality, as shown in Figure 1.

Figure 1

Table 2 – Minimum Provider Interfaces

Object | Any base provider | Updatable providers
DataSource | IDBCreateSession |
 | IDBInitialize |
 | IDBProperties |
 | IPersist |
DataSource Behavior | DBPROP_INIT_PROMPT1 |
Session | IGetDataSource |
 | IOpenRowset |
 | ISessionProperties |
Rowset | IAccessor | IRowsetChange
 | IConvertType |
 | IColumnsInfo |
 | IRowset |
 | IRowsetIdentity |
 | IRowsetInfo |
Rowset Behavior | DBPROP_CANHOLDROWS | DBPROP_ OWNUPDATEDELETE
 |  | DBPROP_OWNINSERT
 |  | DBPROP_ REMOVEDELETED

1DBPROP_INIT_PROMPT is required if any initialization properties are required in order to connect to the provider. If calling Initialize() succeeds on a freshly created Datasource, the provider does not need to support this property.

Supporting an interface means supporting all methods within that interface. No methods return E_NOTIMPL.

Supporting a property means supporting the setting of and associated behavior of all possible values for a property.

Supporting the required rowset properties means that setting any combination of the required properties must yield a rowset that reflects at least those properties. It does not mean that those properties are always true for any rowset if the property has not been requested by the user.

Base Providers

Providers that implement the full set of base interfaces can be consumed by general consumers without the support of additional service components, as shown in Figure 2.

Figure 2

Providers implementing the following functionality in addition to the minimum provider functionality are consumed as base providers:

Table 3 – Full Interfaces for Base Providers

Object | Any base provider | Updatable providers
DataSource | IConnectionPointContainer for: - IDBAsynchNotify |
 | IDBAsynchStatus |
 | ISupportErrorInfo |
Session | ISupportErrorInfo |
Rowset | IDBAsynchStatus |
 | IConnectionPointContainer for: - IRowsetNotify - IDBAsynchNotify |
 | IRowsetFind |
 | IRowsetLocate |
 | IRowsetScroll |
 | ISupportErrorInfo |
Rowset Behavior | DBPROP_ CANSCROLLBACKWARDS1 |
 | DBPROP_ CANFETCHBACKWARDS1 |

1Providers that support the property DBPROP_CANSCROLLBACKWARDS must also support the property DBPROP_CANFETCHBACKWARDS.

Extended Interfaces

General purpose providers may support additional functionality. It is advantageous that providers support as many extended interfaces as apply to their particular type of data.

In addition to common interface extensions, providers may expose specialized interfaces for the following sets of extended functionality:

  • Rowset Processing – Exposed by providers that support native filter or sort capabilities.

  • Commands – Exposed by providers that can execute more sophisticated queries or statements.

  • Transactions – Exposed by providers that can support transactional capabilities.

  • Index Navigation – Exposed by providers built on indexed data to expose index functionality to service components such as query processors. These interfaces are generally not directly consumed by OLE DB consumers other than service components.

Table 4 – Extended Interfaces

Object | Any provider | Updatable providers | Transacted providers
DataSource | IDataSourceAdmin |  |
 | IDBInfo2 |  |
 | IPersistFile |  |
Session | IDBSchemaRowset |  | ITransactionLocal
 | IDBView1 |  | ITransactionJoin
 | IDBCreateCommand2 |  | ITransactionObject
 | IIndexDefinition |  |
 | ITableDefinition |  |
View1 | IViewFilter |  |
 | IViewSort |  |
 | IColumnsInfo |  |
 | IAccessor |  |
 | ISupportErrorInfo |  |
 | IViewRowset |  |
 | IViewChapter |  |
Command2 | IAccessor |  |
 | ICommand |  |
 | ICommandText |  |
 | IColumnsInfo |  |
 | ICommandPrepare |  |
 | IColumnsRowset |  |
 | ICommandProperties |  |
 | ICommandWithParameters |  |
 | IConvertType |  |
 | ISupportErrorInfo |  |
Command Behavior | MAXTABLESINSELECT>1 |  |
Custom Error | ISQLErrorInfo |  |
Multiple Results | IMultipleResults |  |
Rowset | IColumnsRowset | IRowsetResynch |
 | IRowsetIdentity | IRowsetUpdate |
 | IRowsetLocate |  |
 | IChapteredRowset1 |  |
 | IRowsetView1 |  |
 | IRowsetIndex3 |  |
Rowset Behavior | DBPROP_ LITERALBOOKMARKS | DBPROP_CHANGE INSERTEDROWS | DBPROP_ COMMITPRESERVE
 | DBPROP_ ORDEREDBOOKMARKS | DBPROP_RETURN PENDINGROWS | DBPROP_ ABORTPRESERVE
 | DBPROP_ LITERALIDENTITY | DBPROP_ IMMOBILEROWS |
 | DBPROP_ BOOKMARKSKIPPED |  |
 | DBPROP_OTHERINSERT |  |
 | DBPROP_ OTHERUPDATEDELETE |  |
 | DBPROP_ STRONGIDENTITY |  |
BLOB Support | DBPROP_OLEOBJECTS |  |
 | DBPROP_MULTIPLE STORAGEOBJECTS |  |
 | DBPROP_STRUCTURED STORAGE |  |
 | ISequentialStream |  |
 | IStream |  |
 | IStorage |  |
 | ILockBytes |  |
 | DBPROP_BLOCKING STORAGEOBJECTS=FALSE |  |

1Extended functionality that may be exposed by providers which support Rowset Processing.

2Extended functionality that may be exposed by providers which support Commands.

3Extended functionality that may be exposed by providers which support Index navigation.

Generic consumers must be prepared for providers that don't support the extended interfaces. Consumers can handle such providers in several different ways:

  • The consumer can invoke common services to implement extended functionality, where such service components are available.

  • The consumer can implement extra code within the application to compensate for missing functionality.

  • The consumer can reduce the functionality available to the user based on missing functionality in the provider.

  • The consumer can return an error indicating that the provider is not capable of supporting required functionality.


Sample Provider Overview

The OLE DB sample provider, SAMPPROV.DLL, is a learning tool for developers new to OLE DB. Users can displaying the source code in the debugger to learn about building OLE DB providers. Although this provider uses a simple interface to access data in the underlying data source, it demonstrates the general procedures that developers need to follow to expose rowsets over a file-based data source using IOpenRowset.

Sample Provider Features

The sample provider exposes data in a single, comma-separated value file named CUSTOMER.CSV. If you alter the code to use a different file, you must follow the formatting conventions of the sample file, which follow.

The first line of a .csv file that can be read by SAMPPROV must contain the column names in quotation marks. The second line must list the column types and lengths. All additional lines contain data, in comma-separated format. Every line of data in the file must contain the correct number of values. The following example illustrates the file format.

"Column1","Column2","Column3"
Char(20),SLONG,SLONG
"Name1",10,-10
@@@@@@@@@@@@@@@@@@@@@     // Deleted Row
"Name2",110,-110

In addition, the sample provider replaces deleted rows in the file with the at sign (@). These rows are ignored by the sample provider when the file is initialized. If a user updates a row, the new data is appended to the end of the file and the original row is overwritten with at signs, indicating that it is deleted.

Sample Provider Limitations

The following are limitations in SAMPPROV.DLL:

  • This code example is for illustrative purposes; the sample provider has not been rigorously tested.

  • IDBInitialize::Initialize requires the path to the .csv file. If the path is not supplied by the calling application, SAMPPROV prompts the user for the path.

  • The data source object can create only one session; the session can create only one rowset. This is because the underlying Cfileio object opens the .csv text files in exclusive mode, and has no mechanism to share access to the file.

  • The use of properties is very simple. Only a small set of properties are illustrated, and properties are read-only.

Sample Provider Files

The following source files are included with the sample provider. The source files are located in the <install directory>\SAMPLES\SAMPPROV directory. The sample provider executable file SAMPPROV.DLL is located in the <install directory>\BIN directory.

File name | Description
ACCESSOR.cpp | CImpIAccessor object implementation.
ASSERTS.cpp | Simple assertion routines.
ASSERTS.h | Simple assertion routine header file.
BITARRAY.cpp | An implementation of a bit array class used by the internal buffer to mark released or unreleased rows.
BITARRAY.h | Class definitions for the bit array class.
CLASSFAC.cpp | DLL entry and exit points and the OLE ClassFactory class.
CLASSFAC.h | Class definitions for CClassFactory and DLL entry points.
COLINFO.cpp | IColumnsInfo interface implementation.
CRTSESS.cpp | IDBCreateSession interface implementation.
CVTTYPE.cpp | IConvertType interface implementation.
DATASRC.cpp | CDataSource object implementation.
DATASRC.h | CDataSource base object and contained interface definitions.
DBINIT.cpp | IDBInitialize interface implementation.
DBPROP.cpp | IDBProperties and IDBInfo interface implementations.
DBSESS.cpp | CDBSession object implementation.
DBSESS.h | CDBSession base object and contained interface definitions.
EXTBUFF.cpp | The class that provides array-based access to a big block of memory.
EXTBUFF.h | Class definitions for CExtBuffer class.
FILEIDX.cpp | The index array code for a comma-separated value (.csv) provider.
FILEIDX.h | Class definitions for CFileIO class.
FILEIO.cpp | The file manipulation code for a comma-separated value (.csv) provider.
FILEIO.h | Class definitions for CFileIO class.
GLOBALS.cpp | Global initialization object.
GUIDS.h | Internal GUIDs.
HASHTBL.cpp | Hashing routines for row manipulation.
HASHTBL.h | Class definitions for CHashTbl class and miscellaneous bookmark functions.
HEADERS.cpp | Precompiled headers module.
HEADERS.h | Precompiled headers.
IROWSET.cpp | IRowset interface implementation.
LOCAL.RC | Localizable resource.
OPNROWST.cpp | IOpenRowset interface implementation.
PERSIST.cpp | IPersist interface implementation.
RC.h | Resource IDs.
ROWCHNG.cpp | IRowsetChange interface implementation.
ROWINFO.cpp | IRowsetInfo interface implementation.
ROWSET.cpp | CRowset object implementation.
ROWSET.h | CRowset base object and contained interface definitions.
SAMPPROV.DEF | Project definitions.
SAMPPROV.h | Main include file.
SAMPPROV.MAK | Microsoft Developer Studio–generated NMAKE File.
SAMPPROV.RC | Main resource file.
SAMPVER.h | Version include file.
UTILPROP.cpp | Properties utility object implementation.
UTILPROP.h | CUtilProp object definitions.

Building the Sample Provider

The file <install directory>\SAMPLES\SAMPPROV\SAMPPROV.MAK is set up for compilation with NMAKE, the Microsoft® Program Maintenance Utility provided with Microsoft Developer Studio. SAMPPROV must have access to the following OLE DB files:

OLEDB.h

OLEDBERR.h

OLEDB.LIB

Verify that these files are on the path referred to by the INCLUDE environment variable.

The following example syntax builds a debug version of SAMPPROV on the Intel® x86 platform based on the Win32® (x86) Dynamic-Link Library configuration.

NMAKE /f "sampprov.mak" CFG="sampprov - Win32 x86 Debug"

To build SAMPPROV on another platform or with a different configuration, use one of the following configuration options.

Configuration option | Type of file built
"sampprov - Win32 x86 Debug" | Debug version of SAMPPROV based on Win32 (x86) Dynamic-Link Library.
"sampprov - Win32 x86 Release" | Retail version of SAMPPROV based on Win32 (x86) Dynamic-Link Library.
"sampprov - Win32 (ALPHA) axp Debug" | Debug version of SAMPPROV based on Win32 (ALPHA) Dynamic-Link Library.
"sampprov - Win32 (ALPHA) axp Release" | Retail version of SAMPPROV based on Win32 (ALPHA) Dynamic-Link Library.
"sampprov - Win32 (PPC) ppc Debug" | Debug version of SAMPPROV based on Win32 (PPC) Dynamic-Link Library.
"sampprov - Win32 (PPC) ppc Release" | Retail version of SAMPPROV based on Win32 (PPC) Dynamic-Link Library.

For additional information on using NMAKE, see the Developer Studio documentation.

Writing an OLE DB Provider: An Introduction

The purpose of this section is to offer some general guidelines on how to construct an OLE DB data provider.

Information Needed to Write an OLE DB Provider

OLE DB is based on the Component Object Model. Developers must be familiar with this model as well as with basic object-oriented coding practices to create an OLE DB provider. Although many different implementations are possible, the sample provider implements the TDataSource, TDBSession, and TRowset CoTypes. The sample provider uses the IOpenRowset interface, which is a required interface on the session for all providers. IOpenRowset is used by consumers that need access to the full data set and do not need to specify selection criteria.

Developers should become familiar with data source objects, sessions, and rowsets by reading the OLE DB Programmer’s Reference. In addition, developers may want to read the “Overview” in the OLE DB Programmer’s Reference to get a general introduction to OLE DB conventions, design considerations, and a complete introduction to the objects that make up OLE DB.

The Sample Provider

The sample provider offers access to a fixed-length text data file using OLE DB interfaces. The purpose of this sample provider is purely illustrative. The capabilities of the sample provider are limited; these limitations are discussed in “Sample Provider Overview,” previously in this paper.

The sample provider opens the sample data file and enables the consumer to read the data, using the IRowset interface, in a forward-only fashion. The consumer can update or delete the current row but cannot add new rows. The format of the data file is discussed in “Sample Provider Overview,” previously in this paper.

Objects and Interfaces Implemented by the Sample Provider

The sample provider implements three OLE DB objects.

  • The data source object, which enables consumers to connect and initialize the interaction with the data file.

  • The session, which enables consumers to create a rowset for the data set in the data file.

  • The rowset, which exposes a data set to the consumer.

These objects, along with the interfaces implemented in the sample, are described in detail in the following topics, which also contain some of the implementation code for specific interfaces and methods.

The following table lists the interfaces and methods implemented in the sample provider along with the name of the source code file in which each can be found.

Interface name | Method name | Source code file
IDBInitialize | Initialize | DBInit.cpp
 | Uninitialize | DBInit.cpp
IDBCreateSession | CreateSession | CrtSess.cpp
IDBProperties | GetProperties | UtilProp.cpp
 | GetPropertyInfo | UtilProp.cpp
 | SetProperties | UtilProp.cpp
IPersist | GetClassID | Persist.cpp
IGetDataSource | GetDataSource | DBSess.cpp
IOpenRowset | OpenRowset | OpnRowst.cpp
IColumnsInfo | GetColumnInfo | ColInfo.cpp
 | MapColumnIDs | ColInfo.cpp
IConvertType | CanConvert | CvtType.cpp
IAccessor | AddRefAccessor | Accessor.cpp
 | CreateAccessor | Accessor.cpp
 | GetBindings | Accessor.cpp
 | ReleaseAccessor | Accessor.cpp
IRowset | AddRefRows | IRowset.cpp
 | GetData | IRowset.cpp
 | GetNextRows | IRowset.cpp
 | ReleaseRows | IRowset.cpp
 | RestartPosition | IRowset.cpp
IRowsetInfo | GetProperties | RowInfo.cpp
 | GetReferencedRowset | RowInfo.cpp
 | GetSpecification | RowInfo.cpp
IRowsetChange | DeleteRows | RowChng.cpp
 | InsertRow | RowChng.cpp
 | SetData | RowChng.cpp
ISessionProperties | GetProperties | DBSess.cpp
 | SetProperties | DBSess.cpp

Setting and Getting Provider Properties

The data source object is the first object created when a consumer instantiates an OLE DB data provider by calling CoCreateInstance or through some other technique.

The data source object provides the starting point for communications between the provider and consumer. For example, a consumer can call CoCreateInstance and request an IDBInitialize interface pointer to instantiate a data source object. The provider’s developer must generate a CLSID (class ID) and store the ID in the Windows Registry. The consumer can use this CLSID with CoCreateInstance to instantiate the data source object. To facilitate this operation, providers should add their application or library information to the Windows Registry upon installation on a new system. The sample provider setup program registers the sample provider in the Windows Registry.

The data source object is also responsible for setting and returning information about the properties supported by the provider and exposing the list of supported keywords and literals. This functionality is supported through the mandatory IDBProperties interface and the optional IDBInfo interface. The IDBProperties interface contains three methods:

  • GetProperties returns the list of properties currently set on the data source object.

  • GetPropertyInfo returns information about supported rowset and data source properties.

  • SetProperties sets the properties on the data source object.

The IDBInfo interface contains two methods:

  • GetKeywords returns a list of supported keywords.

  • GetLiteralInfo returns information about literals used in text commands.

In the sample provider implementation, no command interfaces are implemented, so GetKeywords returns NULL. GetLiteralInfo is not implemented and returns E_NOTIMPL. GetProperties, GetPropertyInfo, and SetProperties are handled by passing the calls to the utility object that manages properties, which is found in UtilProp.cpp. The structure containing the properties known to the sample provider, which is also found in UtilProp.cpp, follows.

// Struct containing the properties we know about. The GUID and string fields are
// initialized in the constructor, because C++ makes it awkward to do so at declaration
// time. So, if you change this table, be sure to make parallel changes in CUtilProp::CUtilProp.
PROPSTRUCT s_rgprop[] =
 {
/* 0 */ {DBPROP_IAccessor,   FLAGS_ROWSETRO, VT_BOOL, TRUE,  0, NULL,   L"IAccessor"},
/* 1 */ {DBPROP_IColumnsInfo,  FLAGS_ROWSETRO, VT_BOOL, TRUE,  0, NULL,   L"IColumnsInfo"},
/* 2 */ {DBPROP_IConvertType,  FLAGS_ROWSETRO, VT_BOOL, TRUE,  0, NULL,   L"IConvertType"},
/* 3 */ {DBPROP_IRowset,   FLAGS_ROWSETRO, VT_BOOL, TRUE,  0, NULL,   L"IRowset"},
/* 4 */ {DBPROP_IRowsetChange, FLAGS_ROWSETRW, VT_BOOL, TRUE,  0, NULL,   L"IRowsetChange"},
/* 5 */ {DBPROP_IRowsetInfo,   FLAGS_ROWSETRO, VT_BOOL, TRUE,  0, NULL,   L"IRowsetInfo"},
/* 6 */ {DBPROP_ACTIVESESSIONS,  FLAGS_DATASOURCE, VT_I4, FALSE, 1, NULL,   L"Active Sessions"},
/* 7 */ {DBPROP_PROVIDERNAME,  FLAGS_DATASOURCE, VT_BSTR, FALSE, 0, L"SAMPPROV.DLL",L"Provider Name"},
/* 8 */ {DBPROP_PROVIDEROLEDBVER,FLAGS_DATASOURCE, VT_BSTR, FALSE, 0, L"01.10",   L"OLE DB Version"},
/* 9 */ {DBPROP_PROVIDERVER,   FLAGS_DATASOURCE, VT_BSTR, FALSE, 0, _TEXT(VER_PRODUCTVERSION_STR), L"Provider Version"},
/* 10*/ {DBPROP_INIT_DATASOURCE, FLAGS_DBINITRW, VT_BSTR, FALSE, 0, L"",    L"Data Source"},
/* 11*/ {DBPROP_INIT_HWND,   FLAGS_DBINITRW, VT_I4, FALSE, 0, NULL,   L"Window Handle"},
/* 12*/ {DBPROP_INIT_PROMPT,   FLAGS_DBINITRW, VT_I2, FALSE, 4, NULL,   L"Prompt"}
  };

The source code for GetPropertyInfo, which passes property information back to the provider, follows.

// CUtilProp::GetPropertyInfo  -----------------------------------------
//
// @mfunc Returns information about rowset and data source properties
//   supported by the provider
//
// @rdesc HRESULT
//  @flag S_OK    | The method succeeded
//  @flag E_INVALIDARG  | pcPropertyIDSets or prgPropertyInfo was NULL
//  @flag E_OUTOFMEMORY | Out of memory
//

STDMETHODIMP CUtilProp::GetPropertyInfo
  (
  BOOL      fDSOInitialized,   //@parm IN  | if Initialized
  ULONG     cPropertyIDSets,   //@parm IN  | # properties
  const DBPROPIDSET rgPropertyIDSets[],  //@parm IN  | Array of property sets
  ULONG*    pcPropertyInfoSets,  //@parm OUT | # DBPROPSET structures
  DBPROPINFOSET** prgPropertyInfoSets, //@parm OUT | DBPROPSET structures property
               //    | information returned
  WCHAR**     ppDescBuffer   //@parm OUT | Property descriptions
  )
{
 BOOL     fRet     = TRUE;
 BOOL     fPropsinError  = FALSE;
 BOOL     fPropsSucceed  = FALSE;
 BOOL     fIsSpecialGUID   = FALSE;
 BOOL     fIsNotSpecialGUID  = FALSE;
 ULONG    cProps     = 0;
 ULONG    cCount     = 0;
 ULONG    ulPropertySets   = 0;
 WCHAR*   pDescBuffer    = NULL;
 DBPROPINFO*  pPropInfo;
 DBPROPINFOSET* pPropInfoSet;

 // init out params
 if (pcPropertyInfoSets)
  *pcPropertyInfoSets  = 0;
 if (prgPropertyInfoSets)
  *prgPropertyInfoSets = NULL;
 if (ppDescBuffer)
  *ppDescBuffer = NULL;

 // Check Arguments, on failure post HRESULT to error queue
 if( ((cPropertyIDSets > 0) && !rgPropertyIDSets) ||
  !pcPropertyInfoSets || !prgPropertyInfoSets )
  return ResultFromScode( E_INVALIDARG );

 // New argument check for > 1 cPropertyIDs and NULL pointer for
 // array of property ids.
 for(ULONG ul=0; ul<cPropertyIDSets; ul++)
 {
  if( rgPropertyIDSets[ul].cPropertyIDs && !(rgPropertyIDSets[ul].rgPropertyIDs) )
   return ResultFromScode( E_INVALIDARG );

  if (DBPROPSET_ROWSETALL == rgPropertyIDSets[ul].guidPropertySet    ||
   DBPROPSET_DATASOURCEALL == rgPropertyIDSets[ul].guidPropertySet   ||
   DBPROPSET_DATASOURCEINFOALL == rgPropertyIDSets[ul].guidPropertySet ||
   DBPROPSET_SESSIONALL == rgPropertyIDSets[ul].guidPropertySet    ||
   DBPROPSET_DBINITALL == rgPropertyIDSets[ul].guidPropertySet)
   fIsSpecialGUID = TRUE;
  else
   fIsNotSpecialGUID = TRUE;

  if(fIsSpecialGUID && fIsNotSpecialGUID)
   return ResultFromScode( E_INVALIDARG );
 }

 // save the count of PropertyIDSets
 cProps = cPropertyIDSets;

 // If the consumer does not restrict the property sets
 // by specify an array of property sets and a cPropertySets
 // greater than 0, then we need to make sure we
 // have some to return
 if ( (cPropertyIDSets == 0) )
 {
  if( fDSOInitialized )
   cProps = NUMBER_OF_SUPPORTED_PROPERTY_SETS;
  else
   cProps = 1;
 }

 // use task memory allocater to alloc a DBPROPINFOSET struct
 pPropInfoSet = (DBPROPINFOSET*) g_pIMalloc->Alloc(cProps *
               sizeof( DBPROPINFOSET ));
 if ( !pPropInfoSet )
  return ResultFromScode( E_OUTOFMEMORY );

 memset( pPropInfoSet, 0, (cProps * sizeof( DBPROPINFOSET )));

 // Alloc memory for ppDescBuffer
 if ( ppDescBuffer )
 {
  pDescBuffer = (WCHAR*)g_pIMalloc->Alloc(NUMBER_OF_SUPPORTED_PROPERTIES *
   CCH_GETPROPERTYINFO_DESCRIP_BUFFER_SIZE * sizeof(WCHAR) );

  if( pDescBuffer )
  {
   memset(pDescBuffer, 0, (NUMBER_OF_SUPPORTED_PROPERTIES *
    CCH_GETPROPERTYINFO_DESCRIP_BUFFER_SIZE * sizeof(WCHAR)));

   *ppDescBuffer = pDescBuffer;
  }
  else
  {
   g_pIMalloc->Free( pPropInfoSet );
   return ResultFromScode( E_OUTOFMEMORY );
  }
 }

 // For each supported Property Set
 for (ulPropertySets=0; ulPropertySets < cProps; ulPropertySets++)
 {
  BOOL fGetAllProps = FALSE;

  // If no restrictions return all properties from the three supported property sets
  if( cPropertyIDSets == 0 )
  {
   fGetAllProps = TRUE;

   // only do this once
   if (ulPropertySets == 0)
   {
    pPropInfoSet[0].guidPropertySet = DBPROPSET_DBINIT;
    pPropInfoSet[0].cPropertyInfos  = NUMBER_OF_SUPPORTED_DBINIT_PROPERTIES;

    if ( fDSOInitialized )
    {
     pPropInfoSet[1].guidPropertySet = DBPROPSET_DATASOURCEINFO;
     pPropInfoSet[1].cPropertyInfos  = NUMBER_OF_SUPPORTED_DATASOURCEINFO_PROPERTIES;
     pPropInfoSet[2].guidPropertySet = DBPROPSET_ROWSET;
     pPropInfoSet[2].cPropertyInfos  = NUMBER_OF_SUPPORTED_ROWSET_PROPERTIES;
    }
   }
  }
  else
  {
   pPropInfoSet[ulPropertySets].guidPropertySet = rgPropertyIDSets[ulPropertySets].guidPropertySet;
   pPropInfoSet[ulPropertySets].cPropertyInfos  = rgPropertyIDSets[ulPropertySets].cPropertyIDs;

   if ((rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DBINITALL) ||
    (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DBINIT   &&
     rgPropertyIDSets[ulPropertySets].cPropertyIDs == 0))
   {
    fGetAllProps = TRUE;
    pPropInfoSet[ulPropertySets].guidPropertySet = DBPROPSET_DBINIT;
    pPropInfoSet[ulPropertySets].cPropertyInfos  = NUMBER_OF_SUPPORTED_DBINIT_PROPERTIES;
   }
   else if (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCEALL)
   {
    // Since we do not support it should return DB_E_ERRORSOCCURRED with 0 & NULL
    fPropsinError = TRUE;
    pPropInfoSet[ulPropertySets].guidPropertySet = DBPROPSET_DATASOURCE;
    pPropInfoSet[ulPropertySets].cPropertyInfos  = 0;
   }
   else if( fDSOInitialized )
   {
    if( (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCEINFOALL) ||
     ((rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCEINFO) &&
      (rgPropertyIDSets[ulPropertySets].cPropertyIDs == 0)) )
    {
     fGetAllProps = TRUE;
     pPropInfoSet[ulPropertySets].guidPropertySet = DBPROPSET_DATASOURCEINFO;
     pPropInfoSet[ulPropertySets].cPropertyInfos  = NUMBER_OF_SUPPORTED_DATASOURCEINFO_PROPERTIES;
    }
    else if (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_SESSIONALL)
    {
     // Since we do not support it should return a error with 0 & NULL
     fPropsinError = TRUE;
     pPropInfoSet[ulPropertySets].guidPropertySet = DBPROPSET_SESSION;
     pPropInfoSet[ulPropertySets].cPropertyInfos  = 0;
    }
    else if( (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_ROWSETALL) ||
       ((rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_ROWSET) &&
        (rgPropertyIDSets[ulPropertySets].cPropertyIDs == 0)) )
    {
     fGetAllProps = TRUE;
     pPropInfoSet[ulPropertySets].guidPropertySet = DBPROPSET_ROWSET;
     pPropInfoSet[ulPropertySets].cPropertyInfos  = NUMBER_OF_SUPPORTED_ROWSET_PROPERTIES;
    }
    else if (rgPropertyIDSets[ulPropertySets].cPropertyIDs == 0)
     fPropsinError = TRUE;
   }
   else if (rgPropertyIDSets[ulPropertySets].cPropertyIDs == 0)
   {
    // Since we do not support it should return a error with 0 & NULL
    fPropsinError = TRUE;
   }
  }

  if (pPropInfoSet[ulPropertySets].cPropertyInfos)
  {
    // use task memory allocater to alloc array of DBPROPINFO structs
   pPropInfo = (DBPROPINFO*) g_pIMalloc->Alloc(sizeof( DBPROPINFO ) *
         pPropInfoSet[ulPropertySets].cPropertyInfos);

   if (!pPropInfo)
   {
    if ( ppDescBuffer )
     *ppDescBuffer = NULL;
    g_pIMalloc->Free( pPropInfoSet );
    g_pIMalloc->Free( pDescBuffer );

    return ResultFromScode( E_OUTOFMEMORY );
   }

   pPropInfoSet[ulPropertySets].rgPropertyInfos = &pPropInfo[0];

   memset( pPropInfo, 0,
    (pPropInfoSet[ulPropertySets].cPropertyInfos * sizeof( DBPROPINFO )));
  }

   // for each prop in our table..
  for (cCount=0; cCount < pPropInfoSet[ulPropertySets].cPropertyInfos; cCount++)
  {
   // init the Variant right up front
   // that way we can VariantClear with no worried (if we need to)
   VariantInit( &pPropInfo[cCount].vValues );

   // set the description pointer
   pPropInfo[cCount].pwszDescription = pDescBuffer;

   // Check supported property sets
   if ( (pPropInfoSet[ulPropertySets].guidPropertySet == DBPROPSET_DBINIT) &&
     (fGetAllProps) )
   {
    // load up their DBPROPINFO from our table
    fPropsSucceed = TRUE;
    fRet = LoadDBPROPINFO( &m_rgproperties[START_OF_SUPPORTED_DBINIT_PROPERTIES + cCount],
       &pPropInfo[cCount] );
   }
   else if ( (pPropInfoSet[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCEINFO) &&
      fGetAllProps && fDSOInitialized)
   {
    // load up their DBPROPINFO from our table
    fPropsSucceed = TRUE;
    fRet = LoadDBPROPINFO( &m_rgproperties[START_OF_SUPPORTED_DATASOURCEINFO_PROPERTIES + cCount],
       &pPropInfo[cCount] );
   }

   else if ( (pPropInfoSet[ulPropertySets].guidPropertySet == DBPROPSET_ROWSET) &&
       fGetAllProps && fDSOInitialized)
   {
    // load up their DBPROPINFO from our table
    fPropsSucceed = TRUE;
    fRet = LoadDBPROPINFO( &m_rgproperties[START_OF_SUPPORTED_ROWSET_PROPERTIES + cCount],
       &pPropInfo[cCount] );
   }
   else
   {
    ULONG ulIndex;

    pPropInfo[cCount].dwPropertyID = rgPropertyIDSets[ulPropertySets].rgPropertyIDs[cCount];
    pPropInfo[cCount].dwFlags  = DBPROPFLAGS_NOTSUPPORTED;

    if ( (GetPropIndex(rgPropertyIDSets[ulPropertySets].rgPropertyIDs[cCount], &ulIndex)) &&
      (fDSOInitialized || (pPropInfoSet[ulPropertySets].guidPropertySet == DBPROPSET_DBINIT)) )
    {
     fPropsSucceed = TRUE;
     fRet = LoadDBPROPINFO( &m_rgproperties[ulIndex], &pPropInfo[cCount] );
    }
    else
    {
     fPropsinError = TRUE;
     pPropInfo[cCount].pwszDescription = NULL;
    }
   }

   if (!fRet)
   {
    ULONG ulFor;

    // something went wrong
    // clear all variants used so far..
    for (ulFor = 0; ulFor < cCount; ulFor++)
     VariantClear( &pPropInfo[ulFor].vValues );

    // .. delete the pPropInfo array, return failure
    if ( ppDescBuffer ) *ppDescBuffer = NULL;
    g_pIMalloc->Free( pPropInfo );
    g_pIMalloc->Free( pPropInfoSet );
    g_pIMalloc->Free( pDescBuffer );
    return ResultFromScode( E_FAIL );
   }

   // move the description pointer to the next
   if ( pPropInfo[cCount].pwszDescription )
    pDescBuffer += (wcslen(pPropInfo[cCount].pwszDescription) + sizeof(CHAR));
  }
  // Set local back to FALSE
  fGetAllProps = FALSE;
 }

 // set count of properties and property information
 *pcPropertyInfoSets  = cProps;
 *prgPropertyInfoSets = pPropInfoSet;

 if ( !fPropsSucceed && fPropsinError )
 {
  if ( ppDescBuffer ) *ppDescBuffer = NULL;
  g_pIMalloc->Free( pDescBuffer );
  return ResultFromScode( DB_E_ERRORSOCCURRED );
 }
 else if ( fPropsSucceed && fPropsinError )
  return ResultFromScode( DB_S_ERRORSOCCURRED );
 else
  return ResultFromScode( S_OK );
}

The source code for GetProperties, which returns the current settings of all supported properties, follows. Immediately following the creation of the data source object, none of the properties are set. Properties are set by the SetProperties method.

// CUtilProp::GetProperties ----------------------------------------------------
//
// @mfunc Returns current settings of all properties supported by the DSO/rowset
//
// @rdesc HRESULT
//  @flag S_OK    | The method succeeded
//  @flag E_INVALIDARG  | pcProperties or prgPropertyInfo was NULL
//  @flag E_OUTOFMEMORY | Out of memory
//
STDMETHODIMP CUtilProp::GetProperties
  (
  DWORD     dwBitMask,     //@parm IN  | Mask if Initialized
  ULONG     cPropertyIDSets,   //@parm IN  | # of restiction property IDs
  const DBPROPIDSET rgPropertyIDSets[],  //@parm IN  | restriction guids
  ULONG*      pcPropertySets,  //@parm OUT | count of properties returned
  DBPROPSET**   prgPropertySets  //@parm OUT | property information returned
  )
{
 BOOL   fRet     = TRUE;
 BOOL   fPropsinError  = FALSE;
 BOOL   fPropsSucceed  = FALSE;
 ULONG    cProps   = 0;
 ULONG    cCount   = 0;
 ULONG    ulPropertySets = 0;
 DBPROP*  pProp;
 DBPROPSET* pPropSet;

 // save the count of PropertyIDSets
 cProps = cPropertyIDSets;

 // If the consumer does not restrict the property sets
 // by specify an array of property sets and a cPropertySets
 // greater than 0, then we need to make sure we
 // have some to return
 if( cPropertyIDSets == 0 )
 {
  // only allow the DBINIT and DATASOURCE if Initialized
  if ( (dwBitMask & PROPSET_DSOINIT) == PROPSET_DSOINIT )
   cProps = 2;
  else
   cProps = 1;
 }

 // use task memory allocater to alloc a DBPROPINFOSET struct
 pPropSet = (DBPROPSET*) g_pIMalloc->Alloc(cProps *
               sizeof( DBPROPSET ));
 if ( !pPropSet )
  return ResultFromScode( E_OUTOFMEMORY );

 memset( pPropSet, 0, (cProps * sizeof( DBPROPSET )));

 // For each supported Property Set
 for (ulPropertySets=0; ulPropertySets < cProps; ulPropertySets++)
 {
  BOOL fGetAllProps = FALSE;

  // If no restrictions return all properties from the three supported property sets
  if ( cPropertyIDSets == 0 )
  {
    fGetAllProps = TRUE;

    // only do this once
    if ( ulPropertySets == 0 )
    {
     if( !(dwBitMask & PROPSET_SESSION) )
     {
      if ( !(dwBitMask & PROPSET_ROWSET) )
      {
        pPropSet[0].guidPropertySet = DBPROPSET_DBINIT;
        pPropSet[0].cProperties  = NUMBER_OF_SUPPORTED_DBINIT_PROPERTIES;

       if( dwBitMask & PROPSET_INIT )
       {
        pPropSet[1].guidPropertySet = DBPROPSET_DATASOURCEINFO;
        pPropSet[1].cProperties  = NUMBER_OF_SUPPORTED_DATASOURCEINFO_PROPERTIES;
       }
      }
      else
      {
       pPropSet[0].guidPropertySet = DBPROPSET_ROWSET;
       pPropSet[0].cProperties  = NUMBER_OF_SUPPORTED_ROWSET_PROPERTIES;
      }
     }
    }
  }
  else
  {
   pPropSet[ulPropertySets].guidPropertySet = rgPropertyIDSets[ulPropertySets].guidPropertySet;
   pPropSet[ulPropertySets].cProperties  = rgPropertyIDSets[ulPropertySets].cPropertyIDs;

   if( rgPropertyIDSets[ulPropertySets].cPropertyIDs == 0 )
   {
    fGetAllProps = TRUE;

    if( rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DBINIT )
    {
     pPropSet[ulPropertySets].cProperties  = NUMBER_OF_SUPPORTED_DBINIT_PROPERTIES;
    }
    else if( (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCEINFO) &&
       ((dwBitMask & PROPSET_DSOINIT) == PROPSET_DSOINIT) )
    {
     pPropSet[ulPropertySets].cProperties  = NUMBER_OF_SUPPORTED_DATASOURCEINFO_PROPERTIES;
    }
    else if( (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_ROWSET) &&
       (dwBitMask & PROPSET_ROWSET))
    {
     pPropSet[ulPropertySets].cProperties  = NUMBER_OF_SUPPORTED_ROWSET_PROPERTIES;
    }
    else
    {
     fGetAllProps = FALSE;
    }
   }
  }

  if( pPropSet[ulPropertySets].cProperties )
  {
    // use task memory allocater to alloc array of DBPROPINFO structs
   pProp = (DBPROP*) g_pIMalloc->Alloc(sizeof( DBPROP ) *
           pPropSet[ulPropertySets].cProperties);

   if (!pProp)
   {
    for(ULONG ul=0; ul<ulPropertySets; ul++)
    {
     for(ULONG ul2=0; ul2<pPropSet[ul].cProperties; ul2++)
      VariantClear( &pPropSet[ul].rgProperties[ul2].vValue );

     g_pIMalloc->Free( pPropSet[ul].rgProperties );
    }
    g_pIMalloc->Free( pPropSet );

    return ResultFromScode( E_OUTOFMEMORY );
   }

   pPropSet[ulPropertySets].rgProperties = &pProp[0];

   memset( pProp, 0,
    (pPropSet[ulPropertySets].cProperties * sizeof( DBPROP )));
  }

   // for each prop in our table..
  for (cCount=0; cCount < pPropSet[ulPropertySets].cProperties; cCount++)
  {
   // init the Variant right up front
   // that way we can VariantClear with no worried (if we need to)
   VariantInit( &pProp[cCount].vValue );

   // Check supported property sets
   if ( pPropSet[ulPropertySets].guidPropertySet == DBPROPSET_DBINIT &&
     fGetAllProps )
   {
    fPropsSucceed = TRUE;
    // load up their DBPROP from our table
    fRet = LoadDBPROP( &m_rgproperties[START_OF_SUPPORTED_DBINIT_PROPERTIES + cCount],
          &pProp[cCount] );
   }
   else if ( pPropSet[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCEINFO &&
      fGetAllProps )
   {
    fPropsSucceed = TRUE;
    // load up their DBPROPINFO from our table
    fRet = LoadDBPROP( &m_rgproperties[START_OF_SUPPORTED_DATASOURCEINFO_PROPERTIES + cCount],
          &pProp[cCount] );
   }
   else if ( pPropSet[ulPropertySets].guidPropertySet == DBPROPSET_ROWSET &&
      fGetAllProps )
   {
    fPropsSucceed = TRUE;
    // load up their DBPROPINFO from our table
    fRet = LoadDBPROP( &m_rgproperties[START_OF_SUPPORTED_ROWSET_PROPERTIES + cCount],
          &pProp[cCount] );
   }
   else
   {
    ULONG ulIndex;

    pProp[cCount].dwPropertyID = rgPropertyIDSets[ulPropertySets].rgPropertyIDs[cCount];
    pProp[cCount].dwStatus  = DBPROPSTATUS_NOTSUPPORTED;

    if(  (GetPropIndex(rgPropertyIDSets[ulPropertySets].rgPropertyIDs[cCount], &ulIndex)) &&
      (((dwBitMask & PROPSET_DSO) &&
      (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DBINIT)) ||
       (((dwBitMask & PROPSET_DSOINIT) == PROPSET_DSOINIT) &&
      ((rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCE) ||
      (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCEINFO))) ||
       ((dwBitMask & PROPSET_SESSION) &&
      (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_SESSION)) ||
       ((dwBitMask & PROPSET_ROWSET) &&
      (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_ROWSET))) )
    {
     fPropsSucceed = TRUE;
     fRet = LoadDBPROP( &m_rgproperties[ulIndex], &pProp[cCount] );
    }
    else
     fPropsinError = TRUE;
   }

   if (!fRet)
   {
    ULONG ulFor;

    // something went wrong
    // clear all variants used so far..
    for (ulFor = 0; ulFor < cCount; ulFor++)
     VariantClear( &pProp[ulFor].vValue );

    // .. delete the pPropInfo array, return failure
    g_pIMalloc->Free( pProp );
    g_pIMalloc->Free( pPropSet );
    return ResultFromScode( E_FAIL );
   }
  }
  // Set local back to FALSE
  fGetAllProps = FALSE;
 }

 // set count of properties and property information
 *pcPropertySets  = cProps;
 *prgPropertySets = pPropSet;

 if ( !fPropsSucceed && fPropsinError )
  return ResultFromScode( DB_E_ERRORSOCCURRED );
 else if ( fPropsSucceed && fPropsinError )
  return ResultFromScode( DB_S_ERRORSOCCURRED );
 else
  return ResultFromScode( S_OK );
}

The source code for SetProperties, which sets the values of all supported properties, follows. The consumer must set properties on the provider before initializing the data source object. In most providers, these properties provide information such as a database location, database name, user ID, and password. The sample provider requires only one value: a valid directory path to the data file.

// CUtilProp::SetProperties ----------------------------------------------------
//
// @mfunc Set current settings of properties supported by the DSO/rowset
//
// @rdesc HRESULT
//  @flag S_OK    | The method succeeded
//  @flag E_INVALIDARG  | pcProperties or prgPropertyInfo was NULL
//  @flag E_OUTOFMEMORY | Out of memory
//
STDMETHODIMP CUtilProp::SetProperties
  (
  DWORD    dwBitMask,   //@parm IN  Type of PropSet
  ULONG    cPropertyIDSets, //@parm IN  # of DBPROPSET
  DBPROPSET    rgPropertyIDSets[] //@parm INOUT Array of property sets
 )
{
 ULONG ulPropertySets  = 0;
 ULONG cCount    = 0;
 BOOL  fSetAllProps  = TRUE;
 BOOL  fOnePropSet   = FALSE;

 // For each supported Property Set
 for (ulPropertySets=0; ulPropertySets < cPropertyIDSets; ulPropertySets++)
 {
  ULONG ulIndex = 0;

   // for each prop in the propset
  for (cCount=0; cCount < rgPropertyIDSets[ulPropertySets].cProperties; cCount++)
  {
   rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_OK;

   // only allow the DBINIT and DATASOURCE
   if ( (dwBitMask & PROPSET_DSO) &&
     ((rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DBINIT) &&
      (dwBitMask & PROPSET_INIT)) )
   {
     rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_NOTSETTABLE;
     fSetAllProps = FALSE;
   }
   else if ( ((dwBitMask & PROPSET_DSO) &&
      (rgPropertyIDSets[ulPropertySets].guidPropertySet != DBPROPSET_DATASOURCE &&
       rgPropertyIDSets[ulPropertySets].guidPropertySet != DBPROPSET_DBINIT)) ||
       (rgPropertyIDSets[ulPropertySets].guidPropertySet == DBPROPSET_DATASOURCE &&
      !(dwBitMask & PROPSET_INIT)) )
   {
     rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_NOTSUPPORTED;
     fSetAllProps = FALSE;
   }
   // only allow the SESSION
   else if ( (dwBitMask & PROPSET_SESSION) &&
       (rgPropertyIDSets[ulPropertySets].guidPropertySet != DBPROPSET_SESSION) )
   {
     rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_NOTSUPPORTED;
     fSetAllProps = FALSE;
   }
   // only allow the ROWSET
   else if ( (dwBitMask & PROPSET_ROWSET) &&
       (rgPropertyIDSets[ulPropertySets].guidPropertySet != DBPROPSET_ROWSET) )
   {
     rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_NOTSUPPORTED;
     fSetAllProps = FALSE;
   }
   else
   {
    // get the index in the array
    if ( GetPropIndex(rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwPropertyID, &ulIndex) )
    {
     // arg checking for the prop
     if ( (DBPROPOPTIONS_SETIFCHEAP != rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwOptions) &&
       (DBPROPOPTIONS_REQUIRED != rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwOptions) )
     {
      rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_BADOPTION;
       fSetAllProps = FALSE;
     }
     else if ( ((m_rgproperties[ulIndex].vtType != rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue.vt) &&
        (VT_EMPTY != rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue.vt)) ||
         (IsValidValue(&rgPropertyIDSets[ulPropertySets].rgProperties[cCount]) == S_FALSE))
     {
      rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_BADVALUE;
       fSetAllProps = FALSE;
     }
     else if ( !(m_rgproperties[ulIndex].dwFlags & DBPROPFLAGS_WRITE) )
     {
      if ( ((rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue.vt == VT_BOOL) &&
         (rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue.boolVal != m_rgproperties[ulIndex].boolVal)) ||
        ((rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue.vt == VT_I4) &&
         (rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue.lVal != m_rgproperties[ulIndex].longVal)) ||
        ((rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue.vt == VT_BSTR) &&
         (wcscmp(rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue.bstrVal,m_rgproperties[ulIndex].pwstrVal))) )
      {
       rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_NOTSETTABLE;
        fSetAllProps = FALSE;
      }
      else
       fOnePropSet = TRUE;

     }
     else
     {
      fOnePropSet = TRUE;

      if( rgPropertyIDSets[ulPropertySets].rgProperties->vValue.vt == VT_EMPTY )
      {
       memcpy( &m_rgproperties[ulIndex], &s_rgprop[ulIndex], sizeof(PROPSTRUCT));
      }
      else
      {
       // switch on the propid
       switch( m_rgproperties[ulIndex].dwPropertyID )
       {
        case DBPROP_INIT_DATASOURCE:
         wcscpy(m_wszFilePath, V_BSTR(&rgPropertyIDSets[ulPropertySets].rgProperties->vValue));
         m_rgproperties[ulIndex].pwstrVal = m_wszFilePath;
         break;

        case DBPROP_INIT_HWND:
        case DBPROP_INIT_PROMPT:
         m_rgproperties[ulIndex].longVal = V_I4(&rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue);
         break;

        case DBPROP_IRowsetChange:
         m_rgproperties[ulIndex].boolVal = V_BOOL(&rgPropertyIDSets[ulPropertySets].rgProperties[cCount].vValue);
         break;
       }
      }
     }
    }
    else
    {
     rgPropertyIDSets[ulPropertySets].rgProperties[cCount].dwStatus = DBPROPSTATUS_NOTSUPPORTED;
     fSetAllProps = FALSE;
    }
   }
  }
 }

 // Figure out the retcode
 if ( fSetAllProps )
  return ResultFromScode( S_OK );
 else if ( fOnePropSet && !fSetAllProps )
  return ResultFromScode( DB_S_ERRORSOCCURRED );
 else
  return ResultFromScode( DB_E_ERRORSOCCURRED );
}

Initializing and Uninitializing the Data Source Object

The IDBInitialize interface contains two methods: Initialize and Uninitialize. The following sections discuss the implementation of these methods in the sample provider.

Initializing the Data Source Object

Initialize enables consumers to explicitly initialize a data source object. Consumers must set properties on the data source object before attempting to initialize it; and consumers must supply a valid directory path to the data file in IDBProperties::SetProperties. If the directory path is invalid, the sample provider returns an E_FAIL error on initialization. The source code for IDBInitialize::Initialize follows; you can find the complete source code for IDBInitialize in DBInit.cpp.

// CImpIDBInitialize::Initialize ---------------------------------------------
//
// @mfunc Initializes the DataSource object.. For this provider it requires
// that a valid path is given to where the file will be located.
//
// @rdesc HRESULT
//  @flag S_OK    | Path exists
//  @flag E_FAIL    | Invalid path
//  @flag E_INVALIDARG  | Invalid Parameters passed in
//  @flag DB_E_ALREADYINITIALIZED | Datasource Object already initialized
//
STDMETHODIMP CImpIDBInitialize::Initialize
  (
  )
{
 HRESULT   hr;
 DBPROPIDSET rgPropertyIDSets[1];
 ULONG   cPropertySets;
 DBPROPSET*  prgPropertySets;
 DBPROPID  rgPropId[2];

 char szNewVal[MAX_PATH ];
 int  nPrompt = DBPROMPT_NOPROMPT;

 assert( m_pObj );

 if (m_pObj->m_fDSOInitialized)
  return ResultFromScode( DB_E_ALREADYINITIALIZED );

 rgPropId[0]        = DBPROP_INIT_DATASOURCE;
 rgPropId[1]        = DBPROP_INIT_PROMPT;

 rgPropertyIDSets[0].guidPropertySet = DBPROPSET_DBINIT;
 rgPropertyIDSets[0].rgPropertyIDs   = rgPropId;
 rgPropertyIDSets[0].cPropertyIDs  = 2;

 // Get the value of the DBPROP_INIT_DATASOURCE property
 hr = m_pObj->m_pUtilProp->GetProperties(
         PROPSET_DSO,
         1,
         rgPropertyIDSets,
         &cPropertySets,
         &prgPropertySets );

 // On failure treat it as if we were opening with prompt..
 if( SUCCEEDED(hr) && (prgPropertySets[0].rgProperties[0].vValue.vt) )
  WideCharToMultiByte( CP_ACP, 0, V_BSTR(&prgPropertySets[0].rgProperties->vValue), -1,
          szNewVal, sizeof( szNewVal ), NULL, NULL );
 else
  *szNewVal = '\0';

 // Get the Prompt value
 nPrompt = V_I2(&prgPropertySets[0].rgProperties[1].vValue);

 // Free the memory
 if (prgPropertySets)
 {
  for(ULONG ulIndex=0; ulIndex<prgPropertySets[0].cProperties; ulIndex++)
   VariantClear(&prgPropertySets[0].rgProperties[ulIndex].vValue);

  g_pIMalloc->Free(prgPropertySets[0].rgProperties);
  g_pIMalloc->Free(prgPropertySets);
 }

 // if caller didn't supply a directory path, ask the user
 if (nPrompt != DBPROMPT_NOPROMPT)
 {
  if (!BrowseDirs( GetDesktopWindow(), g_hInstance, NULL, szNewVal ))
   {
    return ResultFromScode( E_FAIL );
   }
   else
   {
    lstrcpyn( m_pObj->m_szPath, szNewVal, MAX_PATH );
    m_pObj->m_fDSOInitialized = TRUE;
    return ResultFromScode( S_OK );
   }
 }
 else  // caller did supply a directory path
 {
  // Get the current Directory
  char szCurrentDir[MAX_PATH];
  GetCurrentDirectory( MAX_PATH, szCurrentDir );

   // Check if the directory is a valid directory
  if (SetCurrentDirectory( szNewVal ))
  {
   // Restore to the original directory.
   SetCurrentDirectory( szCurrentDir );

   lstrcpyn( m_pObj->m_szPath, szNewVal, MAX_PATH );
   m_pObj->m_fDSOInitialized = TRUE;
   return ResultFromScode( S_OK );
  }
  else
  {
   return ResultFromScode( E_FAIL );
  }
 }
}

Uninitializing the Data Source Object

Uninitialize enables consumers to return the data source object to an uninitialized state. It is an error to call IDBInitialize::Uninitialize when there are open sessions or rowsets on the data source object. The source code for IDBInitialize::Uninitialize follows.

// CImpIDBInitialize::Uninitialize ---------------------------------------------
//
// @mfunc Returns the Data Source Object to an uninitialized state
//
// @rdesc HRESULT
//  @flag S_OK    | The method succeeded
//  @flag DB_E_OBJECTOPEN | A DBSession object was already created
//
STDMETHODIMP CImpIDBInitialize::Uninitialize
  (
  void
  )
{
 assert( m_pObj );

 if (!m_pObj->m_fDSOInitialized)
 {
  // data source object is not initialized; do nothing
  return ResultFromScode( S_OK );
 }
 else
 {
  if (!m_pObj->m_fDBSessionCreated)
  {
   // DSO initialized, but no DBSession has been created.
   // So, reset DSO to uninitialized state
   m_pObj->m_fDSOInitialized = FALSE;
   return ResultFromScode( S_OK );
  }
  else
  {
   // DBSession has already been created; trying to uninit
   // the DSO now is an error
   return ResultFromScode( DB_E_OBJECTOPEN );
  }
 }
}

Getting the Class ID of the Data Source Object

Consumers can retrieve the class ID of the data source object by calling IPersist::GetClassID. Generally, the consumer does this only when the provider does not support IPersistFile and the consumer wants to persist the class ID and initialization property values for later use.

The source code for IPersist::GetClassID follows; you can find the complete source code for the IPersist interface in Persist.cpp.

// CImpIPersist::GetClassID --------------------------------------------------
//
// @mfunc Get the CLSID of the DSO.
//
// @rdesc HRESULT
//  @flag S_OK      | The method succeeded.
//  @flag E_FAIL      | Provider-specific error.
//
STDMETHODIMP CImpIPersist::GetClassID( CLSID *pClassID )
{
 // Check the pointer
 if (pClassID)
 {
  memcpy( pClassID, &CLSID_SampProv, sizeof(CLSID) );
  return ResultFromScode(S_OK);
 }
 return ResultFromScode(E_FAIL);
}

Creating a Session

After you initialize the data source object, you must create a session object to manage the session and provide the framework needed to create a rowset with IOpenRowset::OpenRowset. The IDBCreateSession::CreateSession interface on the data source object enables you to create a new session object and returns an interface pointer to the session.

The source code for IDBCreateSession::CreateSession follows; you can find the complete source code for the IDBCreateSession interface in CrtSess.cpp.

// CImpIDBCreateSession::CreateSession ------------------------------------------------
//
// @mfunc Creates a new DB Session object from the DSO, and returns the
// requested interface on the newly created object.
//
// @rdesc HRESULT
//  @flag S_OK      | The method succeeded.
//  @flag E_INVALIDARG    | ppDBSession was NULL
//  @flag DB_E_NOAGGREGATION  | pUnkOuter was not NULL (this object does not support
//            being aggregated)
//  @flag E_FAIL      | Provider-specific error. This provider can only create
//            one DBSession
//  @flag E_OUTOFMEMORY   | Out of memory
//  @flag E_NOINTERFACE   | Could not obtain requested interface on DBSession object
//
STDMETHODIMP CImpIDBCreateSession::CreateSession
  (
  IUnknown* pUnkOuter,  //@parm IN | Controlling IUnknown if being aggregated
  REFIID  riid,   //@parm IN | The ID of the interface
  IUnknown**  ppDBSession //@parm OUT | A pointer to memory in which to return the interface pointer
  )
{
 CDBSession* pDBSession = NULL;
 HRESULT hr;

 // check in-params and NULL out-params in case of error
 if (ppDBSession)
  *ppDBSession = NULL;
 else
  return ResultFromScode( E_INVALIDARG );

 if (pUnkOuter)
  return ResultFromScode( DB_E_NOAGGREGATION );

 assert( m_pObj );

 // Check to see if the DSO is Uninitialized
 if (!m_pObj->m_fDSOInitialized)
  return ResultFromScode( E_UNEXPECTED );

 // this Data Source object can only create 1 DBSession...
 if (m_pObj->m_fDBSessionCreated)
  return ResultFromScode( E_FAIL );

 // open a DBSession object
 pDBSession = new CDBSession( NULL );
 if (!pDBSession)
  return ResultFromScode( E_OUTOFMEMORY );

 // initialize the object
 if (!pDBSession->FInit( m_pObj->m_szPath, m_pObj ))
 {
  delete pDBSession;
  return ResultFromScode( E_FAIL );
 }

 // get requested interface pointer on DBSession
 hr = pDBSession->QueryInterface( riid, (void **) ppDBSession );
 if (FAILED( hr ))
 {
  delete pDBSession;
  return ResultFromScode( hr );
 }

 // all went well
 m_pObj->m_fDBSessionCreated = TRUE;
 return ResultFromScode( S_OK );
}

Once the session has been created, the provider must expose the interface pointer to the data source object that created the session. This interface pointer is exposed through the mandatory interface IGetDataSource. The code for IGetDataSource::GetDataSource follows; you can find the complete source code for the IGetDataSource interface in DBSess.cpp.

//-----------------------------------------------------------------------------
// CImpIGetDataSource::GetDataSource
//
// @mfunc Retrieve an interface pointer on the session object
//
// @rdesc
//  @flag S_OK | Session Object Interface returned
//  @flag E_INVALIDARG | ppDataSource was NULL
//  @flag E_NOINTERFACE | IID not supported
//
STDMETHODIMP CImpIGetDataSource::GetDataSource
 (
 REFIID  riid,   // @parm IN  | IID desired
 IUnknown** ppDataSource // @parm OUT | ptr to interface
 )
{
 // Check Function Arguments
 if( ppDataSource == NULL )
  return ResultFromScode(E_INVALIDARG);

 assert( m_pObj->m_pCDataSource );
 return ((m_pObj->m_pCDataSource)->QueryInterface(riid, (LPVOID*)ppDataSource));
}

Creating a Rowset

The session contains the interface that enables consumers to open a data file and create a rowset object containing all rows in the data file. The sample provider implements IOpenRowset on the session to create this rowset. The following section describes this interface.

Instantiating and Exposing a Rowset

The IOpenRowset interface contains a single method: OpenRowset. IOpenRowset is a required interface on the session. IOpenRowset::OpenRowset can be used by providers that do not support command objects to generate a rowset of all rows in a table or index. This provides a way for consumers to open and manipulate individual tables or indexes in a database with relatively low resource overhead. In the sample provider implementation, IOpenRowset accepts a file name as the name of the table and concatenates that with the directory name provided through IDBProperties::SetProperties. IOpenRowset uses the directory name and file name to instantiate a rowset and expose the rowset to the consumer.

The source code for the IOpenRowset::OpenRowset method follows; you can find the complete source code for the IOpenRowset interface in OpnRowst.cpp.

// CImpIOpenRowset::OpenRowset ------------------------------------------------
//
// @mfunc Opens and returns a rowset that includes all rows from a single base table
//
// @rdesc HRESULT
//  @flag S_OK      | The method succeeded
//  @flag E_INVALIDARG    | pTableID was NULL
//  @flag E_FAIL      | Provider-specific error
//  @flag DB_E_NOTABLE    | Specified table does not exist in current Data
//            | Data Source object
//  @flag E_OUTOFMEMORY   | Out of memory
//  @flag E_NOINTERFACE   | The requested interface was not available

STDMETHODIMP CImpIOpenRowset::OpenRowset
 (
 IUnknown*       pUnkOuter,    //@parm IN  | Controlling unknown, if any
 DBID*         pTableID,     //@parm IN  | table to open
 DBID*         pIndexID,     //@parm IN  | DBID of the index
 REFIID        riid,     //@parm IN  | interface to return
 ULONG         cProperties,    //@parm IN  | count of properties
 DBPROPSET       rgProperties[],   //@parm INOUT | array of property values
 IUnknown**      ppRowset    //@parm OUT | where to return interface
 )
{
 int  cCharsCopied;
 char szFileName[MAX_PATH ];
 char szFile[_MAX_PATH ] = "";

 CFileIO*  pFileio = NULL;
 CRowset*  pRowset = NULL;
 HRESULT   hr;
 HRESULT   hrProp  = ResultFromScode( S_OK );

 // NULL out-params in case of error
 if( ppRowset )
  *ppRowset = NULL;

 // Validate in-params
 if (pUnkOuter)
  return ResultFromScode( DB_E_NOAGGREGATION );

 // Check Arguments
 if ( !pTableID && !pIndexID )
  return ResultFromScode( E_INVALIDARG );

 if ( riid == IID_NULL)
  return ResultFromScode( E_NOINTERFACE );

 assert( m_pObj->m_pUtilProp );

 // Check Arguments for use by properties
 hr = m_pObj->m_pUtilProp->SetPropertiesArgChk(cProperties, rgProperties);
 if( FAILED(hr) )
  return hr;

 // If the eKind is not known to use, basically it
 // means we have no table identifier
 if ( (!pTableID ) || ( pTableID->eKind != DBKIND_NAME ) ||
  ( (pTableID->eKind == DBKIND_NAME) && (!(pTableID->uName.pwszName)) ) ||
  ( wcslen(pTableID->uName.pwszName)==0 ) )
  return ResultFromScode(DB_E_NOTABLE);

 // We only accept NULL for pIndexID
 if( pIndexID )
  return ResultFromScode(DB_E_NOINDEX);

 assert( m_pObj );

 // this DBSession object can only create 1 Rowset object
 if (m_pObj->m_fRowsetCreated)
  return ResultFromScode( E_FAIL );

 // set the properties
 if ( cProperties )
  hr = m_pObj->m_pUtilProp->SetProperties(PROPSET_ROWSET, cProperties, rgProperties);

 if( (hr == DB_E_ERRORSOCCURRED) ||
  (hr == DB_S_ERRORSOCCURRED) )
 {
  // If all the properties set were SETIFCHEAP then we can set
  // our status to DB_S_ERRORSOCCURRED and continue.
  for(ULONG ul=0;ul<cProperties; ul++)
  {
   for(ULONG ul2=0;ul2<rgProperties[ul].cProperties; ul2++)
   {
    // Check for a required property that failed, if found, we must return
    // DB_E_ERRORSOCCURRED
    if( (rgProperties[ul].rgProperties[ul2].dwStatus != DBPROPSTATUS_OK) &&
     (rgProperties[ul].rgProperties[ul2].dwOptions != DBPROPOPTIONS_SETIFCHEAP) )
     return ResultFromScode(DB_E_ERRORSOCCURRED);
   }
  }
  hrProp = ResultFromScode(DB_S_ERRORSOCCURRED);
 }

 if( FAILED(hr) && (hrProp==S_OK) )
  return hr;

 // if ppRowset NULL return
 if ( !ppRowset )
  return ResultFromScode( S_OK );

 // get file name
 cCharsCopied = WideCharToMultiByte( CP_ACP, 0, pTableID->uName.pwszName, -1,
              szFileName, sizeof( szFileName ), NULL, NULL );
 if (!cCharsCopied)
  return ResultFromScode( E_FAIL );

 // Concatenate the path and filename
 lstrcat( szFile, m_pObj->m_szPath );
 lstrcat( szFile, "\\" );
 lstrcat( szFile, szFileName );

 // open and initialize a file object
 pFileio = new CFileIO();
 if (!pFileio)
  return ResultFromScode( E_OUTOFMEMORY );

 hr = pFileio->fInit( szFile );
 if (FAILED( hr ))
 {
  delete pFileio;
  return hr;
 }

 // open and initialize a rowset\cursor object
 pRowset = new CRowset( NULL );
 if (!pRowset)
 {
  delete pFileio;
  return ResultFromScode( E_OUTOFMEMORY );
 }
 // Initialize the rowset\cursor.
 // For now, since don't yet support "setable" properties, so no properties to pass.
 // The rowset will always create all of its interfaces.
 // This is all-or-nothing.
 if (!pRowset->FInit( pFileio ))
 {
  delete pFileio;
  delete pRowset;
  return ResultFromScode( DB_E_NOTABLE );
 }
 //At this point we have handed off the pFileio pointer to the sample
 //provider so null it out.
 pFileio = NULL;

 // get requested interface pointer on rowset\cursor
 hr = pRowset->QueryInterface( riid, (void **) ppRowset );
 if (FAILED( hr ))
 {
  delete pRowset;
  delete pFileio;
  return ResultFromScode( hr );
 }

 //Assign creator pointer. Used for IRowsetInfo::GetSpecification
 pRowset->m_pCreator = m_pObj;
 pRowset->m_pCreator->AddRef();
 m_pObj->m_fRowsetCreated = TRUE;

 return (hr == S_OK) ? hrProp : hr;
}

Using the Rowset

Rowsets are the unifying abstraction that enables all OLE DB providers to expose base data, command results, or schema information, in tabular format. Conceptually, a rowset is a collection of rows in which each row contains columns of data.

The IOpenRowset interface exposed by the session generates a rowset object, which encapsulates the provider’s data. A rowset typically has an internal buffer, or row cache, which enables shared access to the provider’s data by multiple consumers. The following section describes how providers and consumers interact with the rowset.

Consumer and Provider Interactions with the Rowset

After receiving the rowset interface pointer, the consumer can request rowset metadata from the provider through IColumnsInfo, if it does not have prior knowledge of the data representation. The consumer uses the metadata to specify bindings, which describe how the consumer wants to receive the data. The consumer creates bindings by requesting IAccessor from the provider and specifying the bindings through IAccessor::CreateAccessor. The provider returns a handle to the accessor to the consumer.

The consumer then requests a number of rows from the provider using IRowset::GetNextRows. The provider retrieves the data for these rows and stores it in the data cache. The provider then returns an array of row handles to the consumer. Each row handle returned by the provider has an initial reference count of one. The consumer is then free to get the data for any rows from the provider using GetData. The consumer supplies GetData with the row handle, the handle of an accessor, and the buffer location into which to return the data; the provider copies the data to the location specified by the consumer.

Consumers using the sample provider can delete or change data in the rowset, but they cannot add new rows. To update rows, consumers call IRowsetChange::SetData, which sets the data in the data cache to the values specified by the consumer. To delete rows from the data cache, the consumer calls IRowsetChange::DeleteRows. A third method on IRowsetChange, IRowsetChange::InsertRow, is not implemented in the sample provider.

When the consumer makes any change to data in the data cache, the effects of the change are written to the data source immediately. OLE DB specifies a change-buffering model, which enables the consumer to make changes that are not realized until the consumer calls IRowsetUpdate::Update; this model is not supported by the sample provider.

When the consumer has finished working with a row, it can release the row by calling IRowset::ReleaseRows. ReleaseRows simply decrements the reference count on the row in the data cache. If the reference count for that row reaches zero, the row data is released from the data cache.

Exposing Metadata

Providers expose information about the columns of a rowset through IColumnsInfo. The information for each column is returned in a DBCOLUMNINFO structure. For more details on the information contained in the DBCOLUMNINFO structure, see IColumnsInfo::GetColumnsInfo in the OLE DB Programmer’s Reference. OLE DB also enables sophisticated providers to expose a richer set of metadata through IColumnsRowset; the sample provider does not implement this interface.

Exposing Metadata to Consumers

The GetColumnInfo method returns metadata that is most commonly used by consumers: column ID, column name, the ordinal number of the column in the rowset, the column’s data type, and so on. This information helps the consumer determine the binding type.

The provider returns the information in an array of DBCOLUMNINFO structures, one DBCOLUMNINFO structure per column in the rowset. The order of the structures returned in the array is the order in which the columns appear in the rowset.

The source code for IColumnsInfo::GetColumnInfo follows; you can find the complete source code for IColumnsInfo in ColInfo.cpp.

// CImpIColumnsInfo::GetColumnInfo -------------------------------------------
//
// @mfunc Returns the column metadata needed by most consumers.
//
// @rdesc HRESULT
//  @flag S_OK      | The method succeeded
//  @flag E_OUTOFMEMORY   | Out of memory
//  @flag E_INVALIDARG  | pcColumns or prginfo or ppStringsbuffer was NULL
//
STDMETHODIMP CImpIColumnsInfo::GetColumnInfo
  (
  ULONG*    pcColumns,  //@parm OUT | Number of columns in rowset
  DBCOLUMNINFO**  prgInfo,    //@parm OUT | Array of DBCOLUMNINFO Structures
  WCHAR**   ppStringsBuffer //@parm OUT | Storage for all string values
  )
{
 ULONG     icol;
 ULONG     icolStart;
 DBCOLUMNINFO* rgdbcolinfo;
 WCHAR*    pstrBuffer;

 // Initialize
 if (pcColumns)
  *pcColumns = 0;
 if (prgInfo)
  *prgInfo = NULL;
 if (ppStringsBuffer)
  *ppStringsBuffer = NULL;

 // Usual argument checking, prescribed by the spec.
 if (pcColumns == NULL || prgInfo == NULL || ppStringsBuffer == NULL)
  return ResultFromScode( E_INVALIDARG );

 icolStart = 1;
 rgdbcolinfo = (DBCOLUMNINFO *) g_pIMalloc->Alloc( m_pObj->m_cCols*sizeof( DBCOLUMNINFO ));
 if (rgdbcolinfo == NULL)
  return ResultFromScode( E_OUTOFMEMORY );

 memcpy( rgdbcolinfo, &(m_pObj->m_rgdbcolinfo[icolStart]), m_pObj->m_cCols*sizeof( DBCOLUMNINFO ));

 // Copy the heap for column names.
 if (m_pObj->m_cbHeapUsed)
 {
  ptrdiff_t dp;

  pstrBuffer = (WCHAR *) g_pIMalloc->Alloc( m_pObj->m_cbHeapUsed );
  if (pstrBuffer == NULL)
  {
   g_pIMalloc->Free( rgdbcolinfo );
   return ResultFromScode( E_OUTOFMEMORY );
  }
  memcpy( pstrBuffer, m_pObj->m_pbHeap, m_pObj->m_cbHeapUsed );
  dp = (LONG) pstrBuffer - (LONG) (m_pObj->m_pbHeap);
  dp >>= 1;

  // Loop through columns and adjust pointers to column names.
  for (icol =0; icol < m_pObj->m_cCols; icol++)
  {
   if (rgdbcolinfo[icol].pwszName)
     rgdbcolinfo[icol].pwszName += dp;
  }
 }

 *prgInfo   = rgdbcolinfo;
 *ppStringsBuffer = pstrBuffer;
 *pcColumns = m_pObj->m_cCols;

 return ResultFromScode( S_OK );
}

Returning Column Ordinals

For purposes of identification, columns in a rowset are identified by a column ID, which is a value of type DBID in the DBCOLUMNINFO structure. Although this is sufficient to uniquely identify the column, some providers may need to refer to the column by its position, or ordinal value.

The MapColumnIDs method returns column ordinals for all column IDs provided in the rgColumnIDs array. Column ordinals do not change during the life of the rowset, but may change between different instances of the rowset. The source code for IColumnsInfo::MapColumnIDs follows.

// CImpIColumnsInfo::MapColumnIDs --------------------------------------------
//
// @mfunc Returns an array of ordinals of the columns in a rowset that are
// identified by the specified column IDs.
//
// @rdesc HRESULT
//  @flag S_OK        | The method succeeded
//  @flag E_INVALIDARG      | cColumnIDs was not 0 and rgColumnIDs was NULL,
//              rgColumns was NULL
//  @flag DB_E_COLUMNUNAVAILABLE  | An element of rgColumnIDs was invalid
//
STDMETHODIMP CImpIColumnsInfo::MapColumnIDs
  (
  ULONG     cColumnIDs,   //@parm IN | Number of Column IDs to map
  const DBID  rgColumnIDs[],  //@parm IN | Column IDs to map
  ULONG     rgColumns[]   //@parm OUT | Ordinal values
  )
{
 ULONG ulError = 0;
 ULONG i;

 // If cColumnIDs is 0 return
 if (0 == cColumnIDs)
  return ResultFromScode( S_OK );

 // Check arguments
 if ((cColumnIDs != 0) && (NULL == rgColumnIDs))
  return ResultFromScode( E_INVALIDARG );

 if (NULL == rgColumns)
  return ResultFromScode( E_INVALIDARG );

 // Walk the Column ID structs and determine
 // the ordinal value
 for (i=0; i < cColumnIDs; i++)
 {
  if ((rgColumnIDs[i].uName.ulPropid < 1)     ||
    (rgColumnIDs[i].uName.ulPropid > m_pObj->m_cCols) ||
    (rgColumnIDs[i].eKind != DBKIND_GUID_PROPID)  ||
    (rgColumnIDs[i].uGuid.guid != GUID_NULL))
  {
   rgColumns[i] = DB_INVALIDCOLUMN;
   ulError++;
  }
  else
   rgColumns[i] = rgColumnIDs[i].uName.ulPropid;
 }

 if (!ulError)
  return ResultFromScode( S_OK );
 else if (ulError < cColumnIDs)
  return ResultFromScode( DB_S_ERRORSOCCURRED );
 else
  return ResultFromScode( DB_E_ERRORSOCCURRED );
}

Creating and Using Accessors

Consumers describe the memory structure for their buffers through a process called binding. An accessor is a group of bindings. The sample provider supports a very basic type of accessor and cannot support reference accessors, which allow the consumer direct access to the rowset’s data cache.

Accessors are implemented through IAccessor. You can create accessors with IAccessor::CreateAccessor and release accessors with IAccessor::ReleaseAccessor. You can use IAccessor::GetBindings to determine the bindings in an existing accessor. IAccessor::AddRefAccessor enables the consumer to add a reference count to an existing accessor. The following sections describe these methods.

Determining Supported Conversions

Before the consumer creates an accessor, it can call IConvertType::CanConvert to determine if the provider supports a particular conversion.

The source code for IConvertType::CanConvert follows; you can find the complete source code for IConvertType in CvtType.cpp.

//----------------------------------------------------------------------------
// CImpIConvertType::CanConvert
//
// @mfunc Used by consumer to determine provider support for a conversion
//
// @rdesc HRESULT indicating the status of the method
//  @flag S_OK      | Conversion supported
//  @flag S_FALSE     | Conversion unsupported
//  @flag DB_E_BADCONVERTFLAG | dwConvertFlags was invalid
//  @flag DB_E_BADCONVERTFLAG | called on rowset for DBCONVERTFLAG_PARAMETER
//  @flag OTHER       | HRESULTS returned from support functions
//
STDMETHODIMP CImpIConvertType::CanConvert
 (
 DBTYPE     wFromType,   //@parm IN | src type
 DBTYPE     wToType,   //@parm IN | dst type
 DBCONVERTFLAGS dwConvertFlags //@parm IN | conversion flags
 )
{
 // Check Arguments
 if( dwConvertFlags != DBCONVERTFLAGS_COLUMN &&
  dwConvertFlags != DBCONVERTFLAGS_PARAMETER )
  return (ResultFromScode(DB_E_BADCONVERTFLAG));

 // Called on rowset, but asking about parameters?
 // Since we only support conversions on a Rowset we always
 // return error for DBCONVERTFLAGS_PARAMETER
 if( dwConvertFlags == DBCONVERTFLAGS_PARAMETER)
  return (ResultFromScode(DB_E_BADCONVERTFLAG));

 // Ask the conversion library for the answer
 return g_pIDataConvert->CanConvert(wFromType, wToType);
}

Creating an Accessor

CreateAccessor associates a set of bindings with an accessor handle that is used to send data to or fetch data from the rowset’s data cache. The sample provider supports only the DBACCESSOR_ROWDATA accessor flag, which specifies that the accessor is to be used for rowset data.

The source code for IAccessor::CreateAccessor follows; you can find the complete source code for IAccessor in Accessor.cpp.

// CImpIAccessor::CreateAccessor -----------------------------------------
//
// @mfunc Creates a set of bindings that can be used to send data
// to or retrieve data from the data cache.
// NOTE:  Currently does not support returning rgStatus[].
//
// @rdesc HRESULT
//  @flag S_OK        | Method Succeeded
//  @flag E_FAIL        | Provider specific Error
//  @flag E_INVALIDARG      | pHAccessor was NULL, dwAccessorFlags was
//              invalid, or cBindings was not 0 and
//              rgBindings was NULL
//  @flag E_OUTOFMEMORY     | Out of Memory
//  @flag DB_E_ERRORSOCCURRED   | dwBindPart in an rgBindings element was invalid, OR
//              | Column number specified was out of range, OR
//              | Requested coercion is not supported.
//  @flag OTHER       | Other HRESULTs returned by called functions
//
STDMETHODIMP CImpIAccessor::CreateAccessor
  (
  DBACCESSORFLAGS dwAccessorFlags,
  ULONG     cBindings,  //@parm IN | Number of Bindings
  const DBBINDING rgBindings[], //@parm IN | Array of DBBINDINGS
  ULONG     cbRowSize,  //@parm IN | Number of bytes in consumer's buffer
  HACCESSOR*  phAccessor,   //@parm OUT | Accessor Handle
  DBBINDSTATUS  rgStatus[]  //@parm OUT | Binding status
  )
{
 PACCESSOR pAccessor;
 ULONG   hAccessor;
 ULONG   ibind;
 ULONG   icol;
 HRESULT   hr;

 // Check Parameters
 if( (cBindings && !rgBindings) ||
  (phAccessor == NULL) )
  return ResultFromScode( E_INVALIDARG );

 // init out params
 *phAccessor = (HACCESSOR) 0;

 // Check for the binding types we don't accept..
 if ((dwAccessorFlags & DBACCESSOR_PASSBYREF)    ||
   (dwAccessorFlags & DBACCESSOR_PARAMETERDATA))
  return ResultFromScode( E_INVALIDARG );

 // .. then check for the binding type that is required
 if (!(dwAccessorFlags & DBACCESSOR_ROWDATA))
  return ResultFromScode( E_INVALIDARG );

 // Check on the bindings the user gave us.
 for (ibind =0, hr =NOERROR; ibind < cBindings; ibind++)
 {
  icol = rgBindings[ibind].iOrdinal;

  // make sure column number is in range
  if (!(0 < icol && icol <= m_pObj->m_cCols))
  {
   TRACE( "CreateAccessor failure: binding %d, bad column number %d\n", ibind, icol );
   hr = ResultFromScode( DB_E_ERRORSOCCURRED );
   break;
  }

  // At least one of these valid parts has to be set. In SetData I assume it is the case.
  if ((rgBindings[ibind].dwPart & DBPART_VALUE)  == 0
   && (rgBindings[ibind].dwPart & DBPART_LENGTH) == 0
   && (rgBindings[ibind].dwPart & DBPART_STATUS) == 0)
  {
   TRACE( "CreateAccessor failure: binding %d, column %d, _VALUE, _LENGTH, _STATUS not specified", ibind, icol );
   hr = ResultFromScode( DB_E_ERRORSOCCURRED );

   if ( rgStatus )
   {
    //  Set Bind status to DBBINDSTATUS_BADBINDINFO
   }
   break;
  }

  // Make sure we can do the coercion that is requested
  if( NOERROR != g_pIDataConvert->CanConvert(rgBindings[ibind].wType, m_pObj->m_rgdbcolinfo[icol].wType) ||
   NOERROR != g_pIDataConvert->CanConvert(m_pObj->m_rgdbcolinfo[icol].wType, rgBindings[ibind].wType) )
  {
   TRACE( "CreateAccessor failure: binding %d, column %d, cannot coerce types\n", ibind, icol );
   hr = ResultFromScode( DB_E_ERRORSOCCURRED );
   if ( rgStatus )
   {
    //  Set Bind status to DBBINDSTATUS_UNSUPPORTEDCONVERSION
   }
   break;
  }
 }

 // Any errors amongst those checks?
 if (hr != NOERROR)
 {
  return hr;
 }

 // Make a copy of the client's binding array, and the type of binding.
 // Note: accessors with no bindings (cBindings=0) are legal.
 pAccessor = (ACCESSOR *) new BYTE[sizeof( ACCESSOR ) + (cBindings - 1) *sizeof( DBBINDING )];
 if (pAccessor == NULL)
  return ResultFromScode( E_OUTOFMEMORY );

 // We store a ptr to the newly created variable-sized ACCESSOR.
 // We have an array of ptrs (to ACCESSOR's).
 // The handle is the index into the array of ptrs.
 // The InsertIntoExtBuffer function appends to the end of the array.
 assert( m_pObj->m_pextbufferAccessor );
 hr = m_pObj->m_pextbufferAccessor->InsertIntoExtBuffer( &pAccessor, hAccessor );
 if (FAILED( hr ))
 {
  delete [] pAccessor;
  return ResultFromScode( E_OUTOFMEMORY );
 }
 assert( hAccessor );

 // Copy the client's bindings into the ACCESSOR.
 pAccessor->dwAccessorFlags = dwAccessorFlags;
 pAccessor->cBindings   = cBindings;
 pAccessor->cRef      = 1;  // Establish Reference count.
 memcpy( &(pAccessor->rgBindings[0]), &rgBindings[0], cBindings*sizeof( DBBINDING ));

 // fill out-param and return
 *phAccessor = (HACCESSOR) hAccessor;
 return ResultFromScode( S_OK );
}

Returning Accessor Bindings

GetBindings returns the bindings in an existing accessor. The source code for IAccessor::GetBindings follows; you can find the complete source code for IAccessor in Accessor.cpp.

// CImpIAccessor::GetBindings --------------------------------------------------
//
// @mfunc Returns the bindings in an accessor
//
// @rdesc HRESULT
//  @flag S_OK        | Method Succeeded
//  @flag E_INVALIDARG      | pdwAccessorFlags/pcBinding/prgBinding were NULL
//  @flag E_OUTOFMEMORY     | Out of Memory
//  @flag DB_E_BADACCESSORHANDLE  | Invalid Accessor given
//
STDMETHODIMP CImpIAccessor::GetBindings
  (
  HACCESSOR    hAccessor,   //@parm IN | Accessor Handle
  DBACCESSORFLAGS* pdwAccessorFlags,  //@parm OUT | Binding Type flag
  ULONG*     pcBindings,    //@parm OUT | Number of Bindings returned
  DBBINDING**  prgBindings    //@parm OUT | Bindings
  )
{
 // Retrieve our accessor structure from the client's hAccessor,
 // make a copy of the bindings for the user, then done.
 PACCESSOR pAccessor;
 ULONG   cBindingSize;
 HRESULT   hr;

 // check parameters
 if (!pdwAccessorFlags || !pcBindings || !prgBindings)
  return ResultFromScode( E_INVALIDARG );

 // init out-params
 *pdwAccessorFlags = 0;
 *pcBindings   = 0;
 *prgBindings  = NULL;

 // Validate Accessor Handle
 hr = m_pObj->m_pextbufferAccessor->GetItemOfExtBuffer((ULONG) hAccessor, &pAccessor );
 if (FAILED( hr ) || pAccessor == NULL)
  return ResultFromScode( DB_E_BADACCESSORHANDLE );

 // Allocate and return Array of bindings
 cBindingSize = pAccessor->cBindings * sizeof( DBBINDING );
 *prgBindings = (DBBINDING *) g_pIMalloc->Alloc( cBindingSize );
 if (*prgBindings)
 {
  *pdwAccessorFlags = pAccessor->dwAccessorFlags;
  *pcBindings = pAccessor->cBindings;
  memcpy( *prgBindings, pAccessor->rgBindings, cBindingSize );
 }
 else
 {
  return ResultFromScode( E_OUTOFMEMORY );
 }

 // all went well..
 return ResultFromScode( S_OK );
}

Adding a Reference Count to an Existing Accessor

AddRefAccessor adds a reference count to an existing accessor. The source code for IAccessor::AddRefAccessor follows.

// CImpIAccessor::AddRefAccessor -----------------------------------------
//
// @mfunc Adds a reference count to an existing accessor
//
// @rdesc HRESULT
//  @flag S_OK        | Method Succeeded
//  @flag E_FAIL        | Provider specific Error
//
STDMETHODIMP CImpIAccessor::AddRefAccessor
 (
 HACCESSOR hAccessor,  //@parm IN  | Accessor Handle
 ULONG*  pcRefCounts   //@parm OUT | Reference Count
 )
{
 // Retrieve our accessor structure from the client's hAccessor,
 // free it, then mark accessor ptr as unused.
 // We do not re-use accessor handles.  This way, we hope
 // to catch more client errors.  (Also, ExtBuffer doesn't
 // maintain a free list, so it doesn't know how to.)

 PACCESSOR pAccessor;
 HRESULT   hr;

 if( pcRefCounts )
  *pcRefCounts = 0;

 hr = m_pObj->m_pextbufferAccessor->GetItemOfExtBuffer((ULONG) hAccessor, &pAccessor );
 if (FAILED( hr ) || pAccessor == NULL)
  return ResultFromScode( DB_E_BADACCESSORHANDLE );

 InterlockedIncrement(&(pAccessor->cRef));

 if( pcRefCounts )
  *pcRefCounts = (ULONG)(pAccessor->cRef);

 return ResultFromScode(S_OK);
}

Releasing an Accessor

ReleaseAccessor decrements the reference count on an accessor; when the reference count reaches zero, the accessor is released. The source code for IAccessor::ReleaseAccessor follows; you can find the complete source code for IAccessor in Accessor.cpp.

// CImpIAccessor::ReleaseAccessor ---------------------------------------
//
// @mfunc Releases an Accessor
//
// @rdesc HRESULT
//  @flag S_OK        | Method Succeeded
//  @flag DB_E_BADACCESSORHANDLE  | hAccessor was invalid
//
STDMETHODIMP CImpIAccessor::ReleaseAccessor
  (
  HACCESSOR hAccessor,   //@parm IN  | Accessor handle to release
  ULONG*  pcRefCounts  //@parm OUT | Reference Count
  )
{
 // Retrieve our accessor structure from the client's hAccessor,
 // free it, then mark accessor ptr as unused.
 // We do not re-use accessor handles.  This way, we hope
 // to catch more client errors.  (Also, ExtBuffer doesn't
 // maintain a free list, so it doesn't know how to.)

 PACCESSOR pAccessor;
 HRESULT   hr;

 if( pcRefCounts )
  *pcRefCounts = 0;

 hr = m_pObj->m_pextbufferAccessor->GetItemOfExtBuffer((ULONG) hAccessor, &pAccessor );
 if (FAILED( hr ) || pAccessor == NULL)
  return ResultFromScode( DB_E_BADACCESSORHANDLE );

 // Free the actual structure.
 InterlockedDecrement(&(pAccessor->cRef));
 assert( pAccessor->cRef >= 0 );
 if( pAccessor->cRef <= 0 )
 {
  delete [] pAccessor;
  if( pcRefCounts )
   *pcRefCounts = 0;
 }
 else
 {
  if( pcRefCounts )
   *pcRefCounts = (ULONG)(pAccessor->cRef);
 }

 // Store a null in our array-of-ptrs,
 // so we know next time that it is invalid.
 // (operator[] returns a ptr to the space where the ptr is stored.)
 *(PACCESSOR*) ((*m_pObj->m_pextbufferAccessor)[ (ULONG) hAccessor]) = NULL;

 return ResultFromScode( S_OK );
}

Exposing Rowset Information

IRowsetInfo enables consumers to learn about the properties of a rowset through IRowsetInfo::GetProperties. In providers that implement bookmarks, consumers can get an interface pointer to a rowset referenced by a bookmark column with IRowsetInfo::GetReferencedRowset. Consumers can get an interface pointer to the object that created the rowset by calling IRowsetInfo::GetSpecification.

In the sample provider implementation, calls to IRowsetInfo::GetProperties are handled by passing the call to the utility object that manages properties, which is found in UtilProp.cpp. The sample provider doesn’t support bookmarks, so all calls to IRowsetInfo::GetReferencedRowset return the error DB_E_NOTAREFERENCECOLUMN. The sample provider doesn’t store an interface pointer to the object that created the rowset, so calls to IRowsetInfo::GetSpecification are returned with the success code S_OK and the pointer to the memory in which to return the interface pointer is set to NULL.

Managing Rowsets

IRowset provides methods for fetching rows sequentially, exposing data from those rows to consumers, and managing the rows in the rowset. IRowset contains five methods: AddRefRows, GetData, GetNextRows, ReleaseRows, and RestartPosition. The source code for IRowset::AddRefRows follows; you can find the complete source code for the IRowset interface in IRowset.cpp.

Incrementing the Reference Count on Row Handles

AddRefRows increments the reference count on the row handles supplied by the caller. AddRefRows enables consumers to make multiple references to a row in the data cache.

// CImpIRowset::AddRefRows --------------------------------------------------
//
// @mfunc Adds a reference count to an existing row handle
//
// @rdesc HResult
//  @flag S_OK      | Method Succeeded
//  @flag E_INVALIDARG  | rghRows was NULL and cRows was not 0
//  @flag DB_E_BADROWHANDLE | An element of rghRows was invalid

STDMETHODIMP  CImpIRowset::AddRefRows
  (
  ULONG     cRows,    // @parm IN   | Number of rows to refcount
  const HROW  rghRows[],  // @parm IN   | Array of row handles to refcount
  ULONG*    pcRefCounted, // @parm OUT  | Number successfully refcounted
  ULONG     rgRefCounts[] // @parm OUT  | Array of refcounts
  )
{
 ULONG ihRow = 0;
 ROWBUFF *pRowBuff;
 ULONG cAddRef;

 // init out param
 if (pcRefCounted)
  *pcRefCounted = 0;

 // check params
 if (cRows && !rghRows)
  return ResultFromScode( E_INVALIDARG );

 cAddRef = 0;

 // for each of the HROWs the caller provided...
 for (ihRow = 0; ihRow < cRows; ihRow++)
 {
  // check the row handle
  if (S_OK != (m_pObj->m_prowbitsIBuffer)->IsSlotSet((ULONG) rghRows[ihRow] ))
   return ResultFromScode( DB_E_BADROWHANDLE );

  // bump refcount
  pRowBuff = m_pObj->GetRowBuff((ULONG) rghRows[ihRow] );
  assert( pRowBuff->ulRefCount != 0 );
  assert( m_pObj->m_ulRowRefCount != 0 );
  ++pRowBuff->ulRefCount;
  ++m_pObj->m_ulRowRefCount;

  // bump total refcounted
  cAddRef++;

  // stuff new refcount into caller's array
  if (rgRefCounts)
   rgRefCounts[ihRow] = pRowBuff->ulRefCount;
 }

 // fill out-param
 if (pcRefCounted)
  (*pcRefCounted) = cAddRef;

 return ResultFromScode( S_OK );
}

Populating the Data Cache

IRowset::GetNextRows gets the next sequence of rows from the file and places them in the rowset’s data cache. When GetNextRows is first called, it starts at the beginning of the file. After that, GetNextRows maintains information about its current position so it can proceed forward from that position. The sample provider does not support rowsets with reversible direction. The sample provider does, however, support IRowset::RestartPosition, which repositions the next fetch position to the beginning of the file. The source code for IRowset::GetNextRows follows.

// CImpIRowset::GetNextRows --------------------------------------------------
//
// @mfunc Fetches rows in a sequential style, remembering the previous position
//
// @rdesc HRESULT
//  @flag S_OK        | Method Succeeded
//  @flag DB_S_ENDOFROWSET    | Reached end of rowset
//  @flag DB_E_CANTFETCHBACKWARDS | cRows was negative and we can't fetch backwards
//  @flag DB_E_ROWSNOTRELEASED  | Must release all HROWs before calling GetNextRows
//  @flag E_FAIL        | Provider-specific error
//  @flag E_INVALIDARG      | pcRowsObtained or prghRows was NULL
//  @flag E_OUTOFMEMORY     | Out of Memory
//  @flag OTHER       | Other HRESULTs returned by called functions
//
STDMETHODIMP CImpIRowset::GetNextRows
  (
  HCHAPTER hReserved,   //@parm IN | Reserved for future use. Ingored.
  LONG   cRowsToSkip,   //@parm IN | Rows to skip before reading
  LONG   cRows,     //@parm IN | Number of rows to fetch
  ULONG  *pcRowsObtained, //@parm OUT | Number of rows obtained
  HROW   **prghRows   //@parm OUT | Array of Hrows obtained
  )
{
 ULONG  cRowsTmp;
 BOOL   fExtraRow;
 ULONG  cSlotAlloc =0;
 ULONG  di;
 LONG   irow, ih;
 ULONG  irowFirst, irowLast;
 PROWBUFF prowbuff;
 HRESULT  hr;
 BYTE   *pbRowFirst;

 // Check validity of arguments.
 if (pcRowsObtained == NULL || prghRows == NULL)
  return ResultFromScode( E_INVALIDARG );

 // init out-params
 *pcRowsObtained = 0;

 // No-op case always succeeds.
 if (cRows == 0 && cRowsToSkip == 0)
  return ResultFromScode( S_OK );

 // This implementation doesn't support scrolling backward.
 if (cRows < 0 || cRowsToSkip < 0)
  return ResultFromScode( DB_E_CANTFETCHBACKWARDS );

 // Are there any unreleased rows?
 if ((m_pObj->m_prowbitsIBuffer)->ArrayEmpty() != S_OK)
  return ResultFromScode( DB_E_ROWSNOTRELEASED );
 assert( m_pObj->m_ulRowRefCount == 0 ); // should be true since array was empty

 // Is the cursor fully materialized (end-of-cursor condition)?
 if (m_pObj->m_dwStatus & STAT_ENDOFCURSOR)
  return ResultFromScode( DB_S_ENDOFROWSET );

 assert( m_pObj->m_rgbRowData );
 if (FAILED( m_pObj->Rebind((BYTE *) m_pObj->GetRowBuff( m_pObj->m_irowMin ))))
  return ResultFromScode( E_FAIL );

 // Sometimes we need an extra row to fetch data and copy it to appropriate place.
 fExtraRow = (cRows > 1);
 cRowsTmp = cRows + (fExtraRow ? 1 : 0);

 if (FAILED( hr = GetNextSlots( m_pObj->m_pIBuffer, cRowsTmp, &irowFirst )))
  return hr;
 cSlotAlloc = cRowsTmp;

 //
 // Fetch Data
 //
 if (cRowsToSkip)
 {
  // Calculate the new position
  m_pObj->m_irowFilePos += cRowsToSkip;

  // Check if skip causes END_OF_ROWSET
  if (m_pObj->m_irowFilePos > m_pObj->m_pFileio->GetRowCnt() ||
    m_pObj->m_irowFilePos <= 0)
  {
   m_pObj->m_dwStatus |= STAT_ENDOFCURSOR;
   return ResultFromScode( DB_S_ENDOFROWSET );
  }
 }

 pbRowFirst = (BYTE *) m_pObj->GetRowBuff( irowFirst );
 for (irow =1; irow <= cRows; irow++)
 {
  // Get the Data from the File into the row buffer
  if (S_FALSE == (hr =  m_pObj->m_pFileio->Fetch( m_pObj->m_irowFilePos + irow )))
  {
   m_pObj->m_dwStatus |= STAT_ENDOFCURSOR;
   break;
  }
  else
  {
   if (FAILED( hr ))
    return ResultFromScode( E_FAIL );
  }

  // Got a row, so copy it from bound row to the destination.
  // Very first row is Fetch buffer, we give out rows [2...cRows+1].
  if (fExtraRow)
   memcpy( pbRowFirst + (m_pObj->m_cbRowSize * irow), pbRowFirst, m_pObj->m_cbRowSize );
 }

 cRowsTmp = irow - 1; //Irow will be +1 because of For Loop
 m_pObj->m_irowLastFilePos = m_pObj->m_irowFilePos;
 m_pObj->m_irowFilePos += cRowsTmp;

 //
 // Through fetching many rows of data
 //
 // Allocate row handles for client.
 // Note that we need to use IMalloc for this.
 //
 // Should only malloc cRowsTmp, instead of cRows.
 //
 // Modified to use IMalloc.
 // Should malloc cRows, since client will assume it's that big.
 //

 *pcRowsObtained = cRowsTmp;
 if (*prghRows == NULL)
 {
  *prghRows = (HROW *) g_pIMalloc->Alloc( cRows*sizeof( HROW ));
 }

 if (NULL == *prghRows)
  return ResultFromScode( E_OUTOFMEMORY );

 //
 // Fill in the status information: Length, IsNull
 // May be able to wait until first call to GetData,
 // but have to do it sometime.
 //
 // Suggest keeping an array of structs of accessor info.
 // One element is whether accessor requires any status info or length info.
 // Then we could skip this whole section.
 //
 // Added check for cRowsTmp to MarkRows call.
 // Don't want to call if cRowsTmp==0.
 // (Range passed to MarkRows is inclusive, so can't specify marking 0 rows.)
 //
 // Note that SetSlots is a CBitArray member function -- not an IBuffer function.
 //
 // Bits are [0...n-1], row handles are [1...n].
 //
 // Cleanup. Row handles, bits, indices are the same [m....(m+n)], where m is some # >0,
 //
 // Added row-wise reference counts and cursor-wise reference counts.
 //

 // Set row handles, fix data length field and compute data status field.//
 di = fExtraRow ? 1 : 0;
 m_pObj->m_cRows = cRowsTmp;
 irowLast = irowFirst + di + cRowsTmp - 1;

 // Cleanup extra slots where no hRow actually was put..
 //  ** Because of less rows than asked for
 //  ** Because of temporary for for data transfer.
 if (fExtraRow)
  if (FAILED( hr = ReleaseSlots( m_pObj->m_pIBuffer, irowFirst, 1 )))
   return hr;
 if (cSlotAlloc > (cRowsTmp + di))
  if (FAILED( hr = ReleaseSlots( m_pObj->m_pIBuffer, irowFirst + cRowsTmp + di, (cSlotAlloc - cRowsTmp - di))))
   return hr;

 for (irow = (LONG) (irowFirst + di), ih =0; irow <= (LONG) irowLast; irow++, ih++)
 {
  // Increment the rows-read count,
  // then store it as the bookmark in the very first DWORD of the row.

  prowbuff = m_pObj->GetRowBuff( irow );

  // Insert the bookmark and its row number (from 1...n) into a hash table.
  // This allows us to quickly determine the presence of a row in mem, given the bookmark.
  // The bookmark is contained in the row buffer, at the very beginning.
  // Bookmark is the row number within the entire result set [1...num_rows_read].

  // This was a new Bookmark, not in memory,
  // so return to user (in *prghRows) the hRow we stored.
  prowbuff->ulRefCount++;
  prowbuff->pbBmk = (BYTE*) m_pObj->m_irowLastFilePos + ih + 1;
  m_pObj->m_ulRowRefCount++;

  (*prghRows)[ih] = (HROW) (irow);
 }

 if (m_pObj->m_dwStatus & STAT_ENDOFCURSOR)
  return ResultFromScode( DB_S_ENDOFROWSET );
 else
  return ResultFromScode( S_OK );
}

Retrieving Data from the Data Cache

IRowset::GetData enables consumers to retrieve data from the data cache. GetData uses the bindings in the accessor to determine how the data should be returned and what data should be returned to the consumer’s buffer. Then, GetData converts the data in the cache to the type specified in the binding and transfers the converted data to the consumer. The source code for IRowset::GetData follows.

// CImpIRowset::GetData --------------------------------------------------
//
// @mfunc Retrieves data from the rowset's cache
//
// @rdesc HRESULT
//  @flag S_OK       | Method Succeeded
//  @flag DB_S_ERRORSOCCURED   | Could not coerce a column value
//  @flag DB_E_BADACCESSORHANDLE | Invalid Accessor given
//  @flag DB_E_BADROWHANDLE  | Invalid row handle given
//  @flag E_INVALIDARG     | pData was NULL
//  @flag OTHER      | Other HRESULTs returned by called functions
//
STDMETHODIMP CImpIRowset::GetData
  (
  HROW    hRow,   //@parm IN | Row Handle
  HACCESSOR hAccessor,  //@parm IN | Accessor to use
  void   *pData   //@parm OUT | Pointer to buffer where data should go.
  )
{
 PACCESSOR pAccessor;
 ULONG   icol, ibind;
 ROWBUFF   *pRowBuff;
 COLUMNDATA  *pColumnData;
 DBBINDING *pBinding;
 ULONG   cBindings;
 ULONG   ulErrorCount;
 DBTYPE  dwSrcType;
 DBTYPE  dwDstType;
 void    *pSrc;
 void    *pDst;
 ULONG   ulSrcLength;
 ULONG   *pulDstLength;
 ULONG   ulDstMaxLength;
 DWORD   dwSrcStatus;
 DWORD   *pdwDstStatus;
 DWORD   dwPart;
 HRESULT   hr;

 // Coerce data for row 'hRow', according to hAccessor.
 // Put in location 'pData'.  Offsets and types are in hAccessor's bindings.
 //
 // Return S_OK if all lossless conversions,
 // return DB_S_ERRORSOCCURED if lossy conversion (truncation, rounding, etc.)
 // Return E_FAIL, etc., if horrible errors.

 // GetItemOfExtBuffer is basically operator[].
 // It takes an index (or handle) (referenced from 1...n),
 // and a ptr for where to write the data.
 //
 // It holds ptrs to a variable-length ACCESSOR struct.
 // So we get the ACCESSOR ptr for the client's accessor handle.

 assert( m_pObj->m_pextbufferAccessor );
 hr = m_pObj->m_pextbufferAccessor->GetItemOfExtBuffer((ULONG) hAccessor, &pAccessor );
 if (FAILED( hr ) || pAccessor == NULL)
  return ResultFromScode( DB_E_BADACCESSORHANDLE );

 assert( pAccessor );
 cBindings = pAccessor->cBindings;
 pBinding  = pAccessor->rgBindings;

 // IsSlotSet returns S_OK  if row is marked.
 //       S_FALSE if row is not marked.
 // The "mark" means that there is data present in the row.
 // Rows are [1...n], slot marks are [0...n-1].
 if (m_pObj->m_prowbitsIBuffer->IsSlotSet((ULONG) hRow ) != S_OK)
  return ResultFromScode( DB_E_BADROWHANDLE );

 // Ensure a place to put data.
 if (pData == NULL)
  return ResultFromScode( E_INVALIDARG );

 pRowBuff = m_pObj->GetRowBuff((ULONG) hRow );

 // Internal error for a 0 reference count on this row,
 // since we depend on the slot-set stuff.
 assert( pRowBuff->ulRefCount );

 ulErrorCount = 0;
 for (ibind = 0; ibind < cBindings; ibind++)
 {
  icol = pBinding[ibind].iOrdinal;
  pColumnData = (COLUMNDATA *) ((BYTE *) pRowBuff + m_pObj->m_rgdwDataOffsets[icol]);

  dwSrcType  = m_pObj->m_rgdbcolinfo[icol].wType;
  pSrc     = &(pColumnData->bData);
  ulSrcLength  = pColumnData->dwLength;
  dwSrcStatus  = pColumnData->dwStatus;
  ulDstMaxLength = pBinding[ibind].cbMaxLen;
  dwDstType  = pBinding[ibind].wType;
  dwPart   = pBinding[ibind].dwPart;

  pDst     = dwPart & DBPART_VALUE ? ((BYTE*) pData + pBinding[ibind].obValue) : NULL;
  pulDstLength = dwPart & DBPART_LENGTH ? (ULONG *) ((BYTE*) pData + pBinding[ibind].obLength) : NULL;
  pdwDstStatus = dwPart & DBPART_STATUS ? (ULONG *) ((BYTE*) pData + pBinding[ibind].obStatus) : NULL;

  hr = g_pIDataConvert->DataConvert(
      dwSrcType,
      dwDstType,
      ulSrcLength,
      pulDstLength,
      pSrc,
      pDst,
      ulDstMaxLength,
      dwSrcStatus,
      pdwDstStatus,
      0, // bPrecision for conversion to DBNUMERIC
      0, // bScale for conversion to DBNUMERIC
      DBDATACONVERT_DEFAULT);
  if (FAILED( hr ))
   return hr;  // fatal error
  if (hr != ResultFromScode( S_OK ))
   ulErrorCount++; // can't coerce
 }

 // We report any lossy conversions with a special status.
 // Note that DB_S_ERRORSOCCURED is a success, rather than failure.
 return ResultFromScode( ulErrorCount ? DB_S_ERRORSOCCURRED : S_OK );
}

Decrementing the Reference Count on Row Handles

IRowset::ReleaseRows decrements the reference count on the rows specified in the rghRows array. A consumer must call ReleaseRows once for each time a row was fetched or each time the row had its reference count incremented by AddRefRow. When the reference count reaches zero, the row is released if the rowset is in immediate update mode.

In providers that implement IRowsetUpdate, rows are released unless there are pending changes on the row; the sample provider always performs rowset updates in immediate mode, which means that changes are immediately applied to the underlying data source. Therefore, the sample provider does not recognize any changes as pending. The source code for IRowset::ReleaseRows follows.

// CImpIRowset::ReleaseRows ---------------------------------------
//
// @mfunc Releases row handles
//
// @rdesc HRESULT
//  @flag S_OK      | Method Succeeded
//  @flag E_INVALIDARG  | Invalid parameters were specified
//  @flag DB_E_BADROWHANDLE | Row handle was invalid
//
STDMETHODIMP CImpIRowset::ReleaseRows
  (
  ULONG   cRow,     //@parm IN  | Number of rows to release
  const HROW  rghRow[],   //@parm IN  | Array of handles of rows to be released
  DBROWOPTIONS  rgRowOptions[], //@parm IN  | Additional Options
  ULONG   *pcRowReleased, //@parm OUT | Count of rows actually released
  ULONG   rgRefCount[]  //@parm OUT | Array of refcnts for the rows
  )
{
 ULONG ihRow;
 BOOL  fEncounteredError;
 ROWBUFF *pRowBuff;
 ULONG cRowReleased = 0;

 // check params
 if (cRow && !rghRow)
  return ResultFromScode( E_INVALIDARG );

 // init out param
 if (pcRowReleased)
  *pcRowReleased = 0;

 fEncounteredError = FALSE;
 ihRow = 0;

 while (ihRow < cRow)
 {
  if ((m_pObj->m_prowbitsIBuffer)->IsSlotSet((ULONG) rghRow[ihRow] ) == S_OK)
  {
   // Found valid row, so decrement reference counts.
   // (Internal error for refcount to be 0 here, since slot set.)
   pRowBuff = m_pObj->GetRowBuff((ULONG) rghRow[ihRow] );
   assert( pRowBuff->ulRefCount != 0 );
   assert( m_pObj->m_ulRowRefCount != 0 );
   --pRowBuff->ulRefCount;
   --m_pObj->m_ulRowRefCount;

   if (rgRefCount)
    rgRefCount[ihRow] = pRowBuff->ulRefCount;

   if (pRowBuff->ulRefCount == 0)
   {
    ReleaseSlots( m_pObj->m_pIBuffer, (ULONG) rghRow[ihRow], 1 );
    cRowReleased++;
   }
   ihRow++;
  }
  else
  {
   // It is an error for client to try to release a row
   // for which "IsSetSlot" is false.  Client gave us an invalid handle.
   // Ignore it (we can't release it...) and report error when done.
   fEncounteredError = TRUE;
   if (rgRefCount)
    rgRefCount[ihRow] = 0;
   // stop looping if we hit an error
   break;
  }
 }

 if (pcRowReleased)
  *pcRowReleased = cRowReleased;

 if (fEncounteredError)
  return ResultFromScode( DB_E_BADROWHANDLE );
 else
  return ResultFromScode( S_OK );
}

Returning to the First Row of the Rowset

IRowset::RestartPosition moves the next fetch position used by GetNextRows to the first row of the rowset. The source code for IRowset::RestartPosition follows.

// CImpIRowset::ResartPosition ---------------------------------------
//
// @mfunc Repositions the next fetch position to the start of the rowset
//
// - all rows must be released before calling this method
// - it is not expensive to Restart us, because we are from a single table
//
//
// @rdesc HRESULT
//  @flag S_OK      | Method Succeeded
//  @flag DB_E_ROWSNOTRELEASED  | All HROWs must be released before calling
//
STDMETHODIMP CImpIRowset::RestartPosition
  (
  HCHAPTER  hReserved    //@parm IN | Reserved for future use.  Ignored.
  )
{
 // make sure all rows have been released
 if (S_OK != (m_pObj->m_prowbitsIBuffer)->ArrayEmpty())
  return DB_E_ROWSNOTRELEASED;
 assert( m_pObj->m_ulRowRefCount == 0 ); // should be true since array was empty

 // set "next fetch" position to the start of the rowset
 m_pObj->m_irowFilePos = 0;

 // clear "end of cursor" flag
 m_pObj->m_dwStatus &= ~STAT_ENDOFCURSOR;

 return ResultFromScode( S_OK );
}

Updating Rows

IRowsetChange enables consumers to change the values of columns in a row of data. If the consumer wants to change the data, it must first construct an accessor for the columns to be changed. IRowsetChange contains three methods: DeleteRows, InsertRow, and SetData. The sample provider does not implement the InsertRow method.

Setting the New Data Values

IRowsetChange::SetData sets new data values for columns in a row. SetData cannot be used to change values in a deleted row. When SetData updates a row, it overwrites the row data in the provider’s data cache and in the underlying data source with at signs (@), causing the row to appear deleted. Then, it appends the new row to the end of the file. Changes made through SetData are applied to the data source immediately. The source code for IRowsetChange::SetData follows; you can find the complete source code for IRowsetChange in RowChng.cpp.

// CImpIRowsetChange::SetData ------------------------------------------------
//
// @mfunc Sets new data values into fields of a row.
//
// @rdesc HRESULT
//  @flag S_OK       | The method succeeded
//  @flag E_OUTOFMEMORY    | Out of memory
//  @flag DB_E_BADACCESSORHANDLE | Bad accessor handle
//  @flag DB_E_READONLYACCESSOR  | Tried to write through a read-only accessor
//  @flag DB_E_BADROWHANDLE  | Invalid row handle
//  @flag E_INVALIDARG     | pData was NULL
//  @flag E_FAIL       | Provider-specific error
//  @flag OTHER      | Other HRESULTs returned by called functions
//
STDMETHODIMP CImpIRowsetChange::SetData
  (
  HROW    hRow,   //@parm IN | Handle of the row in which to set the data
  HACCESSOR hAccessor,  //@parm IN | Handle to the accessor to use
  void*  pData   //@parm IN | Pointer to the data
  )
{
 PACCESSOR   paccessor;
 ULONG     icol, ibind;
 BYTE*     pbProvRow;
 HRESULT   hr;
 ULONG     cBindings;
 DBBINDING*  pBinding;
 ULONG     dwErrorCount;
 DBTYPE    dwSrcType;
 DBTYPE    dwDstType;
 void*     pSrc;
 void*     pDst;
 ULONG     dwSrcLength;
 ULONG*    pdwDstLength;
 ULONG     dwDstMaxLength;
 DWORD     dwSrcStatus;
 DWORD*    pdwDstStatus;
 DWORD     dwPart;
 PCOLUMNDATA   pColumnData;
 BYTE    b;

 BYTE*     rgbRowDataSave = NULL;

 rgbRowDataSave = (BYTE *) malloc( m_pObj->m_cbRowSize );
 if (NULL == rgbRowDataSave)
  return ResultFromScode( E_OUTOFMEMORY );

 if ( m_pObj->m_pextbufferAccessor == NULL
   || FAILED( m_pObj->m_pextbufferAccessor->GetItemOfExtBuffer((ULONG) hAccessor, &paccessor))
   || paccessor == NULL)
 {
  return ResultFromScode( DB_E_BADACCESSORHANDLE );
 }

 assert( paccessor );

 cBindings = paccessor->cBindings;
 pBinding  = paccessor->rgBindings;

 // Is row handle right?
 if ((m_pObj->m_prowbitsIBuffer)->IsSlotSet((ULONG) hRow ) != S_OK)
  return ResultFromScode( DB_E_BADROWHANDLE );

 // Ensure a source of data.
 if (pData == NULL)
  return ResultFromScode( E_INVALIDARG );

 pbProvRow = (BYTE *) (m_pObj->GetRowBuff((ULONG) hRow ));

 // Save the row.
 memcpy( rgbRowDataSave, pbProvRow, m_pObj->m_cbRowSize );

 // Apply accessor to data.
 for (ibind = 0, dwErrorCount = 0; ibind < cBindings; ibind++)
 {
  icol = pBinding[ibind].iOrdinal;
  pColumnData = (COLUMNDATA *) (pbProvRow + m_pObj->m_rgdwDataOffsets[icol]);

  dwDstType  = m_pObj->m_rgdbcolinfo[icol].wType;
  pDst     = &(pColumnData->bData);
  pdwDstLength = (ULONG *) & (pColumnData->dwLength);
  pdwDstStatus = &(pColumnData->dwStatus);
  dwDstMaxLength = m_pObj->m_rgdbcolinfo[icol].ulColumnSize;

  dwPart   = pBinding[ibind].dwPart;
  dwSrcType  = pBinding[ibind].wType;

  if ((dwPart & DBPART_VALUE) == 0)
  {
   if (((dwPart & DBPART_STATUS)
     && (*(ULONG *) ((BYTE*) pData + pBinding[ibind].obStatus) & DBSTATUS_S_ISNULL))
      || ((dwPart & DBPART_LENGTH) && *(ULONG *) ((BYTE*) pData + pBinding[ibind].obLength) == 0))
   {
    pSrc = &b;
    b = 0x00;
   }
   else
    return ResultFromScode( E_FAIL );
   }
  else
  {
   pSrc = (void *) ((BYTE*) pData + pBinding[ibind].obValue);
  }

  dwSrcLength = (dwPart & DBPART_LENGTH) ? *(ULONG *) ((BYTE*) pData + pBinding[ibind].obLength) : 0;

  dwSrcStatus = (dwPart & DBPART_STATUS) ? *(ULONG *) ((BYTE*) pData + pBinding[ibind].obStatus)
    : DBSTATUS_S_OK;

  hr = g_pIDataConvert->DataConvert(
      dwSrcType,
      dwDstType,
      dwSrcLength,
      pdwDstLength,
      pSrc,
      pDst,
      dwDstMaxLength,
      dwSrcStatus,
      pdwDstStatus,
      0, // bPrecision for conversion to DBNUMERIC
      0, // bScale for conversion to DBNUMERIC
      DBDATACONVERT_SETDATABEHAVIOR);
  if (FAILED( hr ))
   return hr;  // fatal error
  if (hr != S_OK)
   dwErrorCount++; // rounding or truncation or can't coerce
 }

 // Carry out the update.
 if (FAILED( m_pObj->m_pFileio->UpdateRow((ULONG) ((PROWBUFF) pbProvRow)->pbBmk, m_pObj->m_rgdwDataOffsets, pbProvRow )))
 {
  // Restore the row to its previous state */
  memcpy( pbProvRow, rgbRowDataSave, m_pObj->m_cbRowSize );
  return ResultFromScode( E_FAIL );
 }

 free( rgbRowDataSave );

 // We report any lossy conversions with a special status.
 // Note that DB_S_ERRORSOCCURED is a success, rather than failure.
 return ResultFromScode( dwErrorCount ? DB_S_ERRORSOCCURRED : S_OK );
}

Deleting Rows

IRowsetChange also enables consumers to delete rows from the rowset. IRowsetChange::DeleteRows deletes rows from the rowset by overwriting the entire row with at signs (@). Deletions made through DeleteRows are applied to the data source immediately. The source code for IRowsetChange::DeleteRows follows; you can find the complete source code for IRowsetChange in RowChg.cpp.

// CImpIRowsetChange::DeleteRows ---------------------------------------
//
// @mfunc Deletes rows from the provider.  If Errors on individual rows
// occur, the DBERRORINFO array is updated to reflect the error and S_FALSE
// is returned instead of S_OK.
//
// @rdesc HRESULT indicating the status of the method
//  @Flag S_OK      | All row handles deleted
//  @Flag DB_S_ERRORSOCCURRED | Some, but not all, row handles deleted
//  @Flag E_INVALIDARG    | Arguments did not match spec.
//  @Flag E_OUTOFMEMORY   | Could not allocated error array
//
STDMETHODIMP CImpIRowsetChange::DeleteRows
  (
  HCHAPTER  hReserved,  //@parm IN | Reserved for future use
  ULONG   cRows,    //@parm IN | Number of rows to delete
  const HROW  rghRows[],  //@parm IN | Array of handles to delete
  DBROWSTATUS rgRowStatus[] //@parm OUT  | Error information
  )
{
 ULONG     ihRow    = 0L;
 ULONG     cErrors  = 0L;
 ULONG     cRowReleased = 0L;
 BYTE*     pbProvRow;

 // If No Row handle, just return.
 if (0 == cRows)
  return ResultFromScode( S_OK );

 // Check for Invalid Arguments
 if ((cRows >= 1) && (NULL == rghRows))
  return ResultFromScode( E_INVALIDARG );

 // Process row handles
 while (ihRow < cRows)
 {
  if (rgRowStatus)
   rgRowStatus[ihRow] = DBROWSTATUS_S_OK;

  // Is row handle valid
  if (S_OK != (m_pObj->m_prowbitsIBuffer)->IsSlotSet((ULONG) rghRows[ihRow] ))
  {
   // Log Error
   if (rgRowStatus)
    rgRowStatus[ihRow]= DBROWSTATUS_E_INVALID;

   cErrors++;
   ihRow++;
   continue;
  }

  // Get RowBuffer to look at which row this applies to
  pbProvRow = (BYTE *) (m_pObj->GetRowBuff((ULONG) rghRows[ihRow] ));

  // Has row already been deleted
  //  S_OK means deleted
  if (S_OK == m_pObj->m_pFileio->IsDeleted((ULONG) ((PROWBUFF) pbProvRow)->pbBmk ))
  {
   if (rgRowStatus)
    rgRowStatus[ihRow] = DBROWSTATUS_E_DELETED;
   cErrors++;
   ihRow++;
   continue;
  }

  // Delete the Row,
  if (S_OK != m_pObj->m_pFileio->DeleteRow((ULONG) ((PROWBUFF) pbProvRow)->pbBmk ))
  {
   // Some better decision as to what rowstatus to set could be done here..
   if (rgRowStatus)
    rgRowStatus[ihRow] = DBROWSTATUS_E_PERMISSIONDENIED;
   cErrors++;
   ihRow++;
   continue;
  }
 } //while

 // If everything went OK except errors in rows use DB_S_ERRORSOCCURRED.
 return cErrors ? (cErrors < cRows) ?
   ResultFromScode(DB_S_ERRORSOCCURRED) :
   ResultFromScode(DB_E_ERRORSOCCURRED) :
   ResultFromScode(S_OK);
}


Data Access for the Masses Through OLE DB

By José A. Blakeley

Abstract

This paper presents an overview of OLE DB, a set of interfaces being developed at Microsoft whose goal is to enable applications to have uniform access to data stored in DBMS and non-DBMS information containers. Applications will be able to take advantage of the benefits of database technology without having to transfer data from its place of origin to a DBMS. Our approach consists of defining an open, extensible collection of interfaces that factor and encapsulate orthogonal, reusable portions of DBMS functionality. These interfaces define the boundaries of DBMS components such as record containers, query processors, and transaction coordinators that enable uniform, transactional access to data among such components. The proposed interfaces extend the Microsoft® OLE/COM object services framework with database functionality, hence these interfaces are collectively referred to as OLE DB. The OLE DB functional areas include data access and updates (rowsets), query processing, schema information, notifications, transactions, security, and access to remote data. In a sense, OLE DB represents an effort to bring database technology to the masses. This paper presents an overview of the OLE DB approach and its areas of componentization.

Introduction

Today, a vast amount of critical information necessary to conduct day-to-day business is found outside the traditional, production corporate databases. Instead, this information is found in file systems, indexed-sequential files (e.g., Btrieve), personal databases (e.g., Microsoft® Access, Paradox), and productivity tools (e.g., spreadsheets, project management, email). To take advantage of the benefits of database technology such as declarative queries, transactions and security, applications must move the data from their original containing system into a DBMS. This process is expensive and redundant. Furthermore, applications want to exploit the advantages of database technology not just when accessing data within a DBMS, but also when accessing data from any other information container. This paper presents an overview of OLE DB, an ongoing Microsoft effort to define a set of OLE interfaces that provide applications with uniform access to data stored in diverse information sources. These interfaces allow a data source to share its data through the interfaces that support the amount of DBMS functionality appropriate to the data source.

Access to Diverse Sources

Consider a salesman who wants to be able to Find all email messages he has received from Seattle customers, including their addresses, within the last two days to which he has not yet replied. This query involves searching the mailbox file containing the salesman’s email, as well as a Customers table stored in an Access DBMS to identify customers. OLE DB enables the development of an application that will access both information sources and assist the salesman to answer this query, which can be formulated in an extended SQL syntax1 as follows:

SELECT  m1.*, c.Address
FROM MakeTable( Mail, d:\mail\smith.mmf ) m1,
MakeTable( Access, d:\access\Enterprise.mdb, Customers) c
WHERE m1.Date >= date( today(), -2 )
AND m1.From = c.Emailaddr AND c.City = “Seattle”
AND NOT EXISTS (SELECT *
    FROM MakeTable( Mail, d:\mail\smith.mmf ) m2
    WHERE m1.MsgId = m2.InReplyTo);

We assume that MakeTable is a constructor function that coerces the mail file (d:\mail\smith.mmf) into a table. It also exposes the Customers table from an Access database (d:access\enterprise.mdb). The function date() takes a date and a number of days as arguments and produces a date.

In general, OLE DB attempts to make it easy for applications to access data stored in diverse DBMS and non-DBMS information sources. DBMS sources may include mainframe databases (e.g., IMS, DB2), server databases (e.g., Oracle, SQL Server™), or desktop databases (e.g., Access, Paradox, FoxPro®). Non-DBMS sources may include information stored in file systems (e.g., Windows NT®, Unix), indexed-sequential files, email, spreadsheets, project management tools, and many other sources. Initial motivation and goals for OLE DB and the Microsoft component databases were outlined by Vaskevitch [10].

Component DBMSs

The benefits of component DBMSs can be discussed from two perspectives: applications and data providers. First, applications have widely varying database management needs. A decision by an application to use a particular DBMS implies a decision to use a particular storage manager, file access methods, security model, query and scripting language, query processor, and transaction manager. Often, applications do not require or use all the functionality packaged within a commercial, monolithic DBMS. Yet, they are forced to pay additional resource overhead for functionality they do not need. Second, there is a large amount of mission-critical data stored in systems that are not classified as DBMSs. Popular data access APIs, such as Open Database Connectivity (ODBC) [2], impose a high entry bar for data providers by requiring them to expose their data through SQL. This requires a non-SQL data provider to implement the equivalent of an SQL engine in the ODBC driver. OLE DB lowers the entry bar for simple tabular data providers by requiring them to implement only the functionality native to their data store. At a minimum, a provider needs to implement the interfaces necessary to expose data as tables. This opens the opportunity for the development of query processor components (e.g., SQL, geographical) that can consume tabular information from any provider that exposes its data through the OLE DB API (see Figure 1). Notice that it is also valid for SQL DBMSs to expose their functionality in a more layered manner using the OLE DB interfaces.

OLE DB addresses these problems by promoting an approach of componentizing DBMS functionality. OLE DB defines an open, extensible collection of interfaces that factor and encapsulate orthogonal, reusable portions of DBMS functionality. These interfaces define the boundaries of DBMS components such as record containers, query processors, and transaction coordinators that enable uniform, transactional access to diverse information sources. A DBMS becomes a conglomerate of cooperating components that consume and produce data through a uniform set of interfaces. The OLE DB functional areas include data access and updates (rowsets), query processing, catalog information, notifications, transactions, security, and remote data access. By defining a uniform set of interfaces to access data, OLE DB components contribute not only to advance the goals for uniform data access among diverse information sources mentioned above, but also help to reduce the application’s footprint by enabling applications to use only the DBMS functionality they need. Initially, OLE DB attempts to provide uniform data access to tabular data. Future versions may provide access to richer models (e.g. object-oriented) and semi-structured data.

Figure 1: A Component DBMS Architecture Using OLE DB Interfaces

OLE DB leverages the OLE Component Object Model (COM) infrastructure [7], which reduces unnecessary duplication of services and provides a higher degree of interoperability not only among diverse information sources, but also among existing programming environments and tools. Indeed, OLE DB will be the way to access data in a COM environment.

Because OLE DB extends the OLE COM services framework, Section 2 provides an overview of COM. Section 3 describes the architecture and main interface areas of OLE DB. Section 4 presents a comparison with related work, Section 5 presents conclusions and directions for future work.

COM Overview

This section provides a summary of key COM concepts. For a detailed discussion of the COM object services framework, the reader is referred to [7, 8].

Interface

A COM object supports a collection of interfaces. An interface is a strongly typed semantic contract between a client and an object. There is no notion of public state. Clients of a COM object have no knowledge of the internal data structures of the object; all interactions with the object are through its interfaces. COM uses the term “interface” in a sense different from that typically used in object-oriented programming with C++. In C++, interface describes all the functions a class supports. A COM interface is a predefined group of related functions that a COM object implements, but does not necessarily represent all the functions that the object supports. A COM object may support multiple interfaces. A COM object is any structure that exposes its functionality through the interface mechanism. An interface is not an object, nor an object class. Interfaces are closer to the concept of abstract classes in C++. That is, an interface is a C++ class that contains nothing but pure virtual member functions. The interface carries no implementation and only prescribes the function signatures for some other class to implement. C++ applications implement COM objects by inheriting these function signatures from one or more interfaces, overriding each interface function, and providing an implementation of each function.

IUnknown

IUnknown is a special interface that is the root of all COM interfaces. It contains three methods: QueryInterface, AddRef, and Release. QueryInterface is the method called by a client on a server object to ask whether the object supports an interface. The process of interrogating an object for the interfaces it supports through the QueryInterface method is sometimes referred to as interface negotiation. If the server object supports the interface, then it returns the client a pointer to it. A pointer to an interface is a pointer to an array of method pointers—a virtual function table in C++ terms—of all the methods supported by the interface. It enables clients to navigate through the multiple interfaces supported by an object. By convention, OLE prefixes the name of all interfaces with the letter “I.” Also, many of the convenience functions provided by COM have the prefix “Co” (e.g., CoCreateInstance).

Explicit Reference Counting

The AddRef and Release methods represent COM’s explicit reference counting mechanism to control the life-cycle of objects. Every time an object hands an interface pointer to a client, for instance as a result of a QueryInterface call, the object increases the reference count of that interface. The client must call Release on each interface pointer it no longer needs. When the reference count of an interface reaches zero, an object can free the memory occupied by the implementation of the interface.

Memory Allocation

When ownership of allocated memory is passed through an interface, COM requires that the memory be allocated with a specific “task allocator.” Most general purpose access to the task allocator is provided through the IMalloc interface instance returned by the function CoGetMalloc.

Error Handling

COM interface member functions and COM library API functions use a specific convention for error codes in order to pass back to the caller both a useful return value and an indication of status or error information. For example, it is useful for a function to be capable of returning a Boolean result (true or false) as well as indicate failure or success. HRESULT is the result type used by all COM interfaces. An HRESULT is a 32-bit value divided up into four fields. One bit indicates the severity (success or error), 2 bits are reserved, 13 bits indicate the group of status codes this error belongs to (e.g., storage, dispatch, RPC), and 16 bits describe the reason for the error.

Registry

The registration database, stored in every Microsoft® Windows®–based system, registers every component object available in a machine. Each component class has a unique class ID (CLSID). The entire registry database as well as each entry in the database has a hierarchical structure. The information included in each registry entry includes: the component’s CLSID, a friendly name associated with the component, and the form in which the component code is packaged (e.g., as a dynamic link library or executable). OLE DB components will also include special registry keys to indicate the type of provider, and the data formats it can interpret.

Other COM Services

Other services provided by COM include structured storage, persistent intelligent names (monikers), uniform data transfer, and access to COM objects from programming languages. Each interface definition prescribes an order in which the methods must occur inside the interface virtual table; this provides the basis for binary compatibility of COM objects developed in multiple programming languages.

OLE DB Components

Before starting a discussion on the OLE DB components, we need to define a few terms. A client is any piece of system or application code that consumes an OLE DB interface; this includes OLE DB components themselves. A provider is any software component that exposes an OLE DB interface.

OLE DB providers can be classified broadly into two classes. A tabular data provider is any OLE DB provider that owns data and wishes to expose its data in a tabular form via the rowset abstraction, which is defined later in this section. Examples of tabular data providers include relational DBMSs, storage managers, spreadsheets, ISAMs, and email. A service provider is any OLE DB component that does not own data, but encapsulates some service by producing and consuming data through OLE DB interfaces. A service provider is both a consumer and a provider. Examples of service providers include command tree to text translators (discussed below), query parsers, query processors, transaction managers, and transaction coordinators. Data is stored in provider-specific formats such as Oracle, SQL Server, Access, Microsoft® Mail, or Microsoft® Excel and it is important to know which providers can access data in what formats. Data providers listed in the registry indicate the data formats they can read.

Connections

The OLE DB connection model defines how data and service providers are located and activated. Two objects are the basis for the OLE DB connection model: the data source object (DSO) and the session object. To access an OLE DB provider, a client must first instantiate a DSO. Each data provider is identified by a unique class identifier (CLSID) in the registry and is instantiated by calling the OLE CoCreateInstance function, which creates an instance of the object through the object’s class factory. The DSO exposes the IDBInitialize interface which the client uses to provide basic authentication information such as user ID and password (for cases when the data source does not exists in an authenticated environment) as well as the name of the data source (file or database) containing the data to be accessed. Once a DSO has been successfully initialized, the DSO exposes the interfaces: (a) IDBInfo through which a client can query the capabilities of a provider. These capabilities include the interfaces, rowset properties (e.g., scrollability), transaction properties (e.g., isolation levels), SQL dialects, command operations (e.g., left-outer joins, text search operators), and security options a provider supports; and (b) IDBCreateSession through which a session object can be created. The DSO, through which a client initializes access to the data source, may expose the IDBEnumerateSources interface which returns a rowset describing all data sources accessible to the DSO. In some ways, this rowset represents a view of data sources published in the registry. The session object acts as a rowset generator, command generator, and transaction generator. A data provider that does not support commands (described below) exposes the IOpenRowset interface which enables providers to expose their data as rowsets.

Figure 2: Rowset Object

Rowsets

Rowsets are the unifying abstraction that enables all OLE DB data providers to expose data in tabular form. Conceptually, a rowset is a multi-set of rows where each row has columns of data. Base table providers present their data in the form of rowsets. Query processors present the result of queries in the form of rowsets. This way it is possible to layer components that consume or produce data through the same abstraction. The most basic rowset object exposes three interfaces: IRowset, which contains methods for iterating through the rows in the rowset sequentially; IAccessor, which permits the definition of groups of column bindings describing the way in which tabular data is bound to client program variables; and IColumnsInfo, which provides information about the columns of the rowset. Using IRowset, data may be traversed sequentially in a bi-directional manner depending on the properties of the rowset. Figure 2 illustrates the data structures a generic rowset object may support.

Other rowset interfaces capture rowset properties a provider may support. For example, there are interfaces to capture the ability to insert, delete, and modify rows. In addition, there are rowset interfaces that expose richer row navigation models, such as direct access and scrollability. Rowset objects are generally manufactured in one of two ways. First, they represent the result of a query for providers that support query facilities. Second, they are exposed directly as a result of an IOpenRowset::OpenRowset method call to a data source (e.g., table, index, file, in-memory structure).

Figure 3: Index Object

Indexes (Figure 3) can be viewed as rowsets whose rows are formed from index entries with the additional property of allowing efficient access to contiguous rows within a range of keys. Indexes are OLE DB objects used primarily by query processor components. Indexes abstract the functionality of B-trees or indexed-sequential files. Indexes in OLE DB represent an interesting way in which common functionality is reused among data with different storage characteristics. Indexes are traversed using the IRowset interface; information about the index entries is obtained via the IColumnsInfo interface; and insertions and deletions are performed through the IRowsetNewRows and IRowsetDelete interfaces, respectively.

Data Binding and Accessors

Data binding describes the relationship between a field in a client structure and a rowset column value, or parameter in a stored procedure. A client uses the binding structure to communicate to the provider how it expects the data to be transferred from the provider to the client’s buffer. The binding structure includes information such as the rowset column being accessed, the data type expected in the client’s buffer, the offset within the client’s data structure where the provider must place the data, the amount of space available for the column value in the client’s buffer, and the precision and scale for values of type numeric. For columns containing OLE objects, the client can also request the interface that must be returned to the client when the object is fetched. An accessor defines a group of binding structures. It permits clients to define simultaneous access to multiple columns and enables providers to optimize access to these multiple columns.

Commands

In OLE DB, data definition (DDL) and data manipulation (DML) statements are referred to as commands. A command object encapsulates the query processing services available in today’s DBMSs. Command objects expose various interfaces representing different pieces of functionality of a query processor including query formulation, validation, and execution. Figure 4 illustrates a typical OLE DB query processor.

Figure 4: Command Object

The main purpose of a command object is to execute a command. Queries are commands that return results in the form of a rowset. Data definition commands are commands that do not return rowsets (e.g., create table, index, or view). Commands can be expressed in the form of text (typically ANSI SQL-92), or in the form of a command tree. A command tree, which is a mixture of parse tree and algebra expression, is a data structure whose nodes represent operators that consume and produce data. The types of data manipulated by these operators include table (ordered, unique, hierarchical), scalar (constants, row), and Boolean.

Command objects also expose other functionality available in a query processor such as the ability to validate a command syntactically or semantically, formulate parameterized queries, define optimization goals or resource consumption limits during execution, and request properties and interfaces on the rowsets resulting from commands.

An OLE DB client wanting to use the services of a command object typically follows the following steps: (a) obtain an interface to the command object, typically ICommandText, (b) build a text string representing the request, (c) pass the text to the command object, (d) request optimization goals or specify resource consumption limits during execution (cost limits), (e) request properties to be supported by the resulting rowset objects including interfaces they will expose, and (f) execute the command. The successful execution of a command results in a rowset or an array of rowsets representing a hierarchical result (linked tables). The user can then use the methods of the rowset to navigate the contents of the rowset as required.

Figure 5: Command Tree

Notice that because command objects consume and produce rowsets, it is possible to compose query processors to process distributed, heterogeneous, or parallel queries. It is also possible to compose specialized query processors (e.g., SQL query processor, text-search query processors, geographical or image query processors). Clients can also formulate queries in the form of command trees. There are two advantages for using command trees as a way to express queries: (a) they provide a single model for applications and tools (e.g., QBE-like design tools) to express queries; (b) command trees are more extensible than text (it is easier to extend the capabilities of a provider by adding a new operator node than to extend the syntax of text). As an example, the text query “SELECT City, CustomerName FROM Customer WHERE Priority > 5 ORDER BY City;” can be equivalently expressed by a command tree as shown in Figure 5.

Rowset Properties

During query formulation, OLE DB clients can request certain properties to be satisfied by the rowsets resulting from a query. Common properties include the set of interfaces to be supported by the resulting rowset objects. Any rowset returned from a query exposes the mandatory interfaces IRowset, IAccessor, and IColumnsInfo described earlier. The basic IRowset interface supported by all rowsets allows clients to, at minimum, navigate the rowset in a forward-only manner. Requesting the “bi-directional” rowset property enables a client to navigate the rowset in both directions. Notice that supporting bi-directional rowsets puts additional requirements on the creation of the rowset object to support this functionality efficiently. A forward-only rowset can be fed directly from the query execution plan output. A bi-directional rowset may require materializing the result. In addition to the default interfaces, a client may request interfaces to enable direct positioning within a rowset via bookmarks (IRowsetLocate), scrollability (IRowset Scroll), immediate updatability of rows (IRowsetNew Rows, IRowsetDelete, IRowsetChange), deferred (or batch) updatability of rows (IRowsetUpdate), asynchronous population of the rowset (IRowset Asynch), and the ability to lock individual rows overriding the isolation level of the transaction within which the rowset is accessed (IRowsetLockRows). It is also possible to request additional properties that specialize the behavior of certain interfaces, for example, to make some columns of the rowset updatable and the rest read-only.

Optimization Goals and Execution Limits

As part of the query formulation, OLE DB clients can also specify certain optimization and execution goals to be met by a query processor component. Optimization goals enable clients to specify the critical resources for which it is important to optimize. For example, a client application running on a TV set top box having limited main memory may wish to optimize for low memory consumption at the expense of network messages. A client running on a laptop may wish to optimize battery consumption by suggesting the query processor to deliver the first feasible execution plan obtained within 10 ms of optimization time.

Execution limits enable clients to request that the query processor not spend execution resources beyond a certain point. In a sense, execution limits act as “query execution governors.” A client may order the query processor to stop execution after reaching 10 seconds of CPU time, or after computing the 100th result row, or after having transmitted 100 KB of result data over a network. Once the query processor reaches the execution limit it suspends execution of the query; the client may then choose to stop, abort, or resume execution.

The resources for which optimization goals or execution limits may be specified include: CPU, memory, I/O, or network messages. The units include time, number of bytes, number of rows, and so on. Both lists are extensible, enabling OLE DB providers to define their own provider-specific resources or units (e.g., battery consumption).

Schema Information

OLE DB clients obtain catalog information through the IDBSchemaRowset interface. This interface enables clients to request information about types, tables, columns, indexes, views, triggers, assertions and constraints, statistics, character sets, collations, and domains, as well as some other relationships among these (e.g., constraints and tables, constraints and columns, columns and domains). All catalog information is presented to the client through rowsets. The schemas of these catalog rowsets are based on the ANSI SQL 92 standard. We will not describe the contents of each schema rowset further in this paper.

Transactions

OLE DB defines a basic set of interfaces for transaction management. A transaction enables clients to define units of work within a provider. These units of work have the atomicity, concurrency, isolation, and durability (ACID) properties [3]. They allow the specification of various isolation levels and optimistic concurrency control policies to allow more flexible access to data among concurrent clients.

Local Transactions

Local transactions refer to transactions running in the context of a resource manager. A provider that supports transactions exposes the ITransactionLocal interface on the session object. A call to the ITransactionLocal::StartTransaction method begins a transaction on the session object. A session object may be inside or outside of a transaction at any point in time. When created, a session is outside of a transaction and all the work done under the scope of the session is immediately committed on each OLE DB method call. When a session object enters a local or coordinated transaction, all the work done under the session, between the StartTransaction and Commit (or Abort) method calls, including other objects created underneath it (commands or rowsets), are part of the transaction. The StartTransaction method supports a rich set of options defining various isolation levels, concurrency control policies, and resource retention behavior (after commit) that clients may request when creating a transaction. OLE DB providers do not need to support all possible transaction options defined. A client can interrogate the transaction capabilities of a provider through the IDBInfo interface.

For providers that support nested transactions, calling ITransactionLocal::StartTransaction with an existing transaction begins a new nested transaction below the current transaction. Calling ::Commit or ::Abort methods on ITransactionLocal commits or aborts the transaction at the lowest level, respectively.

Distributed Transactions

OLE Transactions, another effort within Microsoft which is separate from, but synergistic with OLE DB, defines a model and interfaces for coordinating transactions among multiple (possibly distributed) data providers. Distributed transactions are not discussed further in this paper. Complete details of the distributed transaction coordinator as implemented in the SQL Server product V6.5 are given in [6].

Notifications

OLE DB uses a well known “OLE controls” notifications model to allow notifications among OLE DB components and clients. Currently, OLE DB defines two notification models: local notifications and data source notifications.

Local notifications are used by groups of cooperating clients sharing a rowset. All clients and the rowset-generating action are assumed to be working within the same transaction. Local notifications enable cooperating clients sharing a rowset to be informed about actions on the rowset performed by their peers. For example, consider a form containing two data controls (widgets); one displaying data in a grid and the other displaying data as a two dimensional histogram. Both controls receive their data from the same rowset, and operate individually as clients of the rowset object. An end-user may communicate an update to the rowset through the grid control. Local notifications will enable the other control to be informed of these changes as they occur giving it the opportunity to update the histogram as appropriate, ultimately providing a consistent view of the data within the form. Clients register their interest in receiving local notifications though the IConnectionPointContainer (ICPC) interface. Clients of local notifications support the IRowsetNotify interface, which is called by the rowset object upon occurrence of interesting events.

Data source notifications (watches) are designed to enable clients to be notified about changes to the underlying data source originated by other concurrent clients running under different transaction contexts in either read committed or read repeatable isolation levels. Data source notifications under read uncommitted or serializable isolation levels do not make sense; the former leads to unpredictable results, the latter by definition does not allow updates to data being read by other concurrent transactions. Upon notification, the client can request a list of changes from the data provider. An interesting application of data source notifications is the support of replicated data or materialized views. The client code may represent an incremental refresh algorithm that is triggered by the notification. The client code requests the list of changes from the data source (the deltas). Upon arrival of the list of changes, the client code can compute an incremental algorithm to bring the replica or materialized view up to date. Clients register their interest in receiving data source notifications though the IConnectionPointContainer interface. Clients of data source notifications support the IRowsetWatchNotify interface which is called by the rowset object upon occurrence of changes to the underlying data source.

Figure 6: Rowset Supporting Notifications

Notifications in OLE DB define a basic mechanism on which to implement active database behavior. IRowsetNotify and IRowsetWatchNotify represent two, among many, possible notification contracts between sources and sinks. Providers may define other notification contracts as needed. Figure 6 illustrates typical plumbing between a rowset supporting the two types of notifications and three clients. Clients 1 and 2 receive local notifications, Client 3 receives data source notifications.

Remote Data Access

OLE DB enables efficient and transparent access between clients and providers across process and machine boundaries. In the following discussion, the term “process boundary” refers to a process or machine boundary. We distinguish two forms of remoting: data remoting and object (or interface) remoting. In data remoting, a provider driver exposing OLE DB interfaces is present on the client site. All interaction with the provider is through the driver. All details about communication protocol, data transfer format, and the provider’s native interface calls are handled internally by the driver. An example of this approach would be an OLE DB provider accessing a SQL DBMS via ODBC. In other words, the process or machine boundary cuts through the provider’s driver. The client/provider relationship is entirely within a process.

Figure 7: Clients Always Call In-Process Code. Objects Are Always Called by In-Process Code.

In interface remoting, an OLE DB provider is running remotely in a different process. Interface remoting provides the infrastructure and mechanisms to allow a method invocation to return an interface pointer to an object created in a different process. The infrastructure that performs the remoting of interfaces is transparent to both the client and the server object. Neither the client or server object are aware that the other party is in a different process. Network OLE, another COM technology leveraged by OLE DB, provides an open specification and reference implementation of such an infrastructure. Figure 7 illustrates the various configurations in which client and provider code may exist.

The proxy/stub agents are agents that take care of marshaling arguments across the process boundary, and optimize access over the network by trying to hide the latency and bandwidth. The design of proxies and stubs is an open-ended optimization problem. Proxies and stubs may vary in complexity. Simple proxy/stub pairs provide marshaling of arguments to method calls. Sophisticated proxy/stubs may provide client-side caching combined with bulk data transfer over the process boundary to minimize network trips, or may even include boxcarring of method calls [3], thereby optimizing network trips across multiple interface method calls.

Providers may choose to implement their remote data access by either approach. An OLE DB Software Development Kit (SDK) will provide reference implementations of data and interface remoting. Data remoting will be offered as an OLE DB provider over ODBC where all process and network communications are managed internally by the ODBC driver. Interface remoting will be offered as a library of generic proxy/stub pairs for all OLE DB objects. Providers wishing to have a tighter control over the performance of their OLE DB implementations across process boundaries are free to implement their own remoting agents using the network protocol or optimizations of their choice.

Security

OLE DB defines interfaces that support authentication, authorization, and secure administration of access among users, groups, and resources. The role of security in OLE DB is to: (a) expose the relevant security functionality encapsulated in the security models of individual data providers or the underlying operating system on which they run, (b) provide a pass-through layer of security interfaces through which the underlying security functionality can be made available to OLE DB clients, and (c) to provide a unified view of security.

Authentication

OLE DB allows authentication through different mechanisms depending on the layer that enforces authentication: operating system, network, or data provider. In password-based authentication, the client authenticates itself to the data provider by supplying a name and password. The data provider enforces authentication in this case. When the client and provider operate in-process, passwords can be passed directly to the provider. In situations where a network connection is involved, then the password must be sent to the provider over the network, which presents additional security problems. Domain-based authentication implies the availability of an authentication service provided by the operating system (e.g., Windows NT). In this environment, users authenticate themselves to the domain by providing a password when logging into the system. Once the user is authenticated to the domain, the domain provides identification information on behalf of the user in a trusted manner. Distributed authentication assumes the existence of a distributed authentication service such as the one provided by the Microsoft® Security Support Provider Interface (SSPI) which is modeled after DEC’s General Security Support API. In OLE DB, clients call the IDBInitialize interface to request the type of authentication mechanism they wish to use. They can also request the quality of service they expect when communicating to the provider across a network.

Authorization

Authorization, or access control, is concerned with the enforcement of the privileges defined among users, groups, roles, services, etc. In most cases, authorization is enforced internally by the provider and the client only gets return codes from method invocations determining if the call was successful or a permission violation occurred. OLE DB leverages a set of security interfaces defined by Network OLE to control security of RPC calls between proxies and stubs, the launching of objects, and impersonation of clients by servers.

Administration

Security administration is concerned with managing the granting and revoking of permissions among users, groups, roles, and any provider-specific object (data or code). Data providers supporting commands (e.g., ICommandText) process security administration requests formulated by clients using SQL GRANT and REVOKE statements. Data providers not supporting commands may expose the IDBAccessControl interface which contains methods to get and set permissions among objects, users, and groups. IDBAccessControl is defined for cases where multiple providers, each supporting its own security enforcement mechanisms, are composed or layered. Since each data provider has its own domain space of privileges, users, and objects, OLE DB provides an extensible mechanism to model the difference in models where each type of privilege, user, and object type is defined by a global unique identifier (GUID). In addition, OLE DB defines the IDBTrusteeAdmin interface defined for the administration of trustees. A trustee may be a user, group, role, agent, or service. The IDBTrusteeAdmin interface defines methods for creating and deleting trustees, populate and test membership in trustee collections, query trustee permissions, and manage trustee affiliations to groups or roles. We decided to add this interface since there was no standard interface (e.g., SQL) to manage trustees in a system.

Related Work

OLE DB is related to previous work on extensible DBMS architectures. Genesis [1] was an early effort to factor areas of independently reusable functionality within a DBMS. It contains components defining query language parsing for SQL and QUEL, file access methods, recovery, and concurrency control policies. Genesis provides a compiler-compiler DBMS generation tool that enables a database designer to configure a DBMS from high-level specifications. The tool produces code implementing a DBMS by mixing and matching the various components available in the tool. While Genesis represents a significant achievement in the area of component DBMSs, it has some shortcomings. The interfaces are not open for third parties to reuse or extend; they are only known within the Genesis DBMS generator tool, and the generated components cannot be used outside the tool since its output is a single, monolithic piece of code.

The Open OODB project at Texas Instruments [11] developed an open, extensible architecture for constructing a family of OODBs. The architecture consists of two kinds of services. First, the meta-architecture service houses a collection of support modules providing abstractions for typeless object containers (address space managers), object translation, type repository, and communications. The meta-architecture also includes an infrastructure for defining events, event monitors (sentries), and actions to be called as a result of those events. Sentries represent the mechanism that allows services, built on top of the support modules, to be plugged into the Open OODB framework. Second, the architecture includes an extensible collection of services that implement policies (called policy managers) in various functionality dimensions. There are policy managers for persistence, transactions, queries, remote data access, and change management. The Open OODB project is similar to the componentization goals of OLE DB, but it is different in that it supports object models based on the type systems of C++ and CLOS which are richer than OLE DB’s tabular model. However, the Open OODB project does not attempt to address the issues in providing uniform access to diverse information sources.

OLE DB is also related to Open Database Connectivity (ODBC) [2]. They are similar in that they are both APIs designed to provide access to a wide range of data providers. OLE DB and ODBC are synergistic rather than competing technologies. ODBC is an API designed to provide access to SQL data in a multi-platform environment. OLE DB is designed to provide access to data in a COM environment. OLE DB subsumes all the functionality of ODBC, but it also adds new features such as a richer and sharable cursor model via rowsets, notifications, watches, and command trees. A subset profile of OLE DB provides functionality equivalent to ODBC. ODBC requires all providers to expose an SQL model of data. To comply with ODBC, non-SQL providers have to build functionality equivalent to an SQL engine in their drivers to share their data. In contrast, OLE DB factors various layers of DBMS functionality providing interfaces for each layer. As a result data providers need to expose only the OLE DB interfaces that reflect the DBMS functionality they natively implement. For example, a tabular data provider must support rowsets and can ignore the command interfaces. Because ODBC is now an ANSI/ISO standard, we expect that ODBC will continue to be the preferred way to access standard SQL data.

OMG’s Object Services Architecture [9] consists of an object request broker and a collection of services that communicate through the broker. Some of the object services include persistence, transactions, naming, queries, relationships, events, and security. The OMG object services architecture is more directly related to OLE/COM. OLE DB is the collection of data access services for COM.

Starburst uses table functions, which are a kind of user-defined functions, as a mechanism to import data residing outside the database and to present it as a virtual table [5]. These virtual tables can be filtered, joined with other tables, and similarly manipulated by the full power of SQL without actually having to store that data in the database. In Starburst, the code implementing any user-defined function must be linked with the rest of the Starburst system. It is not clear from the description in [5] what information the implementor of a table function must expose to allow the resulting function tables to be regarded as regular tables. OLE DB aims at making all these interfaces open and does not require linking a provider’s implementation of a rowset with a query processor component.

Conclusions

OLE DB aims to increase the level of interoperability among diverse tabular information sources by providing a uniform abstraction (rowsets) to view this data. It provides an approach to developing a component DBMS technology by defining a collection of open, extensible interfaces describing orthogonal pieces of DBMS functionality, and defining how to build components supporting these interfaces. We presented an overview of the interface areas. The current goal for OLE DB is to provide uniform data access to tabular data providers. Future work includes expanding the access to non-tabular data (e.g., complex objects and self-describing data such as in HTML and SGML documents).

References

  1. D.S. Batory, et al., “Genesis: An Extensible Database Management System,” IEEE Trans. Software Eng., Vol. 14, No. 11, Nov. 1988, pp. 1,711-1,730.

  2. K. Geiger. “Inside ODBC,” Microsoft Press®, 1995.

  3. J. Gray and A. Reuter. “Transaction Processing: Concepts and Techniques,” Morgan Kaufmann, 1995.

  4. D. Haught and J. Ferguson. “Microsoft Jet Database Engine Programmer’s Guide,” Microsoft Press, 1996.

  5. G.M. Lohman et al. “Extensions to Starburst: Objects, Types, Functions, and Rules,” Communications of the ACM, Vol. 34, No. 10, Oct. 1991, pp. 94-109.

  6. Microsoft, “Guide to Microsoft Distributed Transaction Coordinator,” Microsoft SQL Server, Version 6.5, 1996.

  7. Microsoft, “OLE 2 Programmer’s Reference,” Vols. 1-2, Microsoft Press, 1994.

  8. Microsoft, “The Component Object Model Specification,” 1996, Microsoft Development Library, CD-14, January, 1996.

  9. OMG, “The Common Object Request Broker: Architecture and Specification,” The Object Management Group, 1992.

  10. D. Vaskevitch. “Database in Crisis and Transition: A Technical Agenda for the Year 2001,” Proc. of the ACM Sigmod 1994 Conf., pp. 484-489.

  11. D.L. Wells, J.A. Blakeley, C.W. Thompson. “Architecture of an Open Object-Oriented Database Management System,” Computer, Vol. 25, No. 10, Oct. 1992, pp. 74-82.

Appendix. A Rowset Traversal Example

The purpose of this appendix is to illustrate a simple OLE DB program that reads a table through a rowset, printing each row along the way. Familiarity with C++, and OLE is assumed. Code to check status and errors after each OLE call is omitted for clarity. The OLE DB interfaces are targeted to DBMS developers, tool builders, and independent system vendors. It is not an interface for application programmers. Bindings to high-level languages like C++ or Visual Basic® will be done through higher-level object models such as Data Access Objects (DAO) [4].

#include<oledb.h>
int main()
{
// Define all variables used in the program, not shown
...
// Initialize OLE
CoInitialize(NULL);
 // Instantiate a DSO for an email data provider
CoCreateInstance(CLSID_MailProvider,0,CLSCTX_LOCAL_SERVER,
               IID_IDBInitialize, &pIDBInit);
// Initialize the DSO for email data provider
pIDBInit->Initialize( “smith”, “password”, “c:\mail\smith.mmf” );
// Request the IDBCreateSession interface
pIDBInit->QueryInterface(IID_IDBCreateSession, &pIDBCS);
// Create a session returning a pointer  to its IOpenRowset interface
pIDBCS->CreateSession(NULL,IID_IOpenRowset,&pIOpenRowset);

// Open a rowset corresponding to the email file
pIOpenRowset->OpenRowset(NULL,&TableID,0,NULL,IID_IRowset,rgPropertyErrors,
                         &pIRowset );
// Get information about column types
pIRowset->QueryInterface(IID_IColumnsInfo, &pIColsInfo);
pIColsInfo->GetColumnInfo(&cCol, &rgColInfo, &pzColNames);
pIColsInfo->Release();
// Establish bindings using a convenience function
CreateBindingsFromInfo(rgColInfo,&cBindings, &rgBindings, &rgData );
// Request the IAccessor interface from rowset
pIRowset->QueryInterface( IID_IAccessor,&pIAccessor );
// Create an accessor, return an accessor handler
pIAccessor->CreateAccessor(DBBINDIO_READ,cBindings,rgBindings,&rgErrors,&hAccessor );
// Read the rows; 100 rows at a time into the rowset cache
while(SUCCEEDED(hr=pIRowset->GetNextRows(0,NULL,0,100,&cRowsObtained,&rghRows))
      && cRowsObtained)
{
   for( irow = 0; irow < cRowsObtained; irow++ )
   {
   // GetData copies the rows into the local buffers,  performing the  type conversions
      //  specified in the binding structures associated with the accessor
       pIRowset->GetData(rghRows[irow],hAccessor,rgData );
       // Convenience function to print the data
       PrintData( rgData );
   }
      // Release the rows just printed from the rowset cache.
   pIRowset->ReleaseRows(cRowsObtained,rghRows,NULL,NULL );
}
// Release the accessor handle and the rowset
pIAccessor->ReleaseAccessor(hAccessor);
pIAccessor->Release();
pIRowset->Release();
// Release the session and DSO objects
pIDBC->Release();
pIDBInit->Release();
// Uninitialize OLE
CoUninitialize();
return;
}

Footnotes:

1 This syntax was designed for illustration purposes only. It is not implemented in any product that we are aware of.


OLE DB/ADO: Making Universal Data Access a Reality

Abstract

Microsoft® Universal Data Access is a platform for developing multitier enterprise applications that require access to diverse relational or nonrelational data sources across intranets or the Internet. Universal Data Access consists of a collection of software components that interact with each other using a common set of system-level interfaces defined by OLE DB. Universal Data Access components consist of data providers, which contain and expose data; data consumers, which use data; and service components, which process and transform data (for example, query processors and cursor engines).

After reviewing the Universal Data Access platform, this paper describes two key aspects of the platform. First, it presents an overview of ActiveX® Data Objects (ADO), a provider-neutral, application-level data access object model that can be used from languages such as Visual Basic®, Java, C++, Visual Basic, Scripting Edition (VBScript), and JScript™. Second, it describes a service component architecture that enables you to compose services transparently to consumers and providers. Four services in the Universal Data Access platform work with each other via the service component architecture. A cursor service provides an efficient, client-side cache with local scrolling, filtering, and sorting capabilities. A synchronization service provides batch updating and synchronizing of data cached in the client or middle tier. A shape service allows the construction of hierarchically organized data with dynamic behavior under updates. Finally, a remote data service provides efficient marshaling of data in multitier environments over connected or disconnected networking scenarios.

Introduction

Modern data-intensive applications require the integration of information stored not only in traditional database management systems, but also in file systems, indexed-sequential files, desktop databases, spreadsheets, project management tools, electronic mail, directory services, multimedia data stores, spatial data stores, and more. Several database companies are pursuing a traditional database-centric approach, which this paper refers to as the universal database. In this approach, the database vendor extends the database engine and programming interface to support new data types, including text, spatial, video, and audio. The vendor requires customers to move all data needed by the application, which can be distributed in diverse sources across a corporation, into the vendor’s database system. This process can be expensive and wasteful.

Universal Data Access is an effective alternate approach to the universal database approach. However, it also can be implemented as a complementary approach and used with the universal database approach. The premise of Universal Data Access is to allow applications to efficiently access data where it resides without replication, transformation, or conversion. Open interfaces allow connectivity among all data sources. Independent services provide for distributed queries, caching, update, data marshaling, distributed transactions, and content indexing among sources.

Vaskevitch outlined the initial motivation and goals for OLE DB and the Microsoft component database approach in the paper Database in Crisis and Transition: A Technical Agenda for the Year 2001. Blakeley, in Data Access for the Masses through OLE DB, presented an overview of the architecture, objects, and key interfaces for OLE DB. This paper continues the theme of these two previous papers, detailing key components that are making Universal Data Access a reality and describing how the OLE DB service component architecture works to provide run-time extensibility of database functionality to applications.

Universal Data Access Overview

OLE DB is the Microsoft system-level programming interface to diverse data sources. OLE DB specifies a set of Microsoft® Component Object Model (COM) interfaces that encapsulate, or hide, various database management system services. These interfaces enable you to create software components that implement the Universal Data Access platform.

Whereas OLE DB is a system-level programming interface, ADO is an application-level programming interface. ADO, based on Automation, is a database-programming model that allows enterprise programmers to write applications over OLE DB data from any language including Visual Basic, Java, VBScript, JScript, and C/C++.

The following figure illustrates the Universal Data Access architecture. There are three general kinds of database components: providers, services, and consumers.

Data providers are components that represent data sources such as SQL databases, indexed-sequential files, and spreadsheets. Providers expose information uniformly using a common abstraction called the rowset.

Services are components that consume and produce OLE DB data. For example, a cursor engine is a service component that can consume data from a sequential, forward-only data source to produce scrollable data. A relational query engine is an example of a service over OLE DB data that produces rowsets satisfying a Boolean predicate.

Consumers are components that consume OLE DB data. Examples of consumers include services such as a query processor; high-level data access models such as ADO; business applications written in languages like Visual Basic, C++, or Java; and development tools themselves.

All interactions among components in the previous figure, indicated by bidirectional arrows, can occur across process and machine boundaries through network protocols such as Microsoft® Distributed COM (DCOM) or HTTP. Transacted interactions among components are possible via a distributed transaction coordination service such as the one included in Microsoft® Transaction Server.

OLE DB defines a hierarchy of four main objects as illustrated in the following figure.

The Data Source object encapsulates functions that identify a particular data provider, verify that the user has the appropriate permissions to connect to the provider, and initialize the connection to a particular data source (for example, a file name or database name).

The Session object encapsulates the functions to connect to a particular data source, possibly within a transaction.

The Command object encapsulates the functions that enable a consumer to invoke the execution of data-definition or data-manipulation commands such as queries.

Rowset objects are created either directly from a session or as a result of a command execution.

Before each OLE DB object is created, the caller has the opportunity to request the capabilities or behavior of the data source that need to be surfaced by the object through its interfaces. This is an important aspect to remember when reading the section titled “Service Components.”

ActiveX Data Objects

ActiveX Data Objects (ADO) is a set of high-level Automation interfaces over OLE DB data. Although OLE DB is a powerful interface for manipulating data, most application developers do not need the low level of control that OLE DB gives over the data-access process. Most developers are not interested in managing memory resources, manually aggregating components, and other low-level operations. Additionally, developers often use high-level languages that do not support function pointers and other C++ call mechanisms. ADO provides the following advantages to programmers:

Ease of use. The data-access task analogous to “Hello World” requires only three lines of code.

Programming language neutral. ADO can be used from languages such as Visual Basic, Java, C++, VBScript, and JScript.

Provider neutral. ADO can access data from any OLE DB source. In addition, ADO adapts gracefully to support less functional OLE DB providers.

No loss of OLE DB functionality. ADO allows C++ programmers access to the underlying OLE DB interfaces.

Extensible. ADO can dynamically expose properties specific to a data provider via collections of provider properties. In addition, it provides type extensibility by allowing access to COM objects exposed as column values (for example, rowset and stream).

Object Model

ADO is made up of seven objects as described in the following figure.

The Connection, Command, and Recordset objects are top-level objects that you can create and destroy independently of each of the other objects. Though you can create the Parameter object independently of a Command object, it must be associated with a Command before you can use it. The Field, Error, and Property objects each exist only within the context of their parent objects, and can’t be separately created.

The ADO Connection object encapsulates the OLE DB Data Source and Session objects. It represents a single session with the data source. The Connection object defines properties of the connection, assigns the scope of local transactions, provides a central location for retrieving errors, and provides a point for executing schema queries.

The ADO Command object encapsulates the OLE DB Command object. The Command specifies the data-definition or data-manipulation statement to be executed. In the case of a relational provider, this is an SQL statement. The Command object allows you to specify parameters and customize the behavior of the statement to be executed. A collection of Parameter objects exposes the parameters.

The ADO Recordset object encapsulates the OLE DB Rowset object. The Recordset object is the actual interface to the data, whether it is the result of a query or was generated in some other fashion. The Recordset object provides control over the locking mechanism used, the type of cursor to be used, the number of rows to access at a time, and so on. The Recordset object exposes a collection of Field objects that contain the metadata about the columns in the recordset, such as the name, type, length, and precision, as well as the actual data values themselves. Use the Recordset object to navigate through records and change data (assuming the underlying provider is updatable).

Each of the top-level ADO objects contains a collection of Property objects. The Property object allows ADO to dynamically expose the capabilities of a specific provider. Because not all data providers and service components have the same functionality, it is important for the ADO model to allow you to access provider-specific settings in a dynamic way. This also prevents the ADO model from being cluttered with properties that are available only in certain circumstances.

Architecture

ADO is provider neutral. That is, although it allows you to access the full range of OLE DB data, it does not implement its features specific to any one provider or data source. In addition, ADO itself does not add functionality on top of the provider. Augmenting the functionality of providers, such as the ability to scroll or update, is the job of service components. One benefit is that you can take any piece of code that can manufacture an OLE DB Rowset and wrap that object in an ADO Recordset object, thereby enabling scripting of that data. Because the Recordset object isn’t needed to connect to the data source or execute the command, it needs only the core rowset interfaces (that is, IAccessor, IColumnsInfo, IConvertType, IRowset, and IRowsetInfo) to provide access to the data.

Language-Specific Extensions

Automation implements the ADO interface. Automation defines Variants as its data types. Variants work well in languages such as Visual Basic, which are not strongly typed. If you are writing in C++ or in Java, most operations in the code will require native data types to support the standard library features of the language. In these cases, with ADO you can convert data between the Variant and native data types when passing arguments.

ADO has extensions designed specifically to support both Java and C++. For C++, a set of interfaces that directly retrieve and set native C types from the underlying OLE DB provider improve performance and decrease the lines of code required to interact with the ADO interface.

For Java, ADO extensions include a set of Java classes that extend the ADO interfaces and create notifications interesting to the Java programmer. These extensions expose functions that return Java types. In addition, the extensions are optimized to improve performance. The Java class directly accesses the native data types in the OLE DB Rowset object and returns them to the Java programmer as Java types without first converting them to and from a Variant.

Sample Code

Below are two samples, one written using VBScript in the Active Server Pages (ASP) environment (which embeds script code within HTML), and one written in Java.

Service Components

Providing a common interface to diverse data stores containing mission-critical data is not an easy task. Each data store supports different sets of functionality, making it difficult to define a useful level of interaction between a generic application and diverse stores. Defining the level of interaction to be only the common functionality among all stores does not provide a useful set of functionality for the application. Further, this approach limits the ability of the data store to expose innovative extensions. On the other hand, requiring a rich level of functionality would force a simple data store to implement, in full or in part, a relational engine on top of its data. Such barriers prevent many types of simple data from participating in data access.

The Microsoft Universal Data Access solution is to define a rich set of functionality, but to factor that functionality so that data stores are able to expose only the subset that makes sense for their data. Thus, the data store is not limited in the functionality it can expose, but at the same time is not forced to implement extended database features in order to provide common access to its data.

This makes OLE DB attractive for the data store, but to stop there would be near to defining a least-common-denominator approach for the application. In order to be generic, the application would have to be filled with extensive conditional code to verify whether a particular set of functionality was supported. In many cases, the application would be forced to implement extensions internally to compensate for data stores that could not support extended functionality natively.

Generic, Reusable Components

To solve this problem, Universal Data Access provides a component architecture that allows individual, specialized components to implement discrete sets of database functionality, or “services,” on top of less capable stores. Thus, rather than forcing each data store to provide its own implementation of extended functionality, or forcing generic applications to implement database functionality internally, service components provide a common implementation that any application can use when accessing any data store. The fact that some functionality is implemented natively by the data store, and some through generic components, is transparent to the application. In order to understand how OLE DB components are combined to guarantee a rich functionality, it is necessary to understand COM interface factoring and aggregation.

Factored Interfaces

In COM, a single object exposes one or more interfaces. Each interface exposed by the object represents a fixed set of functionality, such that the total functionality of the object is factored among the various interfaces. Each interface inherits functionality directly or indirectly from a common interface on the object called IUnknown, such that the IUnknown interface is the “super parent” of every other interface supported by the object. IUnknown contains AddRef and Release methods, which control the lifetime of the object through reference counting, and a QueryInterface method for navigating between the different interfaces on the object. The following diagram shows a single COM object that implements two interfaces, IRowset and IRowsetScroll, in addition to IUnknown.

The full set of functionality defined in the OLE DB specification is factored into multiple discrete interfaces that group related levels of functionality for each object. This interface factoring is what makes OLE DB so accessible to simple data stores. Rather than forcing every data store to implement a full relational engine in order to be accessible through common tools, the data store implements an object comprised of only those interfaces that represent native functionality for that data store. At a minimum, this includes core interfaces that provide a simple forward-only, read-only view of the data.

COM Aggregation

A COM object can be created using another object’s IUnknown interface, in which case each interface exposed by the newly created object inherits functionality from this existing “Controlling Unknown.” The controlling object then manages the navigation between sets of functionality supported by interfaces implemented on the inner object and sets of functionality supported by interfaces on the controlling object.

The following diagram shows a COM object that supports the IRowset interface, and a controlling object that implements the IRowsetScroll interface. The IUnknown interface exposed by the controlling object navigates between the interfaces supported by the two different objects. To the application, this appears as a single object that implements both IRowset and IRowsetScroll .

Service Components

In OLE DB, a service is implemented as a controlling object that aggregates the native object exposed by the data store. When the application requests an interface that represents some set of functionality natively supported by the data store, the service returns the data store’s native interface and the application interacts with the data store directly. When the application requests some set of functionality not supported by a native interface on the data store, the service can provide a standard implementation of that functionality, interacting with the data store through the same native interfaces exposed to the application. In this manner, the application always gets the most direct, efficient path to the data. Whether the functionality is implemented natively by the data store or through an external service is completely transparent to the application.

To maximize performance and minimize overhead, the services themselves are factored into multiple components—only those services needed at any one time are loaded into memory. Thus, if a particular data store requires multiple services to satisfy the requirements of the application, multiple service components may be invoked.

Invocation of Services through Component Managers

The next problem is the actual invocation of the individual service components. Putting the logic for invoking service components within the application complicates the application code, forcing it to know the particular capabilities of the data store and the available services. Putting the logic within the data store complicates the code for the data store, limiting the ability to extend the functionality of the data store to those services known to the data store at the time its OLE DB interface was written. Furthermore, a growing number of data stores are exposing OLE DB interfaces today without any knowledge of existing services. To work with those existing data stores, the logic must live outside the data store.

The commonly held belief that any problem can be solved given enough levels of indirection proves true in this case, where the dynamic invocation of multiple services at run time is handled through thin controlling objects called component managers. Component managers aggregate each of the primary OLE DB objects exposed by the data store and ensure that the proper services are invoked to provide required functionality. Each interface supported by the data store’s native object, as well as the interfaces supported by each of the individual service components, inherit from this Component Manager’s IUnknown interface. Thus, all interface navigation goes through the Component Manager, which intercepts interfaces at any time and invokes additional services as needed. All of the logic for invoking services can be localized in this Component Manager, and new components can be added that make existing applications more functional without adding any additional code to either the application or the data store.

The following diagram shows a simple OLE DB Rowset object that exposes forward-only data retrieval through IRowset, aggregated by a Service Component that exposes scrollbar support through IRowsetScroll. Even though the Component Manager is present, calls to IRowsetScroll go directly to the Service Component while calls to IRowset go directly to the OLE DB Rowset object.

The Component Manager’s ability to intercept interfaces comes into play particularly in the creation of one object from another. The following diagram shows the Data Source Component Manager (DCM) aggregating the Data Source Object.

When the application creates a Session, the DCM intercepts the call and first creates a Session Component Manager (SCM), then creates a Session aggregated by the SCM. Similarly, when the application creates a Command from the Session, the SCM first creates a Command Component Manager (CCM), then creates a Command aggregated by the CCM. Finally, when the application creates a Rowset, the CCM first creates a Rowset Component Manager (RCM), then creates a Rowset aggregated by the RCM. At any step along the way, the individual component managers can invoke additional services to add additional functionality on top of the data store’s Data Source, Session, Command, or Rowset objects. Thus, once a Data Source Component Manager correctly aggregates the initial Data Source object, the remaining objects are automatically aggregated with their own instances of Component Managers.

At this point, the only remaining problem involves ensuring that a Data Source Component Manager correctly aggregates the initial Data Source object. The application can do this explicitly, or automatically by substituting the Class Factory that creates the initial Data Source object with one that creates the Data Source object aggregated by the Component Manager. In either case, once the Component Manager aggregates the initial Data Source object, neither the application nor the data store needs any knowledge of extended services or the components that support them. The Component Managers invoke the specialized, highly optimized services necessary to provide the application a rich data model when accessing any type of data.

As explained in the previous sections, Universal Data Access enables the data flow between a data client and any kind of data provider. In most scenarios, as the data moves through each tier of the application architecture, it has to be cached, filtered, or reshaped and needs to be synchronized with the original data in the provider. However, most providers deliver their data as forward-only, read-only rowsets. The gap between the rich functionality demanded by a typical data application and that offered by most data providers is filled by services. The following sections describe four specific services that you can combine using the service component architecture just presented.

The Cursor Service

An important challenge for a multitier, distributed application is to find a balance between keeping data current, or live, and reducing network traffic. Live data produces high network traffic and is impractical for most applications. It requires every change at the application level to be immediately propagated to the data source, and concurrent changes to the data source to be immediately pushed to the client application. In addition, every time the application scrolls beyond its local buffer, new data must be fetched to ensure dynamic membership of the data set. Another side effect of using live data is that it virtually kills application scalability. An Internet application where thousands of client requests need to be served from a data source simply does not work with live data.

A Cursor Service proves a good solution for this challenge. It provides a client cache for data sets (or cursors) and a service that implements smart synchronization mechanisms (see “The Synchronization Service.” Using these services dramatically reduces network traffic while giving you full control over data synchronization. The Cursor Service can be installed on each tier of a multitier application, providing a great solution for scalability.

Architecture

The Cursor Service consumes data from a rowset provider, stores the data in a local cursor, and exposes the cursor data plus rich data-manipulation functionality through rowset interfaces. The input data can come from a native data provider or from just another service component. Virtually any rowset produced within an application can be converted, via the service component architecture, into a local cursor by using the Cursor Service implementation.

The Cursor Service is typically invoked when you request a client cursor property for a rowset. All cursors are handled independently by the Cursor Service. Cursors don’t share common data, and no integrity constraints are enforced. The Cursor Service doesn’t try to replicate the provider data model, its integrity restrictions, or its data. It rather manipulates independent data sets as defined and required by the application.

Functionality

The Cursor Service provides the following functions: data fetching, data manipulation, local updates, and local cursors.

Data Fetching

When you, the user, require a rowset of client cursor type, the Cursor Service component is invoked to fetch the data and cache it locally. There is practically no limit to the number of rows a cursor can contain. The data is stored in memory buffers and, if needed, written to temporary files on disk. The data fetch can be performed synchronously or asynchronously. The fetch of large data fields, such as BLOBs, can be delayed until the data is actually required by the application.

Data Manipulation

The cursor is accessible to you via ADO/OLE DB interfaces. In ADO, use the Recordset object to maintain currency and manipulate the data (see “ActiveX Data Objects”). The Recordset exposes methods implemented by the Cursor Service to position on a row, get field values, and filter and sort the cursor rows based on one or more fields. A Recordset can be cloned, which produces a second Recordset based on the same underlying cursor data. Clones have their own currency, filter, and sort order. The Cursor Service also generates events exposed by the associated rowset and used to bind cursor data to visual controls. Cursors can be persisted in an internal binary format or in XML. A client cursor can be remoted between two processes or machines over DCOM or HTTP (see “Remote Data Service”), enabling distributed data access for multitier architectures over intranets and the Internet.

Local Updates

You can make any changes to the local data: modify data fields, delete existing rows, and insert new rows into the cursor. The changes are immediately committed to the cursor cache and are visible from all the cloned Recordset objects on that cursor. Local changes can be committed to the data provider row by row or once for the entire cursor via the Synchronization Service.

Local Cursors

The Cursor Service allows you to create a stand-alone cursor, by first defining the column information associated with the cursor and then opening the cursor. Local cursors can be used as data buffers or to hold and share local temporary data in an application. Indexes can be created on cursors to support sort, filter, and find operations. All data manipulation operations are available on local cursors.

The Synchronization Service

For applications using client cursors, the Synchronization Service component provides the ability to send changes back to the provider and to refresh the data in local cursors with current data from the provider. Both the update and refresh operations can be applied to one or more rows simultaneously and can be coupled together in a single user command. Both operations ensure consistency of multitable cursors if the structure of the join relation between the participating tables is known.

Architecture

The Synchronization Service has two distinct components: the Metadata Gatherer, which provides metadata information such as base table names, base column names, primary keys, and timestamp columns; and the Update/Resync component, which sends changes to the provider and returns data changes to the client. The Synchronization Service operates in conjunction with the Cursor Service, which is responsible for gathering and storing the metadata and for storing the data copies of each changed row necessary for optimistic conflict resolution. The following figure illustrates the interaction of the Synchronization components (indicated by the labeled bracket).

The synchronization process has the following stages:

First, the Cursor Service reads all the data from the underlying provider rowset and populates the cursor. In this process, it collects and stores the basic metadata information for the rowset such as number of columns; column name, type, and precision; base table and column names; primary key columns; and timestamp/version columns.

If some metadata information is not available from the rowset provider, the Cursor Service calls the Metadata Gatherer to obtain this information. If the Metadata Gatherer still cannot obtain the metadata information, the client application can explicitly set it.

After the rowset data has been modified, the client requests to propagate the changes to the data source. The rows to be updated are passed to the Update/Resync component of the Synchronization Service with a flag stating whether the original rowset needs to be refreshed after the update. This component reads the updated metadata from the Cursor Service rowset and submits the updates to the data source. It then sets the update status on each row in the rowset participating in the update.

Depending on the flags set when the update was submitted, the Synchronization Service could update the conflicting rows in the rowset with new values inserted by concurrent users of the data store.

All the update information available for a rowset is considered an integral part of the rowset. The Cursor Service can disconnect the rowset from the underlying data source, persist it in the local file system, or remote it across process/machine boundaries. At a later stage, the updated rowset may be reconnected to the same session that originally created it (if it is still alive) or to a new session. After the reconnection, all the update information is available and the rowset can be updated via the Synchronization component.

Functionality

The Synchronization Service performs the following functions: propagates updates to the data source, refreshes the cursor from the data source, and coordinates conflicting updates.

Updates

The Update Service has two modes of sending the updates back to the data provider: immediate and deferred. In immediate mode, changes are committed to the provider on a row-by-row basis. In deferred mode, the changes made to the cursor are buffered locally and the row’s state is marked as changed. When the application decides to commit the changes, it calls the batch update method on the cursor and all buffered changes are sent to the data source.

Using a client static cursor with deferred updates is a common application scenario, offering the following three benefits. First, it provides fast data navigation because after the data is fetched, most operations are local. Second, it eliminates heavy concurrency in accessing data on the server. Optimistic conflict resolution allows you to resolve any conflicts. Finally, it reduces the overall network traffic associated with propagating changes to the data source by grouping multiple individual updates in a single request.

Refresh

The Refresh function updates the cursor with the most recent data available from the provider. The scope of the Refresh command could be a large data field, a row, a set of rows, or the entire cursor. To perform a refresh, the Update Service requires information about the base tables used to produce the cursor data set and their primary keys.

Optimistic Conflict Resolution

For each row that has changed in a cursor, the Cursor Service maintains a copy of the original row fetched from the provider. When the updates are sent to the server, the Synchronization Service compares original values with the current values on the server to verify whether the row has changed since the cursor data was fetched. The Synchronization Service compares the values using a combination of key columns, row timestamps, changed fields, and sometimes the entire row.

If a conflict is detected, the cursor row status for that row changes to indicate an update conflict. You have the option to get the current values for the conflicting row and compare them with the original row values and the changed values in the cursor. After you resolve the conflict, the update can be submitted again, this time using the current values to detect new update conflicts. The update service has the option to get current data for all conflicting rows in a batch. It also allows you to control how the local cursor is updated when conflicts are detected, to match the transaction model implemented by the application.

The Shape Service

The Shape Service provides a hierarchical view of data and the reshaping capabilities that user interfaces and reporting tools require. This service works on top of the Cursor Service; it can create new hierarchical cursors or aggregate existing cursors into hierarchies. It implements three types of hierarchies, based on relation, parameters, and grouping. Hierarchical cursors offer a flexible view of a data set. For example, an application could initially display the top level of a hierarchy, then let the user drill down into one or more levels of detail. Also, the same hierarchy can then be reshaped locally to offer a different perspective on the data.

Architecture

To implement hierarchical cursors, the Shape Service adds a special column in the parent cursor for each of its children in the hierarchy. For each parent row, this column contains a reference to the corresponding child cursor. This reference is called a chapter handle in OLE DB and a recordset reference in ADO. A shape command defines a hierarchical cursor with clauses for each type of parent-child link (relation, parameter, and grouping).

Each type has a well-defined semantic, which governs the way the component cursors are created and related to each other. A hierarchical cursor can be arbitrarily complex and can include relations of any type among its components. More specifically, relation and group hierarchies can have as building blocks flat cursors and any types of hierarchical cursors. Parameterized hierarchies can have any cursor as parent but not as child, because the child is built dynamically upon accessing the data.

The previous diagram shows the implementation of hierarchical rowsets. When a hierarchy is created, the provider adds a column to the parent rowset to provide, for each row, a reference to its child cursor. When the value of a child column is requested, the Shape Service evaluates the parent-child relation for that row, then applies the corresponding filter to the child rowset and returns the resulting chapter handle. If the call is made from ADO, a recordset based on that chapter handle is returned. Different child cursors of a relation hierarchy may share common rows. Child cursors of a parameterized hierarchy are fetched dynamically and are always considered to have distinct rows. All data-manipulation and synchronization methods available for flat cursors are also available for hierarchies. Similarly, cursor events are available for hierarchical cursors. The following sections briefly define the three hierarchy types.

The Relation Hierarchy

Two source cursors, a parent and a child, create the relation hierarchy, and it usually models a one-to-many relationship between the two rowsets. The definition specifies the relation between the parent and the child via an equality predicate between two fields. When a child reference is requested, the child is filtered for the fields that match the corresponding field in the parent, and the resulting chapter or rowset is returned. Unlike hierarchical snapshots produced by a data source, the relation hierarchy is dynamic. For example, if the controlling parent field changes, the corresponding child cursor will change as well.

The Parameterized Hierarchy

The parameterized hierarchy is similar to the relation hierarchy. The parent cursor is static and fetched up front. But the child cursor is defined using a parameterized command, having parent fields as command parameters. Initially the child contains no data. When a child reference is required, the shape provider executes the command and fetches the child data that matches the current parameter fields in the parent row. The child data for each row can either be saved locally and subsequently treated as static data or refetched each time a child reference is required. Because the child is dynamic, parameterized hierarchies can only be leaves in a multilevel hierarchy.

The Group Hierarchy

This hierarchy requires only one input cursor that will become the child cursor of the group hierarchy. The shape provider performs a grouping on this cursor and stores the result in a new local cursor, the parent of the group hierarchy. The parent has a column to hold the group details for each row. Group hierarchies are static and can be created on top of any other hierarchy.

Functionality

The Shape Service, based on the Cursor Service, provides the following functions:

  • Creates relational, parameter, or group hierarchies.

  • Creates hierarchies by reshaping client cursors.

  • Extends the functionality provided for flat cursors to hierarchical cursors.

  • Allows creation of aggregate columns over children.

  • Enables creation of client-calculated columns within a record.

  • Enables the addition of empty user-defined columns to a result set.

The hierarchy syntax aggregates a cursor, as defined by a command or a name and constructs a new hierarchy. A command is a cursor definition native to the data provider. Commands can contain parameters, which customize the hierarchy when it is created. A name refers to existing local cursors already built by the shape provider. The Shape Service can add columns to a new cursor. The new columns can be an aggregation function over child rowsets, an expression, or just a new user-defined column.

The value of the child reference column is by default different for each row because the Shape Service creates a new chapter every time. The provider can optionally return a unique child reference for a given child cursor, and update the membership of the respective child chapter on row changes to contain the child rows for the current parent row.

The Shape Service applies the requery method called on the root cursor of a hierarchy to the entire hierarchy. All other functions such as navigation and synchronization are associated with each component cursor and should be called as needed on each component of a hierarchy.

Remote Data Service

The Microsoft® Remote Data Service (RDS) component delivers the promise of Universal Data Access to “stateless” environments such as the Internet and the World Wide Web. RDS creates a framework that permits easy and efficient interaction with OLE DB data sources on corporate intranets and over the Internet. RDS provides the advantages of client-side caching of data results, updatability, and support for data-aware ActiveX controls. It provides the rich OLE DB/ADO programming model for manipulating data to browser-hosted applications.

Motivation/Scenarios for Use

Remote Data Service transports the richer client experience, implemented by the Cursor Service component, to applications connecting over the Internet or other networks where it is difficult or expensive to maintain a consistent connection. The following are the typical features of distributed applications running over disconnected networks such as the Internet.

Server Scalability and Network Bandwidth

Scalability of Web or application servers is tested every day by users accessing applications over the Internet. Even when the servers can scale to handle large numbers of Internet users, network bandwidths often cannot. Client computers must be able to accomplish more with less support from the server, and at times without even a server connection. Server resources need to be handled judiciously by new brands of applications.

To solve the scalability problem, RDS moves the application logic (that is, the user interface and client-side data manipulation) and data to the client. It implements a “stateless” model, in which client and server can be disconnected between data access operations. RDS marshals data efficiently between the middle tier or server and the client; it provides support for batch updates via an efficient client-side Cursor Service component that can process data locally without constant server requests. The work done at the client can be saved locally and later uploaded to the server when connection is available and cheap.

Distributed Applications and Untrusted Clients

Some of the most important tasks that Web and application server administrators handle are data integration, security, and application management. Organizations deploy business components that run on the application servers behind the Web servers and control complete access to underlying enterprise data. These components implement business rules and validation logic, allowing client applications to read and update data. The client applications make requests to the components that are under central control of the system administrator. RDS makes it easy to invoke these components over DCOM and HTTP and efficiently marshals input/output parameters, enabling effective distributed computing.

Architecture

Remote Data Service is designed as an OLE DB service component. It builds on the foundation that the Cursor Service component laid and utilizes its services on the client machine as a data buffer. Consumers (that is, applications running on client machines) can use the RDS OLE DB provider to open Recordset objects—these recordsets are originally created on middle tiers or servers where OLE DB data sources reside and then remoted (marshaled by value) to client machines. The following code snippets show how a remote, disconnected Recordset object is opened in ADO:

Dim rs as new ADODB.Recordset
rs.Open "Select * from Customers", "Provider=MS Remote; Remote Server=http://www.mysite.com; Remote provider = MSDASQL; DSN=pubs"

The only difference between the code shown here and that required to open a recordset when direct access to an OLE DB data source is available, is the format of the connection string. In the direct access scenario, the code looks like this:

Dim rs as new ADODB.Recordset
rs.Open "Select * from Customers", "Provider=MSDASQL; DSN=pubs"

In the remoted case, the connection string identifies the server where the data source resides, the OLE DB provider to be used there to open the rowset, and any other information (such as an ODBC data source name) needed to open the rowset. The MS Remote provider is a standard OLE DB provider that supports standard OLE DB objects like data source, session, and command objects, and acts as a front-end for providers across the network. For the Rowset object, it uses the Cursor Service component’s rowset implementation. It buffers user requests on the data source, session, and command objects and processes them one after the other, as singular requests, across the network.

As shown in the previous figure, RDS enables two powerful scenarios for client-side applications. A consumer application can either specify the command and connection strings needed to open a rowset directly through an OLE DB provider, or it can invoke a business object on the server and marshal input and output parameters efficiently over HTTP and DCOM networks. Server administrators configure the server stub of the Remote provider where they can specify commands that can be executed or business components that can be invoked on behalf of remote clients.

Compare the two scenarios. Boxes with boldface labels represent objects implemented by the remoting service component.

In the OLE DB provider scenario, when a request is made to execute a command or open a rowset (step 1), all the settings are streamed across the network to a Remote provider stub on the server (step 2). The stub opens the rowset at the server by executing consumer requests (step 3). The OLE DB provider creates a rowset as the result of the request. This rowset is then streamed out on the wire in an efficient and compact format, and reconstructed using the Cursor Service component at the client by the Remote provider (step 4). An ADO recordset is handed back to the consumer application (step 5).

In an HTTP or DCOM network scenario, when a request is made to invoke an object (steps a and b), all the settings are streamed across the network to a Remote provider stub on the server (step c). The stub invokes the required component by executing consumer requests (step d). The invoked business component creates a rowset and returns it as an out-parameter (step e). This rowset is then streamed out on the wire in an efficient and compact format, and reconstructed using the Cursor Service component at the client by the Remote provider (step f). A client-side rowset is handed back to the consumer application (step g).

No proprietary network protocols are needed to work with RDS. It works over standard protocols like HTTP and DCOM. DCOM is now available on all Win32 platforms, Mac, and UNIX platforms. Here is a code snippet that shows how client applications can invoke business objects on the server:

Dim ProxyGen as new RDS.DataSpace
Dim BusProxy as object

Dim rsCust as ADODB.Recordset
Set CustProxy = ProxyGen.CreateObject("OrderEntry.Customer", "http://www.mysite.com")
Set rsCust = CustProxy.GetCustomer

RDS also allows local persistence of remoted Recordset objects. Thus, Recordset objects can be saved into a local file system and opened later. All the client changes and original values read from the server are persisted. A client can go offline and transmit the changes to the server at convenient times without losing valuable changes. Recordset persistence does not require any sophisticated data store or personal database on the client machine. The following code snippet shows how a recordset can be saved and reopened.

Dim rs as new ADODB.Recordset
rs.Open "Select * from Customer", "Provider=MS Remote;Remote Server="http://www.mysite.com";DSN=pubs"
…
rs("Cust_Status") = "Preferred"   // Update customer records
…
rs.movenext
rs("zip_code") = 98052
…
rs.save "c:\ADO\recordsets\customer.rs"   // Persist the recordset in a local file
rs.close
…
Dim rs1 as new ADODB.Recordset
rs1.open "c:\ADO\recordsets\customer.rs"   // Reopen the recordset
rs1.updatebatch            // Propagate the changes back to the server
…

When a client requests updates to be propagated back to the server or middle tier, the Remote provider streams the rowset, including all changes and original values, back to the application server. At the server, the Cursor Service component reconstructs it. The Cursor Service component and the Synchronization component work together to update and resynchronize the rowset, as explained in the previous sections. Once the rowset has been updated and resynchronized, it can be remoted back to the client, where the consumer application can check the status of updates or read new values obtained from the server.

Encoding Rowset Data with XML

OLE DB Rowsets are self-describing objects that contain data and metadata, created by executing commands against data stores. Because of its self-describing nature, rowset information can be easily represented in a descriptive and flexible language such as XML. XML is, therefore, supported as a streaming and persisting format for rowsets. This opens up the use of rowsets to non-COM platforms—if XML representing structured data can be generated from there, it can be translated into rowsets on other machines and vice versa. This also allows Recordset objects to be transmitted over HTTP as part of standard HTML text. The following is a subsection of an XML stream representing rowsets:

<?XML version="1.0" encoding="UTF-8" ?>
<orders>
<schema id="rss">
<elementType id="Customer" occurs="ONEORMORE">
      <element href="#NameEntry" occurs="ONEORMORE"></>
   <element href="#Order" occurs="ONEORMORE"></>
      <element href="#Date"></>
</elementType>
<elementType id="Date"> <pcdata></> </elementType>
<elementType id="NameEntry">
      <element href="#FirstName"></>
      <element href="#LastName"></>
</elementType>
<elementType id="LastName"> <pcdata></> </elementType>
<elementType id="FirstName"> <pcdata></> </elementType>
…
</schema>
<Customer>
   <NameEntry>
      <FirstName>Pierce</>
      <LastName>Bronson</>
   </NameEntry>
   <Order>
      <Name>Where the Sidewalk Ends</>
      <Author>Shel Silverstein</>
      <Price>$5.95</>
      <Detail>
         <Carrier>United Airlines</>
         <SKU>411AAA</>
      </Detail>
   </Order>
   …
   <Date>7/7/97</>
</Customer>
…
</orders>

Summary

The Universal Data Access architecture is designed to provide uniform access to the variety of data sources available in a corporation. In this platform, ADO, a provider-neutral and language-independent data access object model, works in conjunction with OLE DB and its service component architecture, which extends a provider’s native functionality transparently to the application and the provider. This architecture represents a new approach to extending database functionality dynamically at run time by aggregating OLE DB services using standard COM aggregation.

To date, there are OLE DB providers for most commercial relational DBMSs including Oracle, Microsoft® SQL Server™, IBM DB2, Sybase, Informix, CA-Ingres, and ODBC sources. In addition, there are providers for VSAM, AS-400 files, IMS/DB, and Windows NT® files via a content search engine. OLE DB has recently been extended to other domains such as OLAP, and spatial data. There are distributed query processors over OLE DB data from International Software Group, B2Systems, Intersolv, and Microsoft SQL Server. OLE DB, ADO, and all services described here are shipping commercially as part of the Microsoft Data Access Components.


Microsoft Strategy for Universal Data Access

By David Lazar

Overview and Scope

This paper provides an in-depth look at Microsoft® Universal Data Access, Microsoft Corp.’s broadly supported strategy for providing access to information across an organization, from the desktop to the enterprise.

The following people are the intended audience for this paper:

  • Corporate business and information technology professionals

  • Computer professionals

  • Computer industry analysts and press

The technologies discussed in this paper are complex, spanning many areas of computing, including architecture, programming, networking, and platform integration. The issues are critically important because data and information are at the heart of almost any computer system, and the efficient and effective use of information is what provides business value and strategic advantage. Today, these issues are magnified as organizations begin to broadly implement applications that leverage the Internet and mobile computing. Access to information is being required in new scenarios, and the complexity of that information continues to grow. Whereas organizations previously had data on the mainframe and in various DBMSs, now important information is found in mail stores, file systems, Web-based text and graphical files, and more. Organizations that are able to leverage all of this information, that can expand rather than replace current UNIX and mainframe systems to embrace client/server systems and the Internet, will thrive.

This paper is intended to explain Microsoft’s strategy for helping organizations achieve maximum business advantage by organizing and accessing their information efficiently. This is a strategy paper; readers looking for in-depth technical analysis are directed to a companion paper titled “Microsoft Data Access Technologies,” which can be found along with other technical and strategic documents on the Microsoft data access Web site, http://www.microsoft.com/data/.

Customer Requirements for Data Access Technologies

Microsoft has solicited extensive feedback from data access customers on the criteria they use in judging and selecting data access technologies and products. Our research has shown that there are four main criteria used in the decision-making process:

  • High-performance access to data. Simply stated, new data access methods need to provide the same level of performance that is possible through the data provider’s proprietary API. This means that customers will not compromise their No. 1 criterion, performance, for any other benefit. Similarly, services that augment the native capabilities of data providers must be held to the same standards — they must not stand in the way of native performance. And, because applications and components must frequently scale to support hundreds or thousands of concurrently connected users, performance must be maintained as usage grows.

  • Reliability. Customers want their database solutions to perform reliably. They use terms such as “rock-solid” and “fail-safe” to describe their requirements in this area. Underlying these statements is the need to minimize maintenance and support costs, and reduce the total cost of ownership.

  • Vendor commitment. Customers indicate that they are making strategic commitments to vendors of data access technologies, and are looking for reciprocation. They indicate that database decisions are long term, that they are purchasing not a single release but a string of database and related product releases. On the flip side, customers are very wary of becoming too dependent on a single vendor, a situation they term “vendor lock-in” or “vendor tie-in.” Finally, new technologies should evolve gracefully from current ones, to avoid costly replacement of existing capabilities.

  • Broad industry support. This is defined as market share, as well as the support of vendors of related products and technologies. For customers, broad industry support is a more important gauge than the blessing of a standards body when choosing data access products. Broad industry support carries many benefits — safety in numbers, availability of skilled people to work with the products, and products that work together without expensive integration and customization.

This paper has been structured to explain how Microsoft Universal Data Access meets these criteria. After the terms and technologies are defined, separate sections address how Microsoft has met or intends to meet each of the criteria.

Definition of the Universal Data Access Strategy

Universal Data Access is a platform, application and tools initiative that defines and delivers both standards and technologies and is a key element in Microsoft’s foundation for application development, the Microsoft® Windows® Distributed interNet Applications (DNA) architecture.

Today, companies building client/server and Web-based database solutions seek maximum business advantage from the data and information distributed throughout their organizations. Universal Data Access provides high-performance access to a variety of data and information sources on multiple platforms and an easy-to-use programming interface that works with practically any tool or language, leveraging the technical skills developers already have. The technologies that support Universal Data Access enable organizations to create easy-to-maintain solutions and use their choice of best-of-breed tools, applications and data sources on the client, middle-tier, or server.

Figure 1

Another benefit of Universal Data Access is that it does not require expensive and time-consuming movement of all corporate data into a single data store, nor does it require commitment to a single vendor’s products. Universal Data Access is based on open industry specifications with broad industry support and works with all major established database products. Universal Data Access is an evolutionary step from today’s standard interfaces — including ODBC, RDO and DAO — and significantly extends the functionality of these well-known and well-tested technologies.

A Unified Data Access Model Based on COM

One strength of the Microsoft Universal Data Access strategy is that it is delivered through a common set of modern, object-oriented interfaces. These interfaces are based on the Microsoft Component Object Model (COM), the most widely implemented object technology in the world. COM has become the choice of developers worldwide because it provides the following:

  • The richest integrated services, including transactions, security, message queuing and data access to support the broadest range of application scenarios

  • The widest choice of tools from multiple vendors using multiple development languages

  • The largest customer base for customizable applications and reusable components

  • Proven interoperability with users’ and developers’ existing investments

Because of the consistency and interoperability afforded through COM, the Microsoft Universal Data Access architecture is open and works with virtually any tool or programming language. It also enables Universal Data Access to provide a consistent data access model at all tiers of the modern application architecture.

Microsoft Universal Data Access exposes COM-based interfaces optimized for both low-level and high-level application development: OLE DB and ADO.

Definition of OLE DB

OLE DB is Microsoft’s strategic system-level programming interface to data across the organization. OLE DB is an open specification designed to build on the success of ODBC by providing an open standard for accessing all kinds of data. Whereas ODBC was created to access relational databases, OLE DB is designed for relational and nonrelational information sources, including mainframe ISAM/VSAM and hierarchical databases; e-mail and file system stores; text, graphical and geographical data; custom business objects; and more.

Figure 2

OLE DB defines a collection of COM interfaces that encapsulate various database management system services. These interfaces enable the creation of software components that implement such services. OLE DB components consist of data providers, which contain and expose data; data consumers, which use data; and service components, which process and transport data (e.g., query processors and cursor engines). OLE DB interfaces are designed to help components integrate smoothly so that OLE DB component vendors can bring high-quality OLE DB components to market quickly. In addition, OLE DB includes a bridge to ODBC to enable continued support for the broad range of ODBC relational database drivers available today.

Definition of ActiveX Data Objects

Microsoft® ActiveX® Data Objects (ADO) is the Microsoft strategic application-level programming interface to data and information. ADO provides consistent, high-performance access to data and supports a variety of development needs, including the creation of front-end database clients and middle-tier business objects, using applications, tools, languages or Internet browsers. ADO is designed to be the one data interface needed for one-to-multitier client/server and Web-based data-driven solution development.

ADO provides an easy-to-use application level interface to OLE DB, which provides the underlying access to data. ADO is implemented with a small footprint, minimal network traffic in key scenarios, and a minimal number of layers between the front end and data source — all to provide a lightweight, high-performance interface. ADO is easy to use because it is called using a familiar metaphor — the COM automation interface, available from all leading RAD, database tools and languages on the market today. And because ADO was designed to combine the best features of — and eventually replace — RDO and DAO, it uses similar conventions with simplified semantics to make it a natural next step for today’s developers.

Design Goals for Microsoft Data Access Components

Microsoft® Data Access Components (MDAC) is a set of redistributable technologies that implement Universal Data Access. MDAC consists of the latest versions of ActiveX Data Objects, OLE DB components and Open Database Connectivity, which have now been released as an integrated set. Developers creating client/server and Web-based data-driven solutions select the components they need and call them from their choice of tools, applications, languages or Web browser to create complete database solutions.

In designing MDAC, Microsoft set a number of guiding project goals, including the following:

  • Meet the key customer requirements outlined in the preceding section titled “Customer Requirements for Data Access Technologies.” These requirements include performance, reliability, strongly committed vendors and broad industry support. Discussions of how Universal Data Access and MDAC meet these requirements are provided in separate sections later in this document.

  • Enable access to the broadest possible range of data sources, focusing on those that are most important to customers now and in the future. To address this goal, we’ve taken a three-pronged approach:
    • Provide the best possible reuse of existing ODBC drivers, so organizations can continue to leverage their investments in these technologies. Continue to enhance strategic ODBC drivers from Microsoft as OLE DB components become broadly available.

    • Build OLE DB providers for the most important data sources, both Microsoft- and non-Microsoft.

    • Create a comprehensive strategy for enabling the rapid development of high-quality, high-performance third-party OLE DB components to expose all mainstream data sources now and in the future.
  • Provide a migration path that will enable customers to leverage their investments in, and experience with, DAO, RDO and ODBC.

These are the project goals guiding the development of the Microsoft Data Access Components to support and enhance the Universal Data Access strategy.

Next, we will take a closer look at each of the key customer requirements for data access technologies — performance, reliability, strongly committed vendors and broad industry support — and detail specifically how Universal Data Access and MDAC meet these requirements.

Universal Data Access Is a High-Performance Architecture

We have seen that performance is of paramount concern to developers and users of data access technologies. Universal Data Access consequently has been designed with performance as its No. 1 goal. This section will examine how the Universal Data Access technologies support this requirement and the positive implications for users.

Flexible, Component-Based Services Model

OLE DB, designed to be a high-performance architecture, accomplishes this via a flexible, component-based services model. Rather than having a prescribed number of intermediary layers between the application and the data, OLE DB requires only as many components as are needed to accomplish a particular task. For example, suppose a user wants to run a query. Consider four scenarios:

  • The data resides in a relational database for which there currently exists an ODBC driver, but no native OLE DB provider. The application uses ADO to talk to the OLE DB Provider for ODBC, which then loads the appropriate ODBC driver. The driver passes the SQL statement to the DBMS, which retrieves the data.

  • The data resides in Microsoft® SQL Server™ 7.0 or another data source for which there is a native OLE DB provider. The application uses ADO to talk directly to the OLE DB provider for Microsoft SQL Server. No intermediaries are required.

  • The data resides in Microsoft® Exchange Server, for which there is an OLE DB provider, but which does not expose an engine to process SQL queries. The application uses ADO to talk to the Microsoft Exchange data provider and calls upon an OLE DB query processor component to handle the querying.

  • The data resides in the Microsoft® Windows NT® file system in the form of documents. Data is accessed using a native OLE DB provider over Microsoft® Index Server, which indexes the content and properties of documents in the file system to enable efficient content searches.

In all four cases, the application can query the data. The user’s needs are met with a minimum number of components. In each case, additional components are used only if needed, and, if needed, only the required components are invoked. This demand-loading of reusable components greatly contributes to high performance when OLE DB is used.

Performance Advantages Over ODBC

While ODBC has been a very important and successful data access standard, OLE DB has an improved architecture that provides a significant performance advantage over ODBC. ODBC providers no longer have to implement a SQL relational engine to expose data. With ODBC, services such as cursoring and query processing need to be implemented by every ODBC driver writer. This represents overhead both for the ODBC driver author, as well as for the end user (how many cursor engines and query processors do you need on one machine?). With OLE DB, reusable service components handle the processing chores for a variety of data providers. OLE DB simplifies the process of writing data providers, which means they should come online faster and be of a higher quality; it also reduces the number of components installed on data consumer machines.

ADO Performance Advantages

As with OLE DB, ADO is designed for high performance, which it achieves by reducing the amount of solution code developers must write, by “flattening” the coding model. DAO and RDO, the object models that preceded ADO, are highly hierarchical models. To return results from a data source, the programmer has to start at the top of the object model and traverse down to the layer that contains the recordset. The ADO object model is not hierarchical. The programmer can create a recordset in code and then be ready to retrieve results by setting two properties, and then executing a single method to run the query and populate the recordset with results. The ADO approach dramatically decreases the amount and complexity of code that needs to be written by the programmer. Less code running on the client or middle-tier business object translates to higher performance.

Minimal Network Traffic in Key Internet Scenarios

Microsoft has designed OLE DB and ADO for the Internet, implementing a “stateless” model, in which client and server can be disconnected between data access operations. MDAC contains a Remote Data Service component that provides efficient marshaling of data between the middle-tier or server and the client, including support for batch updates, as well as an efficient client-side cursor engine that can process data locally without constant server requests. Thus MDAC provides greater local functionality and higher performance for Internet applications than previous versions and other approaches.

Ultimately, the performance of Universal Data Access components will be judged in comparison with that of native access methods. Microsoft’s goal is to establish OLE DB as the native interface to major Microsoft and non-Microsoft data stores. To accomplish this, performance tuning for all key scenarios (transactions, decision support, etc.) and with major data sources will be of paramount concern. The No. 1 design goal for ADO and OLE DB is performance, and the architectural foundations to achieve this are in place.

Solutions Built With Universal Data Access Components Are Reliable

As we have seen, reliability is one of the primary requirements for organizations managing and supporting data access applications. Universal Data Access aims to address this need in three areas: increasing the manageability of client-side components, enabling strong run-time coordination and control capabilities by the server, and delivering well-tested components.

Increasing the Manageability of Client-Side Components

One of the most important tools that organizations can use to increase reliability and decrease support costs is the reduction of the number of components to support on client PCs. Universal Data Access supports this approach in several key ways:

  1. Universal Data Access supports new multitier and Web deployment models, in which data access logic and business logic are centralized on middle-tier servers. Front ends provide presentation services via browser-based interfaces, or via custom and packaged applications. In this model, application functionality is mainly centralized, not distributed to end-user PCs, thus reducing the number of components to manage on those machines.

  2. OLE DB and ADO are essentially system components, shipping with Microsoft® Internet Explorer 4.0, Microsoft® Windows® 98, and the Windows NT operating system. (ADO ships as a component of Windows NT 4.0 Service Pack 4, and is planned as a component of Windows NT 5.0 and Windows 98.) Therefore, organizations can frequently rely on these components being available in a run-time environment and don’t have to manage the distribution and maintenance of them.

  3. ADO is tool- and language-independent and in many cases may be able to replace multiple data access libraries on client PCs. Organizations that may have previously supported DAO, RDO and ODBC on each PC will now be able to get the same functionality by deploying Internet Explorer 4.0 to their clients and moving data access functions previously handled by ODBC clients to their servers. Internet Explorer with ADO can thus replace DAO, RDO and ODBC on client PCs.

  4. For two-tier applications, where data access remains on the client PC, Universal Data Access can reduce the number of components to manage as well. OLE DB eliminates the need for a driver manager, used by ODBC, by directly querying the client’s System Registry, a systemwide repository for configuration data. OLE DB components query the registry to find out what components are installed on the PC, and load them as needed, rather than working through an intermediary such as the Driver Manager.

Enabling Strong Server-Side Coordination and Control

Universal Data Access enables transactional control of diverse data sources and components via Microsoft® Transaction Server. To achieve this, OLE DB data sources implement the functionality of a resource manager which handles transactions local to the data source and enables each data source to participate in distributed transactions. MTS provides a distributed transaction coordinator that guarantees atomic operations spanning multiple data sources using a reliable, two-phase commit protocol, and enables applications to scale as user load grows, with minimal additional effort.

Delivering Well-Tested Components

MDAC reliability is further ensured via rigorous testing of all components. Because ADO and OLE DB have been shipping in high volume since the release of Internet Information Server 3.0, they have enjoyed significant field usage. With the advent of MDAC, and the commitment to ship ADO and OLE DB in a synchronized fashion, these components are now developed and tested side by side. Finally, after testing the components for interoperability, Microsoft stress-tests MDACs with the products with which they ship (e.g., Windows NT, Microsoft® Internet Information Server, Internet Explorer, etc.) to guarantee reliable behavior in multithreaded, continuous operation in highly concurrent environments. This testing is designed to help ensure high-performance, highly reliable components that work well in a variety of real-world scenarios.

The net result of the Universal Data Access architecture’s three-pronged approach should be significant reductions in configuration and support expenses and a reduced total cost of ownership — in short, a reliable data access architecture.

Microsoft Commitment to Universal Data Access

The choice of data access technologies is extremely strategic for organizations. While typically other factors, such as the DBMS, largely drive this choice, customers have told us that data access decisions are made for the long haul, and consequently, need to be considered carefully.

Here are some of the questions that customers have told us they need answered when evaluating a data access vendor:

  • Is the vendor here to support me, not just for the current release, but through a string of releases?

  • Does the vendor have a strong and compelling strategy for data access, and does that strategy mesh with the goals of my organization?

  • Has the vendor withstood the test of time by consistently delivering strong products in these markets?

  • Likewise, has the vendor been able to succeed in these markets, thus proving its ability to deliver technological solutions and build market momentum, resulting in broad support for the technologies?

Based on market share, developer acceptance and broad industry support for many technologies, we believe that Microsoft has consistently met this set of criteria and has proved to be a market leader for data access technology. We’ll now examine Microsoft’s commitment to Universal Data Access.

A Short History of Microsoft as a Data Access Vendor

A look at how the Universal Data Access strategy has evolved at Microsoft will help illuminate the long-term commitment the company is making in this area.

Microsoft began investing in data access shortly after the initial release of Microsoft SQL Server 1.0 in 1989. Initial interest in Microsoft SQL Server was high, but the tools available to program it were limited. The SQL standard was in its infancy, but was clearly bound to the coming client/server revolution. Microsoft knew that acceptance of client/server architecture would be highly beneficial and could see that the biggest problem the industry faced was the proliferation of data access interfaces, and the complexity of creating, maintaining and programming against them.

Open Database Connectivity was the result of these factors. ODBC combined important features to make it extremely attractive:

  • It was SQL-based, making it familiar to most database developers of the day.

  • Its call-level interface supported a broad range of tools.

  • Its plug-in model for database drivers simplified implementation.

As ODBC gained broad support as a standard for data access, it became clear that a standards body should be defining its future. Thus Microsoft turned over the specification for ODBC to the SQL Access Group, made up of a broad range of DBMS, middleware and tools vendors.

Despite its features, ODBC had a number of shortcomings, which by 1993 were being addressed in the next phase of data access market development. ODBC was programmed as a Windows API, which made it difficult for the majority of customers to use it. A number of Microsoft tools and applications could use ODBC via the Microsoft® Jet database engine, but ODBC functionality was not extensible except for a few API-level programmers. Thus high-level programming models were created, first Data Access Objects (DAO), and then Remote Data Objects (RDO), which simplified the ODBC programming model and made it accessible to a wider range of programmers.

DAO provided Microsoft® Access and Microsoft® Office programmers, and RDO provided programmers using the Visual Basic® programming system, with higher-level interfaces to ODBC. These interfaces seemed like natural extensions to the Visual Basic language, used in each of these products, and have gained broad usage among database programmers.

By 1995, two major new trends began to shape the next phase of development. These two trends, which are still evolving, are the rise of the Internet as a database applications platform and the rise in importance of nonrelational data, which does not fit the database model encapsulated by ODBC.

One might wonder at this stage why the Internet requires new data access technologies. After all, the fundamental goal of connecting people with data remains the same. The Internet, however, presents new data access challenges on many levels:

  • Scale. The Internet is synonymous with more people accessing more data. This amplifies the priorities for effective data management — performance, reliability and security take on new importance.

  • Distributed medium. The Internet is by definition a distributed medium, with many more potential points of failure and network performance issues that go beyond the boundaries typically relevant to information technologists. On the flip side, the medium presents many opportunities to distribute processing tasks for improved service.

  • Casually connected clients. Client and server, previously bound tightly in time and space, are now virtually independent. Client computers need to be able to accomplish more with less support from the server, and at times, without a server connection.

  • New business opportunities. The Internet opens up the possibility for employees, customers, suppliers and business partners to connect on many levels.

  • New types of data. The Internet, while using existing data and databases in new ways, is also composed of massive amounts of text, images and other media that will grow in importance. Successful organizations will manage new data types effectively and synthesize meaning from them.

As the Internet catalyzes a major paradigm shift in database management and data access, a related shift is occurring: the emergence of nonrelational data sources. While the Internet highlights the need for management of textual and graphical data, organizations today face a proliferation of data in a variety of DBMS and non-DBMS stores, including desktop applications, mail systems, workgroup and workflow systems, and others. Most established organizations face an even larger challenge, that of leveraging the data in mainframe and minicomputer flat files as they extend access to this information to intranet- and Internet-based customers.

Data access today encompasses all of the issues traditionally addressed by DBMS systems, plus a range of new data types, new clients and new access methods. The Universal Data Access strategy was created to meet this new generation of challenges by leveraging the successful strategies of the past and embracing the architectures of the future.

Universal Data Access Is the Successor to ODBC

Universal Data Access is a strategy that includes and builds on the successful foundation of ODBC. ODBC successes include the following:

  • Establishing a market standard for database access. There are more than 170 ODBC drivers available today, providing access to a broad range of data.

  • Achieving portability, so applications can scale to new database platforms as an organization’s requirements change.

  • Responding to customers’ needs by steadily adding new features and performance improvements to enable better database applications.

The most frequent customer issues surrounding ODBC are related to performance and configuration management, defined as matching database drivers on multiple machines with multiple back-end data sources. Microsoft is aware of these problems and will continue to address them through subsequent ODBC releases, including a new and significantly improved ODBC driver for Oracle.

Moving forward, ODBC is a supported technology under the Universal Data Access umbrella. ODBC in the short and medium term is the best way to access a broad range of relational DBMS-based data due to the high number of drivers available. During this period, as ODBC remains as a mature technology and OLE DB components are becoming available, Microsoft does not want to force customers to choose between the two architectures and make the ensuing trade-offs. Our goal is to enable customers to take advantage of existing ODBC technologies, while adopting the Universal Data Access architecture for new applications.

It was thus by design that the evolutionary strategy for migrating from ODBC to OLE DB was created. The very first OLE DB provider released by Microsoft was the OLE DB Provider for ODBC. The OLE DB Provider for ODBC replaces the ODBC Driver Manager component on client machines, and talks directly to existing ODBC drivers. Applications are then written to the ADO or OLE DB interface, and the OLE DB Provider for ODBC connects to the ODBC data source. Should an organization later decide to change data sources, add data sources or change from the ODBC driver to a pure OLE DB provider for the existing data source, then the application can be adapted with minimal changes. The solution code will require no changes whatsoever; the only required changes will be the addition or replacement of some data access components.

This evolutionary strategy for migrating from ODBC to OLE DB carries some additional important benefits. Since OLE DB is a component-based architecture with service components providing processing capabilities on an as-needed basis, and since ODBC data sources can expose their data as OLE DB, OLE DB service component features may be invoked against ODBC data. For example, Find, a new feature in the OLE DB 1.5 specification, provides for sorting and filtering within a result set. Thus, the result set can be reused and further refined, without an additional round-trip to the server. This capability is not available in ODBC, except when the ODBC driver is called by the OLE DB Provider for ODBC. Thus new and existing applications can gain additional data access features by using OLE DB to call broadly supported ODBC drivers.

This is the evolutionary path from ODBC to OLE DB that Microsoft is providing based on consistent customer feedback. Organizations should continue to plan on broad availability and support for ODBC drivers. As they build new applications, they should look to the Universal Data Access architecture, using ADO and OLE DB interfaces. Nonrelational data will be exposed by OLE DB providers. For relational data, organizations may choose between ODBC drivers, and, as they become available, OLE DB providers and components. In the long run, Microsoft believes customer demand will drive the market for OLE DB components and they, too, will become broadly available. Able to freely choose among and mix ODBC and OLE DB components, organizations will benefit from the highest possible application performance and reliability, while gaining new capabilities at a pace that suits their unique requirements.

Universal Data Access Is Strategic for Microsoft

Key to understanding the Universal Data Access strategy is the realization that it is intertwined with most of the major lines of business where Microsoft is serving organization customers, including operating systems, tools, applications and Internet products. Universal Data Access is designed to work consistently across each of these major product lines, to enable organizations to leverage their data access expertise across teams and projects to build high-performance database solutions accessible to employees, customers and business partners.

One of the strongest examples of this can be seen in ADO, which provides a single interface to data whether it’s being called from a developer tool, Web page, Office or a custom business object. Applications using a variety of front ends may now use the same high-performance interface to data, featuring a small memory footprint, demand-loaded reusable OLE DB components, and a familiar semantic derived from Microsoft’s most widely used interfaces. No matter where in the multitier architecture one is writing data access code, the interface is the same — ADO.

Making integrated access to all forms of relational and nonrelational data ubiquitous is strategic for Microsoft products, because it enables those products to add value through tools that utilize the Universal Data Access architecture. Customers are the ultimate beneficiaries as their tools and applications become more highly adept at processing the information they work with every day.

Relationship of Universal Data Access and Windows DNA

The Windows Distributed interNet Applications architecture is Microsoft’s architectural framework for building modern, scalable, multitier distributed computing solutions that can be delivered over any network. Windows DNA provides a unified architecture that integrates the worlds of client/server and Web-based application development. Microsoft Universal Data Access, a central part of the Microsoft Windows DNA strategy, provides data access services for Windows DNA applications.

Figure 3. Windows Distributed interNet Applications architecture

Windows DNA addresses requirements at all tiers of modern distributed applications: user interface and navigation, business process and storage. The core elements of the Windows DNA architecture are these:

  • Pluggable software components

  • Extensible Web browser and application server

  • Richly integrated platform services

  • Scalable distributed operating environment and servers

  • Open protocols and published interfaces

  • Choice of programming languages, tools and hardware platforms

Because Microsoft Universal Data Access is based on COM, it provides a unified, consistent and common data access model for all applications built to the Windows DNA model.

Broad Industry Support for Universal Data Access

Organizations using data access components have indicated that in order to invest in the Universal Data Access architecture, they need to see the support of vendors of related products and technologies. For customers, broad industry support carries many benefits — safety in numbers, availability of skilled people to work with the products, and products that work together without expensive integration and customization. This section details the activities in which Microsoft is engaged to solidify and publicize the broad range of companies supporting Universal Data Access.

Supporters of Universal Data Access

The industry reception for Universal Data Access has been very positive. Companies building components in each architectural segment recognize key benefits for their customers — improved performance and functionality, flexibility, and reduced cost. This section will detail the industries and key vendors supporting Universal Data Access, discuss Microsoft’s strategy for continued growth in industry support and provide instructions for additional federated vendors to join the Universal Data Access strategy.

The key industry segments supporting Universal Data Access are as follows:

  • DBMS vendors. These vendors benefit from Universal Data Access by gaining additional high-performance clients for their engines. Their customers benefit by gaining a broader choice of development tools and other supporting technologies and by connecting to and integrating with data from more sources.

  • Development tools vendors. Tools vendors benefit from Universal Data Access by being able to provide access to a broader range of data sources, thus enabling their customers to build richer, more functional applications. Leading tools vendors are participating by licensing MDAC, including ADO, OLE DB providers and components, and ODBC components and drivers. Tools vendors have thus gained the ability to enable their customers to create applications that access the vast majority of data sources available today.

  • Data access component builders. By building OLE DB components, data access component builders gain the benefits of a broadly accepted standard environment in which to deploy their products. Their customers can access more data, find more readily available support and interoperate with a broad array of products across the platform.

Because the list of vendors in each of the above categories is growing rapidly, the reader is asked to visit http://www.microsoft.com/data/ for a complete, updated list. Leading vendors in each industry segment are represented among the list of Universal Data Access supporters.

OLE DB Provider Strategy

To be successful, OLE DB must gain a broad array of native providers and components so that users can connect to virtually any data source, reuse OLE DB service components, and realize performance and reliability benefits.

The tool that OLE DB provider and component vendors use to simplify their work is the OLE DB SDK. Besides all of the data access consumer components discussed in this paper (ADO, OLE DB and ODBC), users of the SDK receive additional tools, documentation and specifications to help them create high-performance OLE DB components. The latest release of the SDK (in beta as of this writing; due for release fall 1997) contains the following:

  • Leveling specification. Defines two levels of providers, with specified functionality for each. Also defines more rigidly the services that should be supported by various classes of service components.

  • Conformance tests. Based on the Microsoft internal testing procedures for providers and designed to make it easier to determine whether the appropriate interfaces for a given OLE DB level are supported.

  • Provider Writers toolkit

  • Visual C++® development system template classes for OLE DB providers

  • Beta version of the OLE DB provider for the Microsoft Jet database engine

These tools simplify the process of writing OLE DB components, provide a framework for creating components that interoperate in well-defined ways and provide criteria by which OLE DB consumers can easily compare component features. Anyone interested in creating OLE DB components should obtain the OLE DB SDK, available for free download from http://www.microsoft.com/data/oledb/ (connect-time charges may apply).

Going forward, the OLE DB SDK will become part of the Data Access SDK, which will include the following:

  • OLE DB SDK

  • ODBC SDK

  • Control Writer SDK

See the section “Universal Data Access: A Road Map for the Future” for more information about the Data Access SDK.

How Vendors Participate in Universal Data Access

Microsoft is interested in working with vendors of products that support Universal Data Access to help ensure that components address the performance and quality demands of our joint customers. Vendors of DBMS products, development tools and OLE DB service components should visit http://www.microsoft.com/data/ for updated information on programs, products and services. Vendors may also submit e-mail to oledbinf@microsoft.com to register their support.

How Universal Data Access Supports Data on Multiple Platforms

While the Windows NT operating system is emerging as an important platform for database management, many organizations rely on a mixture of operating systems and database platforms. To be successful, any strategy for providing data access must be equally efficient at accessing data on all major platforms. Universal Data Access provides the foundation for supporting efficient and reliable access to data on today’s major computing platforms. Microsoft is actively engaged in supporting third-party development projects involving OLE DB providers for non-Windows-based data. In fact, products using the Universal Data Access architecture to access leading DBMSs on non-Windows platforms are currently available.

Figure 4. Universal Data Access Supports Data Across the Enterprise

Because the OLE DB specification defines interfaces that components support, rather than providing a set of DLLs or actual system components, it is highly portable to other operating environments. OLE DB is based on the COM architecture, which is the Windows object model. This would seem to imply that OLE DB components must run on a Windows- or Windows NT-based PC; however, this is not the case. OLE DB in fact has two separate approaches that provide portability to non-Windows-based DBMS platforms: a full port of COM, available today from several vendors, and implementations of COM interfaces on non-Windows platforms.

Today ISG International Software Group Ltd. (http://www.isgsoft.com/), with its ISG Navigator product, provides an example, using the second approach described above, of Universal Data Access components that integrate data from Windows NT and several non-Windows NT platforms, including the following:

Operating Environments | Database Platforms
Windows 95 Windows NT (Intel and Alpha) HP UX IBM RS/6000 AIX DEC UNIX SUN Solaris DEC OpenVMS (Alpha and VAX) IBM MVS (planned) | Microsoft SQL Server Oracle Sybase Informix RMS C-ISAM CA-Ingres DB2 Adabas (planned) IMS/DB (planned) RdbVSAM (planned) MUMPS (planned)

The important thing to recognize about the ISG Navigator product is that its availability and performance prove the ability of the Universal Data Access architecture to integrate data between Windows and non-Windows platforms. It is not the only approach to satisfying the need for multiplatform data access, but it is a solid implementation available and demonstrable today. The Navigator demo, available to anyone visiting the ISG Web site, demonstrates several important features:

  • An Internet-based front end using ADO code called from Visual Basic Scripting Edition in an Active Server Page

  • ADO taking advantage of OLE DB components on both Windows and non-Windows platforms

  • Navigator, in a single SQL query, joining data from multiple data sources, potentially running on several different platforms

Broad availability of Universal Data Access components that integrate data on multiple platforms will benefit organizations that support multiple DBMS platforms. An additional benefit enables users to take advantage of new OLE DB capabilities when accessing non-Windows-based data. Powerful new service components, running on front ends or middle-tier servers, can be integrated with an OLE DB provider, including those running on non-Windows platforms. For example, general-purpose query processors, cursor engines or custom business objects can all add value to non-Windows-based data exposed by OLE DB. Mainframe and UNIX-based databases that previously did not support remoting of data — an essential feature for the Internet and loosely connected scenarios — may now implement it, thus gaining greater use from existing systems and applications. This powerful extensibility and reusability model is a benefit of component-based software written to a broadly supported specification such as OLE DB.

How Universal Data Access Differs From Other Strategies

A number of leading DBMS vendors have begun shipping new databases and updated versions that follow “universal database” strategies. Many Microsoft customers may be curious about how those strategies differ from Universal Data Access.

In the other approaches, data from across the organization is consolidated in the DBMS, and the DBMS is extended with additional processing capabilities to handle new data types. This strategy is attractive for several reasons:

  • It centralizes data for more efficient management.

  • It places a DBMS “wrapper” around new data types, which can ensure the protection of DBMS security and transaction services, while offering other potential benefits such as content indexing.

  • Many DBMSs are very efficient at serving up data, so organizations can generally expect good performance for applications based on traditional and non-traditional DBMS data.

Microsoft, while recognizing these benefits, believes they may be difficult for some organizations to attain. A universal database approach may require expensive and time-consuming movement to and maintenance of corporate data in the DBMS. It may require tools and applications to support it. And it may require compromises in the selection of supporting products. Customers’ applications will need to either implicitly support this architecture, which is unlikely, or be customized to integrate with it, which could be expensive.

It is very important to note that Universal Data Access does not exclude any data stores, so the two strategies, which may appear to compete, actually cooperate. In fact, OLE DB providers for a number of new “universal database” products are currently under development. Using the Universal Data Access strategy, customers will be able to use data in their existing databases, universal database servers, desktop applications, mainframes, etc. Organizations that combine Universal Data Access and universal database products will ultimately benefit from a broad choice of best-of-breed tools, applications and DBMS products available from leading data access vendors.

Universal Data Access: A Road Map for the Future

Going forward, Microsoft has two important vehicles for shipping the supporting components of its Universal Data Access strategy: the Microsoft Data Access Components and the Data Access SDK. Long-term planning and development for both of these products are under way, and customers may be curious as to the future directions for the Universal Data Access strategy expressed in these plans.

MDAC

The mission for MDAC is to provide in a single synchronized release all of the key data access technologies used across Microsoft tools, applications and platform products. The MDAC release schedule is largely driven by internal “customers” at Microsoft, for which major releases will be delivered. MDAC 2.0 will be shipped concurrently with the next release of Microsoft® Visual Studio™. MDAC 2.5 will be shipped concurrently with the next release of Office. Each subsequent release will be tested and supported for use with the latest versions of following Microsoft products:

  • Windows 95 version 4.0 and higher

  • Windows NT version 3.51 and higher

  • Internet Explorer version 4.0 and higher

  • Internet Information Server version 4.0 and higher

  • Visual Studio (and each of its tools) version 5.0 and higher

  • Office 97 version 8.0 and higher

There are three project-level design goals for the next release of MDAC:

  • Continue to deliver more and better OLE DB components for all major Microsoft data stores and strategic third-party stores. MDAC will include data providers and service components. Tools for building OLE DB components will be included with the Data Access SDK (see the following section).

  • Expand ADO. There are three aspects of this goal. One, to superset the capabilities of DAO and RDO today, so that developers working with Visual Basic and Office will have the option of moving to ADO without sacrificing current functionality. Two, to enable new scenarios, including more remote, disconnected capabilities and more sophisticated data models. Three, to make ADO available to developers working with additional third-party languages and applications.

  • Enable additional RAD database capabilities in Visual Studio and third-party tools. Several design surfaces in Visual Studio products will consume and/or produce ADO and OLE DB code. These higher-level tools will make it easier to build sophisticated database applications, while inheriting the benefits of ADO and OLE DB. Because ADO and OLE DB are tool- and language-independent, third-party vendors should also be able to enable RAD data capabilities using ADO and OLE DB in the 2.0 time frame.

Data Access SDK

The Microsoft Data Access SDK is a set of tools and samples scheduled for release in 1998. It is designed to help developers create solutions using MDAC. The SDK provides a convenient single source for everything needed to learn about and create data access solutions.

The SDK will contain MDAC version 2.0, tools for getting started with data access component development, tools for testing and distributing components, and documentation. The SDK will be activity-based with content designed specifically for both consumer and provider writers, and developers working with various languages and deployment environments.

Conclusion

Organizations of all sizes today are creating business solutions that leverage data from the desktop to the enterprise. As the types of data and types of access have proliferated, the challenge to create business advantage has remained paramount. Microsoft has designed the Universal Data Access strategy to meet the needs of today’s distributed, multiplatform organization building client/server and Web-based data-driven solutions. By building in performance and reliability features, by making Universal Data Access a key part of the Windows DNA architecture, and by enlisting the support of a broad range of industry players, Microsoft is aggressively meeting customer needs.

Universal Data Access helps organizations build on existing systems and data stores as they create new client/server and Web-based solutions. Universal Data Access bridges the gap between existing systems and new technologies to create an evolutionary path for cost-conscious customers. As customers forge new business opportunities, Microsoft will be there to provide tools and technologies to enable success.


Universal Data Access Q&A

February 98

Universal Data Access

Q. What is Universal Data Access?

A. Universal Data Access is the Microsoft® strategy for providing access to all types of information across the enterprise. It provides high-performance access to a variety of information sources, including relational data and non-relational data, including mainframe ISAM/VSAM and hierarchical databases, email and file system stores, text, graphical, and geographical data, and more.

Q. What role does OLE DB play in Universal Data Access?

A. OLE DB is the Microsoft strategic low-level interface that enables Universal Data Access.

Q. What’s the difference between Universal Data Access and ODBC?

A. Universal Data Access provides a common way to access all types of data, both relational and non-relational. ODBC provides a common way to access relational data.

Q. How does Universal Data Access differ from what Oracle, Informix, IBM, and others are offering?

A. These vendors offer some form of Universal Storage, which requires the movement of all data into a single database, not just once but on an ongoing basis. All access to the data is then tied to the database and to that vendor. Universal Data Access offers access to data where it lives when the user wants it. Of course, you can get to the data in these vendors’ databases via OLE DB.

Q. Does Universal Data Access get me to my mainframe data?

A. Several vendors are currently supplying or will supply OLE DB providers to access VSAM, AS/400, HP/UX, IBM AIX, and OpenVMS data. These vendors include ASNA, IBM, ISG, MetaWise, and Microsoft.

Q. Where does Microsoft® SQL Server™ fit into the picture?

A. The next version of SQL Server will use OLE DB as its native interface and will be able to query OLE DB data sources as well. This will allow distributed joins between SQL Server and other OLE DB data sources, including non-relational data.

Q. What is the difference between data consumers, data providers, and service components?

A. Data consumers are applications or components that consume data via ADO or OLE DB. Data providers expose data via OLE DB for consumers to access. Service components provide extended functionality that is not implemented by the data provider.

Q. Does Microsoft currently supply any data providers?

A. The OLE DB Provider for ODBC is a data provider that allows data consumers to access ODBC data via OLE DB. The Microsoft® Data Access Components 2.0 contain OLE DB providers for SQL Server, Oracle and Microsoft® Jet.

Q. Where do I find more information on Universal Data Access?

A. Visit the Universal Data Access web site at http://www.microsoft.com/data/.

OLE DB

Q. What is OLE DB?

A. OLE DB is the Microsoft strategic low-level interface to all kinds of data throughout the enterprise. It is an open specification for accessing both relational and non-relational data.

Q. What’s the difference between ADO and OLE DB?

A. OLE DB is the underlying system service, which enables universal access to any data. ADO is a high-level interface, which simplifies development against OLE DB data sources.

Q. What’s the difference between OLE DB and ODBC?

A. OLE DB provides a common way to access all types of data, both relational and non-relational. ODBC provides a common way to access relational data.

Q. Where does ODBC fit in?

A. ODBC in the short and medium term is the best way to access a broad range of relational data due to the high number of available drivers. An integral part of OLE DB is the OLE DB Provider for ODBC, which enables OLE DB consumers to talk to ODBC drivers and data sources.

Q. Who in the industry backs Universal Data Access?

A. A variety of companies are either already providing or have committed to providing OLE DB components, including ASNA, IBM, ISG, InterSolv, MetaWise, SAS, and Sequiter. In addition, companies such as Computer Associates, Digital Equipment, and Tandem Computers have licensed OLE DB service components from ISG for inclusion in their databases.

Q. How is performance with Universal Data Access?

A. OLE DB is designed for the best possible performance by using the fewest possible layers and by allowing applications to use only the components they need to access data. With well-written data providers, performance should be excellent.

Q. Where do I find more information on OLE DB?

A. Visit the Universal Data Access web site at http://www.microsoft.com/data/.

ADO

Q. What is ADO?

A. ADO is Microsoft’s strategic, high-level interface to all kinds of data. It is an easy-to-use application level interface to OLE DB.

Q. What’s the difference between ADO and OLE DB?

A. OLE DB enables universal access to any data. ADO makes it easy for developers to program. Since ADO is built on top of OLE DB, it benefits from the rich universal data access infrastructure we are building.

Q. What’s the difference between ADO, RDO, and DAO?

A. ADO is an object model for accessing all types of data via OLE DB. RDO is an object model for accessing relational data via ODBC. DAO is an object model for accessing local or SQL data via Jet. All are COM interfaces and can be used from a variety of tools, including Visual Basic, Access, and Office.

Q. Why do I need ADO if I already use RDO/DAO?

A. First, ADO can be used to access all types of data, whereas RDO and DAO only access relational data. Second, the ADO object model is not as complex as that of RDO or DAO and therefore is easier to use with less coding required. Third, ADO combines the best features of, and will eventually replace, RDO and DAO. Finally, ADO is the standard data access object model across Microsoft tools, including Visual Basic®, Microsoft® Access, Microsoft® Office, and Microsoft® Internet Information Server.

Q. What’s new in ADO 2.0?

A. ADO 2.0 includes several exciting new features, including:

  • Data shaping for displaying complex data relationships

  • Persistent recordsets, to support offline users

  • Events, for tighter integration between clients and servers

  • Distributed transactions

  • Integration with Visual Studio tools such as the Data Environment

Q. How do I get ADO?

A. ADO 2.0 ships with Internet Explorer 4.0, Internet Information Server 4.0, and the 6.0 version of each Microsoft® Visual Studio™ tool. ADO also ships with the OLE DB SDK, the Microsoft Data Access Components 2.0, and the Data Access SDK (all available for download from the Microsoft Web site).

Q. How do I use ADO?

A. ADO, RDO, and DAO are all Automation servers that provide object models for developers. Therefore, their usage is very similar. The main ADO objects are Connections, Commands, and Recordsets. These have a wide variety of properties and methods that are used to access and manipulate data.

Q. I know RDO/DAO pretty well. Will I find ADO easy to use?

A. ADO looks very similar to RDO and DAO because Microsoft took the best features of both models and put them together in one programming model.

Q. When should I use ADO?

A. ADO is the standard data access object model across Microsoft tools. Therefore, if you are building new applications you should use ADO unless there are features you need today that are found only in RDO or DAO.

Q. Where do I find more information on ADO?

A. Visit the Universal Data Access web site at http://www.microsoft.com/data/.

Remote Data Service

Q. What is the Remote Data Service?

A. RDS is a feature of ADO and makes full-featured, data-centric Web applications a reality by combining data manipulation of retrieved data, efficient client-side caching, and support for data-aware Microsoft® ActiveX® controls with an elegant and powerful programming model.

Q. What happened to the Advanced Data Connector? Why did the name change from Advanced Data Connector to Remote Data Service?

A. ADC has been integrated with ADO to provide date remoting within the same programming model as ADO. To clarify the relationship of ADC to ADO, ADC is now known as the Remote Data Service (RDS).

Q. Where do I find more information on the Remote Data Service?

A. Visit the Universal Data Access web site at http://www.microsoft.com/data/.

Microsoft Data Access Components

Q. What are the Microsoft Data Access Components?

A. Microsoft Data Access Components is a toolkit that includes key technologies to enable Universal Data Access. MDAC consists of the latest versions of ADO, OLE DB, and ODBC, which are now released and documented on a synchronized schedule.

Q. What is the difference between the Microsoft Data Access Components and Universal Data Access?

A. The Microsoft Data Access Components enable Universal Data Access. They are the key technologies that make Universal Data Access possible.

Q. Is this another new technology?

A. No. This is just a name for the set of data access components that Microsoft ships.

Q. How do I get the Microsoft Data Access Components?

A. Microsoft Data Access Components 2.0 is available for free from the Microsoft Web site, and is supported for use with any Microsoft tools, applications, and Internet products running on Windows® 95 and Windows NT® 4.0.

Q. What’s the relationship between the Microsoft Data Access Components and Visual Studio?

A. The Microsoft Data Access Components can be used with any of the Visual Studio tools to build high performance database solutions.

Q. What types of applications can I build today with Microsoft Data Access Components?

A. All types of applications that access data across the organization, from the desktop to the enterprise, whether it is relational or non-relational.

Q. Where do I find more information on the Microsoft Data Access Components?

A. Visit the Universal Data Access web site at http://www.microsoft.com/data/.


ODBC 3.7 Visual Studio Analyzer Events

ODBC 3.70 will fire both stock and custom events to Visual Studio™ Analyzer to provide the client with performance and debugging information about the ODBC 3.7 layer. All Visual Studio Analyzer outbound events will be fired at the top-level interface so that developers will get as accurate a picture as possible regarding time spent in ODBC components vs. ADO and OLE DB components.

Registration

Visual Studio Analyzer provides a COM method, ISystemDebugEventFire::IsActive, to let an event source find out if there is any event collector listening. This minimizes the overhead to an “if” checking and a function call. However, to call method IsActive, CoInitialize and CoCreateInstance are called first. Some ODBC applications do not use OLE, and in order not to slow the load of ODBC to these applications, Visual Studio Analyzer Events can be enabled/disabled from within the ODBC Administrator. That way OLE and Visual Studio Analyzer are initialized when ODBC is loaded only if it has been enabled.

Visual Studio Analyzer requires any Visual Studio Analyzer Event source to register when the source is set up. There are two ways to register the ODBC Visual Studio Analyzer Event source. One is to call ISystemDebugEventInstall::RegisterSource, RegisterStockEvent, and RegisterCustomEvent, etc. in SQLInstallDriverManager. Another approach is for the user to enable/disable Visual Studio Analyzer Event in the ODBC Control Panel. The first approach has the benefit of only registering once on a machine. However, the setups of some programs are already very complicated and those applications may not find it necessary to view Visual Studio Analyzer events. Therefore, by using the ODBC Administrator, registration is deferred until a user enables Visual Studio Analyzer.

Categories

ODBC will fire the events in following categories:

DEBUG_EVENT_CATEGORY_STARTSTOP,

DEBUG_EVENT_CATEGORY_CALLRETURN,

DEBUG_EVENT_CATEGORY_QUERYRESULT,

DEBUG_EVENT_CATEGORY_TRANSACTION

In addition to the stock categories provided by Visual Studio Analyzer, ODBC 3.7 will add the following:

DEBUG_EVENT_CATEGORY_CONNECTION

ODBC 3.7 Visual Studio Analyzer Events by Handle

No Handle

Category | Type | Event | Fired When
startstop | Generic | STRART | When ODBC32.DLL is loaded
startstop | Generic | STOP | When ODBC32.DLL is unloaded

Connection

Category | Type | Event | Fired When
transaction | Generic | start | SQLSetConnectAttr(…SQL_ATTR_ENLIST_IN_DTC)
transaction | Generic | start | Any function call on an HDBC (or hstmt under this HDBC) after a call to SQLEndTran when SQL_ATTR_AUTOCOMMIT is set to SQL_AUTOCOMMIT_OFF. The overhead might be too high. We may want to fire the event after SQLEndTran returns from the driver.
transaction | Generic | commit | SQLEndTran(…SQL_COMMIT)
transaction | Generic | rollback | SQLEndTran(…SQL_ROLLBACK)
callreturn | Outbound | Enter | At the entrance of SQLConnect/SQLDriverConnect
callreturn | Inbound | Leave_Normal (Exception) | At the exit of SQLConnect/SQLDriverConnect if succeeds (or fails)
connection | Outbound | Open | Before call driver’s SQLConnect/SQLDriverConnect
connection | Inbound | Open | After driver’s SQLConnect/SQLDriverConnect returns
callreturn | Outbound | Enter | At the entrace of SQLDisconnect
Callreturn | Inbound | Leave_Normal (Exception) | At the exit of SQLDisconnect if succeeds (or fails)
Connection | Outbound | Close | Before call driver’s SQLDisconnect
connection | Inbound | close | After return from driver’s SQLDisconnect

Statement

Category | Type | Event | Fired When
queryresult | outbound | Querysend | Before the call of driver’s SQLExecute/SQLExecDirect
queryresult | inbound | Queryresult | After driver’s SQLExecute/SQLExecDirect return
callreturn | outbound | Enter | At enter of SQLPrepare, SQLExecute, SQLExecDirect, SQLFetch, SQLExtendedFetch, SQLFetchScroll, SQLMoreResults, SQLGetData, SQLCancel, SQLTables, SQLColumns, SQLProcedures, SQLGetTypeInfo, SQLProcedureColumns, SQLSpecialColumns, SQLTablePrivileges, SQLSetPos, SQLPutData, SQLBulkOperations, SQLSpecialColumns, SQLColumnPrivileges, SQLForeignKeys, SQLStatistics
callreturn | inbound | Leave_Normal (or exception) | At exit of SQLPrepare, SQLExecute, SQLExecDirect, SQLFetch, SQLExtendedFetch, SQLFetchScroll, SQLMoreResults, SQLGetData, SQLCancel, SQLTables, SQLColumns, SQLProcedures, SQLGetTypeInfo, SQLProcedureColumns, SQLSpecialColumns, SQLTablePrivileges, SQLSetPos, SQLPutData, SQLBulkOperations, SQLSpecialColumns, SQLColumnPrivileges, SQLForeignKeys, SQLStatistics

Visual Studio Analyzer Events Fired by ODBC 3.7

Event Type Args (prgKeys)

query_result | inbound | BSTR SQLText, VARIANT_BOOL fAsync
query_send | outbound | BSTR SQLText VARIANT_BOOL fAsync
call enter | outbound | the input arguments
return_normal | inbound | the output arguments
return_exception | inbound | HRESULT hrException
connection_open* | outbound | BSTR ConnectString, BSTR UserId, BSTR Password
connection_close* | outbound |

* indicates a custom event


ADO for Windows Foundation Classes Overview

The new ActiveX® Data Objects for Windows Foundation Classes (ADO/WFC) components provide a robust, powerful set of data access services. Developers can use these components to write Java-based applications and components for Win32® platforms and premier hosting environments such as Microsoft® Internet Explorer 4.0. The following topics provide an overview of ADO/WFC and demonstrate how to use the components:

  • Architecture and Component Model

  • Programming Model

  • Programming Samples

Use these programming examples as building blocks. By combining several of these simple scenarios, you can create more complex application behaviors. These samples discuss various topics, including asynchronous execution, remote business object invocation, transactional data access, and metadata examination.

For updated information on the continuing evolution of ADO/WFC, visit the Microsoft Data Web site at http://www.microsoft.com/data.


Architecture and Component Model

The ADO/WFC components leverage the rich functionality of Win32 platforms, and are designed for optimal performance with the Virtual Machine (VM) for Java technology. These components are part of the Microsoft® Data Access Components.

The following table summarizes the major ADO/WFC components:

Component | Description
Connection | Provides basic connectivity to data sources and application servers.
Recordset | A data cursoring abstraction. Provides rich navigational semantics, as well as disconnected and batch mode operational modes. Intrinsically marshalable across process/machine boundaries (basic unit of “transport” for tabular data between business objects and client application contexts).
Command | A parameter-varying command execution component.
Field (Fields Collection) | Handles row metadata and values. Used to define the “shape” and dimension of Recordset objects.
AdoProperty (AdoProperties Collection) | A property container for component-specific properties. All ADO components have associated (and in some cases unique) property sets.
Parameter (Parameters Collection) | A value descriptor for objects utilized by Command components.
ObjectProxy | A surrogate for a remote business object.
DataSpace | A business object “factory” component.
DataBinder (and DataBinding) | A data binding and coordination agent for forms in an ADO-deployment context. (DataBinding is a helper class that associates a visual component’s bound fields to Recordset result fields.)
ConnectionEventHandler | A dispatch mechanism for Connection events to registered event listeners.
RecordsetEventHandler | A dispatch mechanism for Recordset events to registered event listeners.
CommandEventHandler | A dispatch mechanism for Command events to registered event listeners.
ConnectionEvent | An event descriptor specific for Connection events.
RecordsetEvent | An event descriptor specific for Recordset events.
CommandEvent | An event descriptor specific for Command events.
AdoException (Errors Collection) | A basic error exception class for reporting anomalous conditions in the ADO execution environment.


Event Handling Sample

This sample introduces the powerful new event handling architecture based on delegates for the Virtual Machine (VM) for Java. The delegate event model is a replacement for the JavaBean model and is not compatible with it. The two styles should not be intermixed in application code. This is a large sample that delineates all of the ADO/WFC Recordset events that can be registered for callback by Java application developers.

public class DemoFramework
{
   Recordset recordSet;

   RecordsetEventHandler handler =
      new RecordsetEventHandler(this,"onWillMove");
   RecordsetEventHandler handler2 =
      new RecordsetEventHandler(this,"onWillMove2");
   RecordsetEventHandler handler3 =
      new RecordsetEventHandler(this,"onMoveComplete");
   RecordsetEventHandler handler4 =
      new RecordsetEventHandler(this,"onEndOfRecordset");
   RecordsetEventHandler handler5 =
      new RecordsetEventHandler(this,"onWillChangeField");
   RecordsetEventHandler handler6 =
      new RecordsetEventHandler(this,"onFieldChangeComplete");

   public void addEventListeners()
   {
      recordSet.addWillMoveHandler(handler);
      recordSet.addWillMoveHandler(handler2);
      recordSet.addMoveCompleteHandler(handler3);
      recordSet.addEndOfRecordsetHandler(handler4);
      recordSet.addWillChangeFieldHandler(handler5);
      recordSet.addFieldChangeCompleteHandler(handler6);
   }

   public void removeEventListeners()
   {
      recordSet.removeWillMoveHandler(handler);
      recordSet.removeWillMoveHandler(handler2);
   }

   public void onEndOfRecordset(RecordsetEvent e)
   {
      System.out.println("onEndOfRecordset:" + e);
   }

   public void onMoveComplete(RecordsetEvent e)
   {
      System.out.println("onMoveComplete:" + e);
   }

   public void onWillMove(RecordsetEvent e)
   {
      System.out.println("onWillMove:" + e);
   }

   public void onWillMove2(RecordsetEvent e)
   {
      System.out.println("onWillMove2:" + e);
   }

   public void onWillChangeField(RecordsetEvent e)
   {
      System.out.println("onWillChangeField:" + e);
   }

   public void onFieldChangeComplete(RecordsetEvent e)
   {
      System.out.println("onFieldChangeComplete:" + e);
   }

   void FieldChange()
   {
      try {
         recordSet.getFields().getItem("au_lname").setString("someWeirdValue");
      } catch (Throwable t)
      {
         t.printStackTrace();
      }
   }

   public Recordset getRecordset(String strConnection, String strSQL)
   {
      Recordset rs;

      rs = new Recordset();
      rs.setCursorLocation(AdoEnums.adUseClient);
      rs.setLockType (AdoEnums.adLockBatchOptimistic);
      rs.setCacheSize (10);
      rs.setMaxRecords (1000);

      rs.open(strSQL, strConnection);
      rs.moveLast();
      rs.moveFirst();
      recordSet = rs;

      return rs;
   }

   public void ScrollForward()
   {

      recordSet.moveFirst();

      int lRows = 0;
      while (!recordSet.getEOF())
      {
         lRows++;
         recordSet.moveNext();
      }
   }

   public boolean run()
   {
      String query = "select * from authors";
      String connection = "dsn=aDSN;database=pubs";

      boolean case1 = false, case2 = false, case0 = false;
      recordSet = getRecordset(connection,query);

      addEventListeners();
      FieldChange();
      ScrollForward();
      removeEventListeners();

      recordSet.close();
      recordSet.release();

      return true;
   }

   public static void main(String args[])
   {
      new DemoFramework().run();
   }
}

A delegate can be thought of as a Java callback pointer (or function pointer). An event listener can arrange to be called back on any method it has. Delegates in ADO/WFC are wrapped within the <type>EventHandler classes. There is one EventHandler for each of the three main types of event-generating ADO components: RecordsetEventHandler, ConnectionEventHandler, and CommandEventHandler. The following code creates six RecordsetEventHandler delegates:

   RecordsetEventHandler handler =
      new RecordsetEventHandler(this,"onWillMove");
   RecordsetEventHandler handler2 =
      new RecordsetEventHandler(this,"onWillMove2");
   RecordsetEventHandler handler3 =
      new RecordsetEventHandler(this,"onMoveComplete");
   RecordsetEventHandler handler4 =
      new RecordsetEventHandler(this,"onEndOfRecordset");
   RecordsetEventHandler handler5 =
      new RecordsetEventHandler(this,"onWillChangeField");
   RecordsetEventHandler handler6 =
      new RecordsetEventHandler(this,"onFieldChangeComplete");

Each of the second parameters to the RecordsetEventHandler constructor statements above designate one of the possible Recordset object events, or conditions, that the Recordset can enter while manipulated within an application context. This sample is set up to listen to all of the events. Typically, a Java component may only be interested in a subset of that total; for example, a business object may be designed for validating any field changes in a form, but is disinterested in currency change notifications.

Note that events are comprised of before (“will”) and after (“complete”) pairs of events. Listeners that want to be able to react to an event before it happens, with the opportunity to reverse the event, typically register for the “will” events. The following code shows how to add event listeners to the source object:

recordSet.addWillChangeFieldHandler(handler);
recordSet.addFieldChangeCompleteHandler(handler2);

Once an event listener is registered, the method specified in the RecordsetEventHandler will be invoked every time an event of the specified type occurs within the object. To show how an event is triggered, examine the FieldChange method. The DemoFramework class registers itself (using the this reference) to receive both the before (“will”) and after (“complete”) events. The FieldChange method, called during the run method, then triggers the event propagation:

void FieldChange()
   {
      try {
         recordSet.getFields().getItem("au_lname").setString("someWeirdValue");
      } catch (Throwable t)
      {
         t.printStackTrace();
      }
   }

Accessing the Field value triggers the event propagation mechanism. The WillChangeFieldHandler routine receives the event first, and outputs a simple console message acknowledging receipt. Then, the FieldChangeCompleteHandler routine receives the event, and also outputs a simple console message. Of course, the behavior of the two event handling methods could have been much more elaborate; for example, throwing a CancelChangeException to roll back the proposed change. (This is equivalent to the VetoableEventListener concept of JavaBeans, but without the immense overhead of the JavaBean model.)

Familiarizing yourself with this new approach to event notifications and callback methods is a requisite for successful ADO/WFC programming in the component assembly paradigm. Classical, non-event-driven applications are still feasible without the delegate model, but most modern form-based UI applications stand to be improved through assimilation of this new event model.


Command Sample

This sample extends the framework code by introducing an important feature of ADO/WFC: the parametrically varying Command. Command objects provide a performance gain for applications by caching execution plans for repetitive operations, which only differ in the value of the parameters plugged in at a given point. The prime example of this is a stored procedure that takes an input parameter ([in]) and returns a result set (Recordset) to the caller. The execution time saved by the program through reuse of the cached execution plan is substantial.

Note The following code assumes that you have access to the Microsoft® SQL Server™ sample database, pubs. If you do not, you can substitute your table of choice.

public boolean run()
{
   Connection c = new Connection();
   c.setIsolationLevel (AdoEnums.adXactReadCommitted);
   c.setCursorLocation (AdoEnums.adUseClientBatch);

   c.open ("dsn=aDSN;database=pubs", "aName", "aPWD");

   Command cmd = new Command();
   cmd.setName ("MyFirstCommand");
   cmd.setCommandText ("select * from authors where au_lname = ?");
   cmd.setCommandTimeout (10);
   cmd.setPrepared (true);
   cmd.setCommandType (AdoEnums.adCmdText);
   cmd.setActiveConnection ("dsn=aDSN;database=pubs ");
   cmd.setActiveConnection (c);

   Parameters p = cmd.getParameters();

   Parameter param = new Parameter();
   param.setName ("MyFirstParam");
   param.setType (AdoEnums.adVarChar);
   param.setDirection (AdoEnums.adParamInput);
   param.setSize (5);
   param.setString ("Smith");
   p.append (param);

   Recordset rs = cmd.execute();
   rs.moveFirst();

   if (rs.getFields().getItem("au_lname").getString().compareTo("Smith") != 0)
      System.out.println("The author is not a Smith.");

   rs.close();
   c.close();

   return true;
}

This sample instantiates and configures a Command object. Like all ADO/WFC components, Command objects are configured through their properties:

cmd.setName ("MyFavoriteSQLQuery");
cmd.setCommandText ("select * from authors where au_lname = ?");
cmd.setCommandTimeout (10);
cmd.setPrepared (true);
cmd.setCommandType (AdoEnums.adCmdText);

The Name property defines a friendly name for the Command. The CommandText property specifies the invariant portion of the Command; this is the portion that will be “prepared,” meaning generated and cached, by the sample code. Placeholders for parameters in the command are specified by the “?” character. The Prepared boolean property is used to compile the execution plan for the command. The CommandTimeout property is identical to the ConnectionTimeout property — it denotes an upper bound on duration for the command to successfully complete execution. The final property, CommandType, is used to specify the type of Command. This property can be used to differentiate parametric SQL statements from other types of commands, such as stored procedures.

Now, the profile of the input parameter must be delineated. Each Command has a collection of Parameter objects. You may add, remove, and edit these Parameter objects. This is precisely what the following lines of code do — specify the parameter’s name, data type, “direction” of the parameter (in, out, in/out), data size, and the value to plug into the Parameter.

Parameters p = cmd.getParameters();
Parameter param = new Parameter();
param.setName ("MyFirstParam");
param.setType (AdoEnums.adVarChar);
param.setDirection (AdoEnums.adParamInput);
param.setSize (5);
param.setString ("Smith");
p.append (param);

Once the Parameter has been defined, and a value loaded into it, the Command is ready to be invoked. In this example, the return value of the Command will be a set of rows matching the predicate of the select statement — a Recordset to be marshaled to the client. The Command is executed by invoking the execute method. The resulting Recordset is a full-fledged cursor, capable of navigation and data modifications. Data in a Recordset may be accessed through several methods, but the preferred methods are enumerated as follows:

rs.getFields().getItem("<column_name>").get<JavaIntrinsicOrStringorObjectAccessor>();
rs.getFields("<column_name>").get<JavaIntrinsicOrStringorObjectAccessor>();
rs.getItem("<column_name>").get<JavaIntrinsicOrStringorObjectAccessor>();

The <column_name> parameter should be replaced with a valid field name, such as “au_lname” in this sample. The ADO/WFC Field component provides a Java-friendly set of accessor methods for pulling the value out of the Field into a Java intrinsic type, or a Java Object. There is no need to utilize Variant types to access data within Java programs. Some data coercion operations are not supported, and will result in AdoExceptions being thrown by the Field object.

The remaining methods in this sample demonstrate the pattern of closing objects (such as Connections and Recordsets) to ensure proper release of the underlying resources.


Connection Sample

This sample is a simple extension of the framework code. Essentially, the only new element introduced is a query execution, which results in a Recordset. You can iterate through each element in the Recordset and move to the very start or end of the Recordset. To programmers familiar with ANSI cursors, the Recordset may be thought of as an elaboration of the cursor concept. It also includes scrolling and remoting (moving the entire result set to another process rather than one row at a time).

The code below only includes the run method, which you can insert into the framework code:

public boolean run()
{
   Connection c = new Connection();
   c.setConnectionString ("dsn=aDSNName;pwd=aName;pwd=aPWD;database=aDatabase");
   c.setCommandTimeout (10);
   c.setConnectionTimeout (10);
   c.setCursorLocation (AdoEnums.adUseClientBatch);

   c.open();
   c.setDefaultDatabase ("aSpecificDatabase");
   System.out.println ("Connection string = " + c.getConnectionString());

   // Verify that the propertiea on the connection
   // were set correctly. If not, return a fail condition.
   if (c.getCommandTimeout() != 10)
      return false;

   if (c.getConnectionTimeout() != 10)
      return false;

   if (c.getCursorLocation() != AdoEnums.adUseClientBatch)
      return false;

   if (c.getMode() != AdoEnums.adModeUnknown)
      return false;

   // Create a new Recordset to contain the results
   // of the SQL DML operation (select * ...).
   Recordset rs= c.execute ("select * from authors");
   rs.moveFirst();
   rs.moveLast();

   // Delete all rows from the authors table.
   c.executeUpdate ("delete from authors");

   // Close the Connection and Recordset.
   rs.close();
   c.close();
   return true;

}

This sample introduces a number of options that were not discussed in the framework code. ADO/WFC can operate in “default” mode, where the developer doesn’t have to set all of the options possible. This is a trade-off with functionality, since the default behavior of ADO/WFC is to create server-side, forward-only cursors. This means that scrolling is not supported by default. To get the convenience of scrolling cursors and locally cached data, rather than performing a network roundtrip for each row fetched, the following options are set in the code:

c.setCommandTimeout (10);
c.setConnectionTimeout (10);
c.setCursorLocation (AdoEnums.adUseClientBatch);

The CursorLocation property tells the ADO/WFC run time to create a client-side cursor and to operate in batch mode. The advantage of this type of cursor is increased navigational speed and the ability to make changes all at once. The other two Connection properties specify a timeout for the connection. This is typically used to ensure that an application doesn’t enter a long wait when a server resource is not available. In this example, the attempt to connect will be aborted if the server/source does not acknowledge the connection request within 10 seconds. The default setting for both of these timeout properties is indefinite; this means the application will go into a wait mode until connected or until the application process is terminated.

The line, c.executeUpdate("delete from authors"), provides the major piece of functionality in this sample. Here, the application uses the open connection to issue a SQL DML statement to delete all rows in the authors table. There are other execute operations that return Recordset objects. These execute operations are typically select statements or stored procedure calls that return one or more Recordsets as “results.” Since this is a delete operation, there is no result data to process (aside from potential error messages), and the executeUpdate method is employed. The Recordset class offers multiple options for controlling the nature of the “result.”

Try experimenting with a few different queries and alter the cursor type and location properties. Depending on your network and machine, you should see some performance differences. ADO/WFC provides the convenience of server cursors or the capabilities of “remoting” your data to the client, where navigation and data access operations can be performed, and the changes sent back to the server, in a "batch.” This greatly reduces the overhead of data marshaling.


Recordset Sample

This sample extends the framework code by making use of the Recordset, one of the core ADO/WFC classes. Recordsets can be opened much like a Connection, and can be used to execute data operations (such as add, remove, update) and to marshal tabular data to and from business objects.

// Establish a global variable to hold the open Recordset
// used in run(), createRecordset(), and useRecordset().
private Recordset rs;

public boolean run()
{
   boolean fSuccess = true;
   fSuccess = createRecordset();
   useRecordset ();
   rs.close();
   return fSuccess;
}

boolean createRecordset ()
{
   Connection c = new Connection();
   c.open ("server=aDSN; ", "aName", "aPWD");

   rs = new Recordset();
   rs.setActiveConnection ("dsn=aDSN;database=pubs");
   rs.setSource ("select * from authors");
   rs.setActiveConnection (c);

   rs.setCursorType (AdoEnums.adOpenStatic);
   rs.setCursorLocation (AdoEnums.adUseClientBatch);
   rs.setLockType (AdoEnums.adLockBatchOptimistic);
   rs.setCacheSize (10);
   rs.setMaxRecords (1000);
   rs.open();

   return true;
}

boolean useRecordset()
{
   // Extract all of the Recordset properties and
   // print out the <attribute, value>-tuples.
   AdoProperties props = rs.getProperties();
   int cProps = props.getCount();
   for( int iProp = 0; iProp < cProps; iProp++ )
   {
      AdoProperty p = props.getItem( iProp );
      System.out.println("[" + p.getName() + " = " + p.getString() + "]");
   }

   // Perform simple navigation and boundary checking
   // (BOF= beginning of file, EOF= end of file).
   rs.moveFirst();
   rs.movePrevious();
   if (! rs.getBOF())
      return false;

   rs.moveLast();
   rs.moveNext();
   if (! rs.getEOF())
      return false;

   int recordCount = rs.getRecordCount();
   System.out.println ("record count = " + recordCount);

   rs.addNew( new Object[] {"899-46-2036", "O'Maley", "Zoltar",
      "67 Seventh Av.", "Salt Lake City", new Boolean (true),
      "801 826-0752", "UT", "84152","409-56-7098"});

   rs.update();
   rs.delete(1);
   rs.addNew();
   rs.cancelUpdate();

   rs.moveFirst();
   rs.delete(1);
   rs.cancelBatch();

   rs.setMarshalOptions (AdoEnums.adMarshalModifiedOnly);
   rs.setPersistFormat (AdoEnums.adPersistADTG);

   if (rs.getMarshalOptions() != AdoEnums.adMarshalModifiedOnly)
      return false;
   if (rs.getPersistFormat() != AdoEnums.adPersistADTG)
      return false;

   rs.moveFirst();
   Object bmk = rs.getBookmark();
   rs.moveLast();
   rs.setBookmark(bmk);
   rs.movePrevious();
   if (! rs.getBOF())
      return false;

   rs.addNew();
   if (rs.getEditMode() != AdoEnums.adEditAdd)
      return false;
   rs.cancelUpdate();

   // Requery the result, overriding any local changes
   // and completely replacing the result.
   rs.requery();

   return true;
}

The createRecordset method creates a Recordset by opening a connection and executing a query against the data source referenced by the System DSN:

rs = new Recordset();
rs.setActiveConnection ("dsn=aDSN;database=pubs");
rs.setSource ("select * from authors");

The ActiveConnection property is used in much the same way that the ConnectionString property is utilized on the Connection object — to specify a data source to connect to. The Source property is an overloaded property that can either reference a SQL DDL/DML string or a Command object. In this case, a string is used to initialize the Recordset. Note that no data has been fetched yet, since the Recordset has not been opened. It is also possible to set the ActiveConnection property of a Recordset to refer to a Connection object instance, as in rs.setActiveConnection(c).

The remaining properties are used to specify aspects of the type of cursor required by the application:

rs.setCursorType (AdoEnums.adOpenStatic);
rs.setCursorLocation (AdoEnums.adUseClientBatch);
rs.setLockType (AdoEnums.adLockBatchOptimistic);
rs.setMaxRecords (1000);
rs.open();

Note that in the absence of explicit property settings, any Recordset created by an open method invocation will result in a static, forward-only, server cursor — the equivalent of a generic ANSI cursor. However, you may need a cursor that supports scrolling and batch behavior; you may need a “disconnected” cursor where you can apply changes locally before deciding whether to propagate those changes back to the data source. Applications can be written to capitalize on this local caching of data, which provides a better solution for Internet applications that cannot afford to keep server resources tied up. The particular property settings chosen above request a static, local cursor that does not keep any locks on server resources. Instead, it uses optimistic concurrency control when it tries to update the data source. The MaxRecords property specifies an upper bound on the amount of rows that should be returned. Finally, the Recordset is opened through the open method. If the connection information has been specified properly, the Recordset is populated with up to 1000 rows of data.

The useRecordset method begins with an iteration over the Recordset properties that were set in the createRecordset method. The properties are returned in a collection, AdoProperties, which is then traversed. Each attribute name of the Recordset is printed, followed by the value of the specific property. This type of information may be considered “component metadata” — information about the state of the component, rather than on what it contains.

AdoProperties props = rs.getProperties();
int cProps = props.getCount();
for( int iProp = 0; iProp < cProps; iProp++ )
{
   AdoProperty p = props.getItem( iProp );
   System.out.println("[" + p.getName() + " = " + p.getString() + "]");
}

The next block of code consists of simple navigation commands. If the Recordset has a scrollable cursor, it supports the following navigational methods: moveFirst, moveNext, movePrevious, and moveLast. Recordsets also provide a bookmark object so that a position may be bookmarked and navigated back to quickly and efficiently. This requires you to use code similar to the following:

rs.moveFirst();
Object bmk = rs.getBookmark();
rs.moveLast();
rs.setBookmark(bmk);
rs.movePrevious();

A bookmark has an “opaque” structure, but two bookmarks can be compared to determine if they reference the same location. This is useful to writers of visual components such as data grids and other complex data-bound components. In the code above, the bookmark is set for the current row pointed to by the Recordset. The current position is then changed to the final row in the set. Then, the bookmark is used to set the position back to where the Recordset was pointing before the position was moved to the final row. Finally, the Recordset is moved back from the new position, placing it before the first row in the set. Recordsets have two special “positions” or states: just before the first row and just after the last row. These states are called BOF and EOF, respectively, and are often used in iteration constructions to terminate navigation. Testing for the values of these boolean properties is quite common in ADO/WFC programming and familiar to programmers from DAO and RDO.

The next cluster of operations performed on the open, populated Recordset revolve around data manipulation, such as adding rows, deleting rows, accepting local updates, and canceling these local changes. The advantage of the local, disconnected cursor can be seen in such circumstances. The programmer has wider discretion to modify local, non-persistent data, rather than alter production data in the data source. This allows for a more efficient partitioning of application logic between the client, the application server (if utilized), and the data source.

rs.addNew( new Object[] {"899-46-2036", "MindFlayer", "Zortan",
   "67 Seventh Ave.","Salt Lake City", new Boolean (true),
   "801 826-0752", "UT", "84152", "409-56-7098"});

rs.update();
rs.delete(1);
rs.addNew();
rs.cancelUpdate();

New rows are added to the Recordset in the form of arrays of Object values. The “shape” of the array should match that of the Recordset. In this case, it is for an author row. Data type checking is performed on the values, and if a type coercion is not supported for a particular column, then an exception (AdoException) will be thrown by the addNew method. Applications may choose to catch this run-time exception, or allow for the core exception handler to catch the condition. This usually means aborting the current process, so you are encouraged to use proper try/catch blocks around such operations. The delete call above marks the first row of the Recordset as deleted. The row is not actually purged from the Recordset until commit is called. (For more information about commit, see the Transaction sample.) Rows that are marked as deleted enter a “row deleted” state and are not visible to the user unless the cancelUpdate method is invoked. New rows may be appended to the Recordset using the addNew method, which creates a row with null values for all fields. The update method is used to actually “push” the local changes to the data source.

Next, two Recordset properties are set to specify how data is marshaled. Data marshaling refers to the packaging, transfer, and unpackaging of data across process and/or machine boundaries. ADO/WFC supports distributed object invocation/operation and data marshaling over protocols such as HTTP 1.0/1.1 and DCOM. The following relevant properties are set for the Recordset:

rs.setMarshalOptions(AdoEnums.adMarshalModifiedOnly);
rs.setPersistFormat(AdoEnums.adPersistADTG);

The MarshalOptions property specifies that when it is time to marshal the Recordset back to a business object or to the data source, only the modified and new data will be sent. If you have a large result and only end up modifying a small minority of rows, this makes perfect sense, assuming that your business rules (objects) do not require the full result set for processing. The PersistFormat property tells the ADO run time to package the rows of the Recordset in a specific, optimized format for transfer — the ADTG tablegram streaming format defined by Microsoft. These properties are activated only when the Recordset is “pushed” from the client to the middle tier or data source.

Finally, the requery method is introduced in this sample. This method has the effect of purging the local Recordset cache and replacing it with the most recent results of the query or SQL DML used to create the Recordset. This is useful if information has become stale on the client. A simple scenario is to refresh a monitor display that lists the current prices of traded securities. ADO/WFC makes it easy to “restart with latest data.” Unlike other data access models, getting the latest data is as straightforward as requerying your Recordset.

The last step in this sample, after returning from the useRecordset method, is to close and release the resources of the Recordset. This is an important habit to get into, especially in your Java business objects when you use ADO/WFC for data access programming.


Framework Code

In order to use the programming samples that are provided, you need a framework in which to plug in the code. Rather than repeat the same framework code for each sample, the necessary code is included here.

Note These programming samples assume that you are familiar with the basic concepts of Visual J++, such as creating a project, adding a new class to the project, and compiling your code. If you are unfamiliar with these concepts, use the online help to view the entries for these concepts.

The framework code consists of three main sections:

  • The necessary import declaration for using the ADO/WFC classes.

  • The instantiation of the Connection object.

  • The releasing of data resources when the application has finished.

Note It is recommended that you close and release the ADO/WFC components before your application terminates. Otherwise, the VM may end up with large cached data sets on its heap, which slows garbage collection and unnecessarily increases the size of the heap. Although none of the samples provided here actually require these measures, developing these habits will help you as your data applications with ADO/WFC become more sophisticated.

Use the following framework code to get your application started. This code assumes that the run method is invoked sometime after the object is instantiated. (It does not have to be invoked from the main method, as shown in this example.)

import com.ms.ado.*;

public class DemoFramework
{
   public boolean run()
   {
      Connection c = new Connection();
      c.setConnectionString ("dsn=aDSNName;pwd=aName;pwd=aPWD;database=aDatabase");
      c.open();
      c.setDefaultDatabase("aSpecificDatabase");

      System.out.println ("Connection string = " + c.getConnectionString());
      System.out.println ("Version = " + c.getVersion());
      System.out.println ("Provider = " + c.getProvider());
      System.out.println ("State = " + c.getState());
      System.out.println ("Attributes = " + c.getAttributes());

      // Most of the code samples will be inserted here, after
      // the connection has been established successfully.

      c.close();
      return true;

   }

   public static void main(String args[])
   {
      new DemoFramework().run();
   }
}

The first line of the framework code is an import statement, which makes the ADO/WFC components available to the class. The next step is to establish a connection to a data source. Connections can be either durable or transitory. Durable connections actually maintain a link to the data source, across potentially several operations. Most client/server data applications are implemented with durable connections. The Internet changes this, however, with its requirements for not maintaining long-running sessions with clients. Therefore, transitory connections release any link to the data source after each operation, allowing the data source to service other requests. When you specify the ConnectionString property of the Connection object, a transitory connection can later reconnect to the data source.

The Connection object is created in the run method, and the ConnectionString property is set to a specific system data source name (DSN) through the call to setConnectionString. (You should replace the DSN and other parameters with values that apply to your site. The names used in this sample are simply placeholders.)

Next, the connection is opened through the open method. By default, the open method establishes a durable connection to the data source. If successful, the application can proceed to execute queries against the source’s data. The diagnostic lines that follow simply print out some common information about the connection: what sort of driver is being used, the attributes of the connection, and the provider. The default database is also set for the connection through the call to setDefaultDatabase. If an update or query fails to specify a specific database when it executes, the connection will assume the command or query should be performed against the default database. This is a convenience function only.

The framework then includes a placeholder comment that indicates where you can insert code from the other samples. Remember to close durable connections when you are through with them. This has the effect of releasing certain resources that will improve the performance of your own code and allow for quicker heap reclamation. Default garbage collection can be sub-optimal when large chunks of memory are in question, such as in the case of a large result set (100K rows). This is critical when the ADO/WFC classes are being used on middle-tier application servers inside business objects.

Finally, the framework code is initiated within the main method of the class. This is not a mandatory construct. You may want to set up your own class to initiate a connection when a certain event is triggered inside your application by an end user, such as a button click event. You can alter the code to suit your application requirements.

Compile the class and verify that you can connect to a data source at your site. The diagnostic messages will confirm whether you have established a connection. If desired, you may then remove the System.out lines from the framework code.


Programming Model

ADO/WFC supports two different methodologies for application development. The first method is the common client/server (or occasionally called “API”) approach. In this approach, the ADO/WFC classes are used much like JDBC, ADO/Win32, or DAO/RDO. The programmer creates applications by writing explicit code. This approach is a familiar paradigm and ADO/WFC offers a rich model for distributed data programming. The programming samples provide several code examples of using ADO/WFC, from simple connectivity to distributed object invocation with transactional safeguards.

The second approach relates to rapid application development. By implementing the IComponent interface, and the new event model for Java components, ADO/WFC presents a convenient set of interfaces and properties for “visual programming.” The ability to support live data hosting in design environments, component “pluggability,” and automatic data binding through well-defined interfaces makes ADO/WFC a powerful tool for Java developers working on Win32 platforms.


DataSpace Sample

This sample extends the framework code to show how a middle-tier business object can be invoked through the ADO/WFC component framework. This sample leverages a pre-packaged business object that ships with Remote Data Services (RDS) and Microsoft® Internet Information Server 4.0 (IIS4): the RDSServer.DataFactory. This business object provides data source querying and updates through a business object intermediary, itself. The basic principles employed in this sample are the same as those that would be used in a custom business object. If you plan to use custom business objects written in Java, Visual Basic, or Visual C++, be sure to register the classes properly with the application server, in order that the launch permissions allow objects to be instantiated over HTTP using the Simple Object Access Protocol (SOAP). The simple code sequence needed to invoke and marshal data is shown below.

public boolean run()
{
   ObjectProxy obj = DataSpace.CreateObject (
                        "RDSServer.DataFactory", "http://aServerName");
   Recordset rs = (Recordset) obj.call ("Query",
                new Object[] {"dsn=aDSN;uid=aUID;pwd=aPWD;database=pubs",
                "select * from authors"});

   rs.moveFirst();
   rs.moveLast();

   rs.close();
   rs.release();

   // VM tip: force garbage collection to
   // reclaim heap more efficiently.
   System.gc();
   System.runFinalization();

   return true;
}

To invoke a business object on an application server, you must create a “proxy”; that is, a stand-in for the object that is used to marshal requests and parameters to and from the client’s application process and the remote business object. ADO/WFC layers its ObjectProxy over the RDS SOAP proxy, and can use this proxy to communicate with any Automation-based business object, whether written in Java, Visual Basic, or Visual C++. The following line of code shows how the ObjectProxy is created:

ObjectProxy obj = DataSpace.CreateObject (
                     "RDSServer.DataFactory", "http://aServerName");

This code creates a new ObjectProxy through the DataSpace class method, CreateObject(prog_id, server_url). This proxy may then be used to invoke calls on the target object (in this case the DataFactory) and unmarshal/marshal Recordsets. Once the proxy is created, the method of choice can be invoked through the overloaded call method:

Recordset rs = (Recordset) obj.call ("Query",
             new Object[] {"dsn=aDSN;uid=aUID;pwd=aPWD;database=pubs",
             "select * from authors"});

This call passes an Object array of two strings (one containing the ConnectionString, the other the Source/Command) to the proxy. This internally converts the Java types into marshalable Variant data types, packages them into a SOAP request, and dispatches the request to the server-side RDS component. The parameters are then unpacked and the remote object server method (“Query”) is invoked with the parameter values. The resulting Recordset is then packaged into an ADTG stream and marshaled back to the client ObjectProxy, which unpacks the Recordset stream, creates a Recordset instance, populates the Recordset with the unmarshaled tabular data, and then returns a reference to the Recordset to the caller. This process is effectively the equivalent of the original “select * from authors” call, but through the auspices of a business object. Obviously, you can create much more sophisticated, domain-based business objects. Try to program your own Visual Basic Automation class, register it with your application server, and then invoke it with the code provided in this sample.

Return Parameter Limitation (Beta 1)

Currently, the only object type that can be returned from a middle-tier business object is an ADO Recordset. However, any valid OLE Automation types may be used as [in] parameters. This limitation will be removed in subsequent releases of ADO/WFC. The default business object, DataFactory, returns an ADO Recordset as an [out] parameter (return value), which can be used, as is, by application developers. Note that an attempt to return a non-Recordset parameter (return value) will result in a NullPointerException being thrown by the ADO/WFC run time system.


Data Navigation Sample

This sample extends the framework code to demonstrate the capabilities of metadata examination of Recordsets. Since ADO/WFC’s Field objects contain both metadata and field values, the metadata for a given Field may be accessed from any position within the Recordset. There is no requirement, as in JDBC, for separate DatabaseMetadata or ResultsetMetadata-style objects. The metadata is always available.

The following code shows the procedure for extracting basic metadata information from the Fields collection of a Recordset, after the Recordset has been opened and populated.

public boolean run()
{
   Recordset rs = new Recordset();
   rs.setCursorType (AdoEnums.adOpenStatic);
   rs.setCursorLocation (AdoEnums.adUseClientBatch);
   rs.setLockType (AdoEnums.adLockBatchOptimistic);
   rs.open("select * from authors",
     "server=aServer;driver=sql server;uid=aUID;pwd=aPWD;database=pubs");

   rs.moveFirst();

   Fields fields = rs.getFields();
   int cFields = fields.getCount();

   for( int iField = 0; iField < cFields; iField++ )
   {
      useField( fields.getItem( iField ) );
   }

   for( int iField = 0; iField < cFields; iField++ )
   {
      System.out.print( fields.getItem( iField ).getName() + "\t" );
   }
   System.out.println("");

   rs.moveFirst();
   while( !rs.getEOF() )
   {
      for( int iField = 0; iField < cFields; iField++ )
      {
         System.out.print( getString( fields.getItem( iField ) ) + "\t" );
      }
      System.out.println("");

      rs.moveNext();
   }

   rs.close();
   rs.release();
   return true;
}

String getString (Field fld)
{
   if (fld.isNull())
      return "NULL";
   else
      return fld.getObject().toString();
}

void useField (Field fld)
{
   AdoProperties props = fld.getProperties();
   int cProps = props.getCount();
   for( int iProp = 0; iProp < cProps; iProp++ )
   {
      AdoProperty p = props.getItem( iProp );
      System.out.println("[" + p.getName() + " = " + p.getString() + "]");
   }

   System.out.println ("ActualSize = " + fld.getActualSize());
   System.out.println ("Attributes = " + fld.getAttributes());
   System.out.println ("DefinedSize = " + fld.getDefinedSize());
   System.out.println ("Name = " + fld.getName());
   System.out.println ("Type = " + fld.getType());
   System.out.println ("Precision = " + fld.getPrecision());
   System.out.println ("NumericScale = " + fld.getNumericScale());

}

This sample begins by using the basic ADO/WFC constants to configure the Recordset’s properties prior to opening the cursor:

rs.setCursorType (AdoEnums.adOpenStatic);
rs.setCursorLocation (AdoEnums.adUseClientBatch);
rs.setLockType (AdoEnums.adLockBatchOptimistic);

The Recordset is now ready to execute the query. After populating the client-side, batch-mode cursor, the application extracts the Fields collection, which contains the navigation mechanism for accessing individual row data values, as well as the Field metadata. The Fields collection provides a method for determining the number of Field objects it contains. This varies with the “shape” of the Recordset. Having established the dimension or shape of the Recordset, the application proceeds to extract the AdoProperty objects associated with each Field.

The useField method is the heart of the sample. This method retrieves the values of each AdoProperty of the Field, which are then written to the System console window. The code prints the core AdoProperties for Field objects: actual size, name, data type, precision, and numeric scale. The useField method demonstrates how you can easily access the metadata of a Recordset.

Having displayed the Recordset’s metadata, displaying the data values in each row is a simple matter of iterating over the Recordset from the first row until recordset.EOF is encountered:

rs.moveFirst();
while( !rs.getEOF() )
   {
      for( int iField = 0; iField < cFields; iField++ )
      {
         System.out.print( getString( fields.getItem( iField ) ) + "\t");
      }
      System.out.println("");

      rs.moveNext();
   }

Each Field in the given row is traversed and the row is accessed with the getItem accessor method. The value is then converted to a String object and written to the System console window. When the end of the current row is reached, the Recordset position is advanced to the next row by the moveNext method. The process repeats until the end of the Recordset is reached, and the program then terminates normally.


Programming Samples

ADO/WFC provides a sophisticated array of data access services, allowing the Visual J++ developer to create rich, data-centric applications. To help you get started using ADO/WFC, this section provides the following set of programming samples:

Sample | Description | Components Used
Framework Code | Provides a runnable framework in which to copy the code examples. |
Connection Sample | Connects to a data source and executes a simple query that results in a forward-only, server cursor. | Connection
Recordset Sample | Opens a disconnected Recordset and populates it with the results of a data source query. Performs several Recordset-oriented operations, such as adding, deleting, updating, and canceling local updates. Note that this is a large code sample. You should thoroughly understand the Connection object before trying this sample. | Connection, Recordset, AdoProperty, AdoProperties (collection)
Command Sample | Uses a Command object to execute a query with parameters. This is a performance optimization mechanism for highly repetitive operations that vary only in the value of the parameters to the operation. | Connection, Recordset, Command, Parameter
Asynchronous Command Execution Sample | Uses asynchronous mode to allow background population of a cursor. | Connection, Recordset, ConnectionEventHandler
Transaction Sample | Uses transaction and isolation levels to control data change visibility in applications. Ensures changes are committed or aborted. | Connection, Recordset, Transaction
DataSpace Sample | Invokes business objects on an application server using the Remote Data Services (RDS) and Internet Information Server (IIS). Marshals the Recordset result back using SOAP (Simple Object Access Protocol). | Connection, Recordset, ObjectProxy, DataSpace
Event Handling Sample | Hooks up event handlers to process changes in Recordsets, allowing all interested “listeners” to track changes. | Connection, Recordset, RecordsetEventHandler, RecordsetEvent
Data Navigation Sample | Opens a Recordset and navigates through its cached data. Examines the metadata of the Recordset by iterating through the Fields collection of the Recordset. | Connection, Recordset, Field, Fields (collection)

Note You should begin with the Framework Code sample, which shows how to set up your application. You can then use the code in the other samples to add the desired functionality.


Asynchronous Command Execution Sample

This sample demonstrates a convenient feature of ADO/WFC: the ability to asynchronously execute commands and populate large Recordsets on background threads. This sample extends the framework code by spawning a thread and using an event listener to catch the “population complete” event.

// This import statement is required for the
// delegate event callback class definition.
import com.ms.lang.*;

... // framework code

boolean done = false;
boolean execOK = false;

// Implement an event handling routine to
// catch the Recordset “population completion”
// event from the population thread.
public synchronized void onExecuteComplete(ConnectionEvent e)
{
   done = true;
   notify();
}

Recordset rs;
Command cmd;

public synchronized boolean run()
{
   execOK = false;

   Connection c = new Connection();
   c.setCursorLocation (AdoEnums.adUseClientBatch);
   c.open ("dsn=aDSN", "aName", "aPWD");

   // Establish a callback handler for the execute complete
   // event and add it to the Connection object.
   ConnectionEventHandler handler =
            new ConnectionEventHandler(this, "onExecuteComplete");
   c.addExecuteCompleteHandler(handler);

   cmd = new Command();
   cmd.setActiveConnection (c);
   cmd.setCommandText ("select * from authors");

   Runnable asyncExecute = new Runnable() {
      public void run() {
         try {
          rs = cmd.execute(null,AdoEnums.adRunAsync);
         } catch (Exception e)
            {
               System.out.println(e);
               synchronized (DemoAsyncCommand.this) {
               done = true;
               execOK = false;
               DemoAsyncCommand.this.notify();
            }
         }
      }
   };

   new Thread(asyncExecute,"RunAsync").start();
   execOK = true;

   while (!done)
      try {
         wait();
      } catch (Exception e)
      {
         execOK = false;
      }

   if (execOK)
   {
      rs.moveFirst();
      rs.moveLast();

      rs.close();
      c.close();
      return true;
   } else
      return false;
}

The first set of initialization code establishes the event handling “listener” for the Connection “execute complete” event — an event fired when the Command associated with the Connection finishes executing. A method, onExecuteComplete(ConnectionEvent e), is defined within the sample class, which implicitly triggers the notify method. The following code is a standard delegate-based event registration call sequence:

ConnectionEventHandler handler = new ConnectionEventHandler(this,"onExecuteComplete");
c.addExecuteCompleteHandler(handler);

This event model diverges sharply from the JavaBean EventSource-EventAdpater-EventListener model that ships with the JDK 1.1. The Delegate class is a member of the com.ms.lang package, and should be imported into applications that rely on event handling between components on Win32 platforms. ADO/WFC components do not support the JDK 1.02 or JavaBean event models. The Command code is basically identical to the code in the Command sample, without the parametric aspect.

Note that the run method is prefixed with the Java access modifier, synchronized. This denotes that only one active thread may be running in the method body at any given time, ensuring that race conditions do not occur. The lines that define a new Runnable object are worth examining, since this is where the execution thread is being defined.

Note The Runnable interface is implemented by classes that want to operate as threaded objects, but do not want to subclass java.lang.Thread. The Runnable interface has only one method: the entry point run, which is invoked by the thread scheduler to start the threaded object.

Runnable asyncExecute = new Runnable() {
      public void run() {
         try {
          rs = cmd.execute(null,AdoEnums.adRunAsync);
         } catch (Exception e)
            {
               System.out.println(e);
               synchronized (DemoAsyncCommand.this) {
               done = true;
               execOK = false;
               DemoAsyncCommand.this.notify();
            }
         }
      }
   };

This code instantiates an anonymous Runnable object with one defined method, run. The body of the run method performs the cmd.execute(null, AdoEnums.adRunAsync). This segment of code only prepares the thread object to run; the object must be “started” through the following call sequence:

new Thread(asyncExecute,"RunAsync").start();

This starts the Command execution, which will perform in asynchronous mode. The Thread constructor used in the sample takes, as parameters, a Runnable instance and a “thread name.” (In this sample, “RunAsync” is the name associated with the thread.) The new thread will begin executing the method body of the asyncExecute object. The first code line is a Command object executing the current command string (“select * from author”) in asynchronous mode, specified by the constant parameter passed to the method. Had no constant been passed to the execute method, the command would have executed in synchronous mode. The ADO and OLE DB components that run underneath the covers of ADO/WFC are free-threaded objects and need to be configured just like their Java counterparts.

While the command-execute thread runs in the background, the foreground thread enters a wait loop until notified, through the callback on the OnExecuteComplete event handler, that the command has finished executing. Once the event is fired from the background thread, the boolean variable done is set to true and the main thread can proceed to the task of navigating through the Recordset.

This sample is for slightly more advanced users. It is not necessary to run ADO/WFC with explicit multi-threading, but there are certain scenarios, especially those involving long wait periods, where efficient use of threads will bolster the end user experience and lead to better control flow. As always, the final lines of the sample release the Recordset and Connection resources.

Try experimenting with multi-threaded versions of the other samples, such as the Connection and Recordset samples. Or, create a version of this sample with multiple Commands being executed simultaneously; for example, “select * from authors” and “select * from publishers, titles where publishers.pubid = titles.pubid”. Also, remember to use Runnable instead of subclassing Thread directly. You can always convert to a full Thread object through the overloaded constructor.


Transaction Sample

This sample extends the framework code to demonstrate ADO/WFC’s support for transactions. Transactional-aware applications are an integral part of modern computing. ADO fully supports both local transactions, as well as distributed, two-phase transactions coordinated by the Microsoft® Distributed Transaction Coordinator (DTC). The following sample utilizes local transaction management capabilities surfaced through the Connection object. You can get the current transaction from the Connection by using the getActiveTransaction method.

public boolean run()
{
   Connection c = new Connection();
   c.setIsolationLevel (AdoEnums.adXactReadCommitted);
   c.setCursorLocation (AdoEnums.adUseClientBatch);
   c.open ("dsn=aDSN", "aName", "aPWD");

   Transaction t = c.getActiveTransaction();

   if (t != null)
   {
      Recordset rs = c.execute ("select * from authors");
      t.beginTrans();

      int recordCount = rs.getRecordCount();
      c.executeUpdate ("delete from authors");
      rs.requery();
      if (rs.getRecordCount() != 0)
      {
         rs.close();
         // Roll back (undo) the changes.
         t.rollbackTrans(false);
         c.close();
         return false;
      }

      // Make the changes permanent in the data source
      // and then refresh the results to verify the operation.
      t.commitTrans(false);
      rs.requery();
      rs.close();
   }

   c.close();
   return true;
}

The IsolationLevel property, set through c.setIsolationLevel (AdoEnums.adXactReadCommitted), is important when dealing with transactionally-scoped data access and application logic. Isolation refers to the level of autonomy that a transaction has from data/state changes made in other transactions. In the case of ReadCommitted isolation (specified by the constant parameter passed to the Connection object), any access to data sources will only see “committed” data, meaning the changes that are durable and have been successfully made. This guards against “phantom writes” that may appear if lower, more relaxed isolation levels are used.

The ADO/WFC Transaction object is dispensed from the Connection object. Once a non-null reference is obtained, the standard transaction operations may be used: beginning, committing, or aborting changes. In this sample, a new transaction is created after the initial select statement is executed from the Connection, resulting in a non-transacted Recordset. Subsequent to the t.beginTrans() call, the Recordset is replenished, after the Connection object is used to execute a SQL DML statement. This statement deletes the entire authors table. The Recordset, now empty, is within a transactional scope. The following lines then verify that the Recordset is indeed within the “scope” of the running transaction:

if (rs.getRecordCount() != 0)
   {
      rs.close();
      // Roll back (undo) the changes.
      t.rollbackTrans(false);
      c.close();
      return false;
   }

The RecordCount of the Recordset should equal zero, since the Connection deleted all of the rows. If it fails to equal zero, the entire delete operation is aborted, rolling back the state of the data source to the state it had before the transaction was started. If, on the other hand, the Recordset is empty, then the transaction has worked appropriately and the changes can be committed, as follows:

t.commitTrans(false);
rs.requery();
rs.close();

The Recordset is requeried, and should be empty, since the deletes were made permanent. The Recordset is then closed and the sample is completed. Transactions are an excellent way to write applications that ensure strange results do not creep into your control logic. ADO/WFC’s full support for both the local and distributed varieties make transactions a flexible and useful framework for building distributed business applications.