C++ ODBC开发历程

C++ ODBC开发历程,第1张

文章目录
  • 前引
  • 一. ODBC?
  • 二. ODBC体系结构
  • 三. ODBC句柄
    • 3.1 环境句柄 SQL_HANDLE_ENV
    • 3.2 连接句柄 SQL_HANDLE_DBC
    • 3.3 语句句柄 SQL_HANDLE_STMT
  • 四. 连接数据库
  • 五. 执行SQL语句
    • 5.1 SQLExecDirect()
    • 5.2 SQLPrepare() + SQLExecute()
    • 5.3 Procedures
      • 5.3.1 About Procedures
      • 5.3.2 Execute Procedures
  • 六.获取结果集
    • 6.1 SQLBindCol() + SQLFetch()
    • 6.2 SQLFetch() + SQLGetData()
    • 6.3 SQLGetData() vs SQLBindCol()
  • 七. CRUD
    • 7.1 Update
    • 7.2 Delete
  • 一些概念
    • 1.句柄 Handle
    • 2.char 与 wchar_t
    • 3.游标 Cursor
    • 4.LPCSTR、LPCTSTR 和 LPTSTR
    • 5. 数据缓冲区 Buffers
      • 5.1 Relation Between Data Buffers and Length/Indicator Buffers
      • 5.2 Length/Indicator Buffers Values !!!
  • 问题:
  • 参考


前引

以下记录了博主学习用C++做ODBC开发实现对class="superseo">数据库进行CRUD的历程。我建议仔细阅读Microsoft官方文档以学习如何进行ODBC开发


一. ODBC?

微软提出的数据库访问接口标准。ODBC定义了访问数据库的API一个规范,这些API独立于不同厂商的DBMS,也独立于具体的编程语言,使用该API集可以访问任何提供了ODBC驱动程序的数据库。


二. ODBC体系结构


客户程序: 调用 ODBC 函数以提交 SQL 语句并检索结果。
驱动程序管理器: 管理对多个DBMS的同时访问
驱动程序: 处理 ODBC 函数调用,将 SQL 请求提交到特定数据源,并将结果返回到应用程序。如有必要,驱动程序会修改应用程序的请求,以便该请求符合关联的 DBMS 支持的语法。每种数据库都提供自己的ODBC驱动程序,ODBC接口通过专门的驱动程序与数据库交换信息。
各种关系数据库: 数据源DSN

应用程序与DBMS通过驱动程序联系起来
驱动程序管理器与驱动程序通过句柄联系起来:
The application uses Driver Manager handles when calling ODBC functions because it calls those functions in the Driver Manager. The Driver Manager uses this handle to find the corresponding driver handle and uses the driver handle when calling the function in the driver.
That is: 应用程序在驱动管理器中调用ODBC函数,驱动管理器通过自身句柄找到相应的驱动句柄,进而实现函数调用。


三. ODBC句柄

句柄是一个不透明的变量,是ODBC驱动程序实现数据库 *** 作的手段。‎‎驱动程序管理器和驱动程序使用句柄查找有关项目的信息。‎环境、连接和语句句柄对于初始化和终止ODBC程序是必需的。
Handle -Microsoft

3.1 环境句柄 SQL_HANDLE_ENV

作用:
①环境句柄是指包含有关应用程序全局状态(如属性和连接)的信息的数据对象。
②此句柄通过调用SQLAllocHandle()进行分配(HandleType 设置为 SQL_HANDLE_ENV),并通过调用SQLFreeHandle()释放(HandleType 设置为 SQL_HANDLE_ENV)。
③使用ODBC的每个程序从创建环境句柄开始,以释放环境句柄结束。环境句柄在每个应用程序中只能创建一个。必须先分配环境句柄,然后才能分配连接句柄。Driver Manager 为连接到它的每个应用程序维护一个单独的环境句柄。

分配:

 SQLHENV hEnv = NULL ;
 ret =  SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);

设置环境句柄属性:

ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, SQL_IS_INTEGER);

Environment Handles

3.2 连接句柄 SQL_HANDLE_DBC

作用:
①连接句柄是指包含与特定数据源的连接相关联的信息的数据对象。该句柄标识每个连接。
②连接句柄的分配和释放与环境句柄一致,区别在于要将HandleType 设置为 SQL_HANDLE_DBC
③一个应用程序可以同时连接到多个数据库服务器。应用程序需要为与数据库服务器的每个并发连接提供一个连接句柄。连接句柄在环境句柄上创建,可以有多个。

分配:

SQLHDBC hDbc = NULL;
ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
3.3 语句句柄 SQL_HANDLE_STMT

作用:
①语句句柄是指描述和跟踪 SQL 语句执行情况的数据对象。驱动程序分配了一个结构来存储有关语句的信息(result set, parameters),并将指向该结构的指针作为语句句柄返回。
②您可以通过调用 SQLAllocHandle() 来分配一个语句句柄来描述 SQL 语句(将 HandleType 设置为 SQL_HANDLE_STMT),必须先执行此 *** 作,然后才能执行语句。
③语句句柄在连接句柄上创建,可以有多个。

分配:

ret = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt); 

Statement Handles


四. 连接数据库

SQLConnect()在ODBC 驱动程序和数据源(MySQL)之间建立连接。

ret =  SQLConnect (hDbc, (SQLCHAR*)"Test", SQL_NTS, (SQLCHAR*)"root", SQL_NTS, (SQLCHAR*)"password", SQL_NTS);

参数分别为: 连接句柄,数据源DSN(data source name), SQL_NTS,数据库登录名(通常是root),SQL_NTS,数据库登录密码,SQL_NTS

若连接失败,则获取错误原因的方法:
(项目属性为多字节字符集)

if (ret == SQL_ERROR)
     {
	   SQLCHAR* state = new SQLCHAR[5]; 
       SQLError (hEnv, hDbc, NULL, state, NULL, NULL, NULL, NULL);
       cout << "错误信息:"<< state << endl; //获取错误信息SQLSTATE
     }

SQLConnect()函数
SQLError()函数


五. 执行SQL语句

Executing a Statement

5.1 SQLExecDirect()
SQLCHAR* SQLToExe = NULL;
SQLToExe = (SQLCHAR*)"SELECT * FROM student";
ret = SQLExecDirect (hStmt, SQLToExe, SQL_NTS);

若执行失败,获取错误信息:

if (ret == SQL_ERROR)
	{
		SQLCHAR* state2 = new SQLCHAR[5];
		SQLError (NULL, NULL, hStmt, state2, NULL, NULL, NULL, NULL);
		cout << "错误信息:" << state2 << endl; //获取错误信息SQLSTATE
	}
5.2 SQLPrepare() + SQLExecute()

主要用于执行带参数的SQL语句。对于多次执行的SQL语句使用此方法相较于SQLExecDirect()效率更高,因为语句只需编译一次
先用SQLPrepare()提交SQL语句,再用’SQLBindParameter()函数绑定参数,最后用SQLExecute()执行语句

    SQLCloseCursor(hStmt);// 关闭游标
    
	SQLCHAR* SQLToExe3 = (SQLCHAR*)"INSERT INTO student VALUES(?,?,?)";
	ret = SQLPrepare(hStmt, SQLToExe3, SQL_NTS);
	Test(ret);

	SQLCHAR SNOInput[SNO_Len], SNameInput[SName_Len], SDepartInput[SDepart_Len];
	cout << "Input student message:";
	cin >> SNOInput >> SNameInput >> SDepartInput;
	SQLLEN SNOLen = SQL_NTS, SNameLen2 = SQL_NTS, SDepartLen2 = SQL_NTS;
	
	SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 20, 0, SNOInput, SNO_Len*sizeof(char), &SNOLen);
	SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 20, 0, SNameInput, SName_Len*sizeof(char), &SNameLen2);
	SQLBindParameter(hStmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 20, 0, SDepartInput, SNO_Len*sizeof(char), &SDepartLen2);
	
	ret = SQLExecute(hStmt);

博主在执行SQLExecute(hStmt)时,未能执行成功,函数返回SQL_NEED_DATA, 博主为此花了近一天来查找错误原因。最终在参考了官方文档 Setting Parameter Values, Using Length and Indicator Values后解决:
原因是我在SQLBindParameter()的最后一个参数StrLen_or_IndPtr未设置正确,在设置为SQL_NTSSQLExecute()正确运行。(此外应注意SQLExecute()BufferLength参数是以字节为单位,应以数组长度*sizeof(数据类型)为实际参数。— 只针对可变长的输出参数)

关于SQLPrepare()SQLExecute()中语句句柄的作用: Handle

SQLPrepare()
SQLBindParameter()
SQLExecute()
Statement Parameters
SQLExecDirect vs SQLPrepare+SQLExecute

5.3 Procedures 5.3.1 About Procedures

存储过程是永久存储在数据源上的可执行对象,在运行时执行一次或多次。
①过程通常是执行 SQL 语句的最快方式。
②修改过程,而无需重新编译应用程序。
③过程可以与应用程序的其余部分分开开发。
④过程被编写用于实现特定任务,因此适用于特定功能的应用程序(custom applications)的开发,而不适用于通用应用程序(generic applicaitions),
⑤可移植性低。ODBC没有提供创建过程的标准语法,所以对不同的DBMS需要编写不同的过程,并且一些DBMS不支持过程。

5.3.2 Execute Procedures

执行过程必须有参数

    cout << "-------" << "Execute Procedures:" << "-------" << endl;
	SQLCloseCursor(hStmt);
	if (ret == SQL_SUCCESS)
		cout << "Close Cursor Successfully!" << endl;
		
	SQLCHAR countDep[SDepart_Len]= "CS";
	SQLLEN countDepInd =SQL_NTS;
	
	SQLLEN retCSNum = 100,retCSNumInd = SQL_IS_INTEGER;
	
	ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR,20,NULL,countDep,SDepart_Len*sizeof(char),&countDepInd);
	if (ret == SQL_SUCCESS)
		cout << "Bind Successfully!" << endl;
	ret = SQLBindParameter(hStmt, 2, SQL_PARAM_OUTPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &retCSNum, sizeof(int), &retCSNumInd);
	if (ret == SQL_SUCCESS)
		cout << "Bind Successfully!" << endl;
	
	ret = SQLExecDirect(hStmt, (SQLCHAR*)"call CSNum(?,?)", SQL_NTS);.// do not use {sql}
	if (ret == SQL_SUCCESS)
		cout << "Exeute Procedures Successfully!" << endl;
	else if (ret == SQL_ERROR)
	{
		SQLCHAR* state2 = new SQLCHAR[5];
		SQLError(NULL, NULL, hStmt, state2, NULL, NULL, NULL, NULL);
		cout << "error message:" << state2 << endl; //SQLSTATE:42S22 --> Column did not exist.
	}
	cout << retCSNum << endl;// return Number of students in CS

在这里遇到的问题:

  1. SQLExecDirect()执行失败,返回错误:42S22。经查阅SQLExecDirect()的诊断信息,博主了解到这个错误的原因是未找到列。起先博主自以为是程序未找到存储过程,为此查看了很久的有关ODBC 执行存储过程的官方文档,但未解决问题。后来博主验证MySQL上的存储过程CSNum确实是没有问题的,因此再次把目光集中到解决找不到列这个问题上,最后发现应用程序不支持调用的过程中出现:Procedures_name.parameter_name,只允许在过程中直接使用parameter.name,在将过程CSNum中的CSNum.Depart 改为 DepartSQLExecDirect()执行成功,MySQL上的过程CSNum如下:
delimiter $$
CREATE PROCEDURE CSNum(in Depart VARCHAR(20), out s_count INTEGER)
BEGIN 
      SELECT COUNT(*) into s_count
	  FROM student
	  WHERE student.SDepart = Depart;
END $$
delimiter;
  1. 解决问题1后,打印输出参数retCSNum发现其值无效,还是等于最初定义的100,这说明Call CSNum
    没有执行成功,在将SQLExecDirect()中的“{call CsNum(?,?)}"改为“call CsNum(?,?)”retCSNum正确返回数量信息。显然这与官方说明Procedure Call有差异,对于具体的数据库系统MySQL,在应用程序中调用存储过程的SQL语句应该是:“[?=]call procedure-name(?,?, ...)]”,而不该加上大括号

Procedures ODBC
Procedure Parameters


六.获取结果集 6.1 SQLBindCol() + SQLFetch()

SQLBindCol()
SQLFetch()

获取结果集的过程: SQLBindCol()将结果集中的某一列绑定到C++程序的一个存储空间(通常是一个由指针开辟的连续存储空间,如数组),接着由SQLFetch()实现以下两点功能:①数据传输:移动游标至下一行,返回关联列的数据 ②数据类型转换:同时将数据类型从 SQL type转换为在 SQLBindCol()fCType参数中指定的C 类型变量。

    cout << "获取结果集---SQLBindCol + SQLFetch()方式:" << endl;
 	SQLCHAR SNO[SNO_Len] = {'0'};
	SQLLEN len =0;
	ret = SQLBindCol(hStmt, 1, SQL_C_DEFAULT, SNO, SNO_Len, &len);
	Test(ret);  // 8 通过打印len的值可以知道,此时还没有取回数据到 SNO数组中,执行SQLFetch后 SNO才有了取回的数据
	SQLRETURN ret = SQL_SUCCESS;
	while (true) // 遍历该列所有记录
	{
		ret = SQLFetch(hStmt);
		if (ret == SQL_NO_DATA_FOUND) // 读完所有行,游标位于结果集之后
		{
			cout << "End of data." << endl;
			break;
		}

		if (ret == SQL_ERROR)
		{
			cout << "Error!" << endl;
			break;
		}
		cout << "SNO:" << SNO << "   ";
	}

SQLBindCol()的最后两个参数 cbValueMaxpcbValue的说明:前者表示应用程序提供的数据缓冲区的大小(BufferLenth),后者用于接收返回的数据在数据缓冲区实际占用大小这一信息(DataLen)。

6.2 SQLFetch() + SQLGetData()

SQLGetData():检索结果集当前行/游标所指向的行中单个列的数据。

必须在SQLGetData()之前调用SQLFetch() 。经博主实 *** 后确认,直接执行SQLGetData()会导致游标无效:重新打开游标后,游标指向结果集之前而不是行。

重新打开游标是因为我已经调用了SQLBindCol()+SQLFetch()遍历过一遍行,所以需要重置游标位置

获取结果集的过程: 先调用SQLFetch()把结果集传到应用程序,接着调用SQLGetData()接收数据。 改变SQLGetData()icol参数可以获得不同列的值

    cout << "获取结果集---SQLFetch()+SQLGetData()方式:" << endl;
	ret = SQLCloseCursor(hStmt);// 关闭游标
	ret = SQLExecDirect(hStmt, SQLToExe, SQL_NTS); // 重新打开游标
	SQLCHAR SNO2[SNO_Len] = {'0'};  
	SQLCHAR SName[SName_Len] = { '0' };
	SQLLEN len2 =0,Name_Len; 
	while (true) // 遍历该列所有记录
	{
		ret = SQLFetch(hStmt);
		if (ret == SQL_NO_DATA_FOUND) // 读完所有行,游标位于结果集之后
		{
			cout << "End of data." << endl;
			break;
		}
		if (ret == SQL_ERROR)
		{
			cout << "Error!" << endl;
			break;
		}
	    SQLGetData(hStmt, 1, SQL_C_DEFAULT, SNO2, SNO_Len, &len2);
		SQLGetData(hStmt, 2, SQL_C_DEFAULT, SName, SNO_Len, &Name_Len);
		cout << "SNO:" << SNO2 << " SNAME:"<< SName << "   ";
	}
6.3 SQLGetData() vs SQLBindCol()

SQLGetData() vs SQLBindCol()
区别在于:SQLGetData()在调用SQLFetch() 之后绑定变量;SQLBindCol() 在调用SQLFetch()前绑定变量。 SQLGetData()可用于检索未绑定列。区别不大,性能上,SQLBindCol()+SQLFetch() 优于 SQLFetch()+SQLGetData(); 但灵活性SQLGetData()优于SQLBindCol(),因为无需提前绑定列即可获取列值。

C 和 SQL 对应的数据类型


七. CRUD 7.1 Update

更新学习小组的任务量属性:

	cout << "-------" << "UPDATE" << "-------" << endl;
	ret = SQLCloseCursor(hStmt);
	Test(ret, hStmt);

	SQLLEN wordTask = 100, wordTaskInd = SQL_IS_INTEGER;
	cout<<"Please input the task to be assigned: ";
	cin >> wordTask;
	ret = SQLBindParameter(hStmt, 1 , SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &wordTask, sizeof(int), &wordTaskInd);
	Test(ret, hStmt);
	ret = SQLExecDirect(hStmt, (SQLCHAR*)"UPDATE stuGroup SET GTask = ?",SQL_NTS); // error: set to be null 
	Test(ret, hStmt);

博主在这里遇到了问题。API全部返回SQL_SUCCESS,但数据库中的GTask却没有被修改为wordTask值,而是被置为了NULL。博主为此排查了几个小时了也没有解决。
在博主仔细阅读有关SQLBindParameter()参数绑定的官方文档后,将 wordTaskInd = SQL_IS_INTEGER改为wordTaskInd = SQL_IS_UINTEGER,或SQLBindParameter()填入0后UPDATE语句成功执行

SQLBindParameter()
Binding Parameter Markers By SQLBindParameter()
Binding Parameters ODBC

7.2 Delete

从学生表中删除指定学号的学生信息:

    cout << "-------" << "DELETE" << "-------" << endl;
	ret = SQLCloseCursor(hStmt);
	Test(ret, hStmt);

	SQLCHAR SnoToDel[SNO_Len];
	cout << "Please input the SN0 to be deleted: ";
	cin >> SnoToDel;
	ret = SQLPrepare(hStmt, (SQLCHAR*)"DELETE FROM STUDENT WHERE SNO = ?", SQL_NTS);
	Test(ret, hStmt);

    ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 20, 0, &SnoToDel, 0, 0);
    Test(ret, hStmt);

	ret = SQLExecute(hStmt);
	Test(ret, hStmt);

一些概念 1.句柄 Handle

句柄是标识特定项目的不透明 32 位值

标识符,标识一个对象。
中间媒介,是获取另一个对象的方法——一个广义的指针,通过这个中间媒介可控制、 *** 作对象。创建句柄的目的就是建立起与被访问对象之间的联系

一个通俗的解释: 什么是句柄?

2.char 与 wchar_t

char用于 ANSI编码系列: 一个字符 → \rightarrow 一个字节
wchar_t用于Unicode编码系列: 一个字符 → \rightarrow 多个字节

关于编码:
Unicode和多字节字符集
ASCII,Unicode 和 UTF-8 - 阮一峰
unicode占几个字节?:
由具体的存储方式而定
UCS-2/UTF-16 : 两个字节
UCS-4/UTF-32 : 四个字节,其中前两个字节表示平面数,后两个字节表示字符。
一般还是说占两个字节(16位)

3.游标 Cursor

①游标是一个数据缓冲区,用于暂时存放SQL语句的执行结果/结果集
②游标由结果集和结果集中指向特定记录的游标位置组成。 游标总是与一条SQL 查询语句相关联,因为游标的结果集是由SELECT语句产生,
③游标充当指针,是一种能从包括多条数据记录的结果集中每次提取一条记录的机制,它还提供对基于游标位置而对表中数据进行删除或更新的能力

当执行创建结果集的语句时,会隐式打开游标。打开游标时,它位于结果集的第一行之前。使用SQLFetch() 可将游标前进到结果集的下一行.在嵌入式 SQL 和 ODBC 中,游标必须在应用程序完成使用后关闭。

游标在结果集中只正向移动而不更新结果集,对于这种应用情况,游标行为相对比较简单。缺省情况下,ODBC 应用程序会请求此行为。ODBC 定义一个只读的单向游标。单向游标只能向前移动,要重置游标,必须先关闭游标SQLCloseCursor(),再通过再次执行 SELECT 语句来重新打开游标。单向游标对与只需要浏览一次的应用非常有用,而且效率很高。

Cursors -Microsoft
SQL游标(cursor)详细说明 -博客园

4.LPCSTR、LPCTSTR 和 LPTSTR

LP == Long Pointer.认为是指针或字符’ * ’
C = Const,在这种情况下,我认为它们意味着字符串是一个const,而不是指针是const。
STR表示字符串
T表示TCHAR, 如果定义了 UNICODE,则 TCHAR =WCHAR,否则 TCHAR = CHAR。因此,如果未定义UNICODE,则LPCTSTR == LPCSTR。

5. 数据缓冲区 Buffers 5.1 Relation Between Data Buffers and Length/Indicator Buffers

缓冲区是用于在应用程序和驱动程序之间传递数据的任何应用程序内存。

①数据缓冲区的地址表现为 SQLPOINTER 类型的参数, 如SQLBindParameter()函数中的 SQLPOINTER ParameterValuePtr参数,SQLBindCol()函数中的SQLPOINTER TargetValuePtr参数,

②数据缓冲区Data buffers用于传递数据本身,而长度/指示符缓冲区length/indicator buffers用于传递数据缓冲区中数据的长度。两者关系如图
③数据缓冲区及其包含的数据的长度均以字节为单位,而不是字符。这也是为什么SQLBindParameter()SQLLEN BufferLength参数要注意数据类型长度。但是只有对输出缓冲区需要说明数据缓冲区长度,输入参数只需将该参数置为0;驱动程序仅在缓冲区包含可变长度数据(例如字符或二进制数据)时才检查数据缓冲区长度,而对固定长度的参数(例如整数或日期结构)驱动程序忽略数据缓冲区长度并假设缓冲区足够大以容纳数据。

5.2 Length/Indicator Buffers Values !!!

绑定参数的函数总要涉及数据长度/指标缓冲区,博主一开始没有搞清楚传什么参才正确,因此总是会出现些未知错误,而花费博主几个小时去找,直到找到该参数上。为避免再次出现这样的情况,博主决定好好总结一下这个参数的用法。

Length/Indicator Buffers有两个作用
① 传递数据缓冲区中数据的字节长度
② 传递一个特殊的指标,用来说明Data Buffers中数据。(通过把传入的实参设置为特殊值。如SQL_NULL_DATA表示数据为NULL数据值,则相应数据缓冲区中的值被忽略。)
Length/Indicator Buffers在函数中通常以参数StrLen_or_Ind 或类似名称来表述。

对Data Buffers中不同类型的数据,Length/Indicator Buffers 传递不同的指标,常用的有以下对应关系
①以空值结尾的字符串 (variable-length data) ------ SQL_NTS
②数字(non-null, fixed-length data) ------ 0(对输入参数) , NULL (对输出参数)

即当数据缓冲区的数据是一个字符串时,把参数StrLen_or_Ind设置为SQL_NTS; 数据是一个固定长的值时如整数,则把参数StrLen_or_Ind设置为0等。目的是向驱动程序管理器指示数据缓冲区的性质。

更多信息见:
Buffers
Using Length and Indicator Values
Data Buffer Length


问题:

1.为什么以unicode编码接受输入,以unicode编码输出时显示乱码(项目属性 使用 Unicode 字符集)

ret =  SQLConnectW (hDbc, (SQLWCHAR*)"Test", SQL_NTS, (SQLWCHAR*)L"root", SQL_NTS, (SQLWCHAR*)L"200111", SQL_NTS);
// Test前不加L是为了连接失败

	// 连接数据库
	if (ret == SQL_ERROR)
     {
	   SQLWCHAR* state = new SQLWCHAR[5];  //UNICODE编码
       SQLErrorW (hEnv, hDbc, NULL, state, NULL, NULL, NULL, NULL);
       cout << "错误信息:"<< state << endl; //获取错误信息SQLSTATE
     }


L 的含义:字符串前的L表示该字符串是一个宽字符串,类型是 wchar_t ,而非char。


参考

ODBC数据库编程 --CSDN博主 超级大洋葱806
数据库访问接口之ODBC -51CTO博客
ODBC函数 -Hitachi
ODBC函数 -IBM
ODBC开发示例 -华为云
SQLxxx类型对应的C类型/源码 -GitHub

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1498107.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-25
下一篇 2022-06-25

发表评论

登录后才能评论

评论列表(0条)

保存