ATL模板库中的OLEDB与ADO

上次将OLEDB的所有内容基本上都说完了,从之前的示例上来看OLEDB中有许多变量的定义,什么结果集对象、session对象、命令对象,还有各种缓冲等等,总体上来说直接使用OLEDB写程序很麻烦,用很大的代码量带来的仅仅只是简单的功能。还要考虑各种缓冲的释放,各种对象的关闭,程序员的大量精力都浪费在无用的事情上,针对这些情况微软在OLEDB上提供了两种封装方式,一种是将其封装在ATL模板库中,一种是使用ActiveX控件来进行封装称之为ADO,这次主要写的是这两种方式

ATL 模板中的OLEDB

由于ATL模板是开源的,这种方式封装简洁,调试简易(毕竟源代码都给你了),各个模块相对独立,但是它的缺点很明显就是使用门槛相对较高,只有对C++中的模板十分熟悉的开发人员才能使用的得心应手。
ATL中的OLEDB主要有两大模块,提供者模块和消费者模块,顾名思义,提供者模块是数据库的开发人员使用的,它主要使用这个模块实现OLEDB中的接口,对外提供相应的数据库服务;消费者模块就是使用OLEDB在程序中操作数据库。这里主要说的是消费者模块

ATL主要封装的类

ATL针对OLEDB封装的主要有这么几个重要的类:

数据库对象

  • CDataConnection 数据源连接类主要实现的是数据库的连接相关的功能,根据这个可以猜测出来它实际上封装的是OLEDB中的数据源对象和会话对象
  • CDataSource:数据源对象
  • CEnumerator: 架构结果集对象,主要用来查询数据库的相关信息,比如数据库中的表结构等信息
  • CSession: 会话对象

访问器对象:

  • CAccessor: 常规的访问器对象
  • CAccessorBase: 访问器对象的基类
  • CDynamicAccessor:动态绑定的访问器
  • CDynamicParamterAccessor:参数绑定的访问器,从之前博文的内容来看它应该是进行参数化查询等操作时使用的对象
  • CDynamicStringAccessor:这个一般是要将查询结果显示为字符串时使用,它负责将数据库中的数据转化为字符串

ALT中针对OLEDB的封装在头文件atldbcli.h中,在项目中只要包含它就行了

模板的使用

静态绑定

针对静态绑定,VS提供了很好的向导程序帮助我们生成对应的类,方便了开发,使用的基本步骤如下:

  1. 在项目上右键,选择添加类
  2. 在类选择框中点击ATL并选择其中的ATL OLEDB使用者
    选择OLEDB类
  3. 选择对应的数据源、数据库表和需要对数据库进行的操作
    选择数据源
    注意如果要对数据库表进行增删改查等操作,一定要选这里的表选项
  4. 点击数据源配置数据源连接的相关属性,最后点击完成。

最终会在项目中生成对应的头文件
这是最终生成的完整代码

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class Caa26Accessor
{
public:
//value
LONG m_aac031;
TCHAR m_aaa146[51];
LONG m_aaa147;
LONG m_aaa148;
//status
DBSTATUS m_dwaac031Status;
DBSTATUS m_dwaaa146Status;
DBSTATUS m_dwaaa147Status;
DBSTATUS m_dwaaa148Status;
//lenth
DBLENGTH m_dwaac031Length;
DBLENGTH m_dwaaa146Length;
DBLENGTH m_dwaaa147Length;
DBLENGTH m_dwaaa148Length;


void GetRowsetProperties(CDBPropSet* pPropSet)
{
pPropSet->AddProperty(DBPROP_CANFETCHBACKWARDS, true, DBPROPOPTIONS_OPTIONAL);
pPropSet->AddProperty(DBPROP_CANSCROLLBACKWARDS, true, DBPROPOPTIONS_OPTIONAL);
pPropSet->AddProperty(DBPROP_IRowsetChange, true, DBPROPOPTIONS_OPTIONAL);
pPropSet->AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE);
}

HRESULT OpenDataSource()
{
CDataSource _db;
HRESULT hr;
hr = _db.OpenFromInitializationString(L"Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa; Password=XXXXXX;Initial Catalog=study;Data Source=XXXX;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Workstation ID=LIU-PC;Use Encryption for Data=False;Tag with column collation when possible=False");
if (FAILED(hr))
{
#ifdef _DEBUG
AtlTraceErrorRecords(hr);
#endif
return hr;
}
return m_session.Open(_db);
}

void CloseDataSource()
{
m_session.Close();
}

operator const CSession&()
{
return m_session;
}

