ZLW-NOTE:ODBC Fundamentals - Buffers
buffer,缓冲区,是应用程序中用于在应用和驱动之间传递数据的任意一块内存区域。例如,可以通过SQLBindCol函数将应用程序缓冲区关联到,或者说绑定到,结果集的列上。当提取每一行时,数据会按列返回到对应的缓冲区中。输入缓冲区用于从应用向驱动传递数据,输出缓冲区用于从驱动向应用传递数据。
NOTE
如果ODBC函数返回SQL_ERROR,这个函数的所有输出参数是未知的。
此处讨论主要涉及不确定类型的缓冲区。 这些缓冲区的地址表现为 SQLPOINTER 类型的参数,例如 SQLBindCol 中的 TargetValuePtr 参数。 但是,这里讨论的一些问题,例如缓冲区相关的参数,也适用于将字符串传递给驱动程序的参数,例如 SQLTables 中的 TableName 参数。
这些缓冲区通常成对使用。数据缓冲区用于传递自身的数据,同时length/indicator缓冲区用于传递数据缓冲区中的数据长度,或者传递如表示空数据的SQL_NULL_DATA这类特殊值。数据缓冲区中的数据长度跟数据缓冲区本身的长度是两个概念。两者之间的关系如下:
当数据缓冲区中包含如字符串、二进制数据等可变长度的数据时,就需要有对应的length/indicator缓冲区。如果数据缓冲区中包含如整数、结构体等固定长度数据时,只有需要传递指示符时需要有对应的length/indicator缓冲区。如果应用程序中对固定长度数据设置了一个length/indicator缓冲区,驱动会忽略这个length/indicator缓冲区中传递的任何长度信息。
数据缓冲区和数据缓冲区中存放的数据长度都是以字节数而非字符数来衡量的。对于那些使用ANSI字符串的程序来说这个区别无关紧要,因为ANSI字符串中字节长度和字符长度是一样的。
当数据缓冲区代表的是驱动定义的描述符域、诊断域或者属性时,应用需要向驱动器管理器说明表示域或属性值的函数参数的性质。应用通过在设置域或属性的函数调用中将长度参数设置为以下值之一来达到目的。(对于获取域或属性的函数也是一样的情况,唯一的区别是指向设置类函数的传入值的参数就在参数中)
- 如果表示域或属性值的函数参数是一个指向字符串的指针,length参数是字符串的长度或者SQL_NTS。
- 如果表示域或属性值得函数参数是一个指向二进制缓冲区的指针,应用程序会将宏SQL_LENGTH_BINARY_ATTR(length)的结果放入lenfth参数中。实际上是将一个负值放入length参数中。
- 如果表示域或属性的函数参数是一个指向除字符串和二进制字符串之外的值的指针,length参数应该给与一个SQL_IS_POINTER值。
- 如果表示域或属性的函数参数是一个固定长度的值,length参数应该是SQL_IS_INTEGER,SQL_IS_UINTEGER,SQL_IS_SMALLINT,或者SQL_IS_USMALLINT(视情况而定)
缓冲区部分讨论下面三个主题 - 延迟缓冲区
- 分配和释放缓冲区
- 使用缓冲区
延迟缓冲区
deffered buffer,延迟缓冲区,是指调用某个指定其为参数的函数时不会立即使用它所存储的值,而是在之后的某个时刻才会用到它所存储的值。例如SQLBindParameter用于将一个数据缓冲区关联到,或者是绑定到,一个SQL语句的参数上。应用程序指定参数的编号并将地址,字节长度,以及缓冲区类型传入。驱动保存这些信息但并不检查缓冲区中的内容。直到后面应用程序指向语句的时候,驱动才获取缓冲区的信息并用它来检索参数所需的数据,并发送给数据源。因此在缓冲区中的输入数据是延迟的。由于延迟缓冲区在一个函数中指定,在其他函数中使用,当驱动还需要它存在的时候释放这个延迟缓冲区导致应用编程错误。具体信息参考分配和释放缓冲区。
输入缓冲区和输出缓冲区都可以是延迟缓冲区。下表是一个延迟缓冲区的汇总。需注意的是绑定到结果集的延迟缓冲区由SQLBindCol指定,绑定到SQL语句参数的延迟缓冲区由SQLBindParameter指定。
用途 | 类型 | 指定函数 | 使用函数 |
---|---|---|---|
发送输入参数类数据 | 延迟输入 | SQLBindParameter | SQLExecute/SQLExecDirect |
发送数据用于更新或插入一个结果集中的一行 | 延迟输入 | SQLBindCol | SQLSetPos |
返回output或input/output类型参数 | 延迟输出 | SQLBindCol | SQLExecute/SQLExecDirect |
返回结果集数据 | 延迟输出 | SQLBindCol | SQLFetch/SQLFetchScroll/SQLSetPos |
分配和释放缓冲区
所有的缓冲区都由应用程序分配和释放。如果缓冲区不是延迟缓冲区,只需要在调用函数的过程中存在即可。例如,SQLGetInfo返回一个由InfoValuePtr参数指向的在缓冲区中的代表一个特殊操作的值。这个缓冲区可以在执行完SQLGetInfo调用后立即释放。
SQLSMALLINT InfoValueLen;
SQLCHAR * InfoValuePtr = malloc(50); // Allocate InfoValuePtr.
SQLGetInfo(hdbc, SQL_DBMS_NAME, (SQLPOINTER)InfoValuePtr, 50,&InfoValueLen);
free(InfoValuePtr); // OK to free InfoValuePtr.
由于延迟缓冲区在一个函数中指定并且在另一个函数中使用,所以当驱动器依然需要一个缓冲区存在的时候释放这个缓冲区是应用程序错误。例如,ValuePtr缓冲区的地址被传递给SQLBindCol并且后面会被SQLFetch用到。这个缓冲区在解除列绑定前都不能释放。解除列绑定可以是另外一个对SQLBindCol的调用或者对SQLFreeStmt的调用。
SQLRETURN rc;
SQLINTEGER ValueLenOrInd;
SQLHSTMT hstmt;
// Allocate ValuePtr
SQLCHAR * ValuePtr = malloc(50);
// Bind ValuePtr to column 1. It is an error to free ValuePtr here.
SQLBindCol(hstmt, 1, SQL_C_CHAR, ValuePtr, 50, &ValueLenOrInd);
// Fetch each row of data and place the value for column 1 in *ValuePtr.
// Code to check if rc equals SQL_ERROR or SQL_SUCCESS_WITH_INFO
// not shown.
while ((rc = SQLFetch(hstmt)) != SQL_NO_DATA) {
// It is an error to free ValuePtr here.
}
// Unbind ValuePtr from column 1. It is now OK to free ValuePtr.
SQLFreeStmt(hstmt, SQL_UNBIND);
free(ValuePtr);
可以通过在函数局部声明一个缓冲区来简单模拟这类错误,当应用退出函数后缓冲区就释放了。下面代码会引发驱动未定义的并且可能是致命的错误。
SQLRETURN rc;
SQLHSTMT hstmt;
BindAColumn(hstmt);
// Fetch each row of data and try to place the value for column 1 in
// *ValuePtr. Because ValuePtr has been freed, the behavior is undefined
// and probably fatal. Code to check if rc equals SQL_ERROR or
// SQL_SUCCESS_WITH_INFO not shown.
while ((rc = SQLFetch(hstmt)) != SQL_NO_DATA) {}
.
.
.
void BindAColumn(SQLHSTMT hstmt) // WARNING! This function won't work!
{
// Declare ValuePtr locally.
SQLCHAR ValuePtr[50];
SQLINTEGER ValueLenOrInd;
// Bind rgbValue to column.
SQLBindCol(hstmt, 1, SQL_C_CHAR, ValuePtr, sizeof(ValuePtr),
&ValueLenOrInd);
// ValuePtr is freed when BindAColumn exits.
}
使用数据缓冲区
数据缓冲区由三部分信息组成:类型、地址、长度。当一个应用程序需要这三个中的某项信息但实际又不知道时,它会有一个参数让应用传入该信息。
该部分内容包含三个话题:
- 数据缓冲区类型
- 数据缓冲区地址
- 数据缓冲区长度
数据缓冲区类型
缓冲区的C数据类型由应用程序指定。对于一个简单变量,当应用程序分配变量时就指定了变量类型,对于通用内存——被void类型指针指向的内存区域——当应用程序将内存转换为特定类型时指定变量类型。驱动通过下面两种办法甄别类型:
- 数据缓冲区类型参数
用于传递参数值和结果集数据的缓冲区通常有一个相关联的类型参数,例如绑定到SQLBindCol函数的TargetValuePtr参数的缓冲区,会在函数中有一个TargetType参数指明该缓冲区的类型。在这个参数中,应用程序传入与缓冲区类型一致的C数据类型标识符。如下例中,调用SQLBindCol时,SQL_C_TYPE_DATE告诉驱动器缓冲区Date是一个SQL_DATE_STRUCT类型的缓冲区。
SQL_DATE_STRUCT Date;
SQLINTEGER DateInd;
SQLBindCol(hstmt, 1, SQL_C_TYPE_DATE, &Date, 0, &DateInd);
更多类型标识符的信息参见Data Types in ODBC模块的描述。
- 预先定义类型
用于发送或接收选项或属性的缓冲区类型是固定的,由选项决定。例如SQLGetInfo的InfoValuePtr参数指向的缓冲区就属于这种。应用程序负责分配这种类型的缓冲区,驱动器会认为数据缓冲区就是这种类型。例如下面例子中调用SQLGetInfo,驱动器会认为缓冲区就是一个32位整型,因为SQL_STRING_FUNCTIONS选项就是需要一个32位整型的数据。
SQLUINTEGER StringFuncs;
SQLGetInfo(hdbc, SQL_STRING_FUNCTIONS, (SQLPOINTER) &StringFuncs, 0,
NULL);
驱动器通过C数据类型解释缓冲区中的数据。
数据缓冲区地址
应用使用一个参数给驱动传递数据缓冲区的地址,这个参数通常命名为ValuePtr或者类似的名字。如下,调用SQLBindCol时,应用程序指定变量Date的地址:
SQL_DATE_STRUCT Date;
SQLINTEGER DateInd;
SQLBindCol(hstmt, 1, SQL_C_TYPE_DATE, &dsDate, 0, &DateInd);
之前在分配和释放缓冲区部分提到过,在缓冲区被解除绑定之前,必须保证延迟缓冲区的地址必须是合法的。
一个数据缓冲区的地址可以是空指针,特别说明禁止的情况除外。对于用于发送数据给驱动的缓冲区,空指针会导致驱动忽略缓冲区中的信息。对于从驱动接收数据的缓冲区,空指针会导致驱动不返回值。在上面两种情形下,驱动会忽略对应的数据缓冲区长度参数。
数据缓冲区长度
应用使用一个参数给驱动传递数据缓冲区的字节长度。这个参数的名称是BufferLength或类似的名字。如下,调用SQLBindCol时,应用指定ValuePtr缓冲区的长度(sizeof(ValuePtr))
SQLCHAR ValuePtr[50];
SQLINTEGER ValueLenOrInd;
SQLBindCol(hstmt, 1, SQL_C_CHAR, ValuePtr, sizeof(ValuePtr), &ValueLenOrInd);
在任何有缓冲区长度参数作为输出参数的函数中,驱动器永远返回字节长度,而非字符长度。
只有输出缓冲区需要有数据缓冲区的长度;驱动器使用缓冲区长度参数避免写数据时超出缓冲区的边界。不过只有当缓冲区包含变长数据(例如字符串数据或二进制数据)的时候,驱动才会检查数据缓冲区的长度。如果缓冲区包含诸如整形或日期结构体这种固定长度的数据,驱动器忽略数据缓冲区长度参数,认为缓冲区的大小足够容纳对应的数据。换句话说,驱动永远不会截断固定长度的数据。因此对应用来说,为固定长度的数据分配一个足够大的缓冲区是十分重要的。
当非数据的输出字符串(如SQLGetCursorName返回的游标名称)被截断时,在缓冲区长度参数中返回的长度是字符串的最大可能长度。
输入缓冲区不需要数据缓冲区长度,因为驱动器不需要往这些缓冲区中写入数据。
这部分包含三个话题:
- 使用Length/Indicator值
- 数据长度、缓冲区长度和截断
- 字符数据和C字符串
使用Length/Indicator值
Length/Indicator缓冲区用于传递数据缓冲区中的字节长度或者一个特殊的指示符,例如标识数据为空的SQL_NULL_DATA指示符。一个length/indicator缓冲区是被定义为SQLINTEGER还是SQLSMALLINT类型,取决于使用它的函数。综上,需要有一个独立的参数来描述length/indicator缓冲区。
如果数据缓冲区是非延迟的输入缓冲区,这个参数中包含数据本身的字节长度或者一个指示符的值。这时参数通常命名为StrLen_or_Ind或者类似的名称。
如下,应用调用SQLPutData传入一个写满数据的缓冲区,由于数据缓冲区ValuePtr是一个输入缓冲区,因此直接传入字节长度ValueLen。
SQLCHAR ValuePtr[50];
SQLINTEGER ValueLen;
// Call local function to place data in ValuePtr. In ValueLen, return the
// number of bytes of data placed in ValuePtr. If there is not enough
// data, this will be less than 50.
FillBuffer(ValuePtr, sizeof(ValuePtr), &ValueLen);
// Call SQLPutData to send the data to the driver.
SQLPutData(hstmt, ValuePtr, ValueLen);
如果数据缓冲区是延迟输入缓冲区、非延迟输出缓冲区或输出缓冲区,这个参数包含length/indicator缓冲区的地址。此时参数通常命名为StrLen_or_IndPtr或类似的名称。
如下,应用调用SQLGetData来接收一个充满数据的缓冲区,因为使用的缓冲区ValuePtr是非延迟的输出缓冲区,因此同时将length/indicator缓冲区的地址传递给驱动,驱动返回数据到ValuePtr的同时,字节长度被返回到length/indicator缓冲区ValueLenOrInd中。
SQLCHAR ValuePtr[50];
SQLINTEGER ValueLenOrInd;
SQLGetData(hstmt, 1, SQL_C_CHAR, ValuePtr, sizeof(ValuePtr), &ValueLenOrInd);
一个length/indicator缓冲区参数可以为0(如果是非延迟输入)或一个空指针(如果是输出或延迟输入),特殊说明禁止的情况除外。对于输入缓冲区来说,这导致驱动忽略数据的字节长度。当传递变长数据时会返回一个报错信息,但是当传递一个非空的固定长度的数据时表现正常,因为对于固定长度的数据而言,长度和指示符都是不必要的。对于输出缓冲区来说,这会导致驱动既不返回字节长度也不返回指示符的值。当驱动器返回数据是空时会返回一个报错,但是当获取固定长度且非空的数据时,表现正常,因为固定长度的数据不需要长度和指示符。
当延迟数据缓冲区的地址传递给驱动程序时,对应的延迟length/indicator缓冲区的地址必须保持有效,直到缓冲区解除绑定。
以下是length/indicator的一些合法length值:
- n, n>0
- 0
- SQL_NTS
length设置为SQL_NTS表示数据缓冲区中被发送给驱动的字符串是以null结尾的;对于C编程来说,这是一种便捷的传递字符串的方式,使用这种方式不用计算字符串字节长度。这个值只在应用传递数据给驱动时是合法的。当驱动返回数据给应用时,总是会返回数据的字节长度。
以下为length/indicator的一些合法indicator?值:
- SQL_NULL_DATA
表示数据是空值,对应数据缓冲区中的值会被忽略。这个值仅在向驱动器发送数据或从驱动器接收数据时合法。 - SQL_DATA_AT_EXEC
表示数据缓冲区不包含任何数据,而是在执行语句或调用SQLBulkOperations、SQLSetPos时用SQLPutData发送数据。这个值仅在向驱动器发送数据时合法。参考SQLBindParameter,SQLBulkOperations和SQLSetPos。 - SQL_LEN_DATA_AT_EXEC(length)宏的结果。
这个值跟SQL_DATA_AT_EXEC类似,更多信息参照Sending Long Data 发送长数据。 - SQL_NO_TOTAL
驱动器不能决定输出缓冲区中待返回的长数据的字节长度。这个值仅在从驱动器接收数据时合法。 - SQL_DEFAULT_PARAM
存储过程要使用存储过程输入参数的默认值而不使用对应数据缓冲区中的值。 - SQL_COLUMN_IGNORE
SQLBulkOperations或SQLSetPos将要忽略数据缓冲区中的值。当调用SQLBulkOperations或SQLSetPos更新一行数据时,列的值将不会被更新。当调用SQLBulkOperations插入一行数据时,列的值将被设置为默认值或者当该列没有默认值时设置为空。
数据长度、缓冲区长度和截断
数据长度是按照存储在应用数据缓冲区中的数据的字节长度计算,而非按照存储在数据源中的长度计算。这个区别非常重要,因为通常数据存储在数据缓冲区中的类型多于存储在数据源中的类型。因此,对于被发送给数据源的数据,数据长度是在数据被转换成数据源数据类型前的字节长度,对于从数据源接收的数据,数据长度是在数据被转换成数据缓冲区类型后、被执行任何截断前的字节长度。
对于固定长度的数据,例如整形数据或者时间类型数据,数据的字节长度总是数据类型的大小。通常来说,应用程序为定长数据分配一个跟数据类型大小一致的数据缓冲区。如果应用程序分配的数据缓冲区小于定长数据的类型长度,后果是未知的,因为驱动器认为定长数据的数据缓冲区长度就是数据类型的大小,因此不会对数据进行截断以适应一个更小的缓冲区。如果应用程序分配的数据缓冲区大于定长数据的类型长度,多余的空间将永远不会被使用。
对于可变长度数据,例如字符或二进制数据,有一点认知至关重要,那就是数据的字节长度和数据缓冲区的字节长度是两个不同的概念并且通常两个值也是不一样的。这两个长度的关系在[缓冲区buffer部分有介绍。如果数据字节长度比缓冲区字节长度大,驱动器会将获取的数据截断为跟缓冲区一样的大小,同时返回SQL_SUCCESS_WITH_INFO和SQLSTAT 01004(数据截断)。但是返回的字节长度依然是原始的未截断的数据长度。
举个例子,假设应用程序为二进制数据分配了50字节长的数据缓冲区。如果驱动返回的数据长度为10字节,它会将这10个字节的数据返回到数据缓冲区中。这里数据的字节长度是10,数据缓冲区的字节长度是50。如果驱动器要返回的二进制数据长度为60字节,它会将数据截断为50字节,并将被截断的50字节返回到数据缓冲区中,同时然会SQL_SUCCESS_WITH_INFO。这里数据的字节长度为60字节(数据被截断前的长度),数据缓冲区的字节长度为50字节。
每一个被截断的列都会有一个对应的诊断记录被创建。驱动器创建这些记录和应用程序处理这些记录都需要花费时间,因此截断会导致性能下降。通常来说,应用程序可以通过分配足够大的缓冲区来避免这个问题,但对于处理长数据来说这没有什么作用。当发生数据节点,有时应用程序可以分配一个更大的缓冲区重新获取数据,但这并不适用于所有情形。如果时在调用SQLGetData获取数据时发生截断,应用程序不需要再次调用SQLGetData,因为数据已经被返回了,更多信息参考获取长数据Getting LongData。
字符数据和C字符串
指向变长字符数据(列名,动态参数,字符串属性值等)的输入参数有一个相关联的长度参数。如果应用程序,如同C中的典型处理一样,对字符串以空字符结尾,长度参数要么是字符串的字节长度(不含空字符),要么是SQL_NTS(标识字符串为空字符结尾的字符串)。指定一个相关字符串实际长度的参数应是一个非负数。长度参数可以是0,代表一个长度为0的区别于空值的字符串,负值SQL_NTS指示驱动器通过定位结尾的空字符来确定字符串的长度。
当驱动将字符数据返回给应用时,必须以空字符结尾。这样可以让应用程序有更多选择:可以选择将其作为字符串来处理还是作为字符数组来处理。如果应用程序缓冲区不够大,不足以存储所有返回的字符数据,驱动器会将字符数据截断为缓冲区字节长度减去结尾空字符占用的字节数,然后将截断后的数据以空字符结尾,并存储到缓冲区中。因此,应用程序必须为接收字符数据的缓冲区分配额外的空间以存储结尾的空字符。例如,需要使用51字节长度的缓冲区来接收50字节长度的字符数据。
当使用SQLPutData或SQLGetData发送或接收长字符数据时,无论应用还是驱动都必须注意。如果数据是以空字符结尾的字符串形式发送,在对数据进行重组前,需要先将这些字符串后面代表字符串结束的空字符剥离出去。
许多 ODBC 程序员混淆了字符数据和 C 字符串。 发生这种情况是在定义 ODBC 函数时使用 C 语言造成的。 如果 ODBC 驱动程序或应用程序使用另一种语言 - 请记住 ODBC 是独立于语言的 - 不太可能出现这种混淆。
当C字符串用于存放字符数据,空白字符结束符并不是数据的一部分,亦不会被计算到字节长度中。例如,字符数据"ABC"可以存储为字符串"ABC\0"或字符数组{'A','B','C'}。不管是当作字符串还是字符数组,数据的字节长度都为3。
尽管应用和驱动器通常使用C字符串来保存字符数据,但并非必须如此。在C中字符数据还可以被当作一个字符数组(没有空结束符)来进行处理,同时字节数据需要单独用length/indicator缓冲区来传递。
由于字符输出可以存储在一个没有空结束符的数组中,并且字节长度可以单独传递,因此传输嵌套有空字符的字符数据是可以实现的。然而这种情况下ODBC函数的行为将是未知的,并且驱动器是否能够正确处理这种情形也是因驱动器而异的。因此可互操作的应用程序总是将可能内嵌空字符的字符数据处理成二进制数据。