65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Visual C++ 和 OLE DB 进行数据库开发:建立连接

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (48投票s)

2005年4月29日

17分钟阅读

viewsIcon

300179

如何使用 ATL OLE DB 消费者类启动连接,以及如何获取会话,以便使用该会话查询或编辑数据库。

引言

在 Windows 编程的许多领域,初始化或建立连接都是一种常见的模式。如果您想在窗口上绘图,则需要先通过调用 GetDCCreateDC 来获取设备上下文,完成后再通过调用 ReleaseDCDeleteDC 来关闭它。如果您想使用 Windows 套接字启动客户端和服务器之间的通信,则首先需要建立连接,完成后再关闭它。类似地,如果您想查询数据库以获取某些记录,则需要建立连接,完成后再关闭它。在本文中,我将向您展示如何使用 ATL OLE DB 消费者类建立连接,以及如何获取会话,以便使用该会话查询或编辑数据库。

要使用 OLE DB 提供程序初始化到数据库的连接,您需要两个 ATL 类:CDataSourceCSession。要使用它们,请包含 atldbcli.h 文件。

#include <atldbcli.h>

为什么是两个类?因为您可以在同一个连接上创建多个会话。一旦使用 OpenOpenFromInitializationString 方法打开了数据源,您很可能也想启动一个会话。通过此会话,您可以创建事务并查询或编辑数据库。即使在使用模式类时,实际上也需要这样做。OpenFromInitializationString 方法只有一个重载,而 Open 方法有九个重载。

  • HRESULT OpenFromInitializationString(LPCOLESTR szInitializationString);

我将解释 Open 方法的所有重载,但主要使用第一个,以防我需要显示“数据链接属性”对话框,允许用户选择 OLE DB 提供程序及其连接属性。

  • HRESULT Open(HWND hWnd = GetActiveWindow(), DBPROMPTOPTIONS dwPromptOptions = DBPROMPTOPTIONS_WIZARDSHEET);
  • HRESULT Open(const CLSID& clsid, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1);
  • HRESULT Open(const CLSID& clsid, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0);
  • HRESULT Open(LPCSTR szProgID, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1);
  • HRESULT Open(LPCWSTR szProgID, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0);
  • HRESULT Open(LPCWSTR szProgID, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1);
  • HRESULT Open(LPCSTR szProgID, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0);
  • HRESULT Open(const CEnumerator& enumerator, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1);
  • HRESULT Open(const CEnumerator& enumerator, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0);

请注意,ATL 消费者类基于 COM,这意味着

  1. 我们必须在调用这些类之前调用 CoInitialize,可能在 InitInstance 中,然后在程序结束时在 ExitInstance 中调用 CoUninitialize
  2. 我们将需要处理 HRESULT 返回值,以指示成功或失败。

使用 CEnumerator 类枚举 OLE DB 提供程序

CEnumerator oProviders;

HRESULT hr = oProviders.Open( );
if(SUCCEEDED(hr))
{
    // The following macro is to initialize
    // the conversion routines
    USES_CONVERSION;

    while(oProviders.MoveNext( ) == S_OK)
    {
// Now you have the provider name
// in oProviders.m_szName and description
// in oProviders.m_szDescription

#ifdef _UNICODE
        TRACE(oProviders.m_szName);
        TRACE(L"\n");
#else
        TRACE(W2A(oProviders.m_szName));
        TRACE("\n");
#endif
    }
    oProviders.Close( );
}

CEnumerator 类提供了枚举系统中所有已安装 OLE DB 提供程序的方法。它是一种方便的方式,可以为用户提供自定义界面来选择提供程序及其连接属性。如果您需要使用标准对话框枚举 OLE DB 提供程序,只需调用 CDataSource 类的 Open 方法,不带任何参数。

hr = ds.Open( );

标准 OLE DB 数据链接属性对话框

请注意,CEnumeratorm_szName 通常是提供程序的 ProgID,可以在 Open 方法的多个重载中使用它来打开连接。

该代码由于使用了 #ifdef,因此在应用程序的 Unicode 和 ANSI 生成中都能正常工作。USES_CONVERSION 宏用于初始化几个转换例程,例如将 ANSI 字符串转换为 Unicode 的 A2W,以及反之亦然的 W2Am_szName 是一个宽字符串指针,可以直接用于 Unicode 函数,但在单字节生成中工作时,我们必须使用 W2A 例程将其转换为 ANSI。

上面示例中描述的算法是 ATL 消费者类的典型方式。我们首先声明一个类,该类实际上包含一个访问器,其中包含几个数据成员,例如 m_szNamem_szDescription。当我们调用 MoveNext 方法时,这些数据成员将被填充下一个可用行的相应值,直到 MoveNext 返回 DB_S_ENDOFROWSET 并退出循环。当我们开始从数据库检索记录时,会发现这种模式很常见。