CSession m_session;

DEFINE_COMMAND_EX(Caa26Accessor, L" \
SELECT \
aac031, \
aaa146, \
aaa147, \
aaa148 \
FROM dbo.aa26")

BEGIN_COLUMN_MAP(Caa26Accessor)
COLUMN_ENTRY_LENGTH_STATUS(1, m_aac031, m_dwaac031Length, m_dwaac031Status)
COLUMN_ENTRY_LENGTH_STATUS(2, m_aaa146, m_dwaaa146Length, m_dwaaa146Status)
COLUMN_ENTRY_LENGTH_STATUS(3, m_aaa147, m_dwaaa147Length, m_dwaaa147Status)
COLUMN_ENTRY_LENGTH_STATUS(4, m_aaa148, m_dwaaa148Length, m_dwaaa148Status)
END_COLUMN_MAP()
};

class Caa26 : public CCommand<CAccessor<Caa26Accessor> >
{
public:
HRESULT OpenAll()
{
HRESULT hr;
hr = OpenDataSource();
if (FAILED(hr))
return hr;
__if_exists(GetRowsetProperties)
{
CDBPropSet propset(DBPROPSET_ROWSET);
__if_exists(HasBookmark)
{
if( HasBookmark() )
propset.AddProperty(DBPROP_IRowsetLocate, true);
}
GetRowsetProperties(&propset);
return OpenRowset(&propset);
}
__if_not_exists(GetRowsetProperties)
{
__if_exists(HasBookmark)
{
if( HasBookmark() )
{
CDBPropSet propset(DBPROPSET_ROWSET);
propset.AddProperty(DBPROP_IRowsetLocate, true);
return OpenRowset(&propset);
}
}
}
return OpenRowset();
}

HRESULT OpenRowset(DBPROPSET *pPropSet = NULL)
{
HRESULT hr = Open(m_session, NULL, pPropSet);
#ifdef _DEBUG
if(FAILED(hr))
AtlTraceErrorRecords(hr);
#endif
return hr;
}

void CloseAll()
{
Close();
ReleaseCommand();
CloseDataSource();
}
};

从名字上来看Caa26Accessor主要是作为一个访问器,其实它的功能也是与访问器相关的,比如创建访问器和数据绑定都在最后这个映射中。而后面的Caa26类主要是用来执行sql语句并根据上面的访问器类来解析数据,其实我们使用上主要使用后面这个类,这些代码都很简单,有之前的OLEDB基础很容易就能理解它们,这里就不再在这块进行展开了

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
int _tmain(int argc, TCHAR *argv)
{
CoInitialize(NULL);
Caa26 aa26;
HRESULT hRes = aa26.OpenDataSource();
if (FAILED(hRes))
{
aa26.CloseAll();
CoUninitialize();
return -1;
}

hRes = aa26.OpenRowset();
if (FAILED(hRes))
{
aa26.CloseAll();
CoUninitialize();
return -1;
}

hRes = aa26.MoveNext();
COM_USEPRINTF();
do
{
COM_PRINTF(_T("|%-30u|%-30s|%-30u|%-30u|\n"), aa26.m_aac031, aa26.m_aaa146, aa26.m_aaa147, aa26.m_aaa148);
hRes = aa26.MoveNext();
} while (S_OK == hRes);

aa26.CloseAll();
CoUninitialize();
return 0;
}

动态绑定

动态绑定主要是使用模板对相关的类进行拼装,所以这里需要关于模板的相关知识,只有掌握了这些才能正确的拼接出合适的类。
一般需要拼接的是这样几个类

  • 结果集类,在结果集类的模板中填入对应的访问器类,表示该结果集将使用对应的访问器进行解析。访问器类可以系统预定义的,也向静态绑定那样自定义。
  • Command类,在命令对象类的模板位置填入与命令相关的类,也就是执行命令生成的结果集、以及解析结果集所用的访问器,之后就主要使用Command类来进行数据库的相关操作了
    下面是一个使用的示例
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
typedef CCommand<CDynamicAccessor, CRowset, CMultipleResults> CComRowset;
typedef CTable<CDynamicAccessor, CRowset> CComTable;
//将所有绑定的数据类型转化为字符串
typedef CTable<CDynamicStringAccessor, CRowset> CComTableString;

