Marina Greenstein
认证的顾问 I/T 专家 - 数据管理, IBM White Plains
2003 年 10 月 对数据库的更改通常意味着对应用程序的更改。本文解释了在将 Oracle Pro*C 程序转换为带有嵌入式 DB2 SQL 调用的 C 程序的过程中所需的一些步骤。
简介
虽然近几年来 IBM® DB2® Universal Database™ 应用程序开发经历了一些变更( 从 C/COBOL/Java™ 到 SQL 过程语言的存储过程,对用户定义函数中 SQl PL 的支持,触发器和内联 SQL,增强的内建函数集,等等),但是对将 SQL 嵌入到其他宿主语言(C/C++)的支持实际上一直都没有变。这导致了在将 Oracle Pro*C 程序转换到 DB2 UDB 以及将应用程序从 Oracle 移植到 DB2 UDB 过程中的很多困难。
本文解释了在将 Pro*C 程序转换为带有嵌入式 DB2 SQL 调用的 C 程序的过程中所需的一些步骤。
注意:本文所包括的例子是从实际程序中摘录而来的,本身不能通过编译。
连接到数据库
不同环境下 C 程序连接到数据库的方式有所差异。在 Oracle 中,每个实例(服务名)只能管理一个数据库。DB2 实例可用于管理多个数据库。因而,连接语句应该显式地提供数据库的名称。为了连接到 Oracle 数据库,您需要指定 Oracle 用户和该用户的密码:
EXEC SQL CONNECT :user_name IDENTIFIED BY :password;
|
在 DB2 中,您需要指定数据库名称、用户 ID 和该用户 ID 的密码。因此上述语句就转换为:
EXEC SQL CONNECT TO :dbname USERID :userid PASSWORD : password;
|
请注意,需要将 dbname 、 userid 和 password 声明为宿主变量(host variable)。
宿主变量声明
宿主变量是指在 SQL 语句中引用到的 C 或 C++ 语言变量。这些变量允许应用程序将输入数据传递给数据库管理器,或者从数据库管理器接收输出数据。在对应用程序进行预编译之后,编译器将宿主变量当作任何其他 C/C++ 变量使用。
宿主变量不仅应该与 DB2 数据类型兼容(被 DB2 预编译器接受),而且还应该被编程语言编译器接受。
当 C 程序使用宿主变量操纵来自表中的值时,第一步是将 Oracle 表定义转换为 DB2 数据类型(参见下面的 表 1)。请注意,这是一对多的映射,因为它取决于对数据的实际使用。例如,如果 Oracle DATE 只存储实际的数据,那么它可以转换为 DB2 DATE,但是如果它存储 DATE 和 TIME,则需要将它转换为 DB2 TIMESTAMP。
接下来的一步是用 C 数据类型匹配 DB2 数据类型。 表 1 显示了数据类型之间的映射:
表 1. Oracle 表定义到 DB2 数据类型
| Oracle 数据类型 |
DB2 数据类型 |
C 数据类型声明 |
| CHAR(n) |
CHAR(n) |
char char_var[n+1]; |
| VARCHAR2(n) |
VARCHAR(n) |
struct{short len; char data[n]} varchar_var; or char varchar_var[n+1]; |
| LONG |
CLOB(2GB) |
SQL TYPE IS CLOB(n) v-name; |
| NUMBER(p,s) |
NUMERIC(p,s) |
double num_var; |
| DECIMAL(p,s) |
DECIMAL(p,s) |
double dec_var; !note that passing parameters using the SQLDA allows an exact representation of DECIMAL/NUMERIC using packed BCD encoding. |
| INTEGER |
INTEGER |
sqlint32 int_var; |
| SMALLINT |
SMALLINT |
sqlint16 int_var; |
| |
BIGINT |
sqlint64 bigint_var; |
| RAW(n) |
CHAR(n) FOR BIT DATA |
char ... [n] |
| LONG RAW |
BLOB(2GB) |
SQL TYPE IS BLOB(n) v_name; |
| DATE |
TIMESTAMP |
char tms_var[27]; |
| DATE (only the date) |
DATE (MM/DD/YYYY) |
char dt_var[11]; |
| DATE(only the time) |
TIME (HH24:MI:SS) |
char tm_var[16]; |
C 程序中的所有宿主变量都需要在一个专门的块中声明,以便 DB2 预编译器能够识别宿主变量以及它们的数据类型。
EXEC SQL BEGIN DECLARE SECTION;
char emp_name[31] = {'\\0'};
sqlint32 ret_code = 0;
EXEC SQL END DECLARE SECTION;
|
在这个声明块中,针对宿主变量数据类型的一些规则不同于 Oracle 预编译器中的规则。Oracle 预编译器允许将宿主变量声明为 VARCHAR 。 VARCHAR[n] 是 Pro*C 预编译器能够识别的伪类型。它用于表示用空格填充的、长度变化的字符串。Pro*C 预编译器将把它转换为由一个 2 字节长字段,后面跟一个 n-字节的字符数组组成的结构。DB2 要求使用标准 C 构造。因此,对变量 emp_name VARCHAR[25] 的声明需要转换为:
struct emp_name {
short var_len;
char var_data[25] };
|
或者,如前所述,对 VARCHAR 数据使用 " char emp_name[n] " 也是允许的。PRO*C 中用户定义类型(使用 typedef)的变量需要转换为源数据类型。例如,类型 theUser_t 以前是 Oracle 对象类型,现在被声明为宿主变量:
typedef struct user_s
{short int userNum;
char userName[25];
char userAddress[40];
} theUser_t;
|
在 Pro*C 程序中,您可以将宿主变量声明为 theUser_t :
EXEC SQL BEGIN DECLARE;
theUser_t *myUser;
EXEC SQL END DECLARE SECTION;
|
为了将该宿主变量用于 DB2,您需要将其从 EXEC SQL DECLARE SECTION 取出并将宿主变量 MyUser 定义为一个结构。
DB2 允许将宿主变量声明为一个指针,不过有以下限制:
- 如果宿主变量被声明为指针,则在同一个源文件中不能用同样的名称再去声明其他的宿主变量。
- 宿主变量声明 char *ptr 是可以接受的,但这并不是指 以零字符结尾的长度不确定的字符串 。相反,它指的是 指向一个固定长度的、单字符的宿主变量的指针。这可能并不是 Oracle 宿主变量声明所希望达到的目的。
我们建议为 INTEGER和 BIGINT 分别使用 sqlint32 和 sqlint64 。在默认情况下,对于那些 long 为 64 位数的平台,例如 64-位 UNIX®,对 long 宿主变量的使用将造成预编译器错误 SQL0402。使用 PREP 选项 LONGERROR NO 强迫 DB2 将 long 作为可接受的宿主变量类型来接受,并把它们当作 BIGINT变量。
Oracle 宿主表
在 Pro*C 程序中,可以使用数组声明宿主变量,然后声明一个您希望从中获取结果的游标。接着可以产生一个 fetch 语句,从该游标中取出所有的行并存放到那个宿主数组中。
下面来自 PRO*C 的一个代码片断演示了这个方法:
EXEC SQL BEGIN DECLARE SECTION;
long int dept_numb[10];
char dept_name[10][14];
char v_location[12];
EXEC SQL END DECLARE SECTION;
/* ... */
EXEC SQL DECLARE CUR1 CURSOR FOR
SELECT DEPTNUMB, DEPTNAME
FROM org_table
WHERE LOCATION = :v_location;
/*.... */
EXEC SQL FETCH CUR1 INTO :dept_num, :dept_name;
|
最后一条语句将从游标中取出 10 行放入数组中。
由于 DB2 并不支持在宿主变量声明中使用数组,因此上述代码需要转换为:
EXEC SQL BEGIN DECLARE SECTION;
sqlint32 h_dept_numb = 0;
char h_dept_name[14] = {'\\0'};
char v_location[12] = {'\\0'};
EXEC SQL END DECLARE SECTION;
/* move array out of DECLARE section - just C variables */
long int dept_numb[10];
char dept_name[10][14];
short int i = 0;
/* ... */
EXEC SQL DECLARE CUR1 CURSOR FOR
SELECT DEPTNUMB, DEPTNAME
FROM org_table
WHERE LOCATION = :v_location;
/*we need Fetch one row at the time and move to corresponding
member of array */
for (i=0;i<11;i++){
EXEC SQL FETCH CUR1 INTO :h_dept_num, :h_dept_name;
if (SQLCODE == 100){
break;
}
dept_numb[i] = h_dept_numb;
strcpy(dept_name[i], h_dept_name);
}
|
异常处理
Oracle 与 DB2 的错误捕捉机制非常相似,两者都使用将错误例程从主线逻辑分离出来的概念。在 DB2 中,有三种不同的 WHENEVER 语句可用于定义出现错误情况下的程序行为:
EXEC SQL WHENEVER SQLERROR GOTO error_routine;
EXEC SQL WHENEVER SQLWARNING CONTINUE;
EXEC SQL WHENEVER NOT FOUND not_found_routine;
|
虽然 WHENEVER 语句像其他 SQL 语句一样以 EXEC SQL 作为前缀,但它并不是可执行的语句。相反,WHENEVER 语句会引起预编译器在每条 SQL 语句之后生成程序中的代码,并执行在 WHENEVER 语句中指定的动作。SQLERROR 意味着某一条 SQL 语句返回一个非正的 SQLCODE,用于表示一个错误情况。SQLWARNING 表示一个非负的 SQLCODE (除了 +100),而 NOT FOUND 指定 SQLCODE = +100,表示没有找到符合请求的数据行。一个编译单元可以包含尽可能多的 WHENEVER 语句,并且这些语句可以放在程序中的任何地方。一条 WHENEVER 语句的作用域很广,从该语句在文件中所在的地方,到文件的字符流之内,一直到下一条合适的 WHENEVER 语句出现或者到达文件的结尾处。在那样的分析中没有考虑函数或程序块。例如,您可能有两条不同的 SELECT 语句,其中一条必须返回至少一行数据,而另一条可以不返回任何数据。您将需要两条不同的 WHENEVER 语句:
EXEC SQL WHENEVER NOT FOUND GOTO no_row_error;
EXEC SQL SELECT address
INTO :address
FROM test_table
WHERE phone = :pnone_num;
.....
EXEC SQL WHENEVER NOT FOUND CONTINUE;
EXEC SQL SELECT commis_rate
INTO :rate :rateind
WHERE prod_id = :prodId;
if (rateind == -1 ) rate = 0.15;
...
|
Oracle 预编译器还支持 DO 和 STOP 语句作为 WHENEVER 语句中的动作。但是 DB2 预编译器并不支持这两种语句,而需要将其转换为 GOTO 语句。
另一种替代的做法是在每条 EXEC SQL 语句之后显式地检查 SQLCODE,因为这将允许更多上下文敏感的错误处理。
错误消息和警告
DB2 中的 SQL Communication Area (SQLCA) 数据结构与 Oracle 中的类似。SQLCA 提供了用于诊断性检查和事件处理的信息。
为了获得全文本的更长的(或嵌套的)错误消息,您需要 sqlglm() 函数:
sqlglm(message_buffer, &buffer_size, &message_length);
|
其中 message_buffer 是字符缓冲区,您希望 Oracle 将错误消息存放在其中。 buffer_size 指定 message_buffer 的大小,单位为字节。Oracle 将错误消息的实际长度存储在 message_length 中。Oracle 错误消息的最大长度是 512 字节。
DB2 Universal Database 为它的用户提供了一个特殊的运行时 API 函数,用以返回一条基于 SQLCODE 的错误消息:
rc=sqlaintp(msg_buffer, 1024, 80, sqlca.sqlcode);
|
其中 "80" 代表字符数,在消息中每过这么多字符就要插入一个换行符(line break)。DB2 将搜索工作边界以便插入那样一个换行符;"1024" 指定了消息缓冲区的长度;例如, char msg_buffer[1024] 。
调用这个函数后,被分配空间的缓冲区将包含描述性错误消息,例如:
SQL0433N Value "TEST VALUES" is too long. SQLSTATE=22001.
|
如果您需要更多关于某一特定错误的信息,DB2 Universal Database 提供了一个 API 函数,该函数返回与特定 SQLSTATE 相关的经过扩展的消息:
rc=sqlogstt(msg_sqlstate_buffer, 1024, 80, sqlca.sqlcode);
|
调用该函数后, char msg_sqlstate_buffer[1024] 将包含以下消息:
SQLSTATE 22001: Character data, right truncation occurred; for example, an update or insert
value is a string that is too long for the column, or datetime value cannot be assigned to a
host variable, because it is too small.
|
从 C 程序中传递数据给存储过程
在 Oracle 中,为了调用一个远程数据库过程,可以使用以下语句:
EXEC SQL EXECUTE
BEGIN
Package_name.SP_name(:arg_in1,:arg_in2, :status_out);
END;
END-EXEC;
|
在调用环境和存储过程之间值的传输可以通过参数达到。您可以为每个参数选择三种模式中的一种:IN、OUT 或 INOUT。例如,上面的存储过程可以声明为:
CREATE PACKAGE package_name IS
PROCEDURE SP_name(
agr_in1 IN NUMBER ,
arg_in2 IN CHAR(30),
status_out OUT NUMBER);
|
当该存储过程被调用时,从调用程序传递过来的值将被存储过程相应地接收。
DB2 客户机应用程序通过使用 CALL 语句来调用存储过程。CALL 语句可以传递参数给存储过程,并接受从存储过程返回的参数。它的语法如下:
CALL procedure_name (:parm1, ?parmN);
|
与所有 SQL 语句一样,先用参数标记准备好 CALL 语句,然后再使用 SQLDA 为标记提供值:
CALL procedure_name USING DESCRIPTOR host_var;
|
如果您有数量不确定的宿主变量,或者有很多变量(100 或更多),那么 SQLDA 将很有帮助。否则在那些情况下管理这么多变量是很麻烦的。
为了从 C 客户机上调用存储过程,首先需要完成以下任务:
- 需要创建一个存储过程,并将其注册到数据库。
- 对于该存储过程的每个 IN 和 INOUT 参数,应该声明和初始化一个宿主变量或参数标记。
考虑一个例子。对于那些当前薪水低于某个值的每一个雇员,程序必须给他加薪。程序将传递那个值给存储过程,执行一次更新,然后返回状态。用 C 编写的客户机代码看上去应该是这样的:
#include <sqlenv.h>
main()
{
EXEC SQL BEGIN DECLARE SECTION;
Sqlint32 salary_val=0;
Sqlint16 salind=1;
Sqlint16 status=0;
Sqlint16 statind=0;
EXEC SQL END DECLARE SECTION;
EXEC SQL INCLUDE SQLCA;
EXEC SQL CONNECT TO sample;
EXEC SQL WHENEVER SQLERROR GOTO err_routine;
salary_val = getSalaryForRaise();
statind = -1; /* set indicator variable to -1 for
status
as output-only variable */
EXEC SQL CALL raiseSal(:salary_val :salind, :status :statind);
if (status == 0){
printf (" The raises has been successfully given \\n ");
EXEC SQL COMMIT;
}
else
if (status ==1)
printf (" NO input values has been provided.\\n ");
else
if(status == 2)
printf("Stored procedure failed.\\n");
err_routine:
printf (" SQL Error, SQLCODE = \\n ", SQLCODE);
EXEC SQL ROLLBACK;
}
|
注意,在 CALL 语句中使用的所有宿主变量都是在 EXEC SQL DECLARE SECTION 中声明和初始化的。
构建一个 C/C++ DB2 应用程序
DB2 提供了用于预编译、编译和链接 C-embedded SQL 程序(在 C 中嵌入 SQL 的程序)的构建脚本(build script)。这些脚本位于 sqllib/samples/c 目录中,与可以用这些文件构建的示例程序放在一起。这个目录还包含 embprep 脚本,该脚本用在构建脚本内,用于预编译一个 *.sqc 文件。
DB2 为受支持平台上的每种语言提供了构建文件,在受支持平台上提供了它们构建的各种程序,与用于每个语言的示例程序放在同一个目录中。除非特别指明,否则这些构建文件是用于所有受支持平台上的各种受支持语言的。在 Windows® 上,构建文件有 .bat (batch) 扩展名,在 UNIX 平台上,构建文件没有扩展名。例如, bldmapp.bat 是一个用于构建 Windows 上的 C/C++ 应用程序的脚本。
DB2 还提供了 utilemb.sqc 和 utilemb.h 文件,这两个文件包含了用于错误处理的函数。为了使用这些实用函数(utility function),必须首先编译实用文件(utility file),然后再在创建可执行的目标程序的过程中链接它的目标文件。在示例目录中的 makefile 文件和构建文件都会为需要错误检查实用查询的程序做这些工作。
要了解更多关于构建 C 应用程序的信息,参见 DB2 UDB V8 应用程序开发指南:构建和运行应用程序 (ISBN SC09-4825-00)。
结束语
本文讨论了如何更改一个 C 程序,使其访问 DB2 Universal Database,而不是 Oracle 数据库。即使对于 C/C++ 开发来说嵌入 Oracle 和 DB2 UDB 数据库访问调用在原理上是类似的,但是两者之间的一些特定差别还是可能带来挑战。本文试图帮助开发者理解这些差异,并克服由这些差异带来的挑战。 |