ADO连接MySQL数据库

  本文旨在指导大家如何通过使用ADO数据库访问技术实现对MySQL数据库的访问和CURD等相关操作。在这个过程中,大家将会初步接触并了解:
    +1.  存在哪些数据库访问技术;
    +2.  什么是Windows COM编程;
    +3.  如何为MySQL数据库配置ODBC数据源;
    +4.  ADO编程如何实现访问和操作MySQL数据库

第一节 数据库访问技术

  当一个项目存在大量的数据需要存储时,通常都会采用数据库来存储这些数据。最初,厂商推出一个新的数据库产品时,会为程序员提供一套访问该数据库的接口(即API),显然不同的数据库产商提供的访问接口是不会完全一样的。这样,在使用一个新的数据库时,就不得不学习一套新的API,无疑加大程序员的开发难度;数据移植性也受到影响,比如想使用另一种数据库的数据,你只能使用那种数据库提供的API重新写代码;总之,不利于数据库在软件开发过程中的应用。
  解决这个问题的关键,就是实现一种隐藏机制,提供一套统一的访问接口,让访问接口与具体数据库实现脱离Windows下一些标准的数据库访问技术应运而生,常用的有:

  +. ODBC(Open Database Connectivity)
  开放数据库互联体系结构如下图


  我们编写的客户程序直接与ODBC驱动程序管理器打交道,在访问不同的数据库时,需要指定相应的ODBC驱动程序ODBC驱动程序管理器将客户的请求转换为相应的ODBC驱动传递给数据库。ODBC提供了一个单一的API,使用ODBC API的应用程序可以与任何具有ODBC驱动程序关系型数据库进行通信。

  +. OLE DB(Object Link and Embed Database)
  对象链接与嵌入数据库体系结构如下图


  OLE DB在两个方面对ODBC进行了扩展
    1. 提供了一个数据库编程的COM接口 (点击查看《COM(Componet Object Model)》一节);  
    2. 提供可用于关系型非关系型的数据源的接口。ODBC只能访问关系型数据库,OLE DB还可以访问电子表格文本文件等非关系型数据源。
  OLE DB应用程序利用不同的OLE DB提供程序可以访问不同的数据源,只要这些数据源提供了相应的OLE DB提供程序;如果某个关系型数据库没有OLE DB提供程序,可以利用访问ODBC的OLE DB提供程序去访问ODBC,然后再利用ODBC去访问支持ODBC的数据库(我们访问MySQL数据库的方式正是如此,不过刚安装好的MySQL数据库还不能通过ODBC直接访问,需要先为其配置好ODBC数据源!!)。

  +. ADO(ActiveX Data Object)
  如上 “图:OLE DB体系结构” 所示,ActiveX数据对象建立在OLE DB之上,本身就是一个OLE DB应用程序(Consumer)ADO应用程序通过ADO再访问OLE DB提供程序 (所以访问速度会变慢一些),实现对不同类型数据源的访问。
  OLE DB通过一系列COM接口提供数据的底层连接,ADO则通过对象模型,将这些接口封装成一个对象,使其成为抽象实体,简化了OLE DB的数据访问过程。
  此外,OLE DB自动化的支持不是很好,像VBScript这种自动化脚本语言就不能使用OLE DB访问数据库,而ADO则能更好的支持自动化

