处理查询结果

当应用程序提交了SQL语句之后,神通数据库会以一个或多个结果集的形式来返回数据结果。 一个结果集就是一组符合查询要求而返回的行和列的集合。 SELECT语句、Catalog函数和一些存储过程都能生成结果集,并且能以表的形式返回给应用程序。 如果执行的SQL语句是一个存储过程、一个包含多条SQL命令的批处理SQL语句, 则会有多个结果集返回。

在下面的例子里,第一条SQL语句生成包含TABLE表中所有行所有列的结果集,而第二条生成STUDENT表中AGE列等于22;

Select * from TABLE;
Select *from STUDENT where AGE=22;

ODBC的Catalog函数也能够获取数据。例如,SQLColumns函数就能够获取数据库中关于列信息的数据。 这些结果集可能包含零行或者多行数据。

注意,有些SQL语句并不返回结果集,例如GRANT或REVOKE语句。 对于这些语句,SQLExecute或SQLExecDirect函数的返回值仅仅用来表示这些语句是否被成功执行了。

INSERT、UPDATE和DELETE语句返回的结果集,包含了这些语句被执行后所修改的数据的行数。 当应用程序调用SQLRowCount函数时,就能获取这个行数。 符合ODBC 3.x规范的应用程序必须通过调用SQLRowCount函数来获取该行数或者必须通过调用SQLMoreResults函数转而处理下一个结果集而不获取该行数。

当应用程序执行一个包含多条INSERT、UPDATE或者DELETE语句的批处理或者存储过程,每条INSERT、UPDATE或者DELETE语句返回的结果集都必须通过调用SQLRowCount函数来处理或者调用SQLMoreResults函数放弃对该结果集的处理。

结果集的元数据

元数据(metadata)是指用来描述其他数据的相关信息的数据。例如,结果集的元数据用来描述结果集诸如结果集中的列数、这些列的数据类型、列名、精度之类的信息。

在大多数的结果集操作中,应用程序都需要结果集元数据。例如,应用程序需要获取结果集中列的数据类型来决定将列绑定到什么类型的变量上。或者应用程序需要获取列中数据的字节长度来决定分配多少的空间来存放该列的数据。 至于获取结果集元数据之后再做什么操作,就完全由应用程序决定了。

有些应用程序,在设计阶段就已经制定好了表的结构以及在这些表上如何进行操作。对于这样的应用程序来说,因为结果集的元数据在程序设计阶段就是已知的,并且可以在程序编写阶段被程序员所操控, 因此元数据可以直接被写入程序代码中。例如,表中由列名为ID的列,在数据库中用4字节的整型值来存储数据,因此应用程序可以总是以4字节的整型来绑定该列。

而对一般的应用程序来说,尤其是支持特殊的查询语句的应用程序, 他们根本不知道返回结果集的元数据。因此这些程序必须在执行时调用SQLNumResultCols、SQLDescribeCol和SQLColAttribute函数。

SQLDescribeCol函数和SQLColAttribute函数可以用来获取结果集元数据。SQLDescribeCol函数只返回列名、数据类型、有效数字长度、小数位数和是否可为空的属性。 而SQLColAttribute函数可以返回更多的元数据,包括列数据的大小写敏感性、显示长度、是否可更新等。

应用程序可以在语句被准备后或者执行后并且在结果集上游标被关闭之前的任何时候获取结果集元数据。元数据的获取对数据库来说是有比较大的代价的。 因此,驱动需要在从数据库中获取元数据后缓存这些元数据,而应用程序也应该在真正有必要时才来要求获取元数据。

绑定结果集

从数据库中取得的数据需要被返回到应用程序的变量中。在返回前,应用程序需要先将变量和结果集中的列绑定。这和将变量和语句中的参数绑定相类似。当应用程序将变量和结果集列绑定时,需要先向驱动描述变量的地址、类型等信息。驱动需要将这些信息保存下来,在数据从结果集返回到程序变量中时需要使用这些信息。

应用程序可以选择需要绑定的结果集列数,也包括不绑定所有列,这完全由应用程序根据自身需求所决定。当一行数据从结果集中取出时,驱动按照被绑定的列将数据返回到程序变量中。没有被绑定的列的数据,可以通过调用SQLGetData函数来获取。该函数一般都是在需要获取大数据量数据时使用,因为大数据量数据有可能造成缓冲区的越界,需要通过多次调用SQLGetData来分部分地获取数据。

