数据库,相信是每个计算机相关专业的大学生无法绕开的课程。在最开始我们都是通过手动键入 $SQL$ 指令来对数据库进行操作,但这样未免会出现局限性,对于更复杂的操作,我们可能想通过 $C++$ 处理好再丢入数据库或者从数据库读取后由 $C++$ 处理后再呈现在我们面前。而 $ODBC$ ,开放数据库连接则完美解决了这一问题。下面我就来为各位带来 $ODBC$ 的保姆级教学。

SQL Server配置

创建数据库

要想实现通过 $C++$ 访问和修改数据库,首先我们得有个数据库。我们新建数据库 $test$,在里面新建查询,键入下列代码:

create table student
    (id         int,
     name       nvarchar(50),
     age        smallint,
     primary key (id)
    );

insert into student values (1001, 'Jvruo', 18);
insert into student values (1002, 'Konjac', 20);

运行并执行成功:
16.png

这是我们看我们的表格,发现已经成功添加了:

18.png

具体到类型,在我们这个 $student$ 表格中,主键是 $int$ 类型的 $id$ 也就是学号,其余还有一个 $smallint$ 类型的 $age$ 也就是年龄,还有一个 $nvarchar(50)$ 类型的 $name$ 也就是名字。那么这个 $nvarchar(50)$ 是什么呢? 如果这里写的是 $varchar(50)$,相信大家都认识,不就是可变长的字符串嘛。而这个 $nvarchar$ 相较于 $varchar$ 的区别就主要在于 $nvarchar$ 指定这个字符串是 $Unicode$ 编码的。相较于多字节编码,$Unicode$ 编码的移植性更胜一筹,所以在接下来的代码里的所有字符串输出,我们都会尽量使用 $Unicode$ 编码。

创建管理员用户

我们在之前的 $SQL \space Server$ 实验当中,使用的登录方式都是直接的 $Windows$ 身份验证。

27.png

但是这在 $ODBC$ 是行不通的,$ODBC$ 留给我们的最便利的接口是使用 $Sql \space Server$ 身份验证。所以在这里我们先要创建一个管理员的用户账号,具体操作如下。

我们进入 $SQL \space Server$,在左侧状态栏依次选择安全性-登录名,然后我们右键登录名,选择新建登录名:

28.png

然后我们在常规中选择 $Sql \space Server$ 身份验证,写入登录名和密码,取消勾选强制实施密码策略:

13.png

然后我们跳到服务器角色,勾选 $public$ 也就是用户以及 $sysadmin$ 也就是管理员:

14.png

最后我们来到用户映射,勾选我们刚刚选好的 $test$ 数据库,下面勾选 $public$ 也就是用户以及 $db_owner$ 也就是有最高权限的数据库拥有者:

15.png

到这里 $SQL \space Server$ 的配置就告一段落了。

ODBC配置

我们打开控制面板,依次选择 系统和安全 - 管理工具 - $ODBC$ 数据库($64$ 位)。

5.png

我们点击进入,在右侧边栏选择新建,在驱动程序中选择 $SQL \space Server$,选择下一步:

6.png

在这里,我们给我们的数据源命名为 $SQLServerODBC$,在服务器的选项中,由于我们连接的是本地的服务器,所以我们写 $localhost$ 或 $127.0.0.1$ 都是可以的。这里顺带科普一下,这里写 $localhost$ 和 $127.0.0.1$ 之所以是等价的,是因为 $localhost$ 的本质是个域名,指向的就是本地也就是 $127.0.0.1$:

7.png

然后我们进入下一步,选择使用用户输入登录 $ID$ 和密码的 $SQL \space Server$ 验证,在下面写上我们的账号 $Jvruo$ 和密码 $Jvruo233$:

8.png

接着我们更改默认数据库为 $test$,其他选项保留默认,进入下一步:

9.png

在这里我们核验一下我们设置的 $ODBC$ 信息是否有误:

10.png

我们可以点击测试数据源检测能否顺利连接:

11.png

确认没问题后点击确定,就可以看到我们多了一条 $ODBC$ 记录:

12.png

到这里 $ODBC$ 的配置也就顺利完成了。

VS编程实现

接下来就进行我们的编程环节了,首先我们进入 $VS$,创建一个控制台应用项目。

ODBC相关环境设置

ODBC分块讲解

头文件设置

首先我们先来看看我们要用到的头文件。首先输入输出的 $iostream$ 肯定是少不了的;由于要用到 $windows.h$;至于处理 $SQL$ 肯定也有相关的库,这里用到的是三个 $sql.h$、$sqlext.h$ 和 $sqltypes.h$;最后我们要考虑我们程序的兼容性,我们正常来说我们程序输出的中文是多字节编码的,但在很多机器上是不支持多字节编码的,所以我们考虑使用 $Unicode$ 编码,而 $tchar.h$ 库就提供了函数让我们把字符串强制转为 $Unicode$ 编码,所以我们的头文件如下:

#include <iostream>
//ODBC用到的头文件
#include <windows.h>
#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include <tchar.h>
错误检查函数

错误检查是健壮的程序不可缺少的重要组成部分,而 $ODBC$ 中的每个函数基本都是有返回值的,返回类型是 $SQLRETURN$,所以我们需要一个函数接受这个返回值看看是否发生错误,也就是其值等于 $SQL_ERROR$ 。所以这个检测函数的大体如下:

void check(SQLRETURN ret, wstring type) {
    if (ret == SQL_ERROR) {
        wcout << type << _T("失败!") << endl;
        exit(-1);
    }
    return ;
}

但是仅仅这样是远远不够的,我们不仅要知道在哪里错了,还想知道为什么错了。所以我们调用 $SQLError$ 函数,传回错误方式和错误类型的字符串 $state$ 和 $msg$。由于 $SQLError$ 函数需要用到环境句柄,连接句柄和语句句柄,所以我们传入时也顺带传入这三个参数。我们代码如下:

void check(SQLRETURN ret, wstring type, SQLHENV hEnv, SQLHDBC hDbc, SQLHSTMT hStmt) {
    if (ret == SQL_ERROR) {
        wcout << type << _T("失败!") << endl;
        //获取错误信息 
        SQLTCHAR state[128] = { 0 };
        SQLTCHAR msg[128] = { 0 };
        SQLRETURN ret = SQLError(hEnv, hDbc, hStmt, state, NULL, msg, sizeof(msg), NULL);
        wcout << state << " " << msg << endl;

        exit(-1);
    }
    return ;
}
句柄设置

我们接下来进入主函数,$ODBC$ 是面向过程的程序设计,需要用到句柄。所以首先我们要声明和分配句柄。

在这里我们一共要声明四种句柄—环境句柄,连接句柄和语句句柄;用作接受返回值进行错误返回的返回值 $ret$ 也勉强算是一个句柄:

//声明环境句柄
SQLHENV hEnv = NULL;
//声明连接句柄
SQLHDBC hDbc = NULL;
//声明语句句柄
SQLHSTMT hStmt = NULL;

接下来我们要给句柄们进行分配:

 //分配环境句柄
 ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
 check(ret, _T("分配环境句柄"), hEnv, hDbc, hStmt);
 //设定ODBC版本
 ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
 check(ret, _T("ODBC版本设置"), hEnv, hDbc, hStmt);
 //分配数据库连接句柄
 ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
 check(ret, _T("分配连接句柄"), hEnv, hDbc, hStmt);
 //根据DSN连接数据库,SQL_NTS为自动计算前面字符串长度

至于语句句柄为什么不在这里进行分配是因为两个原因,一个是语句句柄必须要在连接数据库也就是我们的下一步之后,二是我们的代码还是主张用啥分配啥,我们还是把分配放在后面实际使用 $ODBC$ 函数插入 $SQL$ 语句那部分。

这里大部分都是纯纯的模板,照抄就好了。有两个地方需要注意,一个是在设定 $ODBC$ 版本号这个位置,如果出现错误,就请更新一下 $ODBC$ 的版本或者查询你现有的 $ODBC$ 版本经行更改就好了;第二个是我们每一步都要记得用 $ret$ 接受返回值,然后丢到 $check$ 当中去判断有没有错误。

连接数据库

这是整个环境设置当中比较重要的一环,我们需要用到 $SQLConnect$ 函数,其语法大致为 SQLConnect(连接句柄,数据源字符串,数据源字符串长度,数据库登陆账号字符串,数据库登陆账号字符串长度,数据库登陆密码字符串,数据库登陆密码字符串长度)。数据源字符串就是我们在 $ODBC$ 设置当中设置的 $SQLServerODBC$ ,而账号密码就是我们在 $SQL \space Server$ 配置当中新建的用户 $Jvruo$ 对应密码 $Jvruo233$。这里要提一下,关于这几个字符串长度,你当然可以自己数出有多少多少个字符然后填上字符串长度,当然你也可以使用 $SQL_NTS$ 自动计算前面字符串的长度。可以我们填入相关信息:

//根据DSN连接数据库,SQL_NTS为自动计算前面字符串长度
ret = SQLConnect(hDbc, (SQLTCHAR*)_T("SQLServerODBC"), SQL_NTS, (SQLTCHAR*)_T("Jvruo"), SQL_NTS, (SQLTCHAR*)_T("Jvruo233"), SQL_NTS);
check(ret, _T("连接数据库"), hEnv, hDbc, hStmt);
wcout << _T("连接数据库成功!") << endl;
断开连接&释放句柄

在代码的最后,首先我们要断开和数据库的连接,这就直接使用连接句柄进行操作就可以了。具体来说,我们使用函数 SQLDisconnect(hDbc) 就可以断开我们和数据库之间的连接。

此外就像我们 $malloc$ 了新的内存空间需要释放一样,我们的句柄也需要释放。我们的句柄本质上就是一个 $void$ 类型的指针,所以我们直接判断他们是否指向 $NULL$,如果不指向 $NULL$,就将其指向的句柄内存空间释放就好了。

我们再最后断开和释放的时候要严格遵循 释放语句句柄-->断开数据库连接-->释放连接句柄-->释放环境句柄 的顺序进行。

最后我们输出断开连接的提示语:

//释放语句句柄
if (hStmt) {
    SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
}
//释放连接句柄
if (hDbc) {
    SQLDisconnect(hDbc); //断开连接
    SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
}
//释放环境句柄
if (hEnv) {
    SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
}
wcout << _T("断开数据库连接!") << endl;

ODBC代码模板

综合以上环境设置组件,我们就得到了下面这个 $ODBC$ 的代码模板,这也是我们之后进行任何 $ODBC$ 开发的基础。在日后的开发中,我们只要在连接数据库和释放句柄当中填入我们需要的操作,比如插入数据、查询数据或者修改数据的相关 $ODBC$ 代码就可以了:

#include <iostream>
//ODBC用到的头文件
#include <windows.h>
#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include <tchar.h>
using namespace std;

void check(SQLRETURN ret, wstring type, SQLHENV hEnv, SQLHDBC hDbc, SQLHSTMT hStmt) {
    if (ret == SQL_ERROR) {
        wcout << type << _T("失败!") << endl;
        //获取错误信息 
        SQLTCHAR state[128] = { 0 };
        SQLTCHAR msg[128] = { 0 };
        SQLRETURN ret = SQLError(hEnv, hDbc, hStmt, state, NULL, msg, sizeof(msg), NULL);
        wcout << state << " " << msg << endl;

        exit(-1);
    }
    return ;
}

