如何用一行C++代码读写数据库
这篇文章要表达的并非数据库相关的知识,而是如何使用DBIOWrapper。
DBIOWrapper是一个工作在Windows下、对ODBC式数据访问进行了小型封装的库。其设计目标是提供极简的数据访问模式,使用最少量的代码完成数据访问工作,是目前能找到的最简单的Windows平台下数据库读写操作的库。
文章标题中《如何用一行C++代码读写数据库》是伪命题,要操作数据库我们通常需要以下几个步骤:连接数据库 -> 构建SQL语句 -> 执行SQL语句->读(写)结果集 -> 断开连接(当然,这里所说的“数据库”特指可以使用SQL的关系型数据库)。那么要真正做到用一行代码读写数据库就非常困难了,除非我们设计一种非常激进的,不处理(或保证不出现)任何错误的,操作数据库的接口,但这几乎是不可能的,以上几个步骤都有可能出错。
所以,接下来提供的(名为DBIOWrapper)数据库操作接口在真实环境中可能需要几行(小于15行)代码来完成读取整个SQL语句的结果集。 我相信读完本文的读者一定会希望使用DBIOWrapper来访问数据库或者采用DBIOWrapper的设计方式来封装自己的数据访问操作。
我不打算说一些DBIOWrapper的设计理念,毕竟仅是为满足易用性而对ODBC作的小规模封装,阉割了绝大多数ODBC提供的功能,所以本文接下来只讲述如何使用它,包括示例、教程以及注意事项和不应该使用它部分场景。
示例如何使用DBIOWrapper读取数据:
示例1:
假设有名为“MySQL”的DSN(参见ODBC相关术语“DSN”)指向的MySQL数据库中有名为 Student(即:学生)的表,该表中记录了学生的名字、年龄以及分数,其结构和数据如下表所示:
表结构:(表1.0)
表中存储的数据:(表1.1)
那么,以下代码用以读取并打印Student表中大于80分的学生的相关信息(注意:此示例没有处理任何错误,是错误示范):
CIOWrapper *pIOWrapper = CreateIOWrapper( "DSN=MySQL", NULL );
CRecordWrapper rw( pIOWrapper->Query( "SelectName, Age, Score From Student Where Score > %d;", 80) );
string sName;
int Age, Score;
while( rw.Select( sName,Age, Score ) )
printf( "%s的分数:%d(年龄:%d)\n", sName.c_str(), Score, Age );
下例修正以上代码,增加错误处理,代码体积会膨胀一倍以上,但仍然仅需要十几行代码,如下:
string sErrMsg;
shared_ptr<CIOWrapper > pIOWrapper(
CreateIOWrapper( "DSN=MySQL", &sErrMsg ),DestroyIOWrapper );
if( !pIOWrapper )
throw runtime_error( sErrMsg );
CRecordWrapper rw(pIOWrapper->Query("Select Name, Age, Score From Student Where Score > %d;", 80 ) );
if(rw.IsEmpty() )
throw runtime_error( pIOWrapper->GetErrorMessage() );
string sName;
int Age,Score;
while(rw.Select( sName, Age, Score ) )
printf( "%s的分数:%d(年龄:%d)\n",sName.c_str(),Score,Age );
if(GetLastError() !=ERROR_SUCCESS )
throw runtime_error( rw.GetErrorMessage() );
Select函数可以接受任意数量和类型的参数(实际使用中视SQL语句检索到的字段数量来决定参数的数量)。所以,代码的数量不会随着需要读取的字段增加而增加(上例中读取一个Name字段和读取三个或更多的字段需要的编码成本几乎完全一样)。我目前所能搜索到的所有的用来读写数据库的API或封装库都会随着需要读取的字段数量增加而需要编写更多的代码,这足以成为使用DBIOWrapper的理由。
若需向数据库写入或更改内容,只需调用CIOWrapper::ExecuteSQL 执行 Insert 或 Update 语句即可,过于简单不再例证。
2017年8月5日13时56分;定稿后有人说我在用震惊体标题唬人。于是我补充一个SelectEx函数:
template< typename_Fn1, typename... _Types >
__success(return !=FALSE ) BOOL SelectEx(__in_ _readonly LPCTSTR lpSQL, _Fn1 _Func,__out _Types&...Args )
{
shared_ptr<CIOWrapper> pIOWrapper(
CreateIOWrapper("DSN=MySQL", NULL ), DestroyIOWrapper );
if( !pIOWrapper )
returnFALSE;
CRecordWrapper rw(pIOWrapper->Query(lpSQL ));
if(rw.IsEmpty())
return FALSE;
while(rw.Select(forward<_Types& >(Args)... ) )
_Func(forward<_Types&>(Args)... );
return GetLastError()==ERROR_SUCCESS;
}
然后,这样调用SelectEx:
string sName;
int Age, Score;
SelectEx( (LPCTSTR)String::FastFormat(_T("Select Name, Age, Score From Student Where Score > %d;" ), 80 ),
[](string &sName, int &Age, int &Score ){ printf("%s的分数:%d(年龄:%d)\n",sName.c_str(),Score,Age ); },sName, Age, Score );
这回真的是一行代码了。至于如何编写自己的SelectEx函数就是智者见智之时了。如果你非要把定义sName、Age、Score也算作较大的编码成本,you win!这篇贴已沦为燃起你负面情绪的工具,请移步离开。
注:关于String::FastFormat函数请参见:http://blog.****.net/passfuhao/article/details/52926264
如何使用:
术语、名词解释:
(表1.2)
下例详细解释记录集、结果集、记录和单元之含义:
以表1.1中存储的数据为例,若执行此“Select Name, Age, Score From Student Whele Score >70”SQL语句,得到的记录集如下:
(表1.3)
此记录集中有两条记录,第一条记录为,第二条记录为
。第一条记录第一个字段(“第一条记录第一个字段”即“单元”)中存储的数据为 Jim 第二个字段中存储的数据为 21,以此类推。
下解释DBIOWrapper各部件的功能:
CreateIOWrapper-创建CIOWrapper对象。
CIOWrapper-负责连接数据库、构建并执行SQL语句和获得SQL语句的结果集。
CRecordWrapper-主要以成员函数Select向用户提供读取结果(记录)集中的记录的功能。
CRecordWrapper::Select-本文所讲述的核心所在,提供读取结果集功能,每次调用从记录(结果)集中读取一条记录,直到读取到最后一条记录,再次调用Select返回FALSE。其能接受一个变体转换器CastType(默认的转换器为VariantCast)和不定数量、类型的参数作为输出。
Select函数以结果集的字段索引和输出参数的索引为对应,使用CastType进行类型转换,无法进行类型转换或类型转换失败的将先弹出对话框提示用户类型转换失败,而后根据是否有调试器介入来决定是否抛出断点异常,最后设置错误代码为ERROR_UNSUPPORTED_TYPE并抛出一个SEH异常,其异常代码为EXCEPTION_ACCESS_VIOLATION且不可持续。
用户在构建SQL语句时基本可以确定某个字段的具体类型,从而可以根据字段类型来向Select函数传递对应的C++类型的变量作为输出参数,所以,无法进行类型转换通常预示着某些BUG的存在,那么抛出不可持续的异常是合理做法。而类型转换失败的情况非常少见,即便出现也多因资源不足或糟到破坏引起,所以对应的处理策略只有向Select函数提供特殊的转换器。这虽略显激进,但大多时候是对的。另外,Select函数提供的异常安全级别受其接受的参数在转型过程中的表现的限制,具体请参考Select函数的注释。
DBIOInterface::IDBVariant -变体数据。记录了记录集中某个单元中存储的数据。可通过其成员函数Type取得其记录的数据的类型。
VariantCast - 变体类型转换器,能将记录集中某个单元中(使用DBIOInterface::IDBVariant)存储的数据转换为C++类型。(如,上例中Name 字段为 varchar 类型,IDBVariant中记录的数据类型为VariantTypeAString或VariantTypeWString。VariantCast可将其转换为string或wstring类型)。关于VariantCast支持转换的类型见下表:
(表1.4)
其中,源类型表示 IDBVariant对象中记录的数据的类型(上例Student表中Name字段为varchar类型,在IDBVarianty对象中记录为VariantTypeAString或VariantTypeWString,通过Type成员函数取得),目的类型是传递给CRecordWrapper::Select函数的输出参数的类型。在上例代码中,SQL语句“Select Name, Age, Score From Student Whele Score > 80”所取得的记录集中,第一个字段 Name为 varchar型,IDBVariant将MySQL的varchar型存储为为VariantTypeAString或VariantTypeWString,第二个字段Age为int型,IDBVariant将MySQL的int型存储为VariantTypeLong,第三个字段同上。(我没有提供字段类型到源类型的映射表或任何文档,毕竟,这个库几乎可以操作一切支持SQL的数据库甚至Excel,不可能把所有数据库的字段类型全都整理一遍,但通常数据库都会对标准的SQL数据类型提供支持,你可以参考SQL数据类型。当然,像SQLite和Excel就显的比较特殊,相关知识自行百度吧)。
那么,根据上表,VariantCast可将VariantTypeAString或VariantTypeWString转换为 string、wstring、CStringA和CStringW类型,而数据库中存储的int类型,则可被VariantCast转换为C++的int、long、__int64、float或double类型。
然后,回头看看前例代码中的while(rw.Select(sName, Age, Score ) ) 语句。调用Select向其传递三个参数,包括一个string 类型的 sName参数,和两个 int 类型的 Age、Score。在Select读取记录集过程中,记录集的第一个字段中存储在IDBVariant中的数据转换为传递给Select的第一个参数的类型string,并输出到传递给Select的第一个参数中,其余两个字段分别转换为传递给Select的第二和第三个参数的类型int并输出,以此完成整个读取过程,并在读取试图读取最后一条数据之后的数据时返回FALSE,导致while退出。
如何将DBIOWrapper添加到你的项目:
1、下载并解压以下文件(百度网盘:https://pan.baidu.com/s/1kUDE1eB ,****:http://download.****.net/detail/passfuhao/9917733)。
1、复制IOWrapper/dbio目录到你的项目目录下,并将dbio目录里的DBIOWrapper.cpp添加到你的项目中(VS中打开你的项目-> 点击最上方菜单中的“项目” -> 添加现有项 -> 在弹出的“添加现有项-****”对话框中找到dbio目录DBIOWrapper.cpp并选中,最后点击对话框右下方的“添加”按扭)。
2、在需要使用DBIOWrapper之处包含 DBIOWrapper.h 文件。(注意DBIOWrapper.h文件的路径,如果dbio目录放在项目目录下,且未在项目包含目录中添加dbio的目录,则需要写:#include"dbio/DBIOWrapper.h",否则,视dbio存放路径修改。)
3、复制IOWrapper/Bin目录下的DataIOKernel.dll文件到你项目的输出文件目录下(“输出文件目录”通常指exe文件所在目录。若要修改DataIOKernel.dll的存放目录,可在第一次调用CreateIOWrapper前自行加载DataIOKernel.dll,或调用SetDllDirector,或修改GetIOInterfaceFactorProc函数中加载DataIOKernel.dll的目录)。
注:
DBIOWrapper毕竟由ODBC封装而来,所以其依赖数据源中配置的DSN(或文件DSN),ODBC数据源部分不在本文讲述范围内,此处不再讲述如何使用,若有需要请参考MSDN或数据库提供商相关文档来安装并配置数据源。我在此只建议(强烈建议)开发者能够将安装ODBC驱动和配置数据源操作自动化。可以尝试将数据库的ODBC驱动程序重新打包,打包成静默安装,然后使用文件DSN或完整的连接字符串或编写自动配置数据源工具(使用SQLConfigDataSource函数可配置数据源,请参考:https://msdn.microsoft.com/zh-cn/library/ck4z6wwt.aspx)来完成ODBC驱动的自动安装和数据源的自动配置。我所知的一些人宁愿放弃ODBC的通用性还要去使用数据库提供的API(如MySQLAPI)的原因就是ODBC依赖的数据源需要手动配置。而,现在能搜索到的大部分使用ODBC编程的文章中也在使用手动配置数据源的方法,深深误导了一些人。希望本文的读者不要再被这种错误认识误导。使用ODBC完全能用很简单的方法避免手动配置数据源。
我打包了一个MySQL静默安装的ODBC驱动和一个Oracle静默安装的ODBC驱动一并放入了DBIOWrapper的项目目录下(下载地址在文章最下方)。
注意事项:
1、ODBC数据源分32位和64位,若你的应用程序运行在WOW64环境下,应当使用以下命令打开ODBC数据源管理器配置数据源(DSN):
%SystemDrive%\Windows\SysWOW64\odbcad32.exe
否则,运行在纯32位和64位中的应用程序则应当使用“%SystemDrive%\Windows\System32\odbcad32.exe”(不包括外部双引号)打开ODBC数据源管理器来配置数据源(DSN);
2、为了便于移植到客户机,强烈建议使用文件DSN,和能够静默安装的ODBC驱动程序来避免人工配置数据源。
3、DBIOWrapper使用Windows标准的错误处理方式,当有错误发生后会调用SetLastError设置错误代码,用户可通过GetLastError得到错误。额外的,DBIOWrapper还提供错误消息能力,当在某个对象上执行操作发生错误后,该对象以描述方式记录了错误,可通过GetErrorMessage取得错误描述。
4、DBIOWrapper中除CRecordWrapper::Select和依赖此函数的函数外,所有函数提供“强烈的”异常安全等级。CRecordWrapper::Select提供的异常安全等级受制于其参数在转型过程中的表现,Select函数本身提供“基本的”异常安全等级。
何时不该使用DBIOWrapper:
我不知道。或许对效率要求极其之高的地方不该用吗?但DBIOWrapper并没有因封装而导致效率大幅下降。
特性说明:
1、DBIOWrapper不依赖任何第三方库,包括太不限于MFC、ATL、QT、boost等等。
2、DBIOWrapper依赖Windows的某些数据类型(如VOID、LONG,DOUBLE、FLOAT、SYSTEMTIME等)。
3、DBIOWrapper不可移植到Windows以外的操作系统平台。
4、DBIOWrapper提供的VariantCast可以将IDBVariant转向MFC或ATL的某些类型,如CString、CAtlArray、CArray等。但不支持向QT的QString,QVector转换,如需要,请自行编写转换器或扩充VariantCast。
设计缺陷:
1、Select函数中默认情况下使用的CastType(VariantCast)不提供对转型失败之情形的处理。又因字段类型必须在运行时确认,无法在编译期推导字段类型导致Select函数必须假设VariantCast能够将IDBVariant转换为任意类型,且不允许失败。
2、在CIOWrapper类中提供了Select函数,虽能完成了CRecordWrapper同样功能,但其获取错误消息的语义不对,需要使用CSQLKey对象获取错误消息,也因此,CSQLKey中多余一个记录错误消息的成员(我建议不要使用它)。
3、没有良好的支持写入或更改二进制对象型字段。
附:
DataIOKernel、DBIOWrapper、DBIOWrapperTest 项目源码下载地址:
百度网盘:https://pan.baidu.com/s/1kUDE1eB
****:http://download.****.net/detail/passfuhao/9917733
以上三个项目的简介:
DataIOKernel-封装了MFC的ODBC操作,以虚表形式提供接口,接口被包覆在DBIOInterface名字空间下。编译为一个名为DataIOKernel.dll 的文件。
DBIOWrapper-封装了DBIOInterface名字空间下的接口,并提供一种更简单易用的新接口。
DBIOWrapperTest-DBIOWrapper的测试项目。前述的示例代码即来自该项目。
如何实现自己的转换器:
请参考:http://blog.****.net/passfuhao/article/details/76549754