OLEDB - Custom Rowsets

| 0 Comments
The ATL OLE DB Provider templates appear to rely on the fact that your data is kept in a simple array, but that's not really the case at all!

Implementing a custom OLE DB rowset
This article continues from where we left off in the previous article. We have all of the framework in place to provide ADO recordset interfaces on our simple data object. All we need to do now is replace the wizard-generated OLE DB rowset object with one that allows us to access our object's data.

The rowset object that the ATL wizard has provided us with is of no use to us. It's fine for simple data where you can copy the data to be made available via ADO into the rowset's array. We want to retain control of our data and not be forced to create a copy of it, so the rowset needs to access our data in-place in our data object.

At first it may seem that the design of the ATL OLE DB provider templates is completely inappropriate for our needs. However, the design is actually very flexible and we can replace two simple components and provide the functionality that we require.

The "proxy" rowset
The standard OLE DB template rowset object, CRowsetImpl, provides access to data that's stored in a contiguous array within the rowset object itself. This is fine for simple example programs but almost useless for our required application. We want to keep our data stored within our simple data object, it may be too costly to copy all of the data into a new rowset object just to provide access via ADO. We'd rather leave the data where it is and just provide access to it.

Luckily the CRowsetImpl template relies on two template parameter classes for storing its data. The template itself looks something like this:

template <
   class T, 
   class Storage, 
   class CreatorClass,
   class ArrayType = CSimpleArray<Storage>,
   class RowClass = CSimpleRow,
   class RowsetInterface = IRowsetImpl < T, IRowset, RowClass> >
class CRowsetImpl : etc...

The pieces we're interested in replacing are the Storage and ArrayType classes. These are used by the default implementation to retrieve the rowset data from. By replacing these two template parameters with classes that implement the required functionality we can dictate where the rowset gets its data from.

We'll write a proxy rowset template. It's a proxy because it just takes the place of a rowset object and forwards all of the interesting calls to our data object. The data object stores its data in exactly the same way as before and the proxy rowset accesses the data in-place, on demand. There's no initial startup overhead involved, though there's probably a little overhead in the actual data access.

The proxy rowset template looks like this:

template <
   class DataClass,
   class T, 
   class CreatorClass, 
   class Storage = CRowsetStorageProxy<T>, 
   class ArrayType = CRowsetArrayTypeProxy<T, Storage>,
   class RowClass = CSimpleRow,
   class RowsetInterface = IRowsetImpl < T, IRowset, RowClass> >
class CProxyRowsetImpl:
   public CRowsetImpl<
      T,
      Storage,
      CreatorClass,
      ArrayType,
      RowClass,
      RowsetInterface >

The important things to note are the classes that are defaulted for the Storage and ArrayType template parameters. These two proxy classes simply forward all requests to the proxy rowset rather than fielding them themselves. The proxy rowset can then pass requests on to the data object that it's associated with. By requiring the data object to provide function bodies for several simple data access and information functions the proxy rowset can deal with all of the technicalities of being an OLE DB rowset but still pass the requests for data and information through to the data object itself.

The functions that are data object specific, and must be provided by the class derived from our proxy rowset are as follows:

virtual void GetColumnInformation(
   size_t column, 
   DBTYPE &dbType, 
   ULONG &size, 
   std::string &columnName, 
   DWORD &flags);

Called when building the column information structure for the rowset. Column numbering starts at 0. The type size and flags fields should be filled in with the values that are appropriate for this column of data - see DBCOLUMNINFO for more details. The data type specified in dbType should be the native data type that you are storing your data as. The proxy rowset will handle all data conversion requirements to and from this data type for you.

virtual void GetColumnData(
   size_t row,
   size_t column,
   DBTYPE &dbType, 
   ULONG &size, 
   void *&pData,
   bool &bIsUpdatable);