int _tmain(int argc, TCHAR *argv[])
{
CoInitialize(NULL);
COM_USEPRINTF();
//连接数据库,创建session对象
CDataSource db;
db.Open();
CSession session;
session.Open(db);

//打开数据库表
CComTableString table;
table.Open(session, OLESTR("T_DecimalDemo"));
HRESULT hRes = table.MoveFirst();
if (FAILED(hRes))
{
COM_PRINTF(_T("表中没有数据,退出程序\n"));
goto __CLEAN_UP;
}
do
{
//这里传入的参数是列的序号,注意一下,由于有第0列的行需要存在,所以真正的数据是从第一列开始的
COM_PRINTF(_T("|%-10s|%-20s|%-20s|%-20s|\n"), table.GetString(1), table.GetString(2), table.GetString(3), table.GetString(4));
hRes = table.MoveNext();
} while (S_OK == hRes);

__CLEAN_UP:
CoUninitialize();
return 0;
}

在上面的代码中我们定义了两个模板类,Ctable和CCommand类,没有发现需要的访问器类,查看CTable类可以发现它是继承于CAccessorRowset,而CAccessorRowset继承于TAccessor和 TRowset,也就是说它提供了访问器的相关功能
而且它还可以使用OpenRowset方法不执行SQL直接打开数据表,因此在这里我们选择使用它
在CTable的模板中填入CDynamicStringAccessor表示将会把得到的结果集中的数据转化为字符串。
在使用上先使用CDataSource类的Open方法打开数据库连接,然后调用CTable的Open打开数据表,接着调用CTable的MoveFirst的方法将行句柄移动到首行。
接着在循环中调用table的GetString方法得到各个字段的字符串值,并调用MoveNext方法移动到下一行
其实在代码中并没有使用CCommand类,这是由于这里只是简单的使用直接打开数据表的方式,而并没有执行SQL语句,因此不需要它,在这里定义它只是简单的展示一下

ADO

ATL针对OLEDB封装的确是方便了不少,但是对于像我这种将C++简单的作为带对象的C来看的人来说,它使用模板实在是太不友好了,说实话现在我现在对模板的认识实在太少,在代码中我也尽量避免使用模板。所以在我看来使用ATL还不如自己根据项目封装一套。
好在微软实在太为开发者着想了,又提供了ADO这种针对ActiveX的封装方式。
要使用ADO组件需要先导入,导入的语句如下:

1
#import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename("EOF", "EndOfFile")

这个路径一般是不会变化的,而EOF在C++中一般是用在文件中的,所以这里将它rename一下
ADO中的主要对象和接口有:

  • Connect :数据库的连接对象,类似于OLEDB中的数据源对象和session对象
  • Command:命令对象,用来执行sql语句,类似于OLEDB中的Command对象
  • Recordset: 记录集对象,执行SQL语句返回的结果,类似于OLEDB中的结果集对象
  • Record: 数据记录对象,一般都是从Recordset中取得,就好像OLEDB中从结果集对象通过访问器获取到具体的数据一样
  • Field:记录中的一个字段,可以简单的看做就是一个表字段的值,一般一个记录集中有多条记录,而一条记录中有个Field对象
  • Parameter:参数对象,一般用于参数化查询或者调用存储过程
  • Property:属性,与之前OLEDB中的属性对应
    -
    在ADO中大量使用智能指针,所谓的智能指针是它的生命周期结束后会自动析构它所指向的对象,同时也封装了一些常见指针操作,虽然它是这个对象但是它的使用上与普通的指针基本上相同。ADO中的智能指针对象一般是在类名后加上Ptr。比如Connect对象的智能指针对象是_ConnectPtr
    智能指针有利也有弊,有利的地方在于它能够自动管理内存,不需要程序员进行额外的释放操作,而且它在使用上就像普通的指针,相比于使用类的普通指针更为方便,不利的地方在于为了方便它的使用一般都经过了大量的重载,因此很多地方表面上看是一个普通的寻址操作,而实际上却是一个函数调用,这样就降低了性能。所以在特别强调性能的场合要避免使用智能指针。
    在使用上,一般经过这样几个步骤:
  1. 定义数据库连接的Connect对象
  2. 调用Connect对象的Open方法连接数据库,这里使用的连接字串的方式
  3. 创建Command对象并调用对象Execute方法执行SQL,并获取对应的记录集。这里执行SQL语句也可以使用Recordset对象的Open方法。
  4. 循环调用Recordse对象的MoveNext不断取出对应行的行记录

下面是一个使用的简单例子

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
#import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename("EOF", "EndOfFile")
int _tmain(int argc, TCHAR *argv[])
{
CoInitialize(NULL);
_ConnectionPtr conn;
_RecordsetPtr rowset;
_bstr_t bstrConn = _T("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;");
conn.CreateInstance(_T("ADODB.Connection"));

conn->Open(bstrConn, _T("sa"), _T("123456"), adModeUnknown);
if (conn->State)
{
COM_PRINTF(_T("连接到数据源成功\n"));
}else
{
COM_PRINTF(_T("连接到数据源失败\n"));
return 0;
}

rowset.CreateInstance(__uuidof(Recordset));
rowset->Open(_T("select * from aa26;"), conn.GetInterfacePtr(), adOpenStatic, adLockOptimistic, adCmdText);
while (!rowset->EndOfFile)
{
COM_PRINTF(_T("|%-30u|%-30s|%-30u|%-30u|\n"),
rowset->Fields->GetItem(_T("aac031"))->Value.intVal,
rowset->Fields->GetItem(_T("aaa146"))->Value.bstrVal,
rowset->Fields->GetItem(_T("aaa147"))->Value.llVal,
rowset->Fields->GetItem(_T("aaa148"))->Value.llVal
);
rowset->MoveNext();
}
CoUninitialize();
return 0;
}

ADO与OLEDB混合编程

ADO相比较OLEDB来说确实方便了不少,但是它也有它的问题,比如它是封装的ActiveX控件,从效率上肯定比不上OLEDB,而且ADO中记录集是一次性将结果中的所有数据加载到内存中,如果数据表比教大时这种方式很吃内存。而OLEDB是每次调用GetNextRow时加载一条记录到内存(其实根据之前的代码可以知道它加载的时机,加载的大小是可以控制的),它相对来说比教灵活。
其实上述问题使用二者的混合编程就可以很好的解决,在处理结果集时使用OLEDB,而在其他操作时使用ADO这样既保留了ADO的简洁性也使用了OLEDB灵活管理结果集内存的能力。
在ADO中,可以通过_Recordset查询出ADORecordsetConstruction接口,这个接口提供了将记录集转化为OLEDB中结果集,以及将结果集转化为Recordset对象的能力
下面是一个简单的例子

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
CoInitialize(NULL);
try
{
_bstr_t bsCnct(_T("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;"));
_RecordsetPtr Rowset(__uuidof(Recordset));

Rowset->Open(_T("select * from aa26;")
,bsCnct,adOpenStatic,adLockOptimistic,adCmdText);

//获取IRowset接口指针
ADORecordsetConstruction* padoRecordsetConstruct = NULL;
Rowset->QueryInterface(__uuidof(ADORecordsetConstruction),
(void **) &padoRecordsetConstruct);
IRowset * pIRowset = NULL;
padoRecordsetConstruct->get_Rowset((IUnknown **)&pIRowset);
padoRecordsetConstruct->Release();

DisplayRowSet(pIRowset);

pIRowset->Release();


GRS_PRINTF(_T("\n\n显示第二个结果集:\n"));

//使用OLEDB方法打开一个结果集
IOpenRowset* pIOpenRowset = NULL;
TCHAR* pszTableName = _T("T_State");
DBID TableID = {};

CreateDBSession(pIOpenRowset);

TableID.eKind = DBKIND_NAME;
TableID.uName.pwszName = (LPOLESTR)pszTableName;

HRESULT hr = pIOpenRowset->OpenRowset(NULL,&TableID,NULL,IID_IRowset,0,NULL,(IUnknown**)&pIRowset);
if(FAILED(hr))
{
_com_raise_error(hr);
}
//创建一个新的ADO记录集对象
_RecordsetPtr Rowset2(__uuidof(Recordset));
Rowset2->QueryInterface(__uuidof(ADORecordsetConstruction),
(void **) &padoRecordsetConstruct);
//将OLEDB的结果集放置到ADO记录集对象中
padoRecordsetConstruct->put_Rowset(pIRowset);
ULONG ulRow = 0;

while(!Rowset2->EndOfFile)
{
COM_PRINTF(_T("|%10u|%10s|%-40s|\n")
,++ulRow
,Rowset2->Fields->GetItem("K_StateCode")->Value.bstrVal
,Rowset2->Fields->GetItem("F_StateName")->Value.bstrVal);

Rowset2->MoveNext();
}
}
catch(_com_error & e)
{
COM_PRINTF(_T("发生错误:\n Source : %s \n Description : %s \n")
,(LPCTSTR)e.Source(),(LPCTSTR)e.Description());
}

_tsystem(_T("PAUSE"));
CoUninitialize();
return 0;

这次就不再放上整体例子的链接了,有之前的基础应该很容易看懂这些,而且这次代码比较短,基本上将所有代码全粘贴了过来。