Click here to Skip to main content
Click here to Skip to main content

The CODBCDynamic class

By , 14 Mar 2000
 
  • Download demo project - 14 Kb
  • Download source files - 14 Kb
  • Introduction

    There are times when, as a programmer, you might be faced with scenarios where you do not know the schema of a database until runtime. Examples of this are ad-hoc query and reporting tools. In both cases, the end user is allowed to build their own SQL from a list of tables. As you may already know, it is extremely easy to pass ODBC an SQL string, have it executed, and retrieve the resulting data. But, how can you do this when you don't know what the resulting data will look like when you write your application?

    Luckily ODBC provides several functions that can be used for this very purpose. After connecting to the data source, the next steps needed would be the following:

    1. Prepare the SQL statement via the SQLPrepare function.
    2. Execute the SQL statement with the SQLExecute function.
    3. Call SQLNumResultCols to find out how many columns were returned in the result set.
    4. For each column, call the SQLDescribeCol function to get the column type.
    5. For each column, convert the SQL type returned from SQLDescribeCol to a C type.
    6. For each row in the result set, allocate memory for the data (depending on the C type).
    7. For each row, call SQLGetData to read the data into the allocated memory for that row/column.

    Did I say "luckily"? Actually, I said it with tongue firmly planted in cheek. Therefore, in this article I submit to you a class (CODBCDynamic) that reduces the 400+ lines of code required to fully implement the functionality listed above to 2 lines of code! Here are some examples of how to use the CODBCDynamic class.

    Examples of how to use the CODBCDynamic class

    While this article also includes a full-fledged test application, it's always nice to be able to see what you're getting before you invest the time in downloading, unzipping and running someone else's code. Therefore, here are some code snippets that show how easy the CODBCDynamic class is to use.

    Submitting an SQL statement

    To submit an SQL statement, you simply instantiate a CODBCDynamic object (passing a valid DSN) and then call the CODBCDynamic::ExecuteSQL member function (passing the SQL string to execute). That's it!

    // simply specify the ODBC DSN in the c'tor 
    // and pass the desired SQL to the ExecuteSQL function...
    CODBCDynamic odbcDynamic(_T("YourDsn"));
    odbcDynamic.ExecuteSQL(_T("SELECT * from OrderHeader"));
    

    Retrieving data from a result set

    In the first example above, I showed you how the CODBCDynamic class allows you to submit an SQL statement using the ExecuteSQL member function. However, there are times, when your application will only have the HSTMT to a result set. For example, if you call the ODBC SDK function SQLGetTypeInfo, you will receive a result set with the returned data. Using the CODBCDynamic class, you can read the data into its member variables with the following two lines of code.

    // call a function that returns an hstmt to a result set (e.g., SQLGetTypeInfo)
    odbcDynamic.FetchData(hstmt);
    

    Retrieving all rows and columns of data once ExecuteSQL or FetchData has been called

    Once either the ExecuteSQL or FetchData member functions have been called, the resulting data can be retrieved from the CODBCDynamic object in a very generic manner. The CODBCDynamic class has a templatized array (m_ODBCRecordArray) that represents each of the records that were read. Each entry in the m_ODBCRecordArray is a templatized CMapStringToPtr map of columns and their respective values for that record. The map is keyed by the column name (retrieved automatically) and the data is in the form of a CDBVariantEx object. However, you never have to worry about such technicalities. Assuming that you've already called ExecuteSQL or FetchData, here's an example of how easy it is to iterate through the returned records of an SQL statement.

    // instantiate a CODBCDynamic object (specifying the desired DSN)
    CODBCDynamic odbcDynamic(_T("Forms Express System Database"));
    // execute the desired SQL 
    odbcDynamic.ExecuteSQL(_T("SELECT * from UserMaster"));
    
    // retrieve the record array
    CODBCRecordArray* pODBCRecordArray = &odbcDynamic.m_ODBCRecordArray;
    
    CString strInfo;
    
    // for every returned record...
    for (int iRecord = 0; iRecord < pODBCRecordArray->GetSize(); iRecord++)
    {
      CODBCRecord* pODBCRecord = (*pODBCRecordArray)[iRecord];
    
      POSITION pos;
      CDBVariantEx* pvarValue;
      CString strColName;
    
      CString strValue;
    
      // for every column within the current record
      for (pos = pODBCRecord->GetStartPosition(); pos != NULL;)
      {
        pODBCRecord->GetNextAssoc(pos, strColName, pvarValue);
        pvarValue->GetStringValue(strValue);
    
        strInfo.Format(_T("Record: %ld, Column: %s, Value: '%s'"), 
                       iRecord, strColName, strValue);
        AfxMessageBox(strValue);
      }
    }
    

    Retrieving specific columns once ExecuteSQL or FetchData has been called

    As mentioned above, once the ExecuteSQL or FetchData function has returned, each returned record is stored in an array and each record is a basically a map of column names to CDBVariant values. Therefore, as easy as it is to iterate through all the returned the data, you can just as easily request specific columns by name. Here's an example of how you would do that.

    // instantiate a CODBCDynamic object (specifying the desired DSN)
    CODBCDynamic odbcDynamic(_T("Forms Express System Database"));
    // execute the desired SQL 
    odbcDynamic.ExecuteSQL(_T("SELECT * from UserMaster"));
    
    // retrieve the record array
    CODBCRecordArray* pODBCRecordArray = &odbcDynamic.m_ODBCRecordArray;
    
    // for every returned record...
    for (int iRecord = 0; iRecord < pODBCRecordArray->GetSize(); iRecord++)
    {
     CODBCRecord* pODBCRecord = (*pODBCRecordArray)[iRecord];
     CString strValue;
    
     // retrieve the desired column (by name)
     CDBVariantEx* pvarValue = NULL;
     if (pODBCRecord->Lookup(_T("sUserId"), pvarValue))
     {
      // As shown in the example above, you can use the 
      // CDBVariantEx::GetStringValue to have the value 
      // translated into a CString and returned...
      pvarValue->GetStringValue(strValue);
      AfxMessageBox(strValue);
    
      // ... or you can now use the appropriate CDBVariant member 
      // variable to access the data. For example, if the column's 
      // data type is string, or text...
      AfxMessageBox(*pvarValue->m_pstring);
     }
    }
    

    That's it! That's how easy it is to interrogate any ODBC data source. The last thing that I will point out is that in the example above, I used my CDBVariantEx's GetStringValue member function to retrieve the data in as a CString. However, because I chose to store the data in CDBVariant objects, you can also easily query that object as to the data's exact type by inspecting the CDBVariant::m_dwType member variable. For more documentation on this small, but useful class, please refer to the Visual C++ documentation.

    License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here

    About the Author

    Tom Archer
    Software Developer (Senior) Microsoft
    United States United States
    Member
    I'm a Senior Programming Writer in the Microsoft Windows Server organization where my focus is WMI, BITS, WinRM, and SMI-S.

    Sign Up to vote   Poor Excellent
    Add a reason or comment to your vote: x
    Votes of 3 or less require a comment

    Comments and Discussions

     
    You must Sign In to use this message board.
    Search this forum  
        Spacing  Noise  Layout  Per page   
    GeneralCannot compilemembershiphy21 Mar '10 - 23:56 
    Am using Microsoft Visual C++.NET. I downloaded the demo project but it fails to compile. I get the error message;
    fatal error C1083: Cannot open include file: 'ODBCDynamic.hpp': No such file or directory
     
    I there a way i can access this file.
    This looks like some thing great if it worked. Please help
    Generalcolumn name length vs data lenmemberkyrt13 Sep '08 - 2:57 
    line 431:
    strValue.ReleaseBuffer(nActualSize < (static_cast<CODBCColumnInfo*>(arrODBCColumns[j]))->m_nLen ? nActualSize : ((CODBCColumnInfo*)arrODBCColumns[j])->m_nLen);
     
    What for to cut off the size of the returned line of data on length of the name of a column?
    sorry for my english
    GeneralRe: column name length vs data lenmemberLEKV2 Mar '09 - 14:41 
    I have run into the same problem and have not found a fix yet. The problem I think stems from SQLDescribeCol() call made in FetchData which returns what appears to be the column name length rather than the actual data length of the field. I am not sure whether the error is in SQLDescribeCol() or in Tom's code.
     
    Larry
    Questionhow to convert properly?memberM_Nuaimi29 Apr '06 - 12:00 
    Hi.
    I’m trying to develop an application that must operate on the oracle database using (ODBC) CDatabase & CRecordset. But the application can't show the record (NUMBER) type correctly and it show instead some strange numbers in the edit contrl. like -19841 instead of 50.
     
    // here's an example of the code.
    char str[255];
    int index=0;
    CDBVariant var;
    recordset.GetFieldValue(index,var);
    wsprintf(str, "%d" ,var.m_iVal); // i tried to use 'var.m_dblVal' also.
    EditCtrl->SetWindowText(str);
     
    may you tell me please what i have to do to convert the number and show it correctly? or how to make sure about which data type is in the CDBVariant for further proper conversion?
    I'm using VC++ 6.0 & Oracle 9i.
     
    Best regards Smile | :)
    QuestionAny update to CODBCDynamic ?memberj.f.gratton21 Mar '06 - 6:07 
    Hello,
     
    I was wondering if there's any repository with updates on this (quite usefull !) set of classes ?
     

    Thanks,
     
    -- Jeff
    AnswerRe: Any update to CODBCDynamic ?memberTom Archer - MSFT21 Mar '06 - 6:19 
    While I enjoy giving back to the community in the form of code/articles, continually updating past articles isn't something I can do and still have time to make money to put food on the table Smile | :)
     
    Therefore, this is the only "free" version of the code I've made available.
     
    If you're questions are generic ODBC-type questions, you can probably get loads of help on this site's forums as well as the MSDN Forums.
     
    On the other hand, if you're in a pinch and want these classes customized for your company's needs, I have done that for several clients on an hourly, paid basis. If so, drop me a line and we can discuss further.

     
    Tom Archer (blog)
    Program Manager
    MSDN Online (Windows Vista and Visual C++)
    MICROSOFT
    GeneralCDBVariant questionmemberkocikus13 Mar '06 - 0:20 
    Any idea how to distinguish if a CDBVariant is actually holding a date or time? Confused | :confused:
    GeneralRe: CDBVariant questionmemberTom Archer - MSFT13 Mar '06 - 6:14 
    if (myDbVariant.m_dwType == DBVT_DATE)
    {
     // value is in m_pdate (pointer to a TIMESTAMP_STRUCT)
    }
    
     

     
    Tom Archer (blog)
    Program Manager
    MSDN Online (Windows Vista and Visual C++)
    MICROSOFT
     
    -- modified at 12:15 Monday 13th March, 2006
    GeneralProblem with CODBCRecordmembermpamiro6 Apr '04 - 23:38 
    Thanks for your code!
    I got a problem: when I assign the content of a field to a CString with both CODBCRecord::Lookup or CODBCRecord::GetNextAssoc, only the first N characters of the record cell are copied into the CString, where N is the length of the field name!

    For ex:
    If the value of the field named Name is Michelangelo, the content of the CString will be Mich (as Name is 4 character long).

    The same happens with your demo project.
    I used Access databases for my tests.
    Do you know why this happens?
    Thanks!
     
    Mauro Pamiro
    GeneralRe: Problem with CODBCRecordmemberthomasCAE29 Nov '05 - 2:18 
    I encounter the same problem. And focused it out to the following line
     
    if ((-1 != nActualSize)
    && ((static_cast(arrODBCColumns[j]))->m_nFieldType == SQL_C_CHAR))
    {
    // Release the string buffer
    CString strValue = (CString)*pvarValue->m_pstring;
    strValue.ReleaseBuffer(nActualSize < (static_cast(arrODBCColumns[j]))->m_nLen ? nActualSize : ((CODBCColumnInfo*)arrODBCColumns[j])->m_nLen);
    }
     
    I think the intention was to be able to read out char fields not ended correctly by a \0. Th problem is that m_nLen does not store the size of the column, but the size of the header. This is filled in the following line
     
    if (SQL_SUCCESS == (rc = ::SQLDescribeCol(hstmt, i, reinterpret_cast(lpszColName), MAX_COLNAME, &nLen, &nSQLType, &nPrecision, &nScale, &nNullability)))
    {
    // Determine the default field type and get the data buffer
    short nFieldType = GetFieldTypeFromSQLType(nSQLType);
    pODBCColumnInfo = new CODBCColumnInfo(lpszColName, nLen, nFieldType, nSQLType, nPrecision);
    arrODBCColumns.Add(static_cast(pODBCColumnInfo));
    }
     
    Here he used nLen instead of nPrecision. You should replace the line to
     
    pODBCColumnInfo = new CODBCColumnInfo(lpszColName, nPrecision, nFieldType, nSQLType, nPrecision);

    and all works fine...
    GeneralExecuting Stored Proceduresussmbhagwat18 Jun '03 - 3:15 
    Very easy to use and concise. I have used it to get data from DB2 - Great Effort Tom!
     
    Please let me know how I can execute stored procedure which takes both input and output parameters.
     
    Also I got an error when the resultset(from DB2) contained a VARCHAR. I will apply Steve's fix and see.
     

    GeneralRe: Executing Stored ProcedurememberTom Archer18 Jun '03 - 4:48 
    Thanks. However, I don't know if or when I'm update this code as it's something I haven't used in many years. (It was something I needed for a job iat AT&T back in '98 I believe).
     
    Cheers,
    Tom Archer, Inside C#
    Mainstream is just a word for the way things always have been -- just a middle-of-the-road, tow-the-line thing; a front for the Man serving up the same warmed-over slop he did yesterday and expecting you to say, "Thank you sir, may I have another?"

    GeneralA little Slowmemberttohme23 Apr '03 - 18:13 
    Hi,
     
    Your Classes are very good and straight forward, but when I implemented them into my SQL DB, it took a little too much time retrieving the rows ( 2000row ) and that was only for one column, I didn't try it for the 17 columns that Ihad.
     
    I there a way to speed up the process, maybe using some cahing method.
     
    Thanks
    GeneralIncorrect ordermembermohdalwi19 Apr '03 - 5:00 
    .
    .
    for (pos = pODBCRecord->GetStartPosition(); pos != NULL;)
    {
    pODBCRecord->GetNextAssoc(pos, strColName, pvarValue);
    pvarValue->GetStringValue(strValue);
    TRACE("Record: %ld, Column: %s, Value: '%s'\n",iRecord, strColName, strValue);
    }
    .
    .
     
    everything is ok but the problem is:
    -> the strColName is not in correct order for each record as in database

     
    Mohd Alwi
    GeneralWonderfulmemberThe Devil11 Mar '03 - 19:11 
    Great article man. I was looking out for something like this. Thanks. Wink | ;)
    GeneralRe: WonderfulmemberTom Archer13 Mar '03 - 5:34 
    Thanks man! I know it definitely saved me a lot of time in a couple of gigs once I got it finished.
     
    Cheers,
    Tom Archer, Inside C#
    Mainstream is just a word for the way things always have been -- just a middle-of-the-road, tow-the-line thing; a front for the Man serving up the same warmed-over slop he did yesterday and expecting you to say, "Thank you sir, may I have another?"

    GeneralError. help me.membernvtoan23 Jun '02 - 16:53 
    I usually have some error in ASSERT in line 200 and 300 of ODBCDynamic.cpp
     
    short CODBCDynamic::GetFieldTypeFromSQLType(short nSQLType)
    {
     
    .........................
    default:
    ASSERT(FALSE);
    }
     
    return nFieldType;
    }
     

    void* CODBCDynamic::GetDataBuffer(CDBVariantEx* pvarValue,
    short nFieldType, int* pnLen, short nSQLType, UDWORD nPrecision)
    {
    ...........................................
    default: ASSERT(FALSE);
    }
     
    return pvData;
    }

    GeneralRe: Error. help me.sussSteve Mkandawire3 Oct '02 - 6:23 
    I already posted this error and my solution below: http://www.codeproject.com/database/dynamic_odbc_class.asp?df=100&forumid=480&select=44209#xx44209xx
     
    Cheers
     
    Steve
    GeneralRe: Error. help me.memberTom Archer25 Nov '02 - 6:07 
    Thanks Steve. One of these days, I'll actually have a few minutes to update the article. When I do, I'll definitely give you credit for this fix.
     
    Cheers,
    Tom Archer
    Author - Inside C#, Visual C++.NET Bible

    GeneralError in odbcDynamic.m_ODBCRecordArraymembernvtoan21 Jun '02 - 0:46 
    I have 1 error in line 259 in File ODBCDynamicTestDlg.cpp
     
    CODBCRecordArray* pODBCRecordArray = &odbcDynamic.m_ODBCRecordArray;
     
    I think the &odbcDynamic.m_ODBCRecordArray is NULL
    Can you help me.
     

    GeneralMore DiscussionsmemberRyan White14 May '02 - 15:43 
    I found another message post site for more articles on Tom Archer's CODBCDynamic Class.....
     
    http://www.codeguru.com/mfc_database/dynamic_odbc_class.shtml?0.0767822265625
     

     
    Ryan
    92110.com
     
    Ryan
    GeneralRe: More DiscussionsmemberTom Archer15 May '02 - 0:50 
    Hi Ryan,
     
    Chris Maunder and I originally built CodeGuru so that's where this article first appeared. When Chris started this site, I posted some of my articles over here to help him get started with some content.
     
    Cheers,
    Tom Archer
    Author - Inside C#, Visual C++.NET Bible
    Generalcreate database programatically in oraclememberjesbeer15 Jan '02 - 18:24 
    Hi,
     
    Anyone has idea about how to create a database in oracle from within a vc++ program using the SQL statement "create database"??

    GeneralRe: create database programatically in oraclememberRyan White14 May '02 - 15:49 
    if you mean to create a table it would be....
     
    CREATE TABLE MyTable
    (
    MyNumber NUMBER,
    MyText VARCHAR2(30),
    PRIMARY KEY(MyNumber)
    );
     
    Ryan
    92110.com
    GeneralRe: create database programatically in oraclememberTom Archer15 May '02 - 0:52 
    This space is really intended as a means of me getting feedback on this article. You'll probably have a greater chance of getting help on specific questions regarding other issues if you ask them in the VC++ forum.
     
    Cheers,
    Tom Archer
    Author - Inside C#, Visual C++.NET Bible

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

    Permalink | Advertise | Privacy | Mobile
    Web02 | 2.6.130523.1 | Last Updated 15 Mar 2000
    Article Copyright 2000 by Tom Archer
    Everything else Copyright © CodeProject, 1999-2013
    Terms of Use
    Layout: fixed | fluid