int main()
{
    //设置为中文兼容unicode
    _wsetlocale(LC_ALL, L"chs");

    //声明环境句柄
    SQLHENV hEnv = NULL;
    //声明连接句柄
    SQLHDBC hDbc = NULL;
    //声明语句句柄
    SQLHSTMT hStmt = NULL;
    //声明返回值
    SQLRETURN ret;

    //分配环境句柄
    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
    check(ret, _T("分配环境句柄"), hEnv, hDbc, hStmt);
    //设定ODBC版本
    ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
    check(ret, _T("ODBC版本设置"), hEnv, hDbc, hStmt);
    //分配数据库连接句柄
    ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
    check(ret, _T("分配连接句柄"), hEnv, hDbc, hStmt);
    //根据DSN连接数据库,SQL_NTS为自动计算前面字符串长度
    ret = SQLConnect(hDbc, (SQLTCHAR*)_T("SQLServerODBC"), SQL_NTS, (SQLTCHAR*)_T("Jvruo"), SQL_NTS, (SQLTCHAR*)_T("Jvruo233"), SQL_NTS);
    check(ret, _T("连接数据库"), hEnv, hDbc, hStmt);
    wcout << _T("连接数据库成功!") << endl;

    //…………………………………………

    //释放语句句柄
    if (hStmt) {
        SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
    }
    //释放连接句柄
    if (hDbc) {
        SQLDisconnect(hDbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
    }
    //释放环境句柄
    if (hEnv) {
        SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
    }
    wcout << _T("断开数据库连接!") << endl;
    return 0;
}

这是我们直接运行代码就可以看到我们的数据库成功的连接最后又成功的断开:

19.png

实现插入数据

插入分块讲解

分配语句句柄

和前面分配环境句柄和连接句柄一样,我们在这里分配语句句柄并进行 $check$ 检查:

//分配语句句柄hStmt = NULL;ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);check(ret, _T("语句句柄分配"), hEnv, hDbc, hStmt);
准备SQL语句

在准备 $SQL$ 插入语句这部分,我们分为简单版和进阶版,我们下面进行一一的讲解。

简单版

假如我们想要往数据库里的 $student$ 表里插入一条学号是 $1003$,名字是 $Mushu$,年龄是 $24$ 的记录。那我们用 $SQL$ 写出来就是:

INSERT INTO student VALUES('1003', 'Mushu', 24)

我们用一个 $SQLTCHAR$ 类型的字符串 $sql$ 来存储我们要传入的 $SQL$ 指令字符串,这里的 $SQLTCHAR$ 类型指的是 $Unicode$ 编码下的字符串。接着我们用 $SQLPrepare$ 函数准备或者说编译我们的 $SQL$ 指令字符串,其参数大致是 SQLPrepare(语句句柄, SQL指令字符串, SQL指令字符串长度),我们填入参数进行编译准备就可以了:

//简单版本准备SQL语句
SQLTCHAR sql[] = _T("INSERT INTO student VALUES('1003', 'Mushu', 24)");
ret = SQLPrepare(hStmt, sql, SQL_NTS);
check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);
进阶版

对于简单版,我们一定要将要运行的字符串预先写好存在一个叫 $sql$ 的字符串中,但其实我们想我们传入的参数在后面再进行定义,而不是一步到位,这当然是可以做到的。

首先我们还是先写好我们的 $SQL$ 语句存放在 $sql$ 这个字符串里,这里不同的是,我们参数的部分用了 $?$ 进行替代,这就让我们延迟了传入参数的选择。我们下面还是使用 $SQLPrepare$ 进行预编译准备:

//复杂版本准备SQL语句
SQLTCHAR sql[] = _T("INSERT INTO student VALUES(?, ?, ?)");
ret = SQLPrepare(hStmt, sql, SQL_NTS);
check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);

接下来我们准备好要传入的三个参数,这里要注意传入的参数类型一定要和我们数据库里面的对应列的类型相同:

//三个参数传递到SQL语句中
SQLINTEGER id = 1004;
SQLTCHAR name[50] = _T("CCKK");
SQLSMALLINT age = 88;

最后我们要进行绑定 $SQL$ 语句和我们后面我们定义的参数,这里我们要用到 $SQLBindParameter$ 函数,其参数定义如下:

SQLRETURN SQLBindParameter(
      SQLHSTMT        StatementHandle,     // statement句柄
      SQLUSMALLINT    ParameterNumber,    // 参数位于语句中的序号,最小为1
      SQLSMALLINT     InputOutputType,    // 入参/出参类型标识[1]
      SQLSMALLINT     ValueType,          // 对应的C数据类型标识[2]
      SQLSMALLINT     ParameterType,     // 对应的SQL数据类型标识[2]
      SQLULEN         ColumnSize,          // 对应字段长度
      SQLSMALLINT     DecimalDigits,       // 如果是浮点数,则对应字段精度
      SQLPOINTER      ParameterValuePtr,   // 参数缓存
      SQLLEN          BufferLength,        // 参数缓存字节数
      SQLLEN *        StrLen_or_IndPtr);   // 用于表示字符串长度或NULL值的标识[3]

我们按照参数逐个填入就有了下面代码:

//绑定SQL语句的参数
ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, (SQLPOINTER)&id, 0, NULL);
check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);
ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_WVARCHAR, 50, 0, (SQLPOINTER)name, 0, NULL);
check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);
ret = SQLBindParameter(hStmt, 3, SQL_PARAM_INPUT, SQL_C_SHORT, SQL_SMALLINT, 0, 0, (SQLPOINTER)&age, 0, NULL);
check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);

所以总的代码如下:

//复杂版本准备SQL语句
SQLTCHAR sql[] = _T("INSERT INTO student VALUES(?, ?, ?)");
ret = SQLPrepare(hStmt, sql, SQL_NTS);
check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);
//三个参数传递到SQL语句中
SQLINTEGER id = 1004;
SQLTCHAR name[50] = _T("CCKK");
SQLSMALLINT age = 88;
//绑定SQL语句的参数,2中因为数据库设计的是nvarchar,所以这里是SQL_WVARCHAR ,需要对应
ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, (SQLPOINTER)&id, 0, NULL);
check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);
ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_WVARCHAR, 50, 0, (SQLPOINTER)name, 0, NULL);
check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);
ret = SQLBindParameter(hStmt, 3, SQL_PARAM_INPUT, SQL_C_SHORT, SQL_SMALLINT, 0, 0, (SQLPOINTER)&age, 0, NULL);
check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);
执行SQL语句

