OLEDB数据源

数据源在oledb中指数据提供者,这里可以简单的理解为数据库程序。数据源对象代表数据库的一个连接,是需要创建的第一个对象。而数据源对象主要用于配置数据库连接的相关属性如连接数据库的用户名密码等等

数据源主要完成的功能如下:

  1. 进行数据库身份认证
  2. 为每个连接准备对应的资源,如对应的数据缓冲,网络连接资源
  3. 设置连接属性,给访问者何种权限,设置连接的超时值等等,对象会根据对应的属性打开对应的接口。它的这些设置都是通过属性进行的

OLEDB属性与属性设置

OLEDB虽然是基于COM的一组接口,但是它与标准的COM接口有点不同,它的一大特色在于它自身的属性设置,有的接口虽然对象中存在但是调用QueryInterface是查询不出来的,只有设置相应的接口才会打开,有的接口可以根据属性值表现不同的行为。比如设置了对应的只读属性则不允许使用更新接口。
每个属性都有值、类型、说明和读写属性,对于行集对象,还有一个用于指示是否可以逐列应用它的指示器。
属性由一个GUID和一个整数ID进行唯一标识。
属性集是所有具有相同 组GUID 的一组属性。在逻辑上它们都用于同一种功能,比如有的属性集用于设置数据源连接属性,有的用于设置行集属性等等。它们是应用在同一个特定对象上的一组属性。在每个这样的属性组中都有属性每个属性属于一个或者多个属性组。
属性定义如下:

1
2
3
4
5
6
7
typedef struct tagDBPROP {
DBPROPID dwPropertyID; //属性GUID
DBPROPOPTIONS dwOptions; //属性的操作方式
DBPROPSTATUS dwStatus; //属性设置状态
DBID colid; //属性ID,一般给DB_NULLID
VARIANT vValue; //属性值
} DBPROP;

dwOptions:属性的操作方式有3种,但是一般只使用其中的两种:DBPROPOPTIONS_REQUIRED表示必须设置成功,如果设置失败,则设置属性的操作失败,DBPROPOPTIONS_OPTIONAL,表示可选,即即使该属性设置失败,设置属性的操作也返回成功。DBPROPOPTIONS_SETIFCHEAP表示如果在设置属性操作时在在dwStatus参数中返回该属性设置的状态,是否成功,失败的原因等等。
属性集的定义如下:

1
2
3
4
5
typedef struct tagDBPROPSET {
DBPROP * rgProperties; //属性数组的指针
ULONG cProperties; //属性数组中元素个数
GUID guidPropertySet; //属性集的GUID
} DBPROPSET;

目前属性组包括初始化属性组、数据源属性组、会话属性组、行集属性组、表属性组和列属性组等等。
设置属性一般包含如下几个步骤:

  1. 分配一个属性类型DBPRO的数组,一般倾向于多分配一个,最后一个数组元素全0,作为结尾
  2. 确定每个属性的属性GUID,即明确我们需要设置的是对象的哪个属性
  3. 填充对应的属性值,属性操作方式
  4. 填充对应的属性集DBPROPSET结构。设置该属性集的GUID
  5. 调用对应的接口设置属性

数据源对象接口

数据源对象的接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CoType TDataSource {
[mandatory] interface IDBCreateSession; //创建回话对象
[mandatory] interface IDBInitialize; //创建数据源连接对象
[mandatory] interface IDBProperties; ///创建数据源的属性操作对象
[mandatory] interface IPersist;
[optional] interface IConnectionPointContainer;
[optional] interface IDBAsynchStatus;
[optional] interface IDBDataSourceAdmin;
[optional] interface IDBInfo;
[optional] interface IObjectAccessControl;
[optional] interface IPersistFile;
[optional] interface ISecurityInfo;
[optional] interface ISupportErrorInfo;
[optional] interface ITrusteeAdmin;
[optional] interface ITrusteeGroupAdmin;
}

在上面代码中,mandatory表示是数据源必须提供的接口,optional表示的是可选性提供的接口,在创建对应的接口时尽量使用必须实现的接口,如果需要使用可选择的接口,一定要判断数据源是否支持。在数据源对象中最主要的还是前三个必须提供的接口

连接到数据库

连接到数据源一般使用IDBInitialize接口的Initialize方法,但是生成IDBInitialize接口有几种不同的方式,下面一一列举出来

直接创建IDBInitialize接口

这种方式一般调用CoCreateInstance函数创建,下面是具体的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <tchar.h>
#include <windows.h>
#include <strsafe.h>

