ADO
目
录
3.10.1 CADORecordBinding派生类 18
第1章
基础
1.1 引入ADO库文件
VC++使用ADO,首先需要导入ADO的类型库。可以在StdAfx.h里增加如下代码:
#import "c:/program files/common files/system/ado/msado15.dll" no_namespace rename("EOF","adoEOF")
no_namespace表示不需要命名空间。删除这个词,则ADO的接口函数将被放在ADODB命名空间内。也可以用rename_namespace("ADO")将命名空间更改为ADO。
rename("EOF","adoEOF")表示将EOF更改为adoEOF。因为EOF在某些文件里被定义为(-1),如此一来,VARIANT_BOOL EOF;就变成了VARIANT_BOOL (-1);把它当做类成员变量编译时就会出错。
VC++编译器中的预处理器针对这条指令做了哪些工作呢?
1、根据msado15.dll里的类型库信息生成COM组件的接口文件。具体的就是生成msado15.tlh和msado15.tli,前者是声明文件,后者是实现文件;
2、替换#import语句为#include "msado15.tlh"。这样,源文件里就可以使用COM组件的接口了;
3、msado15.tli是COM接口的实现文件,如果没有它则程序可以编译但无法连接。不过,msado15.tli被msado15.tlh包含起来了(通过#pragma start_map_region或#include),且它的函数全部为内联的。因此,不用再单独编译msado15.tli。
1.1.1 版本
Windows7 sp1里的msado15.dll,其类型库版本为 6.1,而Windows XP下msado15.dll的版本为2.x。最关键的是新版本的类型库并不是向下兼容的!
如果在Windows7下编译代码,然后在Windows XP下运行,就有可能会出现问题。解决方法有两个:
1、#import "msado15.dll"中的msado15.dll请使用低版本的,即Windows XP里的msado15.dll;
2、升级Windows XP里的ADO组件。
1.2 初始化OLE/COM库环境
ADO是一组COM动态库,所以使用ADO前,必须初始化OLE/COM库。在MFC应用程序里,一个比较好的方法是在应用程序主类的InitInstance成员函数里初始化OLE/COM库。
BOOL CMyAdoTestApp::InitInstance()
{
AfxOleInit(); //这就是初始化COM库
……
}
也可以使用 CoInitialize或CoInitializeEx或OleInitialize初始化COM库,退出程序前请使用CoUninitialize或OleUninitialize。
1.3 comdef.h
msado15.tlh文件里,语句#include <comdef.h>包含了comdef.h头文件,这个头文件里又有如下语句:
... ... ...
#include <comutil.h>
... ... ...
#pragma comment(lib, "comsupp.lib")
comdef.h、comutil.h、comsupp.lib包含了一些重要的变量、函数、类。
1.3.1 字符串编码转换
comutil.h头文件里有两个函数可用于字符串的编码转换:
namespace _com_util
{
BSTR __stdcall ConvertStringToBSTR(const char*pSrc);
char* __stdcall ConvertBSTRToString(BSTR pSrc);
}
_com_util::ConvertStringToBSTR将Ansi字符串转换为Unicode字符串,返回值记得要调用SysFreeString释放内存;
_com_util::ConvertBSTRToString 将Unicode字符串转换为Ansi字符串,返回值记得要调用 delete[] 释放内存;
这两个函数的实现代码在comsupp.lib里。
1.3.2 重要的类
在comutil.h和comdef.h这两个头文件里,有三个重要的类:_bstr_t、_variant_t、_com_error。
_bstr_t 封装了BSTR,_variant_t封装了VARIANT,_com_error封装了COM错误。
使用_bstr_t和_variant_t可以极大的简化代码。以下面的代码进行说明:
_RecordsetPtr m_pRecordset;
... ... ...
_variant_t vAge = m_pRecordset->GetCollect(_T("Age"));
上面的代码用来读取字段Age。查看msado15.tli里的Recordset15::GetCollect函数,可以知道COM组件返回的其实是一个VARIANT,GetCollect函数返回前创建了一个临时_variant_t对象,将VARIANT封装了起来并返回给vAge。
vAge析构时将调用VariantClear销毁COM组件返回的VARIANT。如果Age字段是一个BSTR字符串,VariantClear会调用SysFreeString释放BSTR字符串。
如果不借助_variant_t,而直接使用VARIANT,会是什么情况呢?那就是必须显式的调用VariantClear函数,销毁COM组件返回的每一个VARIANT。这样代码就会显得非常臃肿,而且一旦疏忽就会发生内存泄露。
此外,使用_bstr_t也会相当的方便。
如:代码_bstr_t s(m_pRecordset->GetCollect(_T("Age")));会自动将GetCollect返回的_variant_t转换为BSTR字符串。
如:可以使用Ansi字符串构造_bstr_t对象
_bstr_t s1("测试_bstr_t");
如:可以使用Unicode字符串构造_bstr_t对象
_bstr_t s2(L"测试_bstr_t");
如:可以将Ansi字符串或Unicode字符串赋给_bstr_t变量
s1 = "_bstr_t测试";
s2 = L"_bstr_t测试";
如:可以轻松获得Ansi字符串或Unicode字符串
const char* sA = (const char*)s1; //返回Ansi字符串
const wchar_t* sW = (const wchar_t*)s1; //返回Unicode字符串
需要注意的是:上面的sA、sW所指向的内存由_bstr_t维护。_bstr_t对象s1析构时,sA、sW也就成为了野指针。
1.3.3 重要的变量
在comutil.h里,有全局变量vtMissing,其定义如下:
extern _variant_t vtMissing;
它其实是类型为VT_EMPTY的VARIANT。
1.3.4 智能指针
宏_COM_SMARTPTR_TYPEDEF用来声明智能指针。
如:智能指针_ConnectionPtr的声明如下:
_COM_SMARTPTR_TYPEDEF(_Connection, __uuidof(_Connection));
宏展开之后,其实就是:
typedef _com_ptr_t< _com_IIID<_Connection, &__uuidof(_Connection)> > _ConnectionPtr;
_ConnectionPtr的实质就是一个自动维护COM计数的_Connection。
第2章 _ConnectionPtr
_ConnectionPtr表示与数据库的连接。
2.1 连接数据库
下面的代码将实例化一个_ConnectionPtr,并连接数据库
HRESULT hr = S_OK; _ConnectionPtr m_pConnection; {//创建连接 try { //创建 COM 对象,__uuidof(Connection) 可以替换为 "ADODB.Connection" hr = m_pConnection.CreateInstance(__uuidof(Connection)); if(SUCCEEDED(hr)) {//连接 Access 数据库 Demo.mdb m_pConnection->Open("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=Demo.mdb" ,"","",adModeUnknown); } } catch(_com_error& e) { AfxMessageBox(e.Description()); } } |
m_pConnection->Open用来连接数据库。第一个参数用来指定连接字符串;第二、三个参数分别为用户名和密码;第四个参数是enum ConnectModeEnum,对数据库的读写进行控制,如:adModeRead表示只读……
2.2 执行SQL语句
Execute方法将执行SQL语句,其声明如下:
_RecordsetPtr Connection15::Execute(_bstr_t CommandText
,VARIANT*RecordsAffected
,long Options);
CommandText 是命令字串,通常是SQL命令
RecordsAffected 是操作完成后所影响的行数
Options 表示CommandText的类型,取值如下
adCmdText 表明CommandText是文本
adCmdTable 表明CommandText是表名
adCmdProc 表明CommandText是存储过程
adCmdUnknown 未知
Execute执行完后返回一个记录集。
示例代码如下:
_variant_t RecordsAffected; //执行SQL命令,创建表格 m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText); //往表格里面添加记录 m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) VALUES (1, ''''Washington'''',25,''''1970/1/1'''')",&RecordsAffected,adCmdText); //将所有记录old字段的值加一 m_pConnection->Execute("UPDATE users SET old = old + 1" ,&RecordsAffected,adCmdText); //执行SQL统计命令得到包含记录条数的记录集 m_pRecordset = m_pConnection->Execute("SELECT COUNT(*) FROM users" ,&RecordsAffected,adCmdText); //取得第一个字段的值放入vCount变量 _variant_t vCount = m_pRecordset->GetCollect((_variant_t)((long)0)); m_pRecordset->Close();//关闭记录集 CString message; message.Format("共有%d条记录",vCount.lVal); AfxMessageBox(message);///显示当前记录条数 |
2.3 事务处理
ADO中的事务处理也很简单,只需分别在适当的位置调用Connection对象的三个方法即可,这三个方法是:
1、在事务开始时调用
pCnn->BeginTrans();
2、在事务结束并成功时调用
pCnn->CommitTrans();
3、在事务结束并失败时调用
pCnn->RollbackTrans();
在使用事务处理时,应尽量减小事务的范围,即减小从事务开始到结束(提交或回滚)之间的时间间隔,以便提高系统效率。需要时也可在调用BeginTrans()方法之前,先设置Connection对象的IsolationLevel属性值,详细内容参见MSDN中有关ADO的技术资料。
2.4 断开连接
下面的代码将断开与数据库的连接,并销毁_ConnectionPtr实例
if(m_pConnection) {//关闭ADO连接 if(m_pConnection->State) { m_pConnection->Close(); } //下面的语句可有可无。因为m_pConnection是智能指针,析构时会自动Release m_pConnection.Release(); } |
第3章 _RecordsetPtr
_RecordsetPtr表示记录集。
3.1 打开记录集
使用Open方法打开记录集,其声明如下:
HRESULT Recordset15::Open(const _variant_t& Source
,const _variant_t & ActiveConnection
,enum CursorTypeEnum CursorType
,enum LockTypeEnum LockType
,long Options);
Source 数据查询字符串
ActiveConnection 是_Connection*或连接数据库的字符串
CursorType 光标类型
LockType 锁定类型
Options 可以取如下值之一:
adCmdText 表明CommandText是文本命令
adCmdTable 表明CommandText是表名
adCmdProc 表明CommandText是存储过程
adCmdUnknown 未知
下面的代码首先实例化一个_RecordsetPtr,然后在_ConnectionPtr的基础上,打开一个记录集。注意Open函数的第二个参数,表示数据库连接。
_RecordsetPtr m_pRecordset; try { //创建 COM 对象,__uuidof(Recordset) 可以替换为 "ADODB.Recordset" hr = m_pRecordset.CreateInstance(__uuidof(Recordset)); if(SUCCEEDED(hr)) { m_pRecordset->CursorLocation = adUseClient; m_pRecordset->Open(_T("SELECT * FROM DemoTable") ,m_pConnection.GetInterfacePtr() ,adOpenDynamic ,adLockOptimistic //乐观锁 ,adCmdText); } } catch(_com_error& e) { AfxMessageBox(e.Description()); } |
可以将Open函数的第二个参数更换为连接字符串,如:"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=Demo.mdb"。在此情况下,Open函数首先连接数据库,然后再打开一个记录集。
3.1.1 CursorLocation
CursorLocation表示游标的位置,有两个选项:
adUseServer = 2 //默认,表示游标在服务端
adUseClient = 3 //表示游标在客户端
游标在服务端,则记录集对数据变化是敏感的。亦即当其它用户修改了数据库内的数据后,有可能会影响到客户端已经打开的记录集。游标在客户端,就不会有数据敏感性了。
游标在服务端,则客户端打开记录集时数据将保留在服务端,不会通过网络传给客户端。但是访问记录集里的数据时,就会频繁的网络通讯。游标在客户端,则客户端打开记录集时数据将通过网络缓存到客户端。访问记录集里的数据时,将不再需要网络通讯。因此,如果频繁的访问数据则客户端游标的性能较高。
此外,服务端游标不支持书签而客户端游标支持书签。这将影响到某些函数的使用,如:GetRecordCount函数有可能返回-1,而不是实际的记录个数。
3.1.2 CursorType
游标类型有四种:
adOpenForwardOnly = 0, //向前游标
adOpenKeyset = 1, //键集游标
adOpenDynamic = 2, //动态游标
adOpenStatic = 3 //静态游标
adOpenForwardOnly、adOpenStatic 表示记录集是静态的:打开记录集后,它就不会再发生变化,即使其他用户修改了数据库里的数据。两者的区别在于adOpenForwardOnly只能向前移动游标,而adOpenStatic可以任意移动游标。
adOpenKeyset、adOpenDynamic 表示记录集是动态的:打开记录集后,能够反映其他用户所做的修改。
adOpenKeyset 的实现原理:打开的记录集仅仅保存记录的关键字(Key),访问某条记录时是根据 Key 到数据库访问的。所以,其他用户修改了某条记录后,再访问这条记录,是能够发现记录改变了。但是,其他用户如果增加、删除记录,并不会更改关键字记录集,因此通过关键字记录集,无法及时获知增加、删除的记录。
adOpenDynamic 的实现原理:打开的记录集会根据数据库内容的变化及时予以更新。但是需要注意CursorLocation 必须等于 adUseServer,即游标必须在服务端才能实现此功能。
3.1.3 LockType
adLockReadOnly = 1, //只读
adLockPessimistic = 2, //悲观锁
adLockOptimistic = 3, //乐观锁
adLockBatchOptimistic = 4 //批量乐观锁
悲观锁要求CursorLocation = adUseServer。它表示:编辑字段时就锁定记录,Update之后停止锁定。这种方法最保险,但是效率最低;
乐观锁只有在 Update 时才锁定记录,更新完毕后停止锁定。也就是说,每调用一次Update,都会锁定、解锁一次;
批量乐观锁表示调用UpdateBatch时锁定记录,批量更新完毕后停止锁定。也就是说,每调用一次UpdateBatch,都会锁定、解锁一次。
假定使用悲观锁,多用户执行下面三行代码,会发生什么情况呢?
m_pRecordset->MoveFirst(); m_pRecordset->PutCollect("name","Test"); m_pRecordset->Update(); |
用户A执行到m_pRecordset->PutCollect,将锁定记录集的第一行。其它用户运行到m_pRecordset->PutCollect时,也想锁定第一行,但因为已经被A锁定,所以其它用户执行到此行时会等待若干时间后抛出异常。
用户A执行完m_pRecordset->Update后,将解锁记录集的第一行。注意:具体何时解锁,每种数据库好像不尽相同。经笔者测试,发现Access2000是Update后即解锁,而SQL Server2008在记录集关闭后才解锁。
3.2 遍历记录集
如下代码将遍历记录集。
_variant_t var; CString sName,sAge; try { //如果 BOF 和 EOF 均为真,则无记录 if(!m_pRecordset->BOF || !m_pRecordset->adoEOF) { m_pRecordset->MoveFirst(); while(!m_pRecordset->adoEOF) { var = m_pRecordset->GetCollect(_T("Name")); if(var.vt != VT_NULL) {//姓名 sName = (LPCTSTR)_bstr_t(var); } else { sName.Empty(); } var = m_pRecordset->GetCollect(_T("Age")); if(var.vt != VT_NULL) {//年龄 sAge = (LPCTSTR)_bstr_t(var); } else { sAge.Empty(); } m_pRecordset->MoveNext(); } } } catch(_com_error& e) { AfxMessageBox(e.Description()); } |
var = m_pRecordset->GetCollect(_T("Name"));的等价代码如下:
FieldsPtr pFields = m_pRecordset->Fields; FieldPtr pField = pFields->Item[_T("Name")]; var = pField->Value; |
3.3 关闭记录集
下面的代码将关闭记录集,并销毁_RecordsetPtr实例。
if(m_pRecordset) {//关闭记录集 if(m_pRecordset->State) { m_pRecordset->Close(); } //下面的语句可有可无。因为m_pRecordset是智能指针,析构时会自动Release m_pRecordset.Release(); } |
3.4 访问BLOB
在Microsoft SQL中,text、image……被当做二进制数据进行处理。
可以用Field对象的GetChunk和AppendChunk方法来访问。每次可以读出或写入全部数据的一部分,它会记住上次访问的位置。但是如果中间访问了别的字段后,就又得从头来了。
3.4.1 写入
可以使用m_pRecordset->PutCollect("data",varBLOB)将二进制数据一次性写入,也可以使用AppendChunk分多次写入二进制数据。
void CDlgMain::blobFileToDB(LPCTSTR szFile) { CFile f; if(f.Open(szFile,CFile::modeRead | CFile::shareDenyWrite)) { FieldPtr fd = m_pRecordset->Fields->Item["data"]; DWORD dwSize = f.GetLength(); if(dwSize > 0) { const int nBlock = 1024; DWORD dwWrite = 0; //已经写入数据库的字节数 DWORD dwAppend = 0; //单次写入的字节数 UINT uRead = 0; SAFEARRAY* psa = SafeArrayCreateVector(VT_UI1,0,nBlock); _variant_t vBLOB; vBLOB.vt = VT_ARRAY | VT_UI1; vBLOB.parray = psa; for(;;) { dwAppend = dwSize - dwWrite; if(!dwAppend) { break; } if(dwAppend > nBlock) { dwAppend = nBlock; } SafeArrayLock(psa); uRead = f.Read(psa->pvData,dwAppend); SafeArrayUnlock(psa); if(uRead != dwAppend) { break; } psa->rgsabound[0].cElements = dwAppend; fd->AppendChunk(vBLOB); dwWrite += dwAppend; } //SafeArrayDestroy(psa); //vBLOB析构时会自动释放数组 psa } else { _variant_t vNull; vNull.vt = VT_NULL; fd->PutValue(vNull); } f.Close(); } } |
代码fd->PutValue(vNull);相当于m_pRecordset->PutCollect("data",vNull)用来设置BLOB为NULL,也就是清空。也可以使用SQL语句清空某个BLOB字段,如:
UPDATE 表名 SET BLOB字段名 = NULL WHERE ID=1
3.4.2 读取
可以使用m_pRecordset-> GetCollect("data")将二进制数据一次性读取出来,也可以使用GetChunk分多次读取二进制数据。
void CDlgMain::blobDBtoFile(LPCTSTR szFile) { CFile f; if(f.Open(szFile,CFile::modeWrite | CFile::modeCreate)) { FieldPtr fd = m_pRecordset->Fields->Item["data"]; long nSizeActual = fd->ActualSize; if(nSizeActual > 0) { const int nBlock = 1024; long nRead = 0; //已经读取的字节数 long nGet = 0; //单次读取的字节数 _variant_t vBLOB; ULONG uWrite = 0; //单次写入文件的字节数 SAFEARRAY* psa; for(;;) { nGet = nSizeActual - nRead; if(!nGet) { break; } if(nGet > nBlock) { nGet = nBlock; } vBLOB = fd->GetChunk(nGet); nRead += nGet; if(vBLOB.vt == (VT_ARRAY | VT_UI1) && (psa = vBLOB.parray) && psa->cDims == 1) { uWrite = psa->rgsabound[0].cElements; if(uWrite) { SafeArrayLock(psa); f.Write(psa->pvData,uWrite); SafeArrayUnlock(psa); } } } } f.Close(); } } |
3.4.3 更新
通过_CommandPtr,执行带参数的SQL语句,可实现BLOB数据的修改。请参考_CommandPtr这一章。
3.5 书签
书签(bookmark)可以唯一标识记录集中的一个记录,用于快速地将当前记录移回到已访问过的记录,以及进行过滤等等。Provider会自动为记录集中的每一条记录产生一个书签,我们只需要使用它就行了。我们不能试图显示、修改或比较书签。ADO用记录集的Bookmark属性表示当前记录的书签。
用法步骤:
rst->Supports(adBookmark); //判断是否支持书签
_variant_t VarBookmark; //建立书签变量
VarBookmark = rst->Bookmark; //获得书签值
... ... ... //可以移动记录
if(VarBookmark.vt != VT_EMPTY)
{//将记录位置恢复到书签位置
rst->Bookmark = VarBookmark;
}
3.6 过滤
Recordset对象的Filter属性表示了当前的过滤条件。它的值可以是以AND或OR连接起来的条件表达式(不含WHERE关键字)、由书签组成的数组或ADO提供的FilterGroupEnum枚举值。为Filter属性设置新值后Recordset的当前记录指针会自动移动到满足过滤条件的第一个记录。例如:
rst->Filter = _bstr_t ("姓名='赵薇' AND 性别='女'");
在使用条件表达式时应注意下列问题:
1、可以用圆括号组成复杂的表达式
例如:
rst->Filter = _bstr_t ("(姓名='赵薇' AND 性别='女') OR AGE<25");
但是微软不允许在括号内用OR,然后在括号外用AND,例如:
rst->Filter = _bstr_t ("(姓名='赵薇' OR 性别='女') AND AGE<25");
必须修改为:
rst->Filter = _bstr_t ("(姓名='赵薇' AND AGE<25) OR (性别='女' AND AGE<25)");
2、表达式中的比较运算符可以是LIKE
LIKE后被比较的是一个含有通配符*的字符串,星号表示若干个任意的字符。
字符串的首部和尾部可以同时带星号*
rst->Filter = _bstr_t ("姓名 LIKE '*赵*' ");
也可以只是尾部带星号:
rst->Filter = _bstr_t ("姓名 LIKE '赵*' ");
Filter属性值的类型是Variant,如果过滤条件是由书签组成的数组,则需将该数组转换为SafeArray,然后再封装到一个VARIANT或_variant_t型的变量中,再赋给Filter属性。
3.7 Find
以下代码用于查找记录
pRst->Find("姓名 = '赵薇'",1,adSearchForward);
一般情况下,这种查找是顺序查找,效率较低。可针对某个字段进行排序,其方法如下:
//将该字段的Optimize属性设置为True
pRst->Fields->GetItem("姓名")->Properties->
GetItem("Optimize")->PutValue("True");
... ... ...
pRst->Find("姓名 = '赵薇'",1,adSearchForward);
... ... ...
//将该字段的Optimize属性设置为False
pRst->Fields->GetItem("姓名")->Properties->
GetItem("Optimize")->PutValue("False");
3.8 Sort
要排序也很简单,只要把要排序的关键字列表设置到Recordset对象的Sort属性里即可,例如:
pRstAuthors->CursorLocation = adUseClient;
pRstAuthors->Open("SELECT * FROM mytable"
,_variant_t((IDispatch *)pConnection)
,adOpenStatic,adLockReadOnly, adCmdText);
......
pRst->Sort = "姓名 DESC, 年龄 ASC";
关键字(即字段名)之间用逗号隔开,如果要以某关键字降序排序,则应在该关键字后加一空格,再加DESC(如上例)。升序时ASC加不加无所谓。本操作是利用索引进行的,并未进行物理排序,所以效率较高。
但要注意,在打开记录集之前必须将记录集的CursorLocation属性设置为adUseClient,如上例所示。Sort属性值在需要时随时可以修改。
3.9 Index
pRst->Index=""; //首先设置索引(数据库里建立的索引)
pRst->Seek(...,...); //有序查找
3.10 绑定数据
定义一个绑定类,将其成员变量绑定到一个指定的记录集,以方便于访问记录集的字段值。
3.10.1 CADORecordBinding派生类
class CCustomRs : public CADORecordBinding
{
BEGIN_ADO_BINDING(CCustomRs)
ADO_VARIABLE_LENGTH_ENTRY2(3
,adVarChar
,m_szau_fname
,sizeof(m_szau_fname)
,lau_fnameStatus
,false)
ADO_VARIABLE_LENGTH_ENTRY2(2
,adVarChar
,m_szau_lname
,sizeof(m_szau_lname)
,lau_lnameStatus
,false)
ADO_VARIABLE_LENGTH_ENTRY2(4
,adVarChar
,m_szphone
,sizeof(m_szphone)
,lphoneStatus
,true)
END_ADO_BINDING()
public:
CHAR m_szau_fname[22];
ULONG lau_fnameStatus;
CHAR m_szau_lname[42];
ULONG lau_lnameStatus;
CHAR m_szphone[14];
ULONG lphoneStatus;
};
其中将要绑定的字段与变量名用BEGIN_ADO_BINDING宏关联起来。每个字段对应于两个变量,一个存放字段的值,另一个存放字段的状态。字段用从1开始的序号表示,如1,2,3等等。
特别要注意的是:如果要绑定的字段是字符串类型,则对应的字符数组的元素个数一定要比字段长度大2(比如m_szau_fname[22],其绑定的字段au_fname的长度实际是20),不这样绑定就会失败。我分析多出的2可能是为了存放字符串结尾的空字符null和BSTR字符串开头的一个字(表示BSTR的长度)。这个问题对于初学者来说可能是一个意想不到的问题。
CADORecordBinding类的定义在icrsint.h文件里,内容是:
class CADORecordBinding
{
public:
STDMETHOD_(const ADO_BINDING_ENTRY*, GetADOBindingEntries) (VOID) PURE;
};
BEGIN_ADO_BINDING宏的定义也在icrsint.h文件里,内容是:
#define BEGIN_ADO_BINDING(cls) public: /
typedef cls ADORowClass; /
const ADO_BINDING_ENTRY* STDMETHODCALLTYPE GetADOBindingEntries() { /
static const ADO_BINDING_ENTRY rgADOBindingEntries[] = {
ADO_VARIABLE_LENGTH_ENTRY2宏的定义也在icrsint.h文件里:
#define ADO_VARIABLE_LENGTH_ENTRY2(Ordinal, DataType, Buffer, Size, Status, Modify)/
{Ordinal, /
DataType, /
0, /
0, /
Size, /
offsetof(ADORowClass, Buffer), /
offsetof(ADORowClass, Status), /
0, /
classoffset(CADORecordBinding, ADORowClass), /
Modify},
#define END_ADO_BINDING宏的定义也在icrsint.h文件里:
#define END_ADO_BINDING() {0, adEmpty, 0, 0, 0, 0, 0, 0, 0, FALSE}};/
return rgADOBindingEntries;}
3.10.2 绑定
_RecordsetPtr Rs1;
IADORecordBinding *picRs=NULL;
CCustomRs rs;
......
Rs1->QueryInterface(__uuidof(IADORecordBinding),(LPVOID*) picRs);
picRs->BindToRecordset(&rs);
派生出的类必须通过IADORecordBinding接口才能绑定,调用它的BindToRecordset方法就行了。
3.10.3 读取字段
rs中的变量即是当前记录字段的值
//Set sort and filter condition:
// Step 4: Manipulate the data
Rs1->Fields->GetItem("au_lname")->Properties->
GetItem("Optimize")->Value = true;
Rs1->Sort = "au_lname ASC";
Rs1->Filter = "phone LIKE '415 5*'";
Rs1->MoveFirst();
while (VARIANT_FALSE == Rs1->EndOfFile)
{
printf("Name: %s/t %s/tPhone: %s/n"
,(rs.lau_fnameStatus == adFldOK ? rs.m_szau_fname : "")
,(rs.lau_lnameStatus == adFldOK ? rs.m_szau_lname : "")
,(rs.lphoneStatus == adFldOK ? rs.m_szphone : ""));
if (rs.lphoneStatus == adFldOK)
strcpy(rs.m_szphone, "777");
TESTHR(picRs->Update( // Add change to the batch
Rs1->MoveNext();
}
Rs1->Filter = (long) adFilterNone;
......
if (picRs)
{
picRs->Release();
}
Rs1->Close();
pConn->Close();
只要字段的状态是adFldOK,就可以访问。如果修改了字段,不要忘了先调用picRs的Update(注意不是Recordset的Update),然后才关闭,也不要忘了释放picRs(即picRs->Release();)。
3.10.4 添加新记录
此时还可以用IADORecordBinding接口添加新记录
if(FAILED(picRs->AddNew(&rs)))
......
第4章 _CommandPtr
_CommandPtr用来执行SQL语句或调用存储过程。
4.1 执行SQL语句
下面的代码首先实例化一个_CommandPtr,然后执行一条SQL语句,执行返回的结果就是一个记录集:
_CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd->ActiveConnection = theApp.m_pConnection; cmd->CommandText = "select * from file where name like '%.jpg'"; _RecordsetPtr rs = cmd->Execute(NULL,NULL,adCmdText); |
_CommandPtr不仅仅能执行SQL语句,它还可以给SQL语句传递参数。
4.1.1 无名参数
下面的代码中,SQL语句中的?号就是一个无名参数。执行SQL语句时,从左至右第一个?号将被cmd->Parameters->Item[0]->Value代替;第二个?号将被cmd->Parameters->Item[1]->Value代替……
所以,调用cmd->Execute执行SQL语句前,需要调用cmd->CreateParameter创建参数,并调用cmd->Parameters->Append将此参数添加至cmd->Parameters集合。
_CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd->ActiveConnection = theApp.m_pConnection; cmd->CommandText = "select * from file where name like ?"; cmd->Parameters->Append( cmd->CreateParameter("",adChar,adParamInput,-1,"%.jpg")); _RecordsetPtr rs = cmd->Execute(NULL,NULL,adCmdText); |
4.1.2 有名参数
下面的代码中,SQL语句中的@name就是一个有名参数。执行SQL语句时,@name将被cmd->Parameters->Item["@name"]->Value代替。
所以,调用cmd->Execute执行SQL语句前,需要调用cmd->CreateParameter创建有名参数,并调用cmd->Parameters->Append将此参数添加至cmd->Parameters集合。
_CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd->ActiveConnection = theApp.m_pConnection; cmd->CommandText = "select * from file where name like @name"; cmd->Parameters->Append( cmd->CreateParameter("@name",adChar,adParamInput,-1,"%.jpg")); _RecordsetPtr rs = cmd->Execute(NULL,NULL,adCmdText); |
4.2 修改BLOB
通过_CommandPtr,执行带参数的SQL语句,可实现BLOB数据的修改。
_variant_t vBLOB; vBLOB.vt = VT_ARRAY | VT_UI1; vBLOB.parray = SafeArrayCreateVector(VT_UI1,0,30); SafeArrayLock(vBLOB.parray); memset(vBLOB.parray->pvData,0,30); SafeArrayUnlock(vBLOB.parray); _CommandPtr cmd(__uuidof(Command)); cmd->ActiveConnection = theApp.m_pConnection; cmd->CommandText = "UPDATE 表名 SET BLOB字段名=? WHERE ID='1'"; cmd->Parameters->Append( cmd->CreateParameter("",adVarBinary,adParamInput,-1,vBLOB)); cmd->Execute(NULL,NULL,adCmdText); |
4.3 执行存储过程
SQL2008里,执行如下SQL语句,创建一个存储过程:
if exists (select * from sysobjects where id = object_id(N'[sp_1]') and OBJECTPROPERTY(id, N'IsProcedure')= 1) drop procedure sp_1 GO CREATE PROCEDURE sp_1(@pin1 int ,@pin2 CHAR(10) ,@pout1 int OUTPUT ,@pout2 CHAR(10) OUTPUT) AS BEGIN declare @retval int select @pout1 = @pin1 + 100 select @pout2 = left( ltrim(rtrim(@pin2)) + '123' , 10) select * from [file] select @retval = 1236 return @retval END GO exec sp_1 10,'Test',20,'789' GO |
使用_CommandPtr执行这个存储过程,需要传递、接收的参数如下:
@RETURN_VALUE(int,返回值) //第0个参数,返回值
@pin1(int,输入) //第1个参数
@pin2(char(10),输入) //第2个参数
@pout1(int ,输入/输出) //第3个参数
@pout2(char(10),输入/输出) //第4个参数
4.3.1 无名参数
不使用Refresh方法,执行存储过程的代码如下:
_CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd->ActiveConnection = theApp.m_pConnection; cmd->CommandText = "sp_1"; //存储过程名称 //添加参数——返回值 cmd->Parameters->Append( cmd->CreateParameter("",adInteger,adParamReturnValue,sizeof(int))); //添加参数——@pin1 cmd->Parameters->Append( cmd->CreateParameter("",adInteger,adParamInput,sizeof(int),3L)); //添加参数——@pin2 cmd->Parameters->Append( cmd->CreateParameter("",adChar,adParamInput,10,_variant_t(_T("DD1")))); //添加参数——@pout1 cmd->Parameters->Append( cmd->CreateParameter("",adInteger,adParamOutput,sizeof(int))); //添加参数——@pout2 cmd->Parameters->Append( cmd->CreateParameter("",adChar,adParamOutput,10)); //执行存储过程 theApp.m_pConnection->CursorLocation = adUseClient; _RecordsetPtr rs = cmd->Execute(NULL,NULL,adCmdStoredProc); //获取执行结果 _variant_t vRet = cmd->Parameters->Item[0L]->Value; //获得返回值 _variant_t vin1 = cmd->Parameters->Item[1L]->Value; //获得@pin1 _variant_t vin2 = cmd->Parameters->Item[2L]->Value; //获得@pin2 _variant_t vout1 = cmd->Parameters->Item[3L]->Value; //获得@pout1 _variant_t vout2 = cmd->Parameters->Item[4L]->Value; //获得@pout2 |
总结:不使用Refresh方法,则调用pCmd->Parameters->Append增加参数时,必须要注意参数的顺序。
4.3.2 有名参数
如果创建参数时指定参数名称,就可以根据名称获取执行结果了。
//添加参数——返回值 cmd->Parameters->Append( cmd->CreateParameter("@RETURN_VALUE",adInteger,adParamReturnValue,sizeof(int))); //添加参数——@pin1 cmd->Parameters->Append( cmd->CreateParameter("@pin1",adInteger,adParamInput,sizeof(int),3L)); //添加参数——@pin2 cmd->Parameters->Append( cmd->CreateParameter("@pin2",adChar,adParamInput,10,_variant_t(_T("DD1")))); //添加参数——@pout1 cmd->Parameters->Append( cmd->CreateParameter("@pout1",adInteger,adParamOutput,sizeof(int))); //添加参数——@pout2 cmd->Parameters->Append( cmd->CreateParameter("@pout2",adChar,adParamOutput,10)); //执行存储过程 theApp.m_pConnection->CursorLocation = adUseClient; _RecordsetPtr rs = cmd->Execute(NULL,NULL,adCmdStoredProc); //获取执行结果 _variant_t vRet = cmd->Parameters->Item["@RETURN_VALUE"]->Value; _variant_t vin1 = cmd->Parameters->Item["@pin1"]->Value; _variant_t vin2 = cmd->Parameters->Item["@pin2"]->Value; _variant_t vout1 = cmd->Parameters->Item["@pout1"]->Value; _variant_t vout2 = cmd->Parameters->Item["@pout2"]->Value; |
注意:即便参数有了名称,添加参数时的顺序也不能改动。
4.3.3 Refresh
执行cmd->Parameters->Refresh();会做哪些工作呢?
1、设置cmd->Parameters->Item[0L]
设置cmd->Parameters->Item[0L]->Name 为 "@RETURN_VALUE"
设置cmd->Parameters->Item[0L]->Value 为 VT_EMPTY
2、设置adParamInput参数的Value为 VT_EMPTY
3、设置adParamOutput参数的Value为 VT_NULL
当第2、3、……次执行cmd->Execute前,可以这么做:
cmd->Parameters->Refresh(); cmd->Parameters->Item["@pin1"]->Value = 3L; cmd->Parameters->Item["@pin2"]->Value = "C"; cmd->Execute(NULL,NULL,adCmdStoredProc); //第2次执行存储过程 cmd->Parameters->Refresh(); cmd->Parameters->Item["@pin1"]->Value = 4L; cmd->Parameters->Item["@pin2"]->Value = "D"; cmd->Execute(NULL,NULL,adCmdStoredProc); //第3次执行存储过程 |
4.3.4 游标位置
如果游标位置不为adUseClient,那么取return和output参数之前,必须把返回的记录集关闭掉。
下面的代码能够正常工作。因为这行代码执行完毕后,返回的记录集会被销毁,销毁前会关闭记录集。
pCmd->Execute(NULL,NULL,adCmdStoredProc);
下面的代码就得注意了。在 rs 销毁前,能否取得return和output参数,取决于游标位置是否为adUseClient。如果是adUseClient就能正常取值,否则必须关闭rs记录集后,才能正常取值。
_RecordsetPtr rs = pCmd->Execute(NULL,NULL,adCmdStoredProc);
theApp.m_pConnection->CursorLocation的取值会影响到pCmd->Execute返回记录集的游标位置。如:pCmd->Execute执行前,执行theApp.m_pConnection->CursorLocation = adUseClient,则返回记录集的游标位置也是adUseClient。
4.4 重复使用命令对象
一个命令对象如果要重复使用多次(尤其是带参数的命令),则在第一次执行之前,应将它的Prepared属性设置为TRUE。这样会使第一次执行减慢,但却可以使以后的执行全部加快。
第5章 ADOX
5.1 引入库文件
#import "C:/program Files/Common Files/system/ado/msadox.dll"
5.1.1 一个BUG
打开文件msadox.tlh,可以看到以下内容。其中,enum DataTypeEnum是先使用,后声明的。
struct __declspec(uuid("0000061d-0000-0010-8000-00aa006d2ea4")) Columns : _Collection { ... ... ... HRESULT Append ( const _variant_t & Item, enum DataTypeEnum Type, long DefinedSize ); ... ... ... }; enum DataTypeEnum { adEmpty = 0, adTinyInt = 16, ... ... ... }; |
在此情况下,如下代码将会出现编译错误。
#import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") no_namespace #import "C:/program Files/Common Files/system/ado/msadox.dll" |
原因在于:编译msadox.tlh时Columns::Append函数的第2个参数将是msado15.tlh里的enum DataTypeEnum;编译msadox.tli时Columns::Append函数的第2个参数却变成了msadox.tlh里的JRO::DataTypeEnum。
解决方法一:调整import的顺序
#import "C:/program Files/Common Files/system/ado/msadox.dll" #import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") no_namespace |
解决方法二:都使用命名空间
#import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") #import "C:/program Files/Common Files/system/ado/msadox.dll" |
5.2 创建数据库
ADOX::_CatalogPtr pCatalog(__uuidof(ADOX::Catalog));
_bstr_t s("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=G:\\1.mdb");
pCatalog->Create(s);
5.3 创建数据表
ADODB::_ConnectionPtr conn(__uuidof(ADODB::Connection));
conn->Open(sConn,"","",ADODB::adModeUnknown);
conn->Execute("CREATE TABLE TestTable(记录编号 INTEGER,姓名 TEXT,年龄 INTEGER)",NULL,ADODB::adCmdText);
conn->Close();
第6章 JRO
JRO是Jet and Replication Objects的缩写,它可以用来压缩Access数据库文件。
6.1 VB6.0
6.1.1 引用
6.1.2 代码
Dim sSrc As String Dim sDes As String sSrc = "Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=G:\dbFile.mdb" sDes = "Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=G:\1.mdb" Dim oJetEngine As New JRO.JetEngine oJetEngine.CompactDatabase sSrc, sDes Set oJetEngine = Nothing |
注意:"Jet OLEDB:Database Password="可以指定密码。
6.2 VC++
6.2.1 引入
引入JRO需要引入ADO库,代码如下:
#import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") no_namespace #import "C:/Program Files (x86)/Common Files/System/ado/msjro.dll" |
上面的ADO库未使用命名空间,如果使用了命名空间,则代码如下。增加了两条using语句,否则无法完成编译。
#import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") using ADODB::_Recordset; using ADODB::_RecordsetPtr; #import "C:/Program Files (x86)/Common Files/System/ado/msjro.dll" |
6.2.2 代码
AfxOleInit(); _bstr_t sSrc(_T("Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=G:\\dbFile.mdb")); _bstr_t sDes(_T("Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=G:\\1.mdb")); JRO::IJetEnginePtr jet(__uuidof(JRO::JetEngine)); jet->CompactDatabase(sSrc,sDes); |
注意:"Jet OLEDB:Database Password="可以指定密码。
第7章
常见问题
7.1 连接失败的原因
Enterprise Managemer内,打开将服务器的属性对话框,在Security选项卡中,有一个选项Authentication。
如果该选项是Windows NT only,则你的程序所用的连接字符串就一定要包含Trusted_Connection参数,并且其值必须为yes,如:
"Provider=SQLOLEDB;Server=888;Trusted_Connection=yes"
";Database=master;uid=lad;";
如果不按上述操作,程序运行时连接必然失败。
如果Authentication选项是SQL Server and Windows NT,则你的程序所用的连接字符串可以不包含Trusted_Connection参数,如:
"Provider=SQLOLEDB;Server=888;Database=master;uid=lad;pwd=111;";
因为ADO给该参数取的默认值就是no,所以可以省略。我认为还是取默认值比较安全一些。
7.2 改变当前数据库
使用Tansct-SQL中的USE语句即可。
7.3 判断某个数据库是否存在
1、可打开master数据库中一个叫做SCHEMATA的视图,其内容列出了该服务器上所有的数据库名称。
2、更简便的方法是使用USE语句,成功了就存在;不成功,就不存在。例如:
try
{
m_pConnect->Execute("USE INSURANCE_2002",NULL
,adCmdText│adExecuteNoRecords);
}
catch(_com_error&e)
{//数据库INSURANCE_2002不存在
}
7.4 判断某个表是否存在
1、同样判断一个表是否存在,也可以用是否成功地打开它来判断,十分方便,例如:
try
{
m_pRecordset->Open("mytable"
,_variant_t((IDispatch *)m_pConnection,true)
,adOpenKeyset,adLockOptimistic,adCmdTable);
}
catch (_com_error &e)
{//该表不存在
}
2、要不然可以采用麻烦一点的办法,就是在MS-SQL服务器上的每个数据库中都有一个名为sysobjects的表,查看此表的内容即知指定的表是否在该数据库中。
3、同样,每个数据库中都有一个名为TABLES的视图(View),查看此视图的内容即知指定的表是否在该数据库中。
7.5 ADO锁定整张表
Dim oConn As New ADODB.Connection
Dim oRs As New ADODB.Recordset
oConn.ConnectionTimeout = 15
oConn.Open 'Provider=SQLOLEDB.1;Password=***;Persist Security Info=True;User ID=***;Initial Catalog=XSSystem;Data Source=10.108.0.1'
oConn.CommandTimeout = 15
oConn.IsolationLevel = adXactSerializable
oConn.BeginTrans
oRs.CursorLocation = adUseClient
oRs.Open 'SELECT * FROM ShangYaoGuFenBuyTable with(tablockx) where ID='123' ', oConn, adOpenKeyset, adLockPessimistic
If oRs.RecordCount > 0 Then
MsgBox '已经有一条记录了'
Else
oRs.AddNew
oRs('id') = '123'
oRs.Update
End If
oRs.Close
oConn.CommitTrans '在此步骤之前,ShangYaoGuFenBuyTable整张表会被锁住,其他用户不能进行任何访问
oConn.Close
Set oConn = Nothing
7.6 获取记录集行数
可以使用SQL语句:select count(*) from 表名
7.7 解决并发冲突
使用:Field 对象的 UnderlyingValue 和 OriginalValue 属性;Recordset 的 Resync 方法和 Filter 属性。
调用UpdateBatch后,一定要立即检查Errors集合是否有错误。如果有错误,则应检查错误是否为并发冲突:
1、设置 Recordset 的 Filter 属性为adFilterConflictingRecords。若此时 RecordCount 属性等于零,就说明错误是由冲突以外的其他原因引起的。
2、调用 Recordset 的 Resync 方法,将 AffectRecords 参数设置为adAffectGroup,将 ResyncValues 参数设置为 adResyncUnderlyingValues。Resync 方法将用来自基本数据库中的数据刷新在当前 Recordset 对象中的数据。通过使用 adAffectGroup,可以确保只有使用当前筛选设置的情况下可见的记录。
ADO的更多相关文章
- ADO.NET对象的详解
1. Connection 类 和数据库交互,必须连接它.连接帮助指明数据库服务器.数据库名字.用户名.密码,和连接数据库所需要的其它参数.Connection对象会被Command对象使用,这样就能 ...
- WebForm获取GET或者POST参数到实体的转换,ADO.NET数据集自动转换实体
最近在修改维护以前的webform项目(维护别人开发的.....)整个aspx没有用到任何的控件,这个我也比较喜欢不用控件所以在提交信息的时候需要自己手动的去Request.QueryString[] ...
- ADO.NET编程之美----数据访问方式(面向连接与面向无连接)
最近,在学习ADO.NET时,其中提到了数据访问方式:面向连接与面向无连接.于是,百度了一下,发现并没有很好的资料,然而,在学校图书馆中发现一本好书(<ASP.NET MVC5 网站开发之美&g ...
- ADO.NET一小记-select top 参数问题
异常处理汇总-后端系列 http://www.cnblogs.com/dunitian/p/4523006.html 最近使用ADO.NET的时候,发现select top @count xxxx 不 ...
- .NET基础拾遗(6)ADO.NET与数据库开发基础
Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开发基 ...
- 升讯威ADO.NET增强组件(源码):送给喜欢原生ADO.NET的你
目前我们所接触到的许多项目开发,大多数都应用了 ORM 技术来实现与数据库的交互,ORM 虽然有诸多好处,但是在实际工作中,特别是在大型项目开发中,容易发现 ORM 存在一些缺点,在复杂场景下,反而容 ...
- ADO.NET Entity Framework 在哪些场景下使用?
在知乎回答了下,顺手转回来. Enity Framework已经是.NET下最主要的ORM了.而ORM从一个Mapping的概念开始,到现在已经得到了一定的升华,特别是EF等对ORM框架面向对象能力的 ...
- ADO.NET 核心对象简介
ADO.NET是.NET中一组用于和数据源进行交互的面向对象类库,提供了数据访问的高层接口. ADO.NOT类库在System.Data命名空间内,根据我们访问的不同数据库选择命名空间,System. ...
- ODBC、OLE DB、 ADO的区别
转自:http://blog.csdn.net/yinjingjing198808/article/details/7665577 一.ODBC ODBC的由来 1992年Microsoft和Syba ...
- LINQ to SQL语句(19)之ADO.NET与LINQ to SQL
它基于由 ADO.NET 提供程序模型提供的服务.因此,我们可以将 LINQ to SQL 代码与现有的 ADO.Net 应用程序混合在一起,将当前 ADO.NET 解决方案迁移到 LINQ to S ...
随机推荐
- UML建模的要点总结
预备知识: 一.UML的特性与发展现状 UML是一种Language(语言) UML是一种Modeling(建模)Language UML是Unified(统一)Modeling Language 1 ...
- Repeater的Command操作
Repeater的Command操作 1.ItemCommand事件 :在Repeater中所有能触发事件的控件,都会来触发这一个事件 后台创建:在Page_Load中 Repeater1.ItemC ...
- eclipse 智能提示
eclipse 智能提示 1.显示行号 2.android 的xml提示 文本框的内容为: <=:.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU ...
- Twitter数据抓取
说明:这里分三个系列介绍Twitter数据的非API抓取方法.有兴趣的QQ群交流: BitCrawler网络爬虫QQ群 322937592 1.Twitter数据抓取(一) 2.Twitter数据抓取 ...
- 快速编译system.img、userdata.img、boot.img的方法
快速编译system.img和boot.img的方法 快速编译system.img,可以使用这个命令: #make systemimage 快速编译boot.img,可以使用以下命令: #make b ...
- 使用Tesseract OCR识别验证码
1.下载Tessrac OCR,默认安装 2.把验证码code.jpg图片放在D盘 3.打开cmd,进入D盘,输入:tesseract code.jpg result 4.进入D盘,生成了resul ...
- (Theano 1)Theano自述文件
Theano在GitHub上的自述文件 https://github.com/Theano/Theano 也不知道这个Theano好不好,但是从Theano到Lasagne:基于Python的深度学习 ...
- 第五章 consul key/value
1.key/value作用 动态修改配置文件 支持服务协同 建立leader选举 提供服务发现 集成健康检查 2.使用 2.1.查看全部key/value 说明: 使用?recurse参数来指定查看多 ...
- Struts BaseAction工具类,封装Session,Request,Application,ModelDriven
package com.ssh.shop.action; import java.io.InputStream; import java.lang.reflect.ParameterizedType; ...
- STL--map
map--概述: 映射(Map)和多重映射(Multimap)是基于某一类型Key的键集的存在,提供对TYPE类型的数据进行快速和高效的检索. l对Map而言,键只是指存储在容器中的某一成员. lMu ...