执行 $SQL$ 语句非常简单,我们只要传入我们的句柄 $hStmt$ 到 $SQLExecute$ 函数执行我们上面处理好的句柄就可以了:

//执行SQL语句ret = SQLExecute(hStmt);check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);
输出影响行数

我们平常在 $SQL$ 执行了插入操作后,都会显示形如 $共 (1) 行受影响$ 的提示字样告诉我们成功完成了插入。我们在这也可以实现这样的目标。 在这里我们只要定义一个记录行数的参数 $SQLLEN$ 类型的 $n$,使用 SQLRowCount(hStmt, &n) 将我们 $n$ 的应用传入统计句柄影响的函数接受我们影响的函数,最后输出就可以了。

//查询被影响的行数SQLLEN n = 0;ret = SQLRowCount(hStmt, &n);check(ret, _T("查询影响行数"), hEnv, hDbc, hStmt);wcout << _T("插入 ") << n <<_T(" 行数据成功!") << endl;

22.png

插入代码模板

以下就是我们插入数据的代码模板,这里使用的是进阶的语句准备方法,当然也可以改回普通的版本:

#include <iostream>
//ODBC用到的头文件
#include <windows.h>
#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include <tchar.h>
using namespace std;

void check(SQLRETURN ret, wstring type, SQLHENV hEnv, SQLHDBC hDbc, SQLHSTMT hStmt) {
    if (ret == SQL_ERROR) {
        wcout << type << _T("失败!") << endl;
        //获取错误信息 
        SQLTCHAR state[128] = { 0 };
        SQLTCHAR msg[128] = { 0 };
        SQLRETURN ret = SQLError(hEnv, hDbc, hStmt, state, NULL, msg, sizeof(msg), NULL);
        wcout << state << " " << msg << endl;

        exit(-1);
    }
    return ;
}

int main()
{
    //设置为中文兼容unicode
    _wsetlocale(LC_ALL, L"chs");

    //声明环境句柄
    SQLHENV hEnv = NULL;
    //声明连接句柄
    SQLHDBC hDbc = NULL;
    //声明语句句柄
    SQLHSTMT hStmt = NULL;
    //声明返回值
    SQLRETURN ret;

    //分配环境句柄
    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
    check(ret, _T("分配环境句柄"), hEnv, hDbc, hStmt);
    //设定ODBC版本
    ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
    check(ret, _T("ODBC版本设置"), hEnv, hDbc, hStmt);
    //分配数据库连接句柄
    ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
    check(ret, _T("分配连接句柄"), hEnv, hDbc, hStmt);
    //根据DSN连接数据库,SQL_NTS为自动计算前面字符串长度
    ret = SQLConnect(hDbc, (SQLTCHAR*)_T("SQLServerODBC"), SQL_NTS, (SQLTCHAR*)_T("Jvruo"), SQL_NTS, (SQLTCHAR*)_T("Jvruo233"), SQL_NTS);
    check(ret, _T("连接数据库"), hEnv, hDbc, hStmt);
    wcout << _T("连接数据库成功!") << endl;

 //=========================================================================================================
    //插入数据
    //分配语句句柄
    hStmt = NULL;
    ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
    check(ret, _T("语句句柄分配"), hEnv, hDbc, hStmt);

    //简单版本准备SQL语句
    //SQLTCHAR sql[] = _T("INSERT INTO student VALUES('1003', 'Mushu', 24)");
    //ret = SQLPrepare(hStmt, sql, SQL_NTS);
    //check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);

    //复杂版本准备SQL语句
    SQLTCHAR sql[] = _T("INSERT INTO student VALUES(?, ?, ?)");
    ret = SQLPrepare(hStmt, sql, SQL_NTS);
    check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);
    //三个参数传递到SQL语句中
    SQLINTEGER id = 1004;
    SQLTCHAR name[50] = _T("CCKK");
    SQLSMALLINT age = 88;
    //绑定SQL语句的参数,2中因为数据库设计的是nvarchar,所以这里是SQL_WVARCHAR ,需要对应
    ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, (SQLPOINTER)&id, 0, NULL);
    check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);
    ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_WVARCHAR, 50, 0, (SQLPOINTER)name, 0, NULL);
    check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);
    ret = SQLBindParameter(hStmt, 3, SQL_PARAM_INPUT, SQL_C_SHORT, SQL_SMALLINT, 0, 0, (SQLPOINTER)&age, 0, NULL);
    check(ret, _T("绑定SQL语句"), hEnv, hDbc, hStmt);

    //执行SQL语句
    ret = SQLExecute(hStmt);
    check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);

    //查询被影响的行数
    SQLLEN n = 0;
    ret = SQLRowCount(hStmt, &n);
    check(ret, _T("查询影响行数"), hEnv, hDbc, hStmt);
    wcout << _T("插入 ") << n <<_T(" 行数据成功!") << endl;
 //=========================================================================================================

    //释放语句句柄
    if (hStmt) {
        SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
    }
    //释放连接句柄
    if (hDbc) {
        SQLDisconnect(hDbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
    }
    //释放环境句柄
    if (hEnv) {
        SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
    }
    wcout << _T("断开数据库连接!") << endl;
    return 0;
}

我们可以看到,不论是普通的准备版本还是进阶的准备版本,都可以顺利运行并完成插入:

20.png

21.png

实现查询数据

查询分块讲解

分配语句句柄

和插入数据的实现一样,我们还是先分配语句句柄:

//分配语句句柄hStmt = NULL;ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);check(ret, _T("分配语句句柄"), hEnv, hDbc, hStmt);
准备&执行SQL语句
老套路

准备和运行 $SQL$ 语句,我们当然可以直接使用上面的简单版,先设置好 $SQL$ 语句,然后编译准备,接着直接运行:

//SQL查询语句 
SQLTCHAR sql[] = _T("SELECT  *  FROM  student, student as student2");
ret = SQLPrepare(hStmt, sql, SQL_NTS);
check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);
ret = SQLExecute(hStmt);
check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);
新方法

但其实我们对于查询这种往往只允许一次的语句,可以使用 $SQLExecDirect$ 函数来让编译准备和允许一步到位,其参数大致是 $SQLExecDirect(语句句柄,SQL指令字符串,SQL指令字符串长度)$:

//执行SQL语句,SQLExecDirect不用Prepare直接查询
SQLTCHAR sql[] = _T("SELECT  *  FROM  student, student as student2");
ret = SQLExecDirect(hStmt, sql, SQL_NTS); //SQL_NTS自动计算sql语句的长度
check(ret, _T("执行查询SQL语句"), hEnv, hDbc, hStmt);
输出查询结果

对于我们的查询语句,最总要的莫过于结果的返回了。刚刚查询过后,我们的结果存放在了语句句柄的缓冲区,我们考虑把它们取出。

首先我们还是和插入数据类似,要先定义接受结果的变量和类型:

//接受结果的变量和类型
int id = 0;
TCHAR name[32] = { 0 };
short age = 0;

接下来我们使用 $SQLBindCol$ 函数将我们的输出结果实时地绑定在我们上面定义的三个变量上,其函数定义如下:

SQLRETURN SQLBindCol(
    SQLHSTMT        StatementHandle,   //声明的句柄
    SQLUSMALLINT    ColumnNumber,      //结果集里要绑定的列号。列号为从0开始递增的数字编号,,第0列为书签列。如果没有使用                                          书签就从1开始
    SQLSMALLINT     TargetType,        //缓冲的C数据类型的标识符
    SQLPOINTER      TargetValuePtr,    //绑定列的数据缓冲的指针
    SQLINTEGER      BufferLength,      //*TargetValuePtr指向的缓冲的字节数长度
    SQLLEN *        StrLen_or_Ind);    //指向绑定列的长度/指示缓冲

我们填入我们的参数就有了:

//绑定字段
SQLLEN len = SQL_NTS;
SQLBindCol(hStmt, 1, SQL_C_LONG, &id,   sizeof(id), 0);
SQLBindCol(hStmt, 2, SQL_C_WCHAR, name, sizeof(name), &len);
SQLBindCol(hStmt, 3, SQL_C_SHORT, &age, sizeof(age), 0);

我们最后就是通过一个循环,不断获取我们的数据取到我们上面这三个绑定的变量当中输出,其中我们要用到一个函数 $SQLFetch(hStmt)$,其作用是从结果集中提取下一个数据行集, 并返回所有绑定列的数据到我们刚刚绑定好的变量身上,所以我们就不断提取不断输出就可以了:

//逐行遍历,获取数据
ret = SQLFetch(hStmt);
while (ret != SQL_NO_DATA) {
    wcout << id << "\t" << name << "\t" << age << endl;

    //每次清除一下上行的旧数据,保证下次获取的数据干净
    id = 0;
    ZeroMemory(name, sizeof(name));
    age = 0;

    //获取下一行缓冲区的数据填充到id,name,age
    ret = SQLFetch(hStmt);
}

所以这部分总的代码如下:

//查询之后,所有数据放到了一块缓冲区,我们需要把他分离出来int id = 0;TCHAR name[32] = { 0 };short age = 0;//绑定字段SQLLEN len = SQL_NTS;SQLBindCol(hStmt, 1, SQL_C_LONG, &id,   sizeof(id), 0);SQLBindCol(hStmt, 2, SQL_C_WCHAR, name, sizeof(name), &len);SQLBindCol(hStmt, 3, SQL_C_SHORT, &age, sizeof(age), 0);//逐行遍历,获取数据ret = SQLFetch(hStmt);while (ret != SQL_NO_DATA) {    wcout << id << "\t" << name << "\t" << age << endl;    //每次清除一下上行的旧数据,保证下次获取的数据干净    id = 0;    ZeroMemory(name, sizeof(name));    age = 0;    //获取下一行缓冲区的数据填充到id,name,age    ret = SQLFetch(hStmt);}
统计结果长度

和插入数据的影响行数统计是一样的,我们在这里可以使用一样的语句完成对结果长度的统计:

SQLLEN n = 0;ret = SQLRowCount(hStmt, &n);//查询被影响的行数(适用于SELECT ,INSERT,UPDATE,DELETE操作)check(ret, _T("查询询问行数"), hEnv, hDbc, hStmt);wcout << _T("查询 ") << n << _T(" 行数据成功!") << endl;

查询代码模板

综合以上部分,我们最后的查询部分的完整代码就如下所示啦:

#include <iostream>
//ODBC用到的头文件
#include <windows.h>
#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include <tchar.h>
using namespace std;

void check(SQLRETURN ret, wstring type, SQLHENV hEnv, SQLHDBC hDbc, SQLHSTMT hStmt) {
    if (ret == SQL_ERROR) {
        wcout << type << _T("失败!") << endl;
        //获取错误信息 
        SQLTCHAR state[128] = { 0 };
        SQLTCHAR msg[128] = { 0 };
        SQLRETURN ret = SQLError(hEnv, hDbc, hStmt, state, NULL, msg, sizeof(msg), NULL);
        wcout << state << " " << msg << endl;

        exit(-1);
    }
    return ;
}

int main()
{
    //设置为中文兼容unicode
    _wsetlocale(LC_ALL, L"chs");

    //声明环境句柄
    SQLHENV hEnv = NULL;
    //声明连接句柄
    SQLHDBC hDbc = NULL;
    //声明语句句柄
    SQLHSTMT hStmt = NULL;
    //声明返回值
    SQLRETURN ret;

    //分配环境句柄
    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
    check(ret, _T("分配环境句柄"), hEnv, hDbc, hStmt);
    //设定ODBC版本
    ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
    check(ret, _T("ODBC版本设置"), hEnv, hDbc, hStmt);
    //分配数据库连接句柄
    ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
    check(ret, _T("分配连接句柄"), hEnv, hDbc, hStmt);
    //根据DSN连接数据库,SQL_NTS为自动计算前面字符串长度
    ret = SQLConnect(hDbc, (SQLTCHAR*)_T("SQLServerODBC"), SQL_NTS, (SQLTCHAR*)_T("Jvruo"), SQL_NTS, (SQLTCHAR*)_T("Jvruo233"), SQL_NTS);
    check(ret, _T("连接数据库"), hEnv, hDbc, hStmt);
    wcout << _T("连接数据库成功!") << endl;

 //=========================================================================================================
    //查询数据
    //分配语句句柄
    hStmt = NULL;
    ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
    check(ret, _T("分配语句句柄"), hEnv, hDbc, hStmt);

    //SQL查询语句 
    SQLTCHAR sql[] = _T("SELECT  *  FROM  student, student as student2");

    ret = SQLPrepare(hStmt, sql, SQL_NTS);
    check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);
    ret = SQLExecute(hStmt);
    check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);

    //执行SQL语句,SQLExecDirect不用Prepare直接查询
    //ret = SQLExecDirect(hStmt, sql, SQL_NTS); //SQL_NTS自动计算sql语句的长度
    //check(ret, _T("执行查询SQL语句"), hEnv, hDbc, hStmt);

    //查询之后,所有数据放到了一块缓冲区,我们需要把他分离出来
    int id = 0;
    TCHAR name[32] = { 0 };
    short age = 0;

    //绑定字段
    SQLLEN len = SQL_NTS;
    SQLBindCol(hStmt, 1, SQL_C_LONG, &id,   sizeof(id), 0);
    SQLBindCol(hStmt, 2, SQL_C_WCHAR, name, sizeof(name), &len);
    SQLBindCol(hStmt, 3, SQL_C_SHORT, &age, sizeof(age), 0);

    //逐行遍历,获取数据
    ret = SQLFetch(hStmt);
    while (ret != SQL_NO_DATA) {
        wcout << id << "\t" << name << "\t" << age << endl;

        //每次清除一下上行的旧数据,保证下次获取的数据干净
        id = 0;
        ZeroMemory(name, sizeof(name));
        age = 0;

        //获取下一行缓冲区的数据填充到id,name,age
        ret = SQLFetch(hStmt);
    }

    SQLLEN n = 0;
    ret = SQLRowCount(hStmt, &n);//查询被影响的行数(适用于SELECT ,INSERT,UPDATE,DELETE操作)
    check(ret, _T("查询询问行数"), hEnv, hDbc, hStmt);
    wcout << _T("查询 ") << n << _T(" 行数据成功!") << endl;   
    //=========================================================================================================

    //释放语句句柄
    if (hStmt) {
        SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
    }
    //释放连接句柄
    if (hDbc) {
        SQLDisconnect(hDbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
    }
    //释放环境句柄
    if (hEnv) {
        SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
    }
    wcout << _T("断开数据库连接!") << endl;
    return 0;
}

我们对我们的代码进行测试,我们只需要修改 $sql$ 字符串内的内容就可以了。

我们尝试 $SELECT \space * \space FROM \space student$:

24.png

继续尝试 $SELECT \space * \space FROM \space student \space WHERE \space id = 1001$:

25.png

我们最后来个笛卡尔积的查询尝试 $SELECT \space * \space FROM \space student, student \space as \space student2$,发现也是能够正常执行的:

29.png

实现删除数据

删除分块讲解

分配语句句柄

和之前实现一样,我们还是先分配语句句柄:

//分配语句句柄
hStmt = NULL;
ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
check(ret, _T("分配语句句柄"), hEnv, hDbc, hStmt);
准备&执行SQL语句
简单版本

在我们设计的表格当中,$id$ 是我们唯一的主键,假如我们想要删除 $id = 1001$ 的同学,我们当然可以用最开始学的,直接将 $SQL$ 语句放到字符串 $sql$ 里,然后先用 $SQLPrepare$ 函数准备 $SQL$ 语句,再用 $SQLExecute$ 执行该 $SQL$ 函数就可以了。当然使用查询中用到的 $SQLExecDirect$ 将准备和执行一体化当然也是没有问题的。

//简单版本
SQLTCHAR sql[] = _T("DELETE FROM  Student WHERE  id = 1000");
//准备SQL语句
ret = SQLPrepare(hStmt, sql, SQL_NTS);//SQL_NTS自动计算sql语句的长度
check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);
//执行SQL语句
ret = SQLExecute(hStmt);
check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);
进阶版本

说到进阶版本,相信大家都很熟悉了。没错就是先用 $?$ 缺省我们具体要删除的学生的学号,等到要删除的时候再和我们的 $id$ 绑定,这样再删除一大批学生的时候会比较方便。这里我们用到的还是 $SQLBindParameter$ 函数,具体的用法在插入那里已经详细讲述过了,在这里就不再过多赘述了。

//SQL查询语句 
SQLTCHAR sql[] = _T("DELETE FROM  Student WHERE  id = ?");//复杂版本
//准备SQL语句
ret = SQLPrepare(hStmt, sql, SQL_NTS);//SQL_NTS自动计算sql语句的长度
check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);
//复杂版本绑定SQL语句的参数 
int id = 1001;
ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &id, 0, NULL);
check(ret, _T("绑定SQL语句的参数"), hEnv, hDbc, hStmt);
//执行SQL语句
ret = SQLExecute(hStmt);
check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);
统计结果长度

和之前影响行数统计是一样的,我们在这里可以使用一样的语句完成对结果长度的统计:

SQLLEN n = 0;
ret = SQLRowCount(hStmt, &n);//查询被影响的行数(适用于SELECT ,INSERT,UPDATE,DELETE操作)
check(ret, _T("查询询问行数"), hEnv, hDbc, hStmt);
_tprintf(_T("删除%d行数据成功!\n"), n);

删除代码模板

综合以上部分,我们最后的删除部分的完整代码就如下所示啦,可以发现和我们之前的代码大致上都没什么变化 :

#include <iostream>
//ODBC用到的头文件
#include <windows.h>
#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include <tchar.h>
using namespace std;

void check(SQLRETURN ret, wstring type, SQLHENV hEnv, SQLHDBC hDbc, SQLHSTMT hStmt) {
    if (ret == SQL_ERROR) {
        wcout << type << _T("失败!") << endl;
        //获取错误信息 
        SQLTCHAR state[128] = { 0 };
        SQLTCHAR msg[128] = { 0 };
        SQLRETURN ret = SQLError(hEnv, hDbc, hStmt, state, NULL, msg, sizeof(msg), NULL);
        wcout << state << " " << msg << endl;

        exit(-1);
    }
    return ;
}

int main()
{
    //设置为中文兼容unicode
    _wsetlocale(LC_ALL, L"chs");

    //声明环境句柄
    SQLHENV hEnv = NULL;
    //声明连接句柄
    SQLHDBC hDbc = NULL;
    //声明语句句柄
    SQLHSTMT hStmt = NULL;
    //声明返回值
    SQLRETURN ret;

    //分配环境句柄
    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
    check(ret, _T("分配环境句柄"), hEnv, hDbc, hStmt);
    //设定ODBC版本
    ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
    check(ret, _T("ODBC版本设置"), hEnv, hDbc, hStmt);
    //分配数据库连接句柄
    ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
    check(ret, _T("分配连接句柄"), hEnv, hDbc, hStmt);
    //根据DSN连接数据库,SQL_NTS为自动计算前面字符串长度
    ret = SQLConnect(hDbc, (SQLTCHAR*)_T("SQLServerODBC"), SQL_NTS, (SQLTCHAR*)_T("Jvruo"), SQL_NTS, (SQLTCHAR*)_T("Jvruo233"), SQL_NTS);
    check(ret, _T("连接数据库"), hEnv, hDbc, hStmt);
    wcout << _T("连接数据库成功!") << endl;
    //================================================================================================================
    //删除数据

    //分配语句句柄
    hStmt = NULL;
    ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
    check(ret, _T("分配语句句柄"), hEnv, hDbc, hStmt);

    //SQL查询语句 
    //SQLTCHAR sql[] = _T("DELETE FROM  Student WHERE  id = 1001");//简单版本
    SQLTCHAR sql[] = _T("DELETE FROM  Student WHERE  id = ?");//复杂版本

    //准备SQL语句
    ret = SQLPrepare(hStmt, sql, SQL_NTS);//SQL_NTS自动计算sql语句的长度
    check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);

    //复杂版本绑定SQL语句的参数 
    int id = 1001;
    ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &id, 0, NULL);
    check(ret, _T("绑定SQL语句的参数"), hEnv, hDbc, hStmt);

    //执行SQL语句
    ret = SQLExecute(hStmt);
    check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);

    //查询被影响的行数
    SQLLEN n = 0;
    ret = SQLRowCount(hStmt, &n);//查询被影响的行数(适用于SELECT ,INSERT,UPDATE,DELETE操作)
    check(ret, _T("查询被影响的行数"), hEnv, hDbc, hStmt);
    _tprintf(_T("删除%d行数据成功!\n"), n);
    //================================================================================================================

    //释放语句句柄
    if (hStmt) {
        SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
    }
    //释放连接句柄
    if (hDbc) {
        SQLDisconnect(hDbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
    }
    //释放环境句柄
    if (hEnv) {
        SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
    }
    wcout << _T("断开数据库连接!") << endl;
    return 0;
}

我们运行代码,能够顺利进行操作,我们想要删除的 $Jvruo$ 也如愿被顺利删除了。

30.png

31.png

实现修改数据

修改分块讲解

分配语句句柄

还是老样子,我们还是先分配语句句柄,只要是执行 $SQL$ 语句,就必然要先分配句柄:

//分配语句句柄
hStmt = NULL;
ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
check(ret, _T("分配语句句柄"), hEnv, hDbc, hStmt);
准备&执行SQL语句
简单版本

和删除完全一致,我们简单版本还是直接把成形的 $SQL$ 丢到 $sql$ 字符串里,然后依次进行准备和执行。

//简单版本
SQLTCHAR sql[] = _T("UPDATE Student SET  name='DT'   WHERE  id = 1000");
//准备SQL语句
ret = SQLPrepare(hStmt, sql, SQL_NTS);//SQL_NTS自动计算sql语句的长度
check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);
//执行SQL语句
ret = SQLExecute(hStmt);
check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);
进阶版本

进阶版本还是先用 $?$ 缺省我们省略的参数在真实修改的时候再进行参数绑定。这里我们用到的还是 $SQLBindParameter$ 函数,具体的用法在插入那里已经详细讲述过了,还是不清楚的同学可以回去看看。

//SQL查询语句 
SQLTCHAR sql[] = _T("UPDATE Student SET  name=?  WHERE  id = ? ");//复杂版本

//准备SQL语句
ret = SQLPrepare(hStmt, sql, SQL_NTS);//SQL_NTS自动计算sql语句的长度
check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);

//进阶版本绑定SQL语句的参数  
SQLLEN len = SQL_NTS;
SQLTCHAR  newName[50] = _T("DT");
ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_WVARCHAR, 50, 0, (SQLPOINTER)newName, 0, &len);
check(ret, _T("绑定姓名参数"), hEnv, hDbc, hStmt);

int id = 1001;
ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &id, 0, NULL);
check(ret, _T("绑定学号参数"), hEnv, hDbc, hStmt);

//执行SQL语句
ret = SQLExecute(hStmt);
check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);
统计结果长度

和之前影响行数统计是一样的,我们在这里可以使用一样的语句完成对结果长度的统计:

SQLLEN n = 0;
ret = SQLRowCount(hStmt, &n);//查询被影响的行数(适用于SELECT ,INSERT,UPDATE,DELETE操作)
check(ret, _T("查询询问行数"), hEnv, hDbc, hStmt);
_tprintf(_T("修改%d行数据成功!\n"), n);

修改代码模板

综合以上部分,我们最后的删除部分的完整代码就如下所示啦,可以发现和我们之前的代码大致上都没什么变化 :

#include <iostream>
//ODBC用到的头文件
#include <windows.h>
#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include <tchar.h>
using namespace std;

void check(SQLRETURN ret, wstring type, SQLHENV hEnv, SQLHDBC hDbc, SQLHSTMT hStmt) {
    if (ret == SQL_ERROR) {
        wcout << type << _T("失败!") << endl;
        //获取错误信息 
        SQLTCHAR state[128] = { 0 };
        SQLTCHAR msg[128] = { 0 };
        SQLRETURN ret = SQLError(hEnv, hDbc, hStmt, state, NULL, msg, sizeof(msg), NULL);
        wcout << state << " " << msg << endl;

        exit(-1);
    }
    return ;
}

int main()
{
    //设置为中文兼容unicode
    _wsetlocale(LC_ALL, L"chs");

    //声明环境句柄
    SQLHENV hEnv = NULL;
    //声明连接句柄
    SQLHDBC hDbc = NULL;
    //声明语句句柄
    SQLHSTMT hStmt = NULL;
    //声明返回值
    SQLRETURN ret;

    //分配环境句柄
    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
    check(ret, _T("分配环境句柄"), hEnv, hDbc, hStmt);
    //设定ODBC版本
    ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
    check(ret, _T("ODBC版本设置"), hEnv, hDbc, hStmt);
    //分配数据库连接句柄
    ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
    check(ret, _T("分配连接句柄"), hEnv, hDbc, hStmt);
    //根据DSN连接数据库,SQL_NTS为自动计算前面字符串长度
    ret = SQLConnect(hDbc, (SQLTCHAR*)_T("SQLServerODBC"), SQL_NTS, (SQLTCHAR*)_T("Jvruo"), SQL_NTS, (SQLTCHAR*)_T("Jvruo233"), SQL_NTS);
    check(ret, _T("连接数据库"), hEnv, hDbc, hStmt);
    wcout << _T("连接数据库成功!") << endl;
    //================================================================================================================

    //修改数据

    //分配语句句柄
    hStmt = NULL;
    ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
    check(ret, _T("分配语句句柄"), hEnv, hDbc, hStmt);

    //SQL查询语句 
    //SQLTCHAR sql[] = _T("UPDATE Student SET  name='DT'   WHERE  id = 1000");//简单版本
    SQLTCHAR sql[] = _T("UPDATE Student SET  name=?  WHERE  id = ? ");//复杂版本

    //准备SQL语句
    ret = SQLPrepare(hStmt, sql, SQL_NTS);//SQL_NTS自动计算sql语句的长度
    check(ret, _T("准备SQL语句"), hEnv, hDbc, hStmt);

     //复杂版本绑定SQL语句的参数  
    SQLLEN len = SQL_NTS;
    SQLTCHAR  newName[50] = _T("DT");
    ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_WVARCHAR, 50, 0, (SQLPOINTER)newName, 0, &len);
    check(ret, _T("绑定姓名参数"), hEnv, hDbc, hStmt);

    int id = 1001;
    ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &id, 0, NULL);
    check(ret, _T("绑定学号参数"), hEnv, hDbc, hStmt);

    //执行SQL语句
    ret = SQLExecute(hStmt);
    check(ret, _T("执行SQL语句"), hEnv, hDbc, hStmt);

    SQLLEN n = 0;
    ret = SQLRowCount(hStmt, &n);//查询被影响的行数(适用于SELECT ,INSERT,UPDATE,DELETE操作)
    check(ret, _T("查询被影响的行数"), hEnv, hDbc, hStmt);
    _tprintf(_T("修改%d行数据成功!\n"), n);
  //================================================================================================================

    //释放语句句柄
    if (hStmt) {
        SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
    }
    //释放连接句柄
    if (hDbc) {
        SQLDisconnect(hDbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
    }
    //释放环境句柄
    if (hEnv) {
        SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
    }
    wcout << _T("断开数据库连接!") << endl;
    return 0;
}

我们运行代码,能够顺利进行操作,我们的修改顺利执行。

32.png

33.png

一些小插曲

在我们的项目运行执行的时候,我曾经多次遇到 “系统资源不足,无法完成请求的任务” 的提示。经过注释代码排查,我发现是我 $check$ 的问题。

23.png

但是很显然我的 $check$ 是没问题的,这就让我对着空气调试了一个下午。后面我无可奈何只好求助万能的网络,终于在一个远古帖子中发现这可能和防火墙有关。我于是关闭迈克菲的实时扫描,就完美解决了这个问题。

26.png

结语

不得不说,这确实是一篇冗长的 $BLOG$,不过这确实可以算得上是一篇保姆级的教程,也希望后面的同学在进行类似的操作实验的时候可以少走些弯路。有的同学可能要问了,这么多借口和参数,全部都要记住吗?那当然不是的,这篇 $BLOG$ 正是为了解决这一问题的操作手册一般的存在,当你要用到但不记得的时候,打开这篇 $BLOG$ 进行查阅复用就可以了。最后希望你喜欢这篇 $BLOG$!

Last modification:March 1st, 2022 at 09:54 pm
If you think my article is useful to you, please feel free to appreciate