第二节 COM(Componet Object Model)

  软件工程的目的是:软件能像堆积木一样累积起来、组装起来,而不是一点点编出来。大概历经了如下几个过程

   1>. 面向功能的结构化编程
   在这个过程中,程序员注重事物之间的联系,然而联系是多变的,而事物本身不会发生大的变化。软件工程的核心是模块化,联系一变,就是另一种功能,就得写多一个模块了。

   2>. 面向对象编程
   注重事物本身固有的属性,固有的行为。面向对象的着眼点就是事物这种稳定的概念,而事物之间的联系是多变的,运动的。用类描述事物,当类间的联系发生改变时只需重新使用类库即可。然而面向对象方法在重用上存在不少问题。类库的重用是基于源码,限制了编程语言;类库发行新版本时,必须重新编译,重新调试。

   3>. DLL编程
   DLL是基于二进制的代码重用,不存在类库重用时的问题,不过,DLL中存在的函数重名、各编译器对C++函数名字改编不兼容、DLL路径(将DLL放在自己的目录下面,别人的程序找不到,放在系统目录下,又可能有重名的问题,真正的组件应该可以放在任何地方甚至可以不在本机,用户根本不需考虑这个问题)、可执行程序的依赖性(如果DLL发行了一个新版本,很有必要重新链接一次,因为DLL里面导出函数的地址可能已经发生了改变)等问题。

   4>. COM编程
   COM中通过虚函数表查找注册表等手段能够很好的解决上述列举的DLL不足。

  所谓COM(Componet Object Model,组件对象模型) 是一种跨应用和语言共享二进制代码的规范,此规范提供了为保证能够互操作,应用程序和共享组件应遵循的一些二进制和网络标准。COM定义了这样的一种二进制标准
   首先,COM 明确指出二进制模块(DLLs和EXEs)必须被编译成指定的结构,这个标准也确切规定了在内存中如何组织COM对象
   其次,COM 定义的二进制标准必须独立于任何编程语言,即不允许出现类似DLL的名字改编不兼容的问题。

  通过这种标准实现可以任意使用共享组件而不用考虑应用程序与共享组件所处的操作环境是否相同、使用的开发语言是否一致以及是否运行在同一台计算机上。
  遵循COM规范编写的任何一个组件(特指在二进制级别上进行集成和重用而能够被独立开发和配置的软件单元)都可以被用来组合成应用程序。在COM规范下将能够以高度灵活的编程手段来开发、维护应用程序,可以将一个单独的复杂应用程序划分为多个独立的模块进行开发,这里的每一个独立模块都是自给自足的组件,可以采用不同的开发语言,在运行时将这些组件通过接口组装起来以形成所需要的应用程序。构成应用程序的每一个组件都可以在不影响其它组件的前提下升级。
  应用程序在与COM组件进行交互时(如:使用ADO COM库操纵MySQL数据库),只需知道与哪个COM对象进行交互即可,而不必关心组件模块的具体名称和所处的位置,也就是COM对象的位置对客户是透明的。通过调用COM库API来创建对象,通过Release方法减少对象自己保持的引用计数,当引用计数为零时,COM对象将自己从内存中释放(记得调用Release,减少不必要的内存开销)
  最后,得记住的是,COM不是Win32特有的,从理论上讲,它可以被移植到其他操作系统,因为它本身只是一种规范!!

第三节 配置ODBC数据源

  经过上述的描述后,你现在应该大概明白,所谓通过ADO操纵MySQL数据库,即 我们写的应用程序直接与ADO COM库的API打交道,ADO则使用访问ODBC的OLE DB提供程序去访问ODBC,然后再利用ODBC去访问我们的MySQL数据库! MySQL数据库支持ODBC,但需要进行相关配置,安装好MySQL数据库后需要为其安装ODBC驱动程序以及配置ODBC数据源,才可以在应用程序中通过ADO访问该ODBC数据源进而操纵MySQL数据库所有安装文件可以从这里获取,访问密码bbaa
  安装MySQL及其ODBC驱动程序后,我们需要先在MySQL数据库中为项目创建一个数据库,然后才可以为MySQL数据库配置ODBC数据源

  第一步,开始 控制面板 管理工具 打开数据源(ODBC)


  注意: 安装文件中用的是32位的MySQL,需要用32位的ODBC配置管理器,而一般64位系统存在的快捷方式都是64位的ODBC配置管理器(如上图Win7旗舰版管理工具下显示的数据源(ODBC)),这就需要你在 C:\Windows\SysWOW64\ 下找到32位的ODBC配置管理器 “odbcad32.exe”,双击运行它。Win8.1已经在这方面做出改进,如上图,32位和64位的数据源(ODBC)均将其列出!

  第二步,在用户DSN选项卡中点击”添加”按钮,弹出”创建新数据源”窗口

  第三步,选中”MYSQL ODBC 5.1 Driver”,点击”完成”按钮

  最后一步,正确输入数据源信息,进行Test验证


  注意: 此处的数据源信息与 下一节ADO编程使用的连接字符串 (“DSN=datasource_name; Database=database_name; uid=user_name;pwd=password”)密切相关,编程时使用的连接字符串必须与此处的配置信息保持一致才能访问成功。此处的Database下拉菜单中如不意外,将出现上面操作中为项目创建的数据库,我们选择其来创建数据源

  如果出现以下结果,测试成功,数据源已经成功配置!否则,一般是数据库密码没有正确输入,重新输入后再次Test

  数据库开发环境搭建完毕后,就需要结合项目功能需求考虑需要哪几张表,每张表的结构如何设计等问题。DEMO 中的数据库结构简单如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
drop database if exists ZD124UE_DEMO;
create database ZD124UE_DEMO;
use ZD124UE_DEMO;

# 用户余时表
drop table if exists RemainTimeTable;
create table RemainTimeTable(
  UID char(10) primary key,
  RemainTime int default 0
);

# 用户上机表
drop table if exists OnTable;
create table if not exists OnTable(
  UID char(10) primary key,
  RemainTime int default 0,
  # datetime格式必须是"%Y/%m/%d %H:%M:%S"
  StartTime datetime,
  isOvertime boolean default false,
  # 可以增加上机时间记录(单位:秒) UsingTime int default 0,
  foreign key(UID) references RemainTimeTable(UID)
);

第四节 ADO编程

  ADO通过OLE DB实现对不同类型数据源的访问。OLE DB是底层的编程接口,它通过一系列 COM接口 提供数据的底层连接,ADO也是以COM的方式提供,所以它的很多行为遵循COM规范。我们也直接用COM提供的ADO API访问MySQL数据库。首先要导入ADO COM文件
  @-引入ADO COM文件
  使用预处理指令#import导入 msado15.dll 这个 ADO COM文件:

1
2
3
4
5
6
/* 
* 导入ADO的COM文件
* no_namespace是指忽略命名空间,因此需要将EOF重命名为ADOEOF, BOF重命名为ADOBOF防止命名冲突
* 该代码需要在一行中完成,如果写成两行或者多行,行末要加上“\”续行符,表示把这几行看成一行
*/
#import "./libs/msado15.dll" no_namespace rename("EOF","ADOEOF") rename("BOF","ADOBOF")

  在XP及以上的系统都自带有 msado15.dll ,并且都已经在系统中注册(全局路径一般是C:\Program Files\Common Files\System\ado\msado15.dll),所以我们的应用程序打包时,可以不用将 msado15.dll 打包,程序可以直接在XP及以上的系统上运行。
  建议将 msado15.dll 拷贝到”工程目录/libs/“ 第三方库目录下(没有libs可以通过新建文件夹的方式创建),此处的 “./libs/msado15.dll” 正是通过此方式导入
  程序在编译过程中,VC++ 会读出 msado15.dll 中的类型库信息,自动产生两个该类型库的头文件( msado15.tlh )和实现文件( msado15.tli ),在这两个文件里定义了ADO的所有对象和方法,以及一些枚举型的常量等,我们只需直接调用这些方法就行了。

  @-初始化COM
  用ADO写代码前,要将COM 初始化,这时因为 COM组件 在使用前需要初始化 COM库,访问完 COM库 后,程序还需要卸载 COM库。常见的手段是在代码前后加上 CoInitialize(NULL); CoUninitialize(); ,也可以用 AfxOleInit(); AfxOleTerm(); 来初始化库,后一种方法具体实现中除了调用 CoInitialize(NULL); 初始化 COM库 外,还做了一些其他的操作,这些操作对 OLE 应用 来说是必须的。考虑到性能,一般使用第一种方法初始化。

  @-三个核心对象
  ADO主要包含了七个对象,包括三个独立对象(Connection、Recordset、Command)和依赖独立对象才能使用的四个非独立对象(Field、Parameter、Property、Error)。
  +. Connection连接对象: 表示到数据库的连接,管理应用程序和数据库之间的通信

  +. Recordset记录集对象: 存放查询的结果,结果由数据的行(称为记录)和列(称为字段)组成

  +. Command命令对象: 用于执行对数据库的操作

  +. Field字段对象: 依赖于Recordset对象使用,可使用 Fields集合 获得记录集中每个字段的信息

  +. Parameter参数对象: 依赖于Command命令对象使用,用于为参数查询提供数据

  +. Property属性对象: 每个连接、记录集、命令对象以及字段对象都有一个属性对象集合

  +. Error错误对象: 依赖Connection对象使用,连接对象的 Errors集合 保存错误信息

  要实例化Connection、Recordset、Command这三个核心对象,进而使用它们提供的方法,需要使用它们的智能指针_XxxxPtr (其中的”_Xxxx”可以是Connection、Recordset或者是Command)

  @-对象实例化
  通过名字创建对象可能会冲突,通过 uuid(通用唯一识别码) 创建对象则是唯一的。注意: 使用的名字必须为”ADODB.Xxxx”,其中的”Xxxx”可以是Connection、Recordset或者是Command!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 方式一: 通过名字创建
*/
_XxxxPtr m_pXxxx;
m_pXxxx.CreateInstance("ADODB.Xxxx");

# 或者直接初始化
_XxxxPtr m_pXxxx("ADODB.Xxxx");

/*
* 方式二: 通过uuid创建
*/
_XxxxPtr m_pXxxx;
m_pXxxx.CreateInstance(__uuidof(Xxxx));

# 或者直接初始化
_XxxxPtr m_pXxxx(__uuidof(Xxxx));

  所谓 智能指针_XxxxPtr ,在初始化(CreateInstance)或释放(Release)等操作时,它们是一个对象,用.操作符,其他大部分操作则使用->操作符。
  数据类型 _XxxxPtr 实际上是由类模板 _com_ptr_t 而得到的一个具体的实例类, msado15.tlh 中有