#define COM_NO_WINDOWS_H //如果已经包含了Windows.h或不使用其他Windows库函数时
#define OLEDBVER 0x0260 //MSDAC2.6版
#include <oledb.h>
#include <oledberr.h>

#define GRS_ALLOC(sz) HeapAlloc(GetProcessHeap(),0,sz)
#define GRS_CALLOC(sz) HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sz)
#define GRS_SAFEFREE(p) if(NULL != p){HeapFree(GetProcessHeap(),0,p);p=NULL;}

#define GRS_USEPRINTF() TCHAR pBuf[1024] = {}
//定义输出宏
#define GRS_PRINTF(...) \
GRS_USEPRINTF();\
StringCchPrintf(pBuf,1024,__VA_ARGS__);\
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),pBuf,lstrlen(pBuf),NULL,NULL);

//安全释放,为了养成良好的编码习惯,特作此宏定义
#define GRS_SAFERELEASE(I)\
if(NULL != (I))\
{\
(I)->Release();\
(I)=NULL;\
}
//检测上一步的操作是否成功
#define GRS_COM_CHECK(hr,...)\
if(FAILED(hr))\
{\
GRS_PRINTF(__VA_ARGS__);\
goto CLEAR_UP;\
}

int _tmain(int argc, TCHAR* argv[])
{
CoInitialize(NULL);
//创建OLEDB init接口
IDBInitialize *pDBInit = NULL;
IDBProperties *pIDBProperties = NULL;
//设置链接属性
DBPROPSET dbPropset[1] = {0};
DBPROP dbProps[5] = {0};
CLSID clsid_MSDASQL = {0}; //sql server 的数据源对象

HRESULT hRes = CLSIDFromProgID(_T("SQLOLEDB"), &clsid_MSDASQL);
GRS_COM_CHECK(hRes, _T("获取SQLOLEDB的CLSID失败,错误码:0x%08x\n"), hRes);
hRes = CoCreateInstance(clsid_MSDASQL, NULL, CLSCTX_INPROC_SERVER, IID_IDBInitialize,(void**)&pDBInit);
GRS_COM_CHECK(hRes, _T("无法创建IDBInitialize接口,错误码:0x%08x\n"), hRes);

//指定数据库实例名,这里使用了别名local,指定本地默认实例
dbProps[0].dwPropertyID = DBPROP_INIT_DATASOURCE;
dbProps[0].dwOptions = DBPROPOPTIONS_REQUIRED;
dbProps[0].vValue.vt = VT_BSTR;
dbProps[0].vValue.bstrVal = SysAllocString(OLESTR("LIU-PC\\SQLEXPRESS"));
dbProps[0].colid = DB_NULLID;

//指定数据库库名
dbProps[1].dwPropertyID = DBPROP_INIT_CATALOG;
dbProps[1].dwOptions = DBPROPOPTIONS_REQUIRED;
dbProps[1].vValue.vt = VT_BSTR;
dbProps[1].vValue.bstrVal = SysAllocString(OLESTR("Study"));
dbProps[1].colid = DB_NULLID;

//指定链接数据库的用户名
dbProps[2].dwPropertyID = DBPROP_AUTH_USERID;
dbProps[2].vValue.vt = VT_BSTR;
dbProps[2].vValue.bstrVal = SysAllocString(OLESTR("sa"));

//指定链接数据库的用户密码
dbProps[3].dwPropertyID = DBPROP_AUTH_PASSWORD;
dbProps[3].vValue.vt = VT_BSTR;
dbProps[3].vValue.bstrVal = SysAllocString(OLESTR("123456"));


//设置属性
hRes = pDBInit->QueryInterface(IID_IDBProperties, (void**)&pIDBProperties);
GRS_COM_CHECK(hRes, _T("查询IDBProperties接口失败, 错误码:%08x\n"), hRes);
dbPropset->guidPropertySet = DBPROPSET_DBINIT;
dbPropset[0].cProperties = 4;
dbPropset[0].rgProperties = dbProps;
hRes = pIDBProperties->SetProperties(1, dbPropset);
GRS_COM_CHECK(hRes, _T("设置属性失败, 错误码:%08x\n"), hRes);

//链接数据库
hRes = pDBInit->Initialize();
GRS_COM_CHECK(hRes, _T("链接数据库失败:错误码:%08x\n"), hRes);
//do something
pDBInit->Uninitialize();

GRS_PRINTF(_T("数据库操作成功!!!!!\n"));
CLEAR_UP:
GRS_SAFEFREE(pDBInit);
GRS_SAFEFREE(pIDBProperties);
CoUninitialize();
return 0;
}