描述 ProgID ClassID 数据库
IBM OLE DB Provider for DB2 IBMDADB2.1 {1E29B6C3-8EC6-11D2-AF46-000629B3CD56} DB2
Microsoft Jet 4.0 OLE DB Provider Microsoft.Jet.OLEDB.4.0 {DEE35070-506B-11CF-B1AA-00AA00B8DE95} Microsoft Access
Microsoft OLE DB Provider for SQL Server SQLOLEDB {0C7FF16C-38E3-11d0-97AB-00C04FC2AD98} Microsoft SQL Server
MySQL.OLEDB Provider MySQLProv {C86FB69E-3664-11D2-A112-00104BD15372} MySQL
Oracle Provider for OLE DB OraOLEDB.Oracle {3F63C36E-51A3-11D2-BB7D-00C04FA30080} Oracle
Microsoft OLE DB Provider for Oracle MSDAORA {E8CC4CBE-FDFF-11D0-B865-00A0C9081C1D} Oracle

有关上述提供程序的 ProgID 和 ClassID 的 typedef,请参阅随附磁盘上的 ProvDefs.h

使用 DBPROPSET 结构和 CDBPropSet 类

DBPROPSET 结构用于传递有关连接的某些属性,以及稍后我们将看到的检索到的行集的属性。DBPROPSET 结构包含一个 DBPROP 数组,这些 DBPROP 类似于所需的属性。要使用该结构,我们首先创建一个 DBPROP 数组并为其赋值。然后,我们将此数组附加到 DBPROPSET 结构。如果属性属于不同的属性集,则我们将相同的属性集组合到一个 DBPROPSET 中。最后,我们将得到一个属性集数组。在 CDataSource::Open 方法的几个重载中,我们看到了指向 DBPROPSET 结构的指针。该结构用连接属性填充,并与属性集计数一起传递。以下代码片段阐明了我们的观点:

// the macro for conversions
USES_CONVERSION;

// build the property array of 3 properties
DBPROP rgProperties[3];
// property 1: the datasource
rgProperties[0].colid = DB_NULLID;
rgProperties[0].dwOptions = DBPROPOPTIONS_REQUIRED;
rgProperties[0].dwPropertyID = DBPROP_INIT_DATASOURCE;
rgProperties[0].dwStatus = 0;
rgProperties[0].vValue.vt = VT_BSTR;
rgProperties[0].vValue.bstrVal = ::SysAllocString(W2COLE(L"server_name"));
// property 2: the database name
rgProperties[1].colid = DB_NULLID;
rgProperties[1].dwOptions = DBPROPOPTIONS_REQUIRED;
rgProperties[1].dwPropertyID = DBPROP_INIT_CATALOG;
rgProperties[1].dwStatus = 0;
rgProperties[1].vValue.vt = VT_BSTR;
rgProperties[1].vValue.bstrVal = ::SysAllocString(W2COLE(L"Northwind"));
// property 3: the user id
rgProperties[2].colid = DB_NULLID;
rgProperties[2].dwOptions = DBPROPOPTIONS_REQUIRED;
rgProperties[2].dwPropertyID = DBPROP_AUTH_USERID;
rgProperties[2].dwStatus = 0;
rgProperties[2].vValue.vt = VT_BSTR;
rgProperties[2].vValue.bstrVal = ::SysAllocString(W2COLE(L"sa"));

// combine the 3 properties into a property set structure
DBPROPSET rgPropertySet[1];
rgPropertySet[0].cProperties = 3;
rgPropertySet[0].guidPropertySet = DBRGPROPERTIESET_DBINIT;
rgPropertySet[0].rgProperties = rgProperties;

// now open the SQL Server database connection
HRESULT hr = m_ds.Open(L"SQLOLEDB", rgPropertySet, 1);
if(SUCCEEDED(hr))
    m_ds.Close();

指定 DBPROPOPTIONS_REQUIRED 是为了指示该属性对于建立连接是必需的。如果不是,提供程序将返回错误并将 dwStatus 设置为 DBPROPSTATUS_NOTSUPPORTEDdwOptions 的另一个值是 DBPROPOPTIONS_OPTIONAL,它表示此属性是可选的,并且如果属性不满足,提供程序不应失败。

我们可以有一个两个属性集的数组而不是一个。在每个项中,我们定义一组相关属性,如下所示:

// the macro for conversions
USES_CONVERSION;

// build the property array of 3 properties
DBPROP rgProperties1[3];
// property 1: the datasource
rgProperties1[0].colid = DB_NULLID;
rgProperties1[0].dwOptions = DBPROPOPTIONS_REQUIRED;
rgProperties1[0].dwPropertyID = DBPROP_INIT_DATASOURCE;
rgProperties1[0].dwStatus = 0;
rgProperties1[0].vValue.vt = VT_BSTR;
rgProperties1[0].vValue.bstrVal = ::SysAllocString(W2COLE(L"server_name"));
// property 2: the database name
rgProperties1[1].colid = DB_NULLID;
rgProperties1[1].dwOptions = DBPROPOPTIONS_REQUIRED;
rgProperties1[1].dwPropertyID = DBPROP_INIT_CATALOG;
rgProperties1[1].dwStatus = 0;
rgProperties1[1].vValue.vt = VT_BSTR;
rgProperties1[1].vValue.bstrVal = ::SysAllocString(W2COLE(L"Northwind"));
// property 3: the user id
rgProperties1[2].colid = DB_NULLID;
rgProperties1[2].dwOptions = DBPROPOPTIONS_REQUIRED;
rgProperties1[2].dwPropertyID = DBPROP_AUTH_USERID;
rgProperties1[2].dwStatus = 0;
rgProperties1[2].vValue.vt = VT_BSTR;
rgProperties1[2].vValue.bstrVal = ::SysAllocString(W2COLE(L"sa"));