1
2
3
_COM_SMARTPTR_TYPEDEF(_Connection, __uuidof(_Connection));
_COM_SMARTPTR_TYPEDEF(_Recordset, __uuidof(_Recordset));
_COM_SMARTPTR_TYPEDEF(_Command, __uuidof(_Command));

  经宏扩展后就得到了 _XxxxPtr 类,该类封装了 Xxxx 对象、Idispath接口指针 及一些必要的操作,我们就是通过 智能指针_XxxxPtr 来操纵 Connection对象、Recordset对象和Command对象 !!!

  @- _ConnectionPtr实现数据库连接与断开
  ADO使用Connection对象来建立与数据库服务器的连接,只有建立了连接后,才能进行其他有关数据库的访问和操作。

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
/* 
* @function 连接数据库
* @param void
* @retval BOOL
* 连接成功返回true,失败则返回false
*/
BOOL CAdoMySQLHelper::MySQL_Connect(){
  # 初始化OLE/COM库环境
  CoInitialize(NULL); # ADO也是一个COM组件,COM组件在使用时需要调用CoInitialize初始化库
  /*
   * 另外一种方式初始化COM库,建议使用第一种
   * AfxOleInit();
   */
  try {
    # 通过名字创建Connection对象
    HRESULT hr = this->m_pConnection.CreateInstance("ADODB.Connection");
    if(FAILED(hr)){
      AfxMessageBox("创建_ConnectionPtr智能指针失败");
      return false;
    }

    # 设置连接超时时间
    this->m_pConnection->ConnectionTimeout = 600;
    # 设置执行命令超时时间
    this->m_pConnection->CommandTimeout = 120;

    /*
     * 打开数据库连接两种方式
     * Connection对象的Open方法中的连接字符串参数必须是_bstr_t类型,形式为:
     *  "DSN=datasource_name;Database=database_name;uid=user_name;pwd=password"
     * 调用Open方法,即通过ODBC Driver连接到Database Server
     * m_pConnection->Open("DSN=datasource_name;Database=database_name;uid=user_name;pwd=password",
     *            "",
     *            "",
     *            adModeUnknown);
     */
    this->m_pConnection->Open("DSN=MySQL5.5;Server=localhost;Database=ZD124UE_DEMO", #数据库连接字符串
           "root",
           "Tarantula7",
           adModeUnknown);
  } catch(_com_error &e){
    # 不能使用MessageBox函数,在CView CDialog等类之外要用全局函数AfxMessageBox
    AfxMessageBox(e.Description());
    return false;
  }
  return true;
}

  需要注意的是: 连接字符串的格式为:
    “DSN=datasource_name;Database=database_name;uid=user_name; pwd=password”其中各参数的设置需与你的ODBC数据源配置保持一致,如:这里本人的数据源DSN=MySQL5.5,数据库名=ZD124UE_DEMO,用户名=root,密码=Tarantula7
  此外CAdoMySQLHelper是一个自定义的数据库辅助类,m_pConnection是其_ConnectionPtr类型的私有成员变量;#后面内容为注释,这里只是为了显示美观才不用//后续代码与此部分雷同

  类似的,使用Connection对象断开数据库连接则相对简单一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 
* @function 断开数据库连接
* @param void
* @retval void
*/
void CAdoMySQLHelper::MySQL_Close(){
  if((NULL != this->m_pConnection) && (this->m_pConnection->State)){
    this->m_pConnection->Close(); # 关闭连接
    this->m_pConnection.Release(); # 释放连接
    this->m_pConnection = NULL;
  }

  # 访问完COM库后,需要调用CoUninitialize函数卸载COM库
  CoUninitialize();
  /*
   * 另外一种方式卸载COM库,建议使用第一种
   * AfxOleTerm();
   */
}

  注意: 使用Release方法释放智能指针在相应的COM接口的引用计数,回顾上一段有关核心对象实例化的描述,也应该明白此处必须使用·操作符
  此外,数据库的连接与断开通常处于一系列数据库操作的开始与结尾,故此处在数据库连接函数开头处初始化COM库,在数据库断开函数结尾处卸载COM库!

  @-_RecordsetPtr打开数据集,操作数据库
  使用 Recordset记录集对象 几乎可以完成所有数据操作。

   <1. 查询记录
  通过调用 Recordset记录集对象 Open方法即可打开一个数据集。

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
# 打开数据集SQL语句,table_name为表名
_variant_t sql = "SELECT * FROM table_name";