结果集列的绑定可以在任何时间进行,甚至是在结果集中多行的数据已经被取出来之后。然后,绑定只有再下一次取数据时才有效,对绑定前已经取出的数据没有任何影响。

应用程序通过调用SQLBindCol函数绑定列。该函数一次绑定结果中一列。应用程序需要按如下步骤进行:

指定列号。第0列是书签列;该列并不存在于某些结果集中。其他列的列号从1开始递增。但是如果指定的列号超过了结果集中的实际列数,那就会产生错误。但是这个错误在结果集被生成前是不会被检测到的,因此该错误是由SQLFetch函数返回的,而不是SQLBindCol。

指定变量的C数据类型、地址和在内存中的大小。如果结果集列中数据无法从SQL数据类型转换到指定的C数据类型,则也会产生错误。而这样的错误只有在结果集生成以后才能被检测到,因此也只有SQLFetch函数才能返回该错误。

指定长度/指示器缓冲区地址。应用程序可以选择是否使用长度/指示器缓冲区。当结果集返回的是二进制或者字符串类型的数据时,长度/指示器缓冲区用来返回数据的实际长度。当结果集返回的数据为空时,长度/指示器缓冲区返回SQL_NULL_DATA来指示该列数据为空。

当应用程序调用SQLBindCol函数时,驱动将上述信息记录到语句句柄上。当驱动在获取结果集中每行数据时,都要使用这些信息来决定如何将每列数据返回到应用程序的变量中。

结果集数组绑定

结果集的数组绑定一样分为列宽绑定和行宽绑定两种,需要设置语句句柄的SQL_ATTR_ROW_BIND_TYPE属性,属性值为零,带包列宽绑定;属性值为非零,则代表的是按行宽绑定方式进行绑定,此时需要定义结构体。同时需要设置语句句柄的SQL_ATTR_ROW_ARRAY_SIZE属性,代表一次性绑定多少条。如果不设置SQL_ATTR_ROW_BIND_TYPE属性,只设置了SQL_ATTR_ROW_ARRAY_SIZE属性,默认为列宽绑定。

列宽绑定方式示例:

#define ROW_ARRAY_SIZE 10
SQLUINTEGER   Id[ROW_ARRAY_SIZE];
SQLCHAR       Name[ROW_ARRAY_SIZE][20];
SQLINTEGER    IdLenOrInd[ROW_ARRAY_SIZE];
SQLINTEGER    NameLenOrInd[ROW_ARRAY_SIZE];
SQLULEN    NumRowsFetched;
SQLUSMALLINT   RowStatusArray[ROW_ARRAY_SIZE], i;
SQLRETURN      rc;
SQLHSTMT       hstmt;

SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_BIND_TYPE, 0, 0);
SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_ARRAY_SIZE, ROW_ARRAY_SIZE, 0);
SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_STATUS_PTR, RowStatusArray, 0);
SQLSetStmtAttr(hstmt, SQL_ATTR_ROWS_FETCHED_PTR, &NumRowsFetched, 0);

SQLBindCol(hstmt, 1, SQL_C_ULONG, &Id, sizeof(SQLUINTEGER), IdLenOrInd);
SQLBindCol(hstmt, 2, SQL_C_CHAR, Name,  20,  NameLenOrInd);

SQLExecDirect(hstmt, "SELECT ID, NAME FROM STUDENT", SQL_NTS);

//一次最多获取行集大小的行数。打印实际的
//获取的行数;此数字以NumRowsFetched的形式返回.
//检查行状态数组以仅打印成功获取的行
//代码用于检查rc是否等于SQL_SUCCESS_WITH_INFO,或SQL_ERROR 。
while ((rc = SQLFetchScroll(hstmt,SQL_FETCH_NEXT,0)) != SQL_NO_DATA) {
   for (i = 0; i < NumRowsFetched; i++) {
      if (RowStatusArray[i] == SQL_ROW_SUCCESS|| RowStatusArray[i] ==
         SQL_ROW_SUCCESS_WITH_INFO) {
         if (IdLenOrInd[i] == SQL_NULL_DATA)
            printf(" NULL      ");
         else
            printf("%d\t", Id[i]);
         if (NameLenOrInd[i] == SQL_NULL_DATA)
            printf(" NULL      ");
         else
            printf("%s\t", Name[i]);
      }
   }
}
// 关闭游标.
SQLCloseCursor(hstmt);