Called when access to the data in a particular row and column is required. Row and column numbering starts at 0. Type and size are the actual types and sizes of this element of the data (these are likely to be the same as those returned from GetColumnInformation for the column as a whole except in the case of variable length string data when the size returned here can be the actual length of the string, rather than the maximum length that's returned from GetColumnInformation. The void pointer, pData, should be set to point at the start of the data item itself. The proxy rowset will use this pointer to access the data. The pointer should be set to point straight at your data, you shouldn't allocate a buffer or do any copying. The bIsUpdatable flag isn't relavant until we add functionality to the proxy rowset in a later article to make it support read/write rowsets rather than simple read only rowsets.

virtual size_t GetNumColumns();
virtual size_t GetNumRows() const;
virtual HRESULT AddRow();
virtual HRESULT RemoveRow(int nIndex);

The others are all fairly obvious.

The proxy rowset contains two pointers that refer to the object that it is representing. These are set up automatically when the rowset is connected to the object. You can access these from within your derived class to provide access to your data object. One pointer is a pointer to your data object's IUnknown, you're unlikely to need to use this, it's only really for maintaining a reference on your object whilst the proxy rowset object is connected to it. The second pointer is a pointer to your object itself. Through this your proxy rowset derived class can get direct access to your data object's internals. It's allowed to do this because we take great care when creating the proxy rowset to make sure it's created in the same COM apartment as the data object.

Connecting the rowset to your object
Now that we have a rowset object and all of the ADO and OLE DB framework in place all that's left is to create the rowset and connect it to your data object.

We need to move back inside our OLE DB provider objects and intercept the rowset creation request at the command object's execute method. This is where the action will happen...

At present the method probably looks something like this:

CSimpleDataObjectRowset *pRowset;
return CreateRowset(
   pUnkOuter, 
   riid,
   pParams, 
   pcRowsAffected, 
   ppRowset, 
   pRowset);
The ATL OLE DB template function CreateRowset is being called which does the work of building a standard rowset of the type specified and then calls the rowset's execute method to populate it. We don't want any of this to happen, so we can rip out the above and replace it with some code that works with the custom command object we created in the ADO layer. This command has passed us the IUnknown pointer of the data object that we're providing a rowset onto. First we need to get hold of this IUnknown pointer using the parameter accessor that we are passed, then we can get to work...

As mentioned above we need to take care to create the rowset object in the same COM apartment as the data object itself. This will allow us to access the data object directly from the rowset object using a normal C++ pointer to the data object. The easiest way of making all of this work is for us to ask the data object itself to create and return the rowset object. Because the data object creates the rowset object as a C++ object we know that the rowset is in the same COM apartment as the data object. Of course, this means we need to get from the OLE DB provider's command object back into the data object.

The easiest way into the data object from the command object is via a COM call. In the command object we QueryInterface on the data object's IUnknown pointer for the _IGetAsOLEDBRowset interface. We then call the interface's only method, GetAsRowset() and pass our own IUnknown pointer in, along with a bunch of other stuff.

The work continues back inside the data object as we execute the GetAsRowset() method of the _IGetAsOLEDBRowset interface... The template implementation of this method simply calls into the data object on a method called AsRowset() and this is where the work of actually creating the rowset and linking it to the data object actually occurs.

We've jumped through a lot of hoops to get to this point, but all of the code up to now has been generic template implementations which do the right thing. We're now inside a method on our data object and we have derived a class from the proxy rowset implementation to act as our rowset class. With a little custom code like the following, we can create our rowset and link it to our data.

HRESULT CMyDataObject::AsRowset(
   /* [in] */ IUnknown *pUnkCreator,
   /* [in] */ IUnknown *pUnkOuter,	
   /* [in] */ REFIID riid,				
   /* [out] */ LONG *pcRowsAffected,			  
   /* [out, iid_is(riid)] */ IUnknown **ppRowset)
{
   CDataObjectRowset *pRowset = 0;
   
   HRESULT hr = CreateRowset(
       pUnkCreator, 
       pUnkOuter, 
       riid, 
       ppRowset, 
       pRowset);
  
   if (SUCCEEDED(hr))
   {
      IUnknown *pUnknown = 0;
     	
      hr = QueryInterface(IID_IUnknown, (void**)&pUnknown);
  
      if (SUCCEEDED(hr))
      {
         hr = pRowset->LinkToObject(this, pUnknown, pcRowsAffected);    
  
         pUnknown->Release();
      }
   }
   return hr;
}

We can then hand the rowset back to the template implementations and they will take care of returning the object to the ADO layer which will wrap it in an ADO recordset and return it to our Visual Basic client code. We can even defer the most complex part of the code, that of creating and wiring up the rowset object, back to the _IGetAsOLEDBRowset implementation template, the call to CreateRowset() is a template member function which is paramaterised on the rowset class we pass into it. It handles creating the rowset COM object and copying the command object's properties into it - in the same way that the standard ATL OLE DB rowset is created.

The complete working example code for the above can be downloaded from here.

So, how do I give ADO access to my object?

  • Include IGetAsADORecordset.idl and IGetAsOLEDBRowset.idl in your object's IDL file, and have your object support the IGetAsADORecordset, _Recordset and IGetAsOLEDBRowset interfaces.
  • Add the IGetAsADORecordsetImpl and IGetAsOLEDBRowsetImpl implementation templates to your class's inheritance hierarchy in your header file.
  • Add the following macros to your class's COM Map:
    COM_INTERFACE_ENTRY(_IGetAsOLEDBRowset)
    COM_INTERFACE_ENTRY_CHAIN(IGetAsADORecordsetImpl<CMyDataObject>)
  • Derive a class from CProxyRowsetImpl and provide function bodies for the virtual functions required. These should use the pointer to your data object that's available to them as a protected member variable inherited from CProxyRowsetImpl to access your object's data.
  • Implement AsRowset(), probably just as shown above.

All of this assumes that you have an OLE DB conversion provider that will do the conversion for you. If you are only doing this for one object, package the conversion provider inside the same DLL as the object, if you're doing lots of objects like this, create a separate provider dll and have all your objects use the one provider.

Download
The following source built using Visual Studio 6.0 SP3. Using the July 2000 edition of the Platform SDK. If you don't have the Platform SDK installed then you may find that the compile will fail looking for "msado15.h". You can fix this problem by creating a file of that name that includes "adoint.h".

If your system drive isn't D:\ then you'll have to change the #import statements in IGetAsADORecordsetImpl.h and IGetAsADORecordset.cpp.

Revision history

  • 15th September 1999 - Initial revision www.jetbyte.com.
  • 17th September 1999 - Hacked in a simple solution to a stupid cyclical reference counting problem.
  • 16th October 1999 - Removed the 1st release zip file from the second release zip file (all the code was in there twice!)
  • 22nd July 2000 - Bug fix, IGetAsADORecordsetImpl.h, line 77, _Recordset15 should be Recordset15. Thanks to Nie Jiantao for reporting this.
  • 2nd October 2000 - Fixed some build configuration errors. Thanks to Charles Finley for reporting these.
  • 12th October 2005 - reprinted at www.lenholgate.com.

Other articles in the series
  • Objects via ADO - ADO seems to be the ideal way to expose tabular data from your own COM objects and the ATL OLE DB Provider templates can help!
  • Custom Rowsets - The ATL OLE DB Provider templates appear to rely on the fact that your data is kept in a simple array, but that's not really the case at all!
  • IRowsetLocate and Bookmarks - Adding bookmark functionality is relatively easy and it enables our ADO recordset to be used with a greater number of data bound controls.
  • Updating data through an ADO recordset - The ATL OLE DB Provider templates only seem to support read-only rowsets, and making them support updating of data isn't as easy as you'd expect!
  • Client Cursor Engine updates - Making the ADO Client Cursor Engine believe that your rowset is updateable involves jumping through a few extra hoops...
  • Disconnected Recordsets - If you are going to use the client cursor engine then often it's a good idea to disconnect your recordset...

Leave a comment

About this Entry

OLEDB - Objects via ADO was the previous entry in this blog.

OLEDB - IRowsetLocate and Bookmarks is the next entry in this blog.

I usually write about C++ development on Windows platforms, but I often ramble on about other less technical stuff...

Find recent content on the main index or look in the archives to find all content.

I have other blogs...

Subscribe to feed The Server Framework - high performance server development
Subscribe to feed Lock Explorer - deadlock detection and multi-threaded performance tools
Subscribe to feed l'Hexapod - embedded electronics and robotics
Subscribe to feed MegèveSki - skiing