try{
  # 定义_RecordsetPtr智能指针,然后通过它调用Recordset记录集对象的Open方法
  _RecordsetPtr m_pRecordset;
  HRESULT hr = m_pRecordset.CreateInstance(__uuidof(Recordset));
  if(FAILED(hr)){
    AfxMessageBox("创建_RecordsetPtr智能指针失败");
    return;                  # 中止退出
  }

  # 打开连接,获取数据集
  m_pRecordset->Open( sql,
             _variant_t((IDispatch*)(this->m_pConnection), true),
             adOpenForwardOnly,  # 游标类型
                         # adOpenStatic(其他用户所做的添加、更改获删除不可见)
                         # adOpenDynamic(其他用户所做的添加、更改获删除均可见)
             adLockReadOnly,    # 表示数据库的锁定类型
                         # adLockReadOnly(只读记录集)
                         # adLockOptimistic(仅在调用m_pRecordset->Update方法时锁定记录)
                         # adLockBatchOptimistic(乐观批更新)
             adCmdText);      # 如果Source是SQL语句,则是adCmdText
                         # 如果Source是表名,则是adCmdTableDirect

  # 确定表不为空
  if(!m_pRecordset->ADOEOF){
    # 移动游标到最前,即ADOBOF
    m_pRecordset->MoveFirst();

    # 循环遍历数据集,可以获取每行记录每列字段的属性值
    while(!m_pRecordset->ADOEOF){
      # 方式一
      _variant_t var1 = m_pRecordset->Fields->GetItem("字段名")->Value;
      # 此处需要进行类型转换
      var1.ChangeType(VT_INT);
      int varInt = var1.intVal;

      # 方式二
      _variant_t var2 = m_pRecordset->Fields->GetItem("字段名")->GetValue();
      # 此处需要进行类型转换
      var2.ChangeType(VT_BOOL);
      BOOL varBool = (var2.boolVal == VARIANT_FALSE);

      # 方式三
      _variant_t var3 = m_pRecordset->GetCollect("字段名");
      # 此处需要进行类型转换
      CString varString = (LPCSTR)_bstr_t(var3);
      # 游标向前移动
      m_pRecordset->MoveNext();
    } // end of while(!m_pRecordset->ADOEOF)
    m_pRecordset->Close();
  } // end of if(!m_pRecordset->ADOEOF)
}catch(_com_error &e){
  AfxMessageBox(e.ErrorMessage());
}

  Open方法
    第一个参数可以是SQL语句、表名或一个命令对象;
    第二个参数就是前面建立的连接对象智能指针,编程时参考上述示例代码进行转换以满足参数类型要求;
    第三个参数为游标类型
    第四个参数为数据库的锁定类型
    第五个参数指定第一个参数Source的命令类型
  第3-5个参数取值和说明如下表


  建议:
    游标类型采用默认的adOpenForwardOnly即可。
    数据库的锁定类型如果只是查询记录,使用adLockReadOnly 只读即可如果涉及记录的修改,添加和删除的,则使用adLockOptimistic。
    至于命令类型,如果第一个参数是SQL语句,采用adCmdText,如果是表名,则是adCmdTableDirect

  另外,Fields Recordset对象 的容器,GetItem方法返回的是Field对象,而Value 则是Field对象 的一个属性,即该字段的值(即方式一),也可以利用属性的Get方法获得属性(即方式二)。

  判断是否达到记录集的末尾,使用记录集的EOF属性,其值为真则到了结尾;判断是否到达记录集的开头,则可用BOF属性(注意二者的重命名问题)
  上述过程中的类型转换将在后续的《数据使用与类型转换》部分详细说明。

   <2. 修改记录
  在上述查询记录中,改变了Value属性的值,即改变了字段的值,同样地,也有三种方式。

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
# table_name为表名
_variant_t table_name = "table_name";

try{
  # 定义_RecordsetPtr智能指针,然后通过它调用Recordset记录集对象的Open方法
  _RecordsetPtr m_pRecordset;
  HRESULT hr = m_pRecordset.CreateInstance(__uuidof(Recordset));
  if(FAILED(hr)){
    AfxMessageBox("创建_RecordsetPtr智能指针失败");
    return;                  # 中止退出
  }

  # 打开连接,获取数据集
  m_pRecordset->Open( table_name,
             _variant_t((IDispatch*)(this->m_pConnection), true),
             adOpenForwardOnly,   # 游标类型
             adLockOptimistic,    # 仅在下方调用m_pRecordset->Update方法时锁定记录
             adCmdTableDirect);   # Source是表名,则是adCmdTableDirect

  # 确定表不为空
  if(!m_pRecordset->ADOEOF){
    # 移动游标到最前,即ADOBOF
    m_pRecordset->MoveFirst();

    # 循环遍历数据集,获取每行记录每列字段的属性值
    while(!m_pRecordset->ADOEOF){
      # 寻找满足某个条件的记录
      if(属性值 == (m_pRecordset->Fields->GetItem("字段名")->Value)){
        # 方式一
        _variant_t var1;
        var1.ChangeType(VT_INT);
        var1.intVal = 123;
        m_pRecordset->Fields->GetItem("字段名")->Value = var1;

        # 方式二
        _variant_t var2;
        var2.ChangeType(VT_BOOL);
        var2.boolVal == VARIANT_TRUE;
        m_pRecordset->Fields->GetItem("字段名")->PutValue(var2);

        # 方式三
        _variant_t var3 = "xxx";
        m_pRecordset->PutCollect("字段名", var3);

        # 必须在移动游标前执行更新!!
        m_pRecordset->Update();

        break;
      }
      # 游标向前移动
      m_pRecordset->MoveNext();

    } // end of while(!m_pRecordset->ADOEOF)
    m_pRecordset->Close();

  } // end of if(!m_pRecordset->ADOEOF)
}catch(_com_error &e){
  AfxMessageBox(e.ErrorMessage());
}

  注意: 此处涉及对数据库的修改,故第四个参数数据库锁定类型需采用adLockOptimistic,这样,数据集仅在执行第49行代码Update方法 时锁定又第一个参数Source是表名,相应的最后一个参数应使用adCmdTableDirect。采用三种方式中的任何一种改变字段的值之后,必须在移动游标或者关闭数据库前调用Update 方法 更新数据集到数据库。

   <3. 添加记录
  利用 Recordset对象 也能实现增、删操作,不过建议使用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