// build another property array of 1 property
DBPROP rgProperties2[1];
// Property 1: application name
rgProperties2[0].colid = DB_NULLID;
rgProperties2[0].dwOptions = DBPROPOPTIONS_REQUIRED;
rgProperties2[0].dwPropertyID = SSPROP_INIT_APPNAME;
rgProperties2[0].dwStatus = 0;
rgProperties2[0].vValue.vt = VT_BSTR;
rgProperties2[0].vValue.bstrVal = 
   ::SysAllocString(W2COLE(L"my application title"));

// combine the 3 properties into a property set structure
DBPROPSET rgPropertySet[2];
rgPropertySet[0].cProperties = 3;
rgPropertySet[0].guidPropertySet = DBPROPSET_DBINIT;
rgPropertySet[0].rgProperties = rgProperties1;
rgPropertySet[1].cProperties = 1;
rgPropertySet[1].guidPropertySet = DBPROPSET_SQLSERVERDBINIT;
rgPropertySet[1].rgProperties = rgProperties2;

// now open the SQL Server database connection
HRESULT hr = m_ds.Open(L"SQLOLEDB", rgPropertySet, 2);
if(SUCCEEDED(hr))
    m_ds.Close();

这个应用程序名称属性有什么用?在调用 m_ds.Close 方法之前,打开“master”数据库并查找“sysprocesses”表。打开该表,您将在 program_name 字段中找到“my application title”。这意味着它是当前打开 SQL Server 数据库的进程之一。

以前示例中属性的设置可能看起来有点复杂。我不喜欢以这种方式处理结构。因此,我将介绍一个 ATL 类,它可以缩短处理属性和属性集的学习曲线。这个类是 CDBPropSet。看看使用这个类时上面的代码会是什么样子:

CDBPropSet rgPropertySet[1] = {DBPROPSET_DBINIT};

rgPropertySet[0].AddProperty(DBPROP_INIT_DATASOURCE, L"server_name");
rgPropertySet[0].AddProperty(DBPROP_INIT_CATALOG, L"Northwind");
rgPropertySet[0].AddProperty(DBPROP_AUTH_USERID, L"sa");

hr = m_ds.Open("SQLOLEDB", rgPropertySet, 1);

就是这样。AddProperty 方法负责添加不同类型的属性并对其进行初始化。传递给类的指针与传递给 DBPROPSET 结构的指针相同,因为该类是从该结构派生的。

与连接相关的属性

有许多与连接相关的属性。我不会在此全部列出,但要获取完整列表,请参阅 MSDN 库。下表显示了对我们最重要的属性。这些属性部分或全部适用于大多数关系数据库。

为了避免浏览器滚动,一些术语已被连字符 (-) 分开。

属性 属性集 类型 适用对象
DBPROP_INIT_DATASOURCE DBPROPSET_DBINIT VT_BSTR
  • Microsoft.Jet.OLEDB.4.0
  • MSDAORA
  • MySQLProv
  • OraOLEDB.Oracle
  • SQLOLEDB
  • IBMDADB2.1
DBPROP_INIT_CATALOG DBPROPSET_DBINIT VT_BSTR
  • SQLOLEDB
DBPROP_AUTH_USERID DBPROPSET_DBINIT VT_BSTR
  • Microsoft.Jet.OLEDB.4.0
  • MSDAORA
  • MySQLProv
  • OraOLEDB.Oracle
  • SQLOLEDB
  • IBMDADB2.1
DBPROP_AUTH_PASSWORD DBPROPSET_DBINIT VT_BSTR
  • Microsoft.Jet.OLEDB.4.0
  • MSDAORA
  • MySQLProv
  • OraOLEDB.Oracle
  • SQLOLEDB
  • IBMDADB2.1
DBPROP_AUTH_INTEGRATED DBPROPSET_DBINIT VT_BSTR
  • MySQLProv
  • SQLOLEDB
DBPROP_AUTH_MASK_PASSWORD DBPROPSET_DBINIT VT_BOOL
  • MySQLProv
  • SQLOLEDB
DBPROP_AU-TH_ENCRYPT_PASSWORD DBPROPSET_DBINIT VT_BOOL
  • MySQLProv
  • SQLOLEDB
DBPROP_INIT_ASYNCH DBPROPSET_DBINIT VT_I4
  • SQLOLEDB
DBPROP_INIT_GENERALTIMEOUT DBPROPSET_DBINIT VT_I4
  • SQLOLEDB