这是一份完整的可执行代码,后续的部分对于重复的代码将不再给出。
在上述代码中我们首先根据字符串SQLOLEDB查找到SQL Server对应的数据源对象,然后根据数据源对象查询出IDBProperties对象,接着分配一些空间来设置属性和属性集,调用IDBProperties接口的SetProperties函数来设置对应的数据源对象的接口。最后调用IDBInitialize接口的Initialize链接数据源,调用Uninitialize函数来断开连接。
一般数据源对象的属性集合的GUID为DBPROPSET_DBINIT,下面包含的属性最主要的有:

  1. DBPROP_INIT_DATASOURCE:数据连接实例(具体的DBMS实例名)
  2. DBPROP_INIT_CATALOG:目录名(在SQL Server中对应的是具体的数据库名称,对于ORACLE来说没有意义)
  3. DBPROP_AUTH_USERID: 用户名
  4. DBPROP_AUTH_PASSWORD: 密码

我们也注意到上面调用SysAllocString的BSTR类型的字符串并没有调用对应的函数进行释放,会不会发生内存泄露?其实不用担心OLEDB在断开连接的时候已经帮助我们释放了这部分空间。

使用IDBPromptInitialize接口来创建数据源对象

上述方法是依托于标准的COM,虽然也成功创建的数据源连接,但是无法在标准的com之上进行更多的初始化操作,导致了有些特定的高级功能无法使用,所以在实践中常用的还是利用IDBPromptInitialize和IDataInitialize的方式比较多。
IDBPromptInitialize创建时会弹出一个数据源选择的对话框,供用户选择相关配置信息(数据源/用户名/密码等)然后根据这些配置自动生成连接对象。
下面看一个弹出数据源对话框的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void ConnectSQLServerByDialog() //通过弹出对话框来链接SQL SERVER数据库
{
DECLARE_BUFFER();
DECLARE_OLEDB_INTERFACE(IDBPromptInitialize);
DECLARE_OLEDB_INTERFACE(IDBInitialize);

HWND hDesktop = GetDesktopWindow();
HRESULT hRes = CoCreateInstance(CLSID_DataLinks, NULL, CLSCTX_INPROC_SERVER, IID_IDBPromptInitialize, (void**)&pIDBPromptInitialize);
COM_CHECK_SUCCESS(hRes, _T("创建IDBPromptInitialize接口失败: %08x"), hRes);
//调用该函数弹出数据源对话框
hRes = pIDBPromptInitialize->PromptDataSource(NULL, hDesktop, DBPROMPTOPTIONS_PROPERTYSHEET, 0, NULL, NULL, IID_IDBInitialize, (IUnknown**)&pIDBInitialize);
COM_CHECK_SUCCESS(hRes, _T("弹出数据源对话框失败:%08x\n"), hRes);

hRes = pIDBInitialize->Initialize();
COM_CHECK_SUCCESS(hRes, _T("链接数据库失败:%08x\n"), hRes);
COM_PRINTF(_T("链接数据库成功\n"));

hRes = pIDBInitialize->Uninitialize();
__CLEAN_UP:
SAFE_RELEASE(pIDBPromptInitialize);
SAFE_RELEASE(pIDBInitialize);
}

除了这种方式,他还可以直接创建出IDBInitialize接口,利用之前设置属性的方式来连接到数据库,下面是一个演示的例子:

1
2
3
4
5
6
7
8
9
10
HRESULT hRes = CoCreateInstance(CLSID_MSDAINITIALIZE, NULL, CLSCTX_INPROC_SERVER, IID_IDataInitialize, (void**)&pIDataInitialize);
COM_CHECK_SUCCESS(hRes, _T("创建接口IDBInitialize失败:%08x\n"), hRes);
hRes = CLSIDFromProgID(_T("SQLOLEDB"), &clsid);
COM_CHECK_SUCCESS(hRes, _T("查询SQLOLEDB CLSID 失败:%08x\n"), hRes);
hRes = pIDataInitialize->CreateDBInstance(clsid, NULL,
CLSCTX_INPROC_SERVER, NULL, IID_IDBInitialize,
(IUnknown**)&pIDBInitialize);
COM_CHECK_SUCCESS(hRes, _T("创建IDBInitialize接口失败:%08x\n"), hRes);

//后续的代码就是我们之前写的那段定义属性,设置属性,连接数据库的代码

使用IDataInitialize接口来创建数据源对象

使用IDataInitialize接口可以直接使用连接字串连接到数据库,下面是使用连接字串的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ConnectSQLServerByConnstr() //通过连接字符串连接数据库
{
DECLARE_OLEDB_INTERFACE(IDataInitialize);
DECLARE_OLEDB_INTERFACE(IDBInitialize);
DECLARE_BUFFER();
HRESULT hRes = CoCreateInstance(CLSID_MSDAINITIALIZE, NULL, CLSCTX_INPROC_SERVER, IID_IDataInitialize, (void**)&pIDataInitialize);
COM_CHECK_SUCCESS(hRes, _T("创建IDataInitialize接口失败:%08x!\n"), hRes);

hRes = pIDataInitialize->GetDataSource(NULL, CLSCTX_INPROC_SERVER,
OLESTR("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;"),
IID_IDBInitialize, (IUnknown**)&pIDBInitialize);
COM_CHECK_SUCCESS(hRes, _T("获取IDBInitialize接口失败:%08x!\n"), hRes);
hRes = pIDBInitialize->Initialize();
COM_CHECK_SUCCESS(hRes, _T("连接数据库失败:%08x!\n"), hRes);
COM_PRINTF(_T("连接数据库成功\n"));
pIDBInitialize->Uninitialize();
__CLEAN_UP:
SAFE_RELEASE(pIDataInitialize);
SAFE_RELEASE(pIDBInitialize);
}

#获取连接字串
其实除了上面这种直接创建IDataInitialize接口的方法外,还可以使用IDBPromptInitialize接口Query出一个IDataInitialize接口,然后再设置连接字串连接到数据库。
其实在OLEDB中,可以认为连接字串最终被翻译为对应的属性,也就是说OLEDDB保存着对应连接的属性,我们可以通过不同的方式来获取不同类型的属性,比如使用IDBProperties接口来获取对应的链接属性,或者使用IDataInitialize的GetInitializationString函数来获取连接的链接字串。
既然它保存着每个连接的对应属性,那么是不是可以将用户在数据源对话框上的操作最终保存为数据连接字串呢,答案是肯定的。实现的思路如下:

  1. 调用IDBPromptInitialize接口的PromptDataSourc方法弹出数据源对话框,让用户操作
  2. 根据IDBPromptInitialize接口Query出IDataInitialize接口
  3. 调用IDataInitialize接口的GetInitializationString来获取连接字串
    下面是具体实现的代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    void GetConnectString()
    {
    DECLARE_OLEDB_INTERFACE(IDBPromptInitialize);
    DECLARE_OLEDB_INTERFACE(IDataInitialize);
    DECLARE_OLEDB_INTERFACE(IDBInitialize);
    DECLARE_BUFFER();

    LPOLESTR pConnStr = NULL;
    HWND hDeskTop = GetDesktopWindow();
    HRESULT hRes = CoCreateInstance(CLSID_DataLinks, NULL, CLSCTX_INPROC_SERVER, IID_IDBPromptInitialize, (void**)&pIDBPromptInitialize);
    COM_CHECK_SUCCESS(hRes, _T("创建IDBPromptInitialize接口失败:%08x!\n"), hRes);
    hRes = pIDBPromptInitialize->PromptDataSource(NULL, hDeskTop, DBPROMPTOPTIONS_PROPERTYSHEET, 0, NULL, NULL, IID_IDBInitialize, (IUnknown**)&pIDBInitialize);
    COM_CHECK_SUCCESS(hRes, _T("弹出数据源对话框失败:%08x\n"), hRes);


    hRes= pIDBPromptInitialize->QueryInterface(IID_IDataInitialize, (void**)&pIDataInitialize);
    COM_CHECK_SUCCESS(hRes, _T("创建IDataInitialize接口失败:%08x!\n"), hRes);

    hRes = pIDataInitialize->GetInitializationString(pIDBInitialize, TRUE, &pConnStr);
    COM_CHECK_SUCCESS(hRes, _T("获取连接字串失败失败:%08x\n"), hRes);

    COM_PRINTF(_T("连接字符串:%s"), pConnStr);
    SysAllocString(pConnStr);
    __CLEAN_UP:
    SAFE_RELEASE(pIDataInitialize);
    SAFE_RELEASE(pIDBInitialize);
    SAFE_RELEASE(pIDBPromptInitialize);
    }

为了节约篇幅,这些笔记内容只会列举部分关键的代码,至完整的代码我会随着博客内容的进度慢慢上传到GitHub项目中,并在博文的最末尾给出对应文件的地址
本次代码地址1
本次代码地址2