行宽绑定方式示例如下:

#define ROW_ARRAY_SIZE 10
typedef struct {
   SQLUINTEGER   Id;
   SQLCHAR       Name[20];
SQLINTEGER    IdLenOrInd;
SQLINTEGER    NameLenOrInd;
} STUDENT;
STUDENT stuInfoArray[ROW_ARRAY_SIZE];

SQLULEN    NumRowsFetched;
SQLUSMALLINT   RowStatusArray[ROW_ARRAY_SIZE], i;
SQLRETURN      rc;
SQLHSTMT       hstmt;

SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_BIND_TYPE, sizeof(STUDENT), 0);
SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_ARRAY_SIZE, ROW_ARRAY_SIZE, 0);
SQLSetStmtAttr(hstmt, SQL_ATTR_ROW_STATUS_PTR, RowStatusArray, 0);
SQLSetStmtAttr(hstmt, SQL_ATTR_ROWS_FETCHED_PTR, &NumRowsFetched, 0);

SQLBindCol(hstmt, 1, SQL_C_ULONG, &stuInfoArray[0].Id, 0, &stuInfoArray[0].IdLenOrInd);
SQLBindCol(hstmt, 2, SQL_C_CHAR, stuInfoArray[0].Name,  sizeof(stuInfoArray[0].Name),
& stuInfoArray[0].NameLenOrInd);

SQLExecDirect(hstmt, "SELECT ID, NAME FROM STUDENT", SQL_NTS);

//一次最多获取行集大小的行数。打印实际的
//获取的行数;此数字以NumRowsFetched的形式返回.
//检查行状态数组以仅打印成功获取的行
//代码用于检查rc是否等于SQL_SUCCESS_WITH_INFO,或SQL_ERROR 。
while ((rc = SQLFetchScroll(hstmt,SQL_FETCH_NEXT,0)) != SQL_NO_DATA) {
   for (i = 0; i < NumRowsFetched; i++) {
      if (RowStatusArray[i] == SQL_ROW_SUCCESS|| RowStatusArray[i] ==
         SQL_ROW_SUCCESS_WITH_INFO) {
         if (stuInfoArray [i].IdLenOrInd == SQL_NULL_DATA)
            printf(" NULL      ");
         else
            printf("%d\t", stuInfoArray[i].Id);
         if (stuInfoArray[i].NameLenOrInd == SQL_NULL_DATA)
            printf(" NULL      ");
         else
            printf("%s\t", stuInfoArray[i].Name);        }
   }
}
// 关闭游标.
SQLCloseCursor(hstmt);

获取数据

应用程序通过调用SQLFetch函数从结果集中获取一行数据。SQLFetch函数可以使用任何类型的游标,但是它只能向前移动行集游标。 SQLFetch函数将游标移动到下一行数据处,并将该行数据中被绑定列的数据返回到应用程序变量中。当游标到达结果集末尾时,SQLFetch函数返回SQL_NO_DATA。

SQLFetch函数如何实现是由驱动决定的,但总的模式是:从数据源中获取被绑定列的数据,将数据按绑定变量所需的数据类型进行转换,最后将处理完的数据赋值到绑定变量中。 如果驱动不能完成数据转换,则SQLFetch会返回一个错误。应用程序虽然可以继续获取其他行的数据,但是当前行的数据将被丢失。

驱动在获取数据后会向被绑定列的长度/指示器缓冲区中赋值。驱动向该列的长度/指示器缓冲区赋值该列数据被转换后的字节长度。 如果无法获取某列数据的准确长度时,驱动会向该列的长度/指示器缓冲区赋值SQL_NO_TOTAL,表示当前数据很长无法在本次函数调用中获取数据的完整长度。 对于整型或者数据结构之类的定长数据类型,驱动所要设置的数据字节长度就是这类数据的大小。

对于字符串或者二进制串之类的变长类型数据,驱动需要检查转换后数据所需的缓冲区空间大小是否超过绑定列上分配的缓冲区大小,该缓冲区大小是在调用SQLBindCol函数时由BufferLength参数指定的。 如果转化后数据所需的缓冲区空间大小超过了绑定列上分配的缓冲区大小,驱动会将转换后数据截取并填充到绑定列的缓冲区中,并在长度/指示器缓冲区中赋值转换后数据没有被截取时的实际长度。 同时返回SQL_SUCCESS_WITH_INFO提示返回数据被截取,并在诊断记录中放置SQLSTATE 01004。

定长类型数据永远不会被截取,因为驱动总是假定绑定列上分配的缓冲区空间是足够的,其大小就是定长类型数据的大小。 如果应用程序总是为将要获取的数据分配足够大的缓冲区空间,则返回数据被截取的情况就会很少发生。应用程序可以通过结果集的元数据来决定需要为返回的数据分配多大的缓冲区。 当然,应用程序也可以故意为返回的数据分配较小的缓冲区从而显式的导致返回数据被驱动截取。例如,结果集中某列是有1000个字节的文本数据,而应用程序只要求获取该列的数据中前100个字节的数据。

字符串类型数据在被返回给应用程序时,都会被驱动在数据末尾加上字符串终结符,甚至当数据被截取时也是如此。 字符串终结符并不计算在返回数据的长度内,但是需要在绑定列上分配的缓冲区中占用空间。例如,假设驱动有长度为50个字节的字符串数据需要返回,而应用程序分配的缓冲区空间为25个字节。 则在应用程序分配的缓冲区中,驱动会返回所需返回数据的前24个字节并在第25个字节处加上字符串终结符。在长度/指示器缓存区中,驱动会返回原有数据的实际长度50。

判定NULL值

驱动在获取数据后会向用SQLBindCol绑定该列的长度/指示器(StrLen_or_Ind)缓冲区中赋值。 如果某列的数据为空(NULL),则驱动向该列的长度/指示器缓冲区赋值SQL_NULL_DATA。 如果某列的数据不为空,则驱动向该列的长度/指示器缓冲区赋值该列数据被转换后的字节长度。 如果无法获取某列数据的准确长度时,驱动会向该列的长度/指示器缓冲区赋值SQL_NO_TOTAL,表示当前数据很长无法在本次函数调用中获取数据的完整长度。 对于整型或者数据结构之类的定长数据类型,驱动所要设置的数据字节长度就是这类数据的大小。

处理多结果集

SELECT语句会返回结果集。 UPDATE,INSERT和DELETE语句会返回受作用的行数。 如果这些语句被放在一起批量执行,或者使用参数数组执行,或者调用一个存储过程,都会返回多个结果集或者多个受作用的行数。

在执行了批量执行之后,应用程序被定位在第一个结果集上。 应用程序可以在第一个结果集或者其后的任意结果集上调用SQLBindCol、SQLBulkOperation、SQLFetch、SQLGetData、SQLFetchScroll、SQLSetPos和所有的元数据函数,就像在单个结果集上处理一样。 如果应用程序处理完第一个结果集之后,可以通过调用SQLMoreResults函数转而定位到下一个结果集上继续处理。 如果该结果集是有效的,则SQLMoreResults函数会返回SQL_SUCCESS,并为接下来对该结果集的处理做初始化工作。

如果当前结果集是无效的,无法获取数据,则SQLMoreResults函数会抛弃该结果集转而处理下一个结果集。 如果所有的结果集都被处理,SQLMoreResults函数返回SQL_NO_DATA。

动态绑定

SQLGetData用于在不绑定列值的情况下检索结果集数据。可以在同一列上连续调用SQLGetData,以从具有文本、text或lob数据类型的列中检索大量数据。

应用程序不需要绑定变量来获取结果集数据。任何列的数据都可以通过以下方式从数据库客户端ODBC驱动程序中检索:

ODBC驱动程序不支持使用SQLGetData以随机列顺序检索数据。 使用SQLGetData处理的所有未绑定列的列序号必须高于结果集中的绑定列序号。 应用程序必须处理从最低的未绑定序数列值到最高的数据。试图从序号较低的列检索数据会导致错误。

比如一个查询有三列,第一列绑定,第二列和第三列 没有绑定,用SQLGetData从第二列开始获取数据是可以的; 而如果第一列没有绑定,第二列和第三列绑定了,用SQLGetData获取数据则会报错。

结果集的关闭

若要正在处理一个结果集时,在 SQLFetch 返回 SQL_NO_DATA 前,想用同一个语句句柄去执行其他语句 (即取消当前正在处理的结果集),需要先调用 SQLCloseCursor,否则新执行其他语句会失败。 良好的编程习惯是语句句柄在执行新语句之前,执行SQLCloseCursor关闭游标。