DBPROP_INIT_LOCATION DBPROPSET_DBINIT VT_BSTR
  • MySQLProv
DBPROP_INIT_MODE DBPROPSET_DBINIT VT_I4
  • MySQLProv
  • Microsoft.Jet.OLEDB.4.0
  • IBMDADB2.1
DBPROP_INIT_HWND DBPROPSET_DBINIT VT_I8
  • Microsoft.Jet.OLEDB.4.0
  • MSDAORA
  • MySQLProv
  • OraOLEDB.Oracle
  • SQLOLEDB
DBPROP_INIT_PROMPT DBPROPSET_DBINIT VT_I2
  • Microsoft.Jet.OLEDB.4.0
  • MSDAORA
  • MySQLProv
  • OraOLEDB.Oracle
  • SQLOLEDB
DBPROP_INIT_TIMEOUT DBPROPSET_DBINIT VT_I4
  • MySQLProv
  • SQLOLEDB
  • DBPROP_INIT_DATASOURCE

    服务器名称或数据库名称。如果是服务器名称,则 DBPROP_INIT_CATALOG 包含数据库名称。

  • DBPROP_INIT_CATALOG

    DBPROP_INIT_DATASOURCE 包含服务器名称的情况下,数据库名称。

  • DBPROP_AUTH_USERID

    登录用户 ID。

  • DBPROP_AUTH_PASSWORD

    登录密码。

  • DBPROP_AUTH_INTEGRATED

    指示使用的安全系统,无论是用户 ID-密码还是适用于使用当前登录用户凭据的 Windows 安全系统的 SSPI。

  • DBPROP_AUTH_MASK_PASSWORD

    以屏蔽形式将密码发送给提供程序。

  • DBPROP_AUTH_ENCRYPT_PASSWORD

    以加密形式将密码发送给提供程序。

  • DBPROP_INIT_ASYNC

    指示连接是同步还是异步的。如果是异步的,Open 方法将立即返回,并在后台执行连接。在同步模式下,打开尝试不会返回,直到成功或失败。

  • DBPROP_INIT_GENERALTIMEOUT

    获取行集和执行命令时使用的默认超时周期。

  • DBPROP_INIT_LOCATION

    数据库的位置。

  • DBPROP_INIT_MODE

    打开的数据库的共享模式。这通常适用于 Microsoft Access 等文件数据库。

  • DBPROP_INIT_HWND

    提示对话框父窗口的窗口句柄。

  • DBPROP_INIT_PROMPT

    指示是否提示用户提供缺失的连接信息。

  • DBPROP_INIT_TIMEOUT

    数据库连接尝试的超时周期(以秒为单位)。如果此属性中指定的时间到期且连接尝试仍在进行中,则 Open 方法将返回不成功。

CDataSource::Open 方法

既然我们已经描述了建立连接的所有组件,现在我们可以描述 CDataSource::Open 方法的重载了。我将列出每个重载的使用代码示例,但每次都使用不同的数据库类型。OLE DB 是一个强大的框架,因为一旦您打开到所选数据库的连接,您就可以以相似的方式查询它或执行命令。

  • HRESULT Open(HWND hWnd = GetActiveWindow(), DBPROMPTOPTIONS dwPromptOptions = DBPROMPTOPTIONS_WIZARDSHEET);

    这是唯一允许您打开标准“数据链接属性”对话框以可视化指定连接参数(包括提供程序)的方法。hWnd 是标准对话框父窗口的窗口句柄,默认为当前活动窗口。第二个参数用于设置对话框的样式。默认值为 DBPROMPTOPTIONS_WIZARDSHEET,它适用于向导样式的属性页。您可以指定 DBPROMPTOPTIONS_PROPERTYSHEET 用于属性表样式。

    HRESULT hr = m_ds.Open( );
    if(SUCCEEDED(hr))
        m_ds.Close( );

    其余的重载是参数组合的混合。这些参数是:

    • clsid:要使用的提供程序的 ClassID
    • pPropSet:属性集数组
    • nPropertySets:属性集数组计数
    • pName:服务器名称或数据库名称
    • pUserName:登录用户 ID
    • pPassword:登录密码
    • nInitMode:Microsoft Access 等文件数据库的共享模式
    • szProgID:要使用的提供程序的 ProgID
    • enumerator:用于指定提供程序的 CEnumerator
  • HRESULT Open(const CLSID& clsid, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1);
    // Open Microsoft SQL Server database named "Northwind" installed on a 
    // server name "server_name" using the currently Windows logged-in user 
    // credentials
    CDBPropSet rgPropertySet[1] = {DBPROPSET_DBINIT};
    
    rgPropertySet[0].AddProperty(DBPROP_INIT_DATASOURCE, L"server_name");
    rgPropertySet[0].AddProperty(DBPROP_INIT_CATALOG, L"Northwind");
    rgPropertySet[0].AddProperty(DBPROP_AUTH_INTEGRATED, L"SSPI");
    
    CLSID clsid = {0xc7ff16cL,0x38e3,0x11d0,
                  {0x97,0xab,0x0,0xc0,0x4f,0xc2,0xad,0x98}};
    hr = m_ds.Open(clsid, rgPropertySet, 1);
  • HRESULT Open(const CLSID& clsid, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0);
    // Open a Microsoft Access database with
    // path C:\Program Files\Microsoft
    //      Office\Office10\Samples\Northwind.mdb exclusively
    
    CLSID clsid = {0xdee35070L,0x506b,0x11cf, 
                  {0xb1,0xaa,0x0,0xaa,0x0,0xb8,0xde,0x95}};
    hr = m_ds.Open(clsid, _T("C:\\Program Files\\Microsoft Office" 
         "\\Office10\\Samples\\Northwind.mdb"), 
         NULL, NULL, DB_MODE_SHARE_EXCLUSIVE);
  • HRESULT Open(LPCSTR szProgID, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1);
    // Open an Oracle database using Microsoft OLE DB Provider for Oracle
    // Open OraDB1 database with user name SYS and password 
    // change_on_install
    // Note that it is using the ANSI string for the provider
    CDBPropSet rgPropertySet[1] = {DBPROPSET_DBINIT};
    
    rgPropertySet[0].AddProperty(DBPROP_INIT_DATASOURCE, L"OraDB1");
    rgPropertySet[0].AddProperty(DBPROP_AUTH_USERID, L"SYS");
    rgPropertySet[0].AddProperty(DBPROP_AUTH_PASSWORD, L"change_on_install");
    
    hr = m_ds.Open("MSDAORA", rgPropertySet, 1);
  • HRESULT Open(LPCWSTR szProgID, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0);
    // Open an Oracle database using Oracle Provider for OLE DB
    // Open OraDB1 database with user name SYS and password 
    // change_on_install
    // Note that it is using the UNICODE string for the provider ProgID
    
    hr = m_ds.Open(L"OraOLEDB.Oracle", _T("OraDB1"), 
                   _T("SYS"), _T("change_on_install"));
  • HRESULT Open(LPCWSTR szProgID, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1);
    // Open an MySQL database using a location "localhost", a database 
    // name of "mysql", a user id of "root"
    
    CDBPropSet rgPropertySet[1] = {DBPROPSET_DBINIT};
    
    rgPropertySet[0].AddProperty(DBPROP_INIT_LOCATION, L"localhost");
    rgPropertySet[0].AddProperty(DBPROP_INIT_DATASOURCE, L"mysql");
    rgPropertySet[0].AddProperty(DBPROP_AUTH_USERID, L"root");
    
    hr = m_ds.Open("MySQLProv", rgPropertySet, 1);
  • HRESULT Open(LPCSTR szProgID, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0);
    // Open a DB2 database named TOOLSDB with dbadmin user id and password
    hr = m_ds.Open("IBMDADB2.1", "TOOLSDB", "dbadmin", "dbadmin");
  • HRESULT Open(const CEnumerator& enumerator, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1);
    // Open Northwind SQL Server database in "server_name" using SSPI 
    // Integrated security
    CEnumerator oProviders;
    
    CDBPropSet rgPropertySet[1] = {DBPROPSET_DBINIT};
    
    rgPropertySet[0].AddProperty(DBPROP_INIT_DATASOURCE, L"server_name");
    rgPropertySet[0].AddProperty(DBPROP_INIT_CATALOG, L"Northwind");
    rgPropertySet[0].AddProperty(DBPROP_AUTH_INTEGRATED, L"SSPI");
    
    hr = oProviders.Open( );
    if(SUCCEEDED(hr))
    {
        // The following macro is to initialize the conversion routines
        USES_CONVERSION;
    
        while((hr = oProviders.MoveNext( )) == S_OK)
        {
    #ifdef _UNICODE
            if(lstrcmpi(oProviders.m_szName, L"SQLOLEDB") == 0)
    #else
            if(lstcmpi(W2A(oProviders.m_szName), "SQLOLEDB") == 0)
    #endif
            {
                hr = m_ds.Open(oProviders, rgPropertySet, 1);
                break;
            }            
        }
        oProviders.Close( );
    }
  • HRESULT Open(const CEnumerator& enumerator, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0);
    // Open Northwind Access database exclusively
    CEnumerator oProviders;
    
    hr = oProviders.Open( );
    if(SUCCEEDED(hr))
    {
        // The following macro is to initialize
        // the conversion routines
        USES_CONVERSION;
    
        while((hr = oProviders.MoveNext( )) == S_OK)
        {
    #ifdef _UNICODE
            if(lstrcmpi(oProviders.m_szName, 
               L" Microsoft.Jet.OLEDB.4.0") == 0)
    #else
            if(lstcmpi(W2A(oProviders.m_szName), 
                    " Microsoft.Jet.OLEDB.4.0") == 0)
    #endif
            {
                hr = m_ds.Open(oProviders, T("C:\\Program Files\\" 
                     "Microsoft Office\\Office10\\Samples\\Northwind.mdb"), 
                     NULL, NULL, DB_MODE_SHARE_EXCLUSIVE);
                break;
            }            
        }
        oProviders.Close( );
    }

我特意逐一说明了每个重载,以演示连接到不同类型的数据库。

OpenFromInitializationString 方法

HRESULT OpenFromInitializationString(LPCOLESTR szInitializationString, 
                                             bool fPromptForInfo = false);

第一个参数是连接字符串,它指定执行连接的所有属性,包括提供程序 ProgID。该字符串应以宽格式提供。可以使用 A2WW2COLE 函数来获取 ANSI 字符串的宽格式。第二个参数指示是否提示用户提供缺失的信息。默认情况下不提示,连接尝试将失败。连接字符串必须遵循特定的格式,其中键值对用分号分隔。键是属性名,值是其值。下表显示了属性名及其对应的属性 ID:

属性 ID 属性关键字
提供程序 ProgID Provider
DBPROP_INIT_DATASOURCE Data Source
DBPROP_INIT_LOCATION Location
DBPROP_INIT_CATALOG Initial Catalog
DBPROP_AUTH_USERID User Id
DBPROP_AUTH_PASSWORD Password
DBPROP_AUTH_INTEGRATED Integrated Security
DBPROP_INIT_HWND Window Handle
DBPROP_INIT_PROMPT Prompt
DBPROP_INIT_TIMEOUT Connect Timeout

以下显示了正确的连接字符串:

// Open an Oracle database using Microsoft Provider
Provider=MSDAORA;Data Source=OraDB1;User Id=SYS;Password=change_on_install

// Open an MySQL database
Provider=MySQLProv;Location=localhost;Data Source=mysql;User Id=root

// Open a Microsoft Access database
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Program Files\
             Microsoft Office\Office 10\Samples\Northwind.mdb

到目前为止,我们已经了解了如何打开数据库。要关闭它,只需调用 CDataSourceClose 方法。打开数据库是一项耗时的任务,因此,除非您特别需要在查询后关闭连接,否则您应该在程序加载时打开数据库,并在主窗口或应用程序类的 CDataSourceCSession 成员中创建它们,并在整个应用程序中使用此相同连接来查询或编辑数据库。

CSession 类

现在,当我们打开了连接,我们就可以创建一个会话以便与数据库进行交互了。CSession 类处理会话的打开和事务的创建。要打开会话,请使用 CSession::Open 方法。

HRESULT Open(const CDataSource& ds);

它接受一个参数,该参数是对已打开数据源的引用。例如:

CDataSource ds;
CSession session;
HRESULT hr = ds.OpenFromInitializationString(L"Provider=MySQLProv;" 
             "Location=localhost;Data Source=mysql;User Id=root");

if(SUCCEEDED(hr))
{
    hr = session.Open(ds);
    if(SUCCEEDED(hr))
    {
        // The session is now open
        // use it to query or edit the database

        session.Close();
    }
    ds.Close();
}

如示例所示,应调用 Close 方法来关闭会话。

会话最重要的特点是能够创建事务。事务是指在数据库上执行的一个或多个命令集,必须要么提交所有命令,要么丢弃所有命令。例如,如果我们想更改 Orders 表,然后在历史记录日志表中添加一个条目,那么我们需要确保该序列已完全执行,而不是因为过程中的中断而仅更新 Orders 表而未更新历史记录日志。因此,我们将该序列包装在事务中,在所有命令执行之前,任何更改都不会持久化到数据库。因此,有三种方法可以在会话对象中处理事务。StartTransaction 开始事务过程。然后,用户必须调用 Commit 方法将更改实际持久化到数据库,或调用 Abort 来丢弃自调用 StartTransaction 以来所做的任何更改。如果未调用 CommitAbort,然后又在同一会话上启动了另一个事务,那么新事务将被视为嵌套事务,稍后调用 Commit 将会提交两个事务。以下显示了该过程的草稿:

hr = m_session.Open(m_ds);
hr = m_session.StartTransaction( );
…. Do update 1
…. Do update 2
…. Do update n
m_session.Commit( );

错误处理

正如我们到目前为止所看到的,所有方法都返回 HRESULT。我们可以使用 SUCCEEDED(hr) 宏来检查此返回值是否为成功指示器。如果我们想检查失败,则使用 FAILED(hr) 宏。

hr = m_ds.Open( );
if(SUCCEEDED(hr))
{
    // The connection was opened
}
else
{
    // The connection failed
}

Visual Studio 中的一个实用小功能是能够在 Watch 窗口中输入变量名以在调试时显示其值。如果我们输入 hr,我们可以看到它的值。此外,如果我们输入 hr,hr,我们将获得错误代码的常量名称。下图显示 hr 的值为 E_FAIL 而不是纯数字。

这在许多情况下对调试打开连接或执行命令的失败很有用,但如果我们还需要有关已发生错误的更详细描述呢?我将讨论三种获取错误描述的方法。

方法 1:IErrorInfo 接口

OLE DB 应用程序可以通过调用 SetErrorInfo API 方法来指示发生了错误。消费者应用程序可以通过 GetErrorInfo 检查此错误。GetErrorInfo 返回指向 IErrorInfo 接口的指针。通过调用 IErrorInfo::GetDescription 方法,用户可以获得错误描述。以下代码片段展示了如何做到这一点:

IErrorInfo* pErrInfo;
HRESULT hr = ::GetErrorInfo(0, &pErrInfo);
if(SUCCEEDED(hr))
{
    BSTR bstrErrDescription;
    pErrInfo->GetDescription(&bstrErrDescription);

    CString strDescription = bstrErrDescription;
    AfxMessageBox(LPCTSTR(strDescription);

    pErrInfo->Release();
    ::SysFreeString(bstrErrDescription);
}

GetDescription 填充一个 BSTR 变量。将此类型的字符串转换为 ANSI 或 Unicode 版本有多种方法。一种方法是直接将其分配给 CString。另一种方法是使用 OLE2T 方法。因此,我将其留给用户。完成后,我们需要做一些清理工作,通过调用接口指针上的 ReleaseSysFreeString 来释放 GetDescription 返回的 BSTR

方法 2:CDBErrorInfo 类

USES_CONVERSION;
CDBErrorInfo einfo;
BSTR bstrDescription;
ULONG nRecords = 0;
HRESULT hr = einfo.GetErrorRecords(&nRecords);
if(SUCCEEDED(hr) && nRecords > 0)
{
LCID lcid = GetSystemDefaultLCID();
    for(ULONG nIndex = 0; nIndex < nRecords; nIndex++)
    {
        hr = einfo.GetAllErrorInfo(nIndex, lcid, &bstrDescription);
        if(SUCCEEDED(hr))
        {
            AfxMessageBox(OLE2T(bstrDescription));
            SysFreeString(bstrDescription);
        }
    }
}

如代码所示,调用 GetErrorRecords 来用错误数组(命名为 records)填充 CDBErrorInfo。我们循环遍历记录并对每条记录调用 GetAllErrorInfo 来获取描述。这是另一种做法,并且在 AtlTraceErrorRecords 函数中实现了类似的方法,该函数在方法 3 中进行了描述。

方法 3:AtlTraceErrorRecords

如果您想在查询返回时在调试器窗口中看到结果错误,您可以调用 AtlTraceErrorRecords。它使用方法 2 中描述的算法来跟踪结果错误。您可以直接调用此方法,而无需编写代码来获取该信息。

即时创建数据库

您可能遇到过需要在用户首次使用您的应用程序登录时为用户创建数据库的情况,或者可能需要在失败时通过以编程方式重新创建数据库来恢复数据库。幸运的是,对于许多数据库类型,有一种方法可以做到这一点。像 Oracle 这样的某些数据库,至少通过 OLE DB 提供程序,不会仅仅允许您通过发出 CREATE DATABASE 方法来创建数据库,因为它需要许多步骤才能完成,但其他数据库可能允许您仅通过执行几行代码来创建数据库。我将描述一些数据库的方法,并为未能涵盖所有数据库表示歉意。

以编程方式创建 Microsoft Access 数据库

我建议两种以编程方式创建 Microsoft Access 数据库的方法:

  1. 这可能看起来不寻常,但它确实有效。创建一个空白的 Microsoft Access 数据库,然后根据需要复制并重命名它来创建一个新的空白数据库。以下代码片段显示了如何做到这一点。假设空白数据库副本名为 db.mdb
    BOOL CreateAccessDatabase(LPCTSTR szTemplatePath, 
                               LPCTSTR szNewDatabasePath)
    {
        return CopyFile(szTemplatePath, szNewDatabasePath, TRUE);
    }

    szTemplatePath 是主副本数据库的路径和名称,szNewDatabasePath 是创建的数据库的新路径和名称。

  2. 第二种方法是使用 IDBDataSourceAdmin 接口的 CreateDataSource 方法。该接口在 Microsoft.Jet.OLEDB.4.0 提供程序中实现,可用于显式创建或销毁数据源。以下函数显示了如何使用此接口在 szDatabasePath 参数指定的路径创建一个空白的 .mdb 文件。此方法在知识库文章中有解释,但我在此尝试尽量避免使用接口指针,而是使用 ATL 类。
    BOOL CreateAccessDatabase(LPCTSTR szDatabasePath)
    {
        CDataSource ds;
        IDBDataSourceAdmin* pIDBDataSourceAdmin = NULL;
    
        CLSID clsid = {0xdee35070L,0x506b,0x11cf, 
                      {0xb1,0xaa,0x0,0xaa,0x0,0xb8,0xde,0x95}};
        HRESULT hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, 
                     __uuidof(IDBInitialize), (void**)&ds.m_spInit);
        if (FAILED(hr))
            return FALSE;
    
        USES_CONVERSION;
    
        CDBPropSet rgPropertySet(DBPROPSET_DBINIT);
        rgPropertySet.AddProperty(DBPROP_INIT_DATASOURCE, 
                                  T2BSTR(szDatabasePath));
    
        hr = ds.m_spInit->QueryInterface(IID_IDBDataSourceAdmin, 
            (void**)&pIDBDataSourceAdmin);
        if(FAILED(hr))
        {
            ds.Close();
            return FALSE;
        }
    
        hr = pIDBDataSourceAdmin->CreateDataSource(1, 
             &rgPropertySet, NULL, IID_NULL, NULL);
        if(FAILED(hr))
        {
            pIDBDataSourceAdmin->Release();
            ds.Close();
            return FALSE;
        }
    
        pIDBDataSourceAdmin->Release();
    
        ds.Close();
    
        return TRUE;
    }

以编程方式创建 MySQL 数据库

我将尝试通过在 MySQL 安装时自动设置的默认数据库“mysql”的上下文中发出“CREATE DATABASE”命令来创建 MySQL 数据库。我将使用 CCommand 类来执行此操作,但将推迟讨论该类。以下方法在 szLocation 参数指定的目录以及 szDatabaseName 参数传递的名称处创建一个 MySQL 数据库。此外,我们还需要用户 ID 和密码才能首先连接到“mysql”数据库。

BOOL CreateMySQLDatabase(LPCTSTR szLocation, 
       LPCTSTR szDatabaseName, LPCTSTR szUserId, LPCTSTR szPassword)
{
    USES_CONVERSION;
    
    CDBPropSet rgPropertySet[1] = {DBPROPSET_DBINIT};
    rgPropertySet[0].AddProperty(DBPROP_INIT_LOCATION, T2BSTR(szLocation));
    rgPropertySet[0].AddProperty(DBPROP_INIT_DATASOURCE, T2BSTR(_T("mysql")));
    rgPropertySet[0].AddProperty(DBPROP_AUTH_USERID, T2BSTR(szUserId));
    rgPropertySet[0].AddProperty(DBPROP_AUTH_PASSWORD, T2BSTR(szPassword));

    CDataSource ds;
    HRESULT hr = ds.Open(_T("MySQLProv"), rgPropertySet, 1);
    if(FAILED(hr))
        return FALSE;

    CSession session;
    hr = session.Open(ds);
    if(FAILED(hr))
    {
        ds.Close();
        return FALSE;
    }

    CCommand<CNoAccessor, CNoRowset> cmd;
    CString strCommand;
    strCommand.Format(_T("CREATE DATABASE '%s';"), szDatabaseName);
    hr = cmd.Open(session, LPCTSTR(strCommand), 
                  NULL, NULL, DBGUID_DBSQL, false);
    if(FAILED(hr))
    {
        session.Close();
        ds.Close();
        return FALSE;
    }

    session.Close();
    ds.Close();

    return TRUE;
}

以编程方式创建 Microsoft SQL Server 数据库

与创建 MySQL 数据库类似,我们可以打开“master”数据库并通过它发出“CREATE DATABASE”命令。区别在于我们将服务器位置指定为数据源,将数据库名称指定为目录。以下方法正是这样做的:

BOOL CreateSQLServerDatabase(LPCTSTR szServerName, 
            LPCTSTR szDatabaseName, LPCTSTR szUserId, LPCTSTR szPassword)
{
    USES_CONVERSION;
    
    CDBPropSet rgPropertySet[1] = {DBPROPSET_DBINIT};
    rgPropertySet[0].AddProperty(DBPROP_INIT_CATALOG, T2BSTR(_T("master")));
    rgPropertySet[0].AddProperty(DBPROP_INIT_DATASOURCE, T2BSTR(szServerName));
    rgPropertySet[0].AddProperty(DBPROP_AUTH_USERID, T2BSTR(szUserId));
    rgPropertySet[0].AddProperty(DBPROP_AUTH_PASSWORD, T2BSTR(szPassword));

    CDataSource ds;
    HRESULT hr = ds.Open(_T("SQLOLEDB"), rgPropertySet, 1);
    if(FAILED(hr))
        return FALSE;

    CSession session;
    hr = session.Open(ds);
    if(FAILED(hr))
    {
        ds.Close();
        return FALSE;
    }

    CCommand<CNoAccessor, CNoRowset> cmd;
    CString strCommand;
    strCommand.Format(_T("CREATE DATABASE '%s';"), szDatabaseName);
    hr = cmd.Open(session, LPCTSTR(strCommand), NULL, NULL, DBGUID_DBSQL, false);
    if(FAILED(hr))
    {
        session.Close();
        ds.Close();
        return FALSE;
    }

    session.Close();
    ds.Close();

    return TRUE;
}

我们省略了一些问题,例如对于 MySQL 或 Microsoft SQL Server 数据库需要 SSPI 身份验证的集成安全性属性。只需添加一个布尔属性 DBPROP_INIT_INTEGRATED 即可打开 mysqlmaster 数据库。

摘要

现在我们可以打开到许多主要数据库类型的连接并启动会话来完成工作。我们还看到了以编程方式创建数据库的示例。

© . All rights reserved.