# table_name为表名
_variant_t table_name = "table_name";

try{
  # 定义_RecordsetPtr智能指针,通过它调用Recordset记录集对象的Open方法
  _RecordsetPtr m_pRecordset;
  HRESULT hr = m_pRecordset.CreateInstance(__uuidof(Recordset));
  if(FAILED(hr)){
    AfxMessageBox("创建_RecordsetPtr智能指针失败");
    return;                  # 中止退出
  }

  # 打开连接,获取数据集
  m_pRecordset->Open( table_name,
             _variant_t((IDispatch*)(this->m_pConnection), true),
             adOpenForwardOnly,    # 游标类型
             adLockOptimistic,     # 仅在调用m_pRecordset->Update方法时锁定记录
             adCmdTableDirect);    # Source是表名,则是adCmdTableDirect
  if(!m_pRecordset->Supports(adAddNew)){
    return;  # 不允许添加记录,中止退出
  }
  # 新纪录添加成功后,即自动成为当前记录
  m_pRecordset->AddNew();

  # 使用三种方式中的任意一种为新添加的记录各个字段赋值
  # ...

  # 关闭数据库前执行更新
  m_pRecordset->Update();
  m_pRecordset->Close();
}catch(_com_error &e){
  AfxMessageBox(e.ErrorMessage());
}

   <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
# 打开数据集SQL语句,table_name为表名字符串,cond为类似"字段=属性值"的条件字符串
_variant_t sql = "SELECT * FROM " + table_name + " where " + cond;

try{
  # 定义_RecordsetPtr智能指针,通过它调用Recordset记录集对象的Open方法
  _RecordsetPtr m_pRecordset;
  HRESULT hr = m_pRecordset.CreateInstance(__uuidof(Recordset));
  if(FAILED(hr)){
    AfxMessageBox("创建_RecordsetPtr智能指针失败");
    return;                  # 中止退出
  }

  # 打开连接,获取数据集
  m_pRecordset->Open( sql,
             _variant_t((IDispatch*)(this->m_pConnection), true),
             adOpenForwardOnly,   
             adLockOptimistic,
             adCmdText);

  # 确定表不为空
  if(!m_pRecordset->ADOEOF){
    # 移动游标到最前,即ADOBOF
    m_pRecordset->MoveFirst();

    # 循环遍历数据集,删除所有满足条件的记录
    while(!m_pRecordset->ADOEOF){
      # 删除的是当前游标所在记录
      m_pRecordset->Delete(adAffectCurrent);
   
      # 必须在移动游标前执行更新!!
      m_pRecordset->Update();
      m_pRecordset->MoveNext();
    } // end of while(!m_pRecordset->ADOEOF)
    m_pRecordset->Close();

  } // end of if(!m_pRecordset->ADOEOF)
}catch(_com_error &e){
  AfxMessageBox(e.ErrorMessage());
}

  注意: cond条件字符串 查询条件为文本格式 时要求用引号,如varchar、char、time、datetime类型等(单引号、双引号一般都有效);反之数值格式不要加引号,如bit、double、float、int类型等(不过加了引号也不会报错,会自动转换类型,但是最好不要加引号)。 SQL语句 拼接时注意拼接字符串间是否需要添加空格,防止拼接字符串中的两个关键字因连接在一次而出错。

  @-_CommandPtr执行SQL语句
  使用 Command对象 的关键就是把表示命令的MySQL语句设置到CommandText属性 中,然后再调用 Command对象 Execute方法 执行。一般情况下在命令中无需使用参数,但有时使用参数,可以增加其灵活性和效率。SQL语句 中的问号?就代表参数,如果有多个参数,就多放几个问号?,每个问号?代表一个参数。

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
/* 
* @function 对数据库进行删除操作
* @param
* @CString uid唯一标识删除记录
* @Cstring table指定删除记录所在表名
* @retval BOOL
* 成功删除返回true,失败则返回false
*/
BOOL CAdoMySQLHelper::MySQL_Delete(CString uid, CString table){
  _CommandPtr m_pCommand;
  try{
    HRESULT hr = m_pCommand.CreateInstance("ADODB.Command");
    if(FAILED(hr)){
      AfxMessageBox("创建_CommandPtr智能指针失败");
      return false;
    }
    # 定义为无参数
    _variant_t vNULL;
    vNULL.vt = VT_ERROR;
    vNULL.scode = DISP_E_PARAMNOTFOUND;
    # 注意文本格式数据需要使用引号,\'此处使用转义符;拼接字符串间合理使用空格!
    m_pCommand->CommandText = "delete from " + (_bstr_t)table + " where UID=\'" + (_bstr_t)uid +"\'";
    
    # 非常关键的一句,将建立的连接与命令对象绑定
    m_pCommand->ActiveConnection = this->m_pConnection;

    # 执行删除命令
    m_pCommand->Execute(&vNULL, &vNULL, adCmdText);
  } catch(_com_error &e) {
    AfxMessageBox(e.ErrorMessage());
    return false;
  }
  return true;
}

  当命令对象的SQL语句 带有参数时,可以通过以下方式为其添加参数数据

1
2
3
4
5
_ParameterPtr Param;
# adInteger为DataTypeEnum,表示数据类型;adParamInput则表示参数方向
Param = m_pCommand->CreateParameter(_bstr_t("参数名"), adInteger, adParamInput, -1);
Param->Value = _variant_t("参数值");
m_pCommand->Parameters->Append(Param);

  第4个参数为长度,如果是固定长度的数据类型该参数为-1;如果是字符串等可变长度类型,该参数的值为实际长度。Parameters Command对象 的一个容器,Append进去的参数按先后顺序与SQL语句中的?从左到右一一对应。

  注意: Command 和 Connection对象 也能得到一个记录集,不过这种方式得到的Recordset 是只读的,因为在打开Recordset 之前,我们无法设置它的 LockType(故使用默认值adLockReadOnly),而在打开之后设置的LockType 将不起作用。故一般我们通过 Command对象 来执行Insert、Delete、Update三类SQL语句,并不需要操作数据集; Command对象 代码简洁( 你可以比较上述两种实现),能够达到简化数据库操作的目的。

  @-数据使用与类型转换
  计算机语言多种多样,各自又都有自己的数据类型,COM 产生目的,其中之一就是跨语言。_variant_t 数据类型就具有跨语言的特性,同时它可以表示(存储)任意类型的数据。从C语言的角度来讲,_variant_t 其实是一个结构,结构中用一个域(vt)表示该变量到底表示的是什么类型数据,同时真正的数据则存贮在union空间中。常见的COM类型 类型转换使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_variant_t var;

# 对_variant_t变量赋值
var.ChangeType(VT_BOOL/VT_INT/VT_BSTR...);
var.intVal/boolVal/bstrVal... = xxx;

# 从_variant_t变量取值
if(var.vt != VT_NULL){
  var.ChangeType(VT_BOOL/VT_INT/VT_BSTR...);
  int/bool/CString... xxx = var.intVal/boolVal/bstrVal...;
}

# 对于字符串,建议通过以下方式进行赋值和取值操作
var = "...";
xxx = (LPCSTR)_bstr_t(var);

  其中需要留意的是VT_BOOL类型与更为常见的bool 类型的区别


  注意到VARIANT_TRUE 的值并不是1,而是-1! 另外,任何布尔类型的”假”都是0,因此作为一个好习惯,在做布尔判断的时候,不要和”真值”相比较,而要与”假值”做比较
  
  @-关闭与释放
  用完三大核心对象之后,需要关闭并释放之,方法还是通过智能指针来实现,这一点在上述的实例代码中并没有很好的实现,是一个值得考虑和改进的地方(参见下一段错误捕获的示例代码)
1
2
3
4
5
6
m_Recordset->Close();
m_pConnection->Close();
# 注意Command对象没有Close方法
m_Recordset.Release();
m_pCommand.Release();
m_pConnection.Release();

  @-错误捕获
  数据库操作难免出现错误(连接串错误,SQL语句错误等)。对所有调用ADO COM库的语句一定要用 try-catch 语句捕捉异常,否则发生异常时,程序会异常退出。

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
try {
  /*
   * 调用ADO API的所有语句
   */
} catch(_com_error &e){
  # 若连接打开,需要在异常处理中关闭和释放连接
  if((NULL != this->m_pConnection) && this->m_pConnection->State){
    this->m_pConnection->Close();
    this->m_pConnection.Release();
    this->m_pConnection = NULL;
  }

  # 若记录集打开,需要在异常处理中关闭和释放记录集
  if((NULL != m_pRecordset) && m_pRecordset->State){
    m_pRecordset->Close();
    m_pRecordset.Release();
    m_pRecordset = NULL;
  }

  # 需要在异常处理中释放命令对象
  if((NULL != m_pCommand) && m_pCommand->State){
    m_pCommand.Release();
    m_pCommand = NULL;
  }

  # 不能使用MessageBox函数,在CView、CDialog等之外要用全局函数AfxMessageBox
  AfxMessageBox(e.Description());
}

  注意: “&”只是一个引用,写不写无所谓,切忌写成 “_com_error *e” ,因为这时异常是 _com_error 类型却对着 _com_error* 来捕获,当然捕获不到!! 可能还是会导致程序异常退出。
  另外,catch块 中只需释放try 块中操纵的对象即可,上述代码只是为了演示所有例子。 
  
  @-封装自己的数据库辅助类
  习惯上,我们会将对数据库的一系列重复的操作封装起来,构建一个数据库辅助类,提供数据库连接、断开、以及基本的增、删、改、查操作。 这样,在应用开发过程中,我们就可以直接通过 数据库辅助类 完成需要实现的功能,大大简化代码,也使应用开发更有目的,更有层次;提高效率,方便维护。
  VC++类视图选中”工程名 classes”右键 New Class Class Type选择Generic Class,输入数据库辅助类类名, 即可自动生成辅助类代码框架。DEMO中的数据库辅助类定义如下

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
# RemainTimeTable余时表记录结构
struct RemainTime{
  CString UID;
  int RemainSeconds;
};

# OnTable上机表记录结构
struct OnRecord{
  CString UID;
  int RemainSeconds;
  CString StartTime;
  BOOL isOvertime;
};

class CAdoMySQLHelper
{
public:
  # 构造函数&析构函数
  CAdoMySQLHelper();
  # virtual是因为当用基类操作派生类(派生类引用赋值给基类指针),在析构时防止只析构基类而不析构派生类的情况
  virtual ~CAdoMySQLHelper();

  # 连接数据库
  BOOL MySQL_Connect();
  # 关闭数据库
  void MySQL_Close();

  # 数据库增、删、改、查操作函数
  # 对数据库进行插入操作
  BOOL MySQL_Insert(struct RemainTime record);
  BOOL MySQL_Insert(struct OnRecord record);
  # 对数据库进行删除操作
  BOOL MySQL_Delete(CString uid, CString table);
  # 对数据库进行更新操作
  BOOL MySQL_UpdateRemainTime(CString uid, int updateTime);
  # 对数据库进行查询操作,查询语句为cond,例如: "UID = xxxx"
  void* MySQL_Query(CString cond, CString table);
  # 定时扫描OnTable,使用扫描周期timer更新当前上机用户余时,捕捉超时用户
  void MySQL_ScanOnTable(int timer);

private:
  # 保存打开的数据库连接智能指针
  _ConnectionPtr m_pConnection;
};

  说明:
   除了数据库的连接和断开属于典型、通用的功能外,其他关于数据库的 增、删、改、查 操作建议在需要时才添加,同时根据自己项目的实际需求对接口进行调整(调整参数、调整返回值等),必要时也需要添加新的特定功能的接口函数。
   另外,强烈建议采用 static 将各部分接口调整为类方法,这样使用辅助类将更加方便,直接通过辅助类调用接口 即可;也更高效,不需要对辅助类进行实例化 即可使用接口!

最后,小结

  本文通过对当前常用的几种数据库访问技术的介绍,引出项目中使用数据库的开发原理,进而推出项目数据库的开发流程。详细地介绍了如何通过ADO编程,实现操纵MySQL数据库。出于ADO编程采用其COM库API这种方式,故跟大家简短叙说了COM规范 的由来,主要是领会其中蕴含的编程思想。相信到这里,你对如何使用ADO操纵MySQL数据库 肯定有了比较深刻的认识。思路清晰了,那就开始Coding吧!!!

附录 - - 常用的SQL语句整理

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
# 修改数据库密码
use mysql;
update user set password=password('新密码') where user='root';
flush privileges;

# 查看数据库是32位还是64位
show variables like "%version_%";
# 查询结果中的version_compile_os即为数据库位数,Win32和Win64分别对应32位和64位

# 创建数据库
drop database if exists database_name;
create database database_name;

# 查看所有数据库名
show databases;

# 表操作前需要使用某个数据库 注意database_name大小写不敏感
use database_name;

# 创建表
drop table if exists table_name;
create table table_name(
  字段1名字 字段1类型 字段1属性,
  字段2名字 字段2类型 字段2属性,
  ...
);

# 查看所有表名
show tables;

# 查看表的结构时使用,注意table_name大小写不敏感
show create table table_name;

# 调整表某个字段的数据类型
alter table table_name modify column 字段 新的数据类型;

# 删除表
drop table if exists table_name;

# 删除数据库
drop database if exists database_name;

# 查询记录(where不写则查询所有记录)
select * from table_name [where condition];

# 添加记录
insert into table_name(字段1, 字段2...) values(字段1值, 字段2值...);
insert into table_name values(字段1值, 字段2值, 字段3值, ..., 最后一个字段值);

# 更新记录(where不写则更新所有记录)
update table_name set 字段=字段值 [where condition];

# 删除记录(where不写则删除所有记录)
delete from table_name [where condition];

# 其他的自行度娘"MySQL下如何xxxx"

Version Control


版本号日期内容作者
V0.12015.11.29起草博客Tarantula-7
文章目录
  1. 1. 第一节 数据库访问技术
  2. 2. 第二节 COM(Componet Object Model)
  3. 3. 第三节 配置ODBC数据源
  4. 4. 第四节 ADO编程
  5. 5. 最后,小结
  6. 6. 附录 - - 常用的SQL语句整理
  7. 7. Version Control