在 Section 34.3 中你看到了如何从嵌入式 SQL 程序执行 SQL 语句。其中一些语句只使用了固定的值, 没有提供把用户提供的值插入语句、 或让程序处理查询返回值的方法。 这类语句在实际应用中并不真正有用。 本节详细解释如何使用一种称为 宿主变量的简单机制 在你的 C 程序与嵌入式 SQL 语句之间传递数据。 在嵌入式 SQL 程序中,我们把 SQL 语句视为 C 程序代码(即宿主语言)中的 客人。 因此 C 程序的变量被称为宿主变量。
在 PostgreSQL 后端与 ECPG 应用之间交换值的另一种方式 是使用 SQL 描述符,见 Section 34.7。
在嵌入式 SQL 中,在 C 程序与 SQL 语句之间传递数据 特别简单。你不必让程序把数据粘贴到语句中 (那会带来各种复杂性,例如正确地给值加引号), 只需在 SQL 语句中写上一个 C 变量的名字并在前面加一个冒号。 例如:
EXEC SQL INSERT INTO sometable VALUES (:v1, 'foo', :v2);
这条语句引用了名为
v1 和 v2 的两个 C 变量,
同时也使用了一个普通的 SQL 字符串字面值,
以说明你并不局限于只使用其中一种数据。
这种在 SQL 语句中插入 C 变量的风格 可以用在 SQL 语句中任何期望值表达式的地方。
要把数据从程序传递到数据库(例如作为查询中的参数), 或者把数据从数据库传回程序, 用于容纳这些数据的 C 变量需要在特殊标记的区域中声明, 这样嵌入式 SQL 预处理器才能感知它们。
该区域以下列语句开始:
EXEC SQL BEGIN DECLARE SECTION;
并以下列语句结束:
EXEC SQL END DECLARE SECTION;
在这两行之间必须是普通的 C 变量声明,例如:
int x = 4; char foo[16], bar[16];
可以看到,你可以选择性地给变量赋予一个初始值。 变量的作用域由其声明区域在程序中的位置决定。 你也可以用以下语法声明变量, 它会隐式地创建一个声明区域:
EXEC SQL int i = 4;
一个程序中可以有任意多个声明区域。
这些声明也会作为普通 C 变量回显到输出文件中, 因此不需要再次声明它们。 不打算在 SQL 命令中使用的变量 可以在这些特殊区域之外正常声明。
结构(structure)或联合(union)的定义也必须列在
DECLARE 区域内。否则预处理器无法处理这些类型,
因为它不知道其定义。
现在你应该能够把程序产生的数据传入 SQL 命令了。
但是如何检索查询的结果呢?为此,
嵌入式 SQL 提供了常用命令 SELECT 和
FETCH 的特殊变体。
这些命令带有一个特殊的 INTO 子句,
用于指定检索到的值要存放到哪些宿主变量中。
SELECT 用于只返回单行的查询,
FETCH 用于返回多行的查询,
后者需要配合游标使用。
下面是一个示例:
/* * assume this table: * CREATE TABLE test1 (a int, b varchar(50)); */ EXEC SQL BEGIN DECLARE SECTION; int v1; VARCHAR v2; EXEC SQL END DECLARE SECTION; ... EXEC SQL SELECT a, b INTO :v1, :v2 FROM test;
可以看到,INTO 子句出现在选择列表与
FROM 子句之间。
选择列表中的元素个数与
INTO 后面的列表(也称目标列表)中的元素个数
必须相等。
下面是一个使用 FETCH 命令的示例:
EXEC SQL BEGIN DECLARE SECTION;
int v1;
VARCHAR v2;
EXEC SQL END DECLARE SECTION;
...
EXEC SQL DECLARE foo CURSOR FOR SELECT a, b FROM test;
...
do
{
...
EXEC SQL FETCH NEXT FROM foo INTO :v1, :v2;
...
} while (...);
当 ECPG 应用在 PostgreSQL 服务器与 C 应用之间交换值时 (例如从服务器检索查询结果,或执行带输入参数的 SQL 语句), 这些值需要在 PostgreSQL 数据类型与宿主语言 (C)数据类型之间进行转换。
在这方面,数据类型分为两类:一些简单的 PostgreSQL 数据类型,
例如 integer 和 text,
可以被应用直接读写。其他 PostgreSQL 数据类型,
例如 timestamp 和 numeric,
则需要通过 pgtypes 库中的特殊库函数来处理。
见 Section 34.6。
Table 34.1 展示了哪些 PostgreSQL 数据类型对应哪些 C 数据类型。 当你希望发送或接收某个给定 PostgreSQL 数据类型的值时, 应当在声明区域中声明一个对应 C 数据类型的 C 变量。
Table 34.1. PostgreSQL 数据类型与 C 变量类型之间的映射
| PostgreSQL 数据类型 | 主变量类型 |
|---|---|
smallint | short |
integer | int |
bigint | long long int |
decimal | decimal[a] |
numeric | numeric[a] |
real | float |
double precision | double |
smallserial | short |
serial | int |
bigserial | long long int |
oid | unsigned int |
character(, varchar(, text | char[, VARCHAR[ |
name | char[NAMEDATALEN] |
timestamp | timestamp[a] |
interval | interval[a] |
date | date[a] |
boolean | bool[b] |
bytea | char *, bytea[ |
[a] 这种类型只能通过特殊的库函数访问;参见 Section 34.4.4.2。 [b] 如果不是原生类型,则在 | |
要处理 SQL 字符串数据类型,例如
varchar 和 text,
有两种可能的宿主变量声明方式。
一种方式是使用 char[],即
char 的数组,
这是 C 中处理字符数据最常见的方式。
EXEC SQL BEGIN DECLARE SECTION;
char str[50];
EXEC SQL END DECLARE SECTION;
注意,你必须小心处理字符串的长度。 如果一个宿主变量被用作接收 超过其容量的字符串的 SELECT 或 FETCH 语句的目标, 就会发生缓冲区溢出。
另一种方式是使用 VARCHAR 类型,
这是 ECPG 提供的一种特殊类型。类型
VARCHAR 的数组定义会被转换为
每个变量一个命名的 struct。类似这样的声明:
VARCHAR var[180];
会被转换为:
struct varchar_var { int len; char arr[180]; } var;
要在 SQL 语句中把这样的变量用作字符字符串数据的目标,
应当不带 * 解引用地使用该结构变量。
另外请注意,在使用 VARCHAR 类型时,
它必须始终是宿主变量,不能用在一个 SQL 语句中
期望值表达式的地方。
VARCHAR 可以用大写或小写书写,但不能大小写混用。
char 和 VARCHAR 宿主变量
也可以保存其他 SQL 类型的值,这些值会以它们的字符串形式存储。
ECPG 包含一些特殊类型,
帮助你轻松地与来自 PostgreSQL 服务器的某些特殊数据类型交互。
特别地,它实现了对 numeric、
decimal、date、
timestamp 和 interval
类型的支持。
以下小节描述这些特殊数据类型。关于 pgtypes 库函数的更多细节, 见 Section 34.6。
下面是在 ECPG 宿主应用程序中处理
timestamp 变量的一种模式。
首先,程序必须包含 timestamp
类型的头文件:
#include <pgtypes_timestamp.h>
接下来,在声明段中把一个主变量声明为
timestamp 类型:
EXEC SQL BEGIN DECLARE SECTION; timestamp ts; EXEC SQL END DECLARE SECTION;
在把一个值读入宿主变量之后,可以用 pgtypes 库函数处理它。
在下面的例子中,timestamp 值被
PGTYPEStimestamp_to_asc() 函数
转换为文本(ASCII)形式:
EXEC SQL BEGIN DECLARE SECTION;
timestamp ts;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT now() INTO :ts;
printf("ts = %s
", PGTYPEStimestamp_to_asc(ts));
此外,DATE 类型可以用相同的方式处理。
程序必须包含 pgtypes_date.h,
把一个宿主变量声明为 date 类型,
并使用 PGTYPESdate_to_asc() 函数
把一个 DATE 值转换为文本形式。更多细节见
Section 34.6.3。
interval 类型的处理也与
timestamp 和 date 类型相似。
不过,必须为 interval 类型的值显式分配内存。
换句话说,变量的内存空间必须是堆上分配的内存空间。
更多细节见 Section 34.6.5。
下面是一个示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <pgtypes_interval.h>
int
main(void)
{
EXEC SQL BEGIN DECLARE SECTION;
interval *in;
EXEC SQL END DECLARE SECTION;
EXEC SQL CONNECT TO testdb;
EXEC SQL SELECT pg_catalog.set_config('search_path', '', false); EXEC SQL COMMIT;
in = PGTYPESinterval_new();
EXEC SQL SELECT '1 min'::interval INTO :in;
printf("interval = %s\n", PGTYPESinterval_to_asc(in));
PGTYPESinterval_free(in);
EXEC SQL COMMIT;
EXEC SQL DISCONNECT ALL;
return 0;
}
numeric 和 decimal 类型的
处理方式与 interval 类型类似:
它要求定义一个指针,在堆上分配一些内存空间,
并使用 pgtypes 库函数访问该变量。
更多细节见 Section 34.6.2。
没有为 decimal 类型专门提供函数。
应用必须用 pgtypes 库函数把它转换为一个
numeric 变量,才能进行进一步的处理。
下面是一个处理 numeric
和 decimal 类型变量的示例程序。
#include <stdio.h>
#include <stdlib.h>
#include <pgtypes_numeric.h>
EXEC SQL WHENEVER SQLERROR STOP;
int
main(void)
{
EXEC SQL BEGIN DECLARE SECTION;
numeric *num;
numeric *num2;
decimal *dec;
EXEC SQL END DECLARE SECTION;
EXEC SQL CONNECT TO testdb;
EXEC SQL SELECT pg_catalog.set_config('search_path', '', false); EXEC SQL COMMIT;
num = PGTYPESnumeric_new();
dec = PGTYPESdecimal_new();
EXEC SQL SELECT 12.345::numeric(4,2), 23.456::decimal(4,2) INTO :num, :dec;
printf("numeric = %s\n", PGTYPESnumeric_to_asc(num, 0));
printf("numeric = %s\n", PGTYPESnumeric_to_asc(num, 1));
printf("numeric = %s\n", PGTYPESnumeric_to_asc(num, 2));
/* Convert decimal to numeric to show a decimal value. */
num2 = PGTYPESnumeric_new();
PGTYPESnumeric_from_decimal(dec, num2);
printf("decimal = %s\n", PGTYPESnumeric_to_asc(num2, 0));
printf("decimal = %s\n", PGTYPESnumeric_to_asc(num2, 1));
printf("decimal = %s\n", PGTYPESnumeric_to_asc(num2, 2));
PGTYPESnumeric_free(num2);
PGTYPESdecimal_free(dec);
PGTYPESnumeric_free(num);
EXEC SQL COMMIT;
EXEC SQL DISCONNECT ALL;
return 0;
}
bytea 类型的处理方式与
VARCHAR 类似。类型
bytea 的数组定义会被转换为
每个变量一个命名的结构。类似这样的声明:
bytea var[180];
会被转换为:
struct bytea_var { int len; char arr[180]; } var;
bytea 变量只能在 bytea_output
设置为 hex 时使用。
作为主变量,你还可以使用数组、typedef、结构和指针。
数组作为宿主变量有两种使用场景。
第一种是在 char[] 或
VARCHAR[] 中存储一些文本字符串,
如 Section 34.4.4.1 中所解释的。
第二种使用场景是从一个查询结果中检索多行,
否则一次只能检索一个结果行。
下面的例子扫描 pg_database
系统表,并显示所有可用数据库的 OID 和名称:
int
main(void)
{
EXEC SQL BEGIN DECLARE SECTION;
int dbid[8];
char dbname[8][16];
int i;
EXEC SQL END DECLARE SECTION;
memset(dbname, 0, sizeof(dbname));
EXEC SQL SELECT oid, datname INTO :dbid, :dbname FROM pg_database;
for (i = 0; i < 8; i++)
printf("id=%d name=%s
", dbid[i], dbname[i]);
EXEC SQL COMMIT;
return 0;
}
下面的例子扫描 pg_database
系统表并显示所有可用数据库的 OID 和名称:
int
main(void)
{
EXEC SQL BEGIN DECLARE SECTION;
int dbid[8];
char dbname[8][16];
int i;
EXEC SQL END DECLARE SECTION
一种其成员名称与查询结果的列名相匹配的结构, 可以用来一次检索多个列。这种结构 能够在单个宿主变量中处理多个列的值。
下面的例子从 pg_database
系统表中检索可用数据库的 OID、名称和大小,其中使用了
pg_database_size() 函数。
在这个例子中,一个结构变量
dbinfo_t,
其成员与
SELECT 的结果相匹配,
被用于把多行
保存到一个变量中:
EXEC SQL BEGIN DECLARE SECTION;
typedef struct
{
int oid;
char datname[65];
long long int size;
} dbinfo_t;
dbinfo_t dbval;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT oid, datname, pg_database_size(oid)
INTO :dbval
FROM pg_database;
这个示例显示如下结果。(具体值取决于本地环境。)
oid=1, datname=template1, size=4324580 oid=11510, datname=template0, size=4243460 oid=11511, datname=postgres, size=4324580 oid=313780, datname=testdb, size=8183012
结构宿主变量会按结构中的字段数量
“吸收”尽可能多的列。
附加的列可以赋给其他宿主变量。例如,
上面的程序也可以像这样重构,
让 size 变量
位于结构之外:
EXEC SQL BEGIN DECLARE SECTION;
typedef struct
{
int oid;
char datname[65];
} dbinfo_t;
dbinfo_t dbval;
long long int size;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT oid, datname, pg_database_size(oid)
INTO :dbval, :size
FROM pg_database;
使用 typedef 关键字可以把新类型映射到
已经存在的类型。
EXEC SQL BEGIN DECLARE SECTION;
typedef char mychartype[40];
typedef long serial_t;
EXEC SQL END DECLARE SECTION;
注意,你也可以使用:
EXEC SQL TYPE serial_t IS long;
这种声明不需要位于声明区域内。
你声明为 typedef 的任何单词
都不能在同一个程序后面的
EXEC SQL 命令中用作 SQL 关键字。
例如,下面这样是行不通的:
EXEC SQL BEGIN DECLARE SECTION;
typedef int start;
EXEC SQL END DECLARE
在 v16 之前的 PostgreSQL 版本中, 把 SQL 关键字用作 typedef 名称很可能导致与 typedef 本身的使用相关的语法错误, 而不是与把该名称用作 SQL 关键字相关的错误。 新的行为不太可能出现这种情况。
你可以为最常用的类型声明指针。 但要注意,在没有自动分配的情况下, 你不能把指针用作查询的目标变量。 关于自动分配的更多信息见 Section 34.7。
EXEC SQL BEGIN DECLARE SECTION;
int *intp;
char **charp;
EXEC SQL END DECLARE SECTION;
本节介绍如何在 ECPG 应用中处理非标量和用户定义的 SQL 层面的数据类型。注意,这与上一节描述的 非原始类型宿主变量的处理是不同的。
ECPG 并不直接支持多维 SQL 层面的数组。 一维 SQL 层数组可以映射到 C 数组宿主变量,反之亦然。 不过,在创建语句时,ecpg 并不知道这些列的类型, 所以它无法判断一个 C 数组是否应该映射到 SQL 数组。 在大多数情况下(见上文),这并不是问题, 但当你需要使用多维 SQL 数组时,这就成了问题。
如果一个查询分别访问数组的元素,
那么就可以避免在 ECPG 中使用数组。
这时应使用一个其类型可以映射到元素类型的宿主变量。
例如,如果某列的类型是 integer[],
那么可以使用一个类型为 int 的宿主变量。
此外,也可以按照下面描述的方式
把数组的所有元素映射到数组类型的宿主变量中。
下面是一个示例。假设有如下表:
CREATE TABLE t3 (
ii integer[]
);
testdb=> SELECT * FROM t3;
ii
-------------
{1,2,3,4,5}
(1 row)
下面的示例程序检索该数组的第 4 个元素,
并把它保存到一个类型为 int 的宿主变量中:
EXEC SQL BEGIN DECLARE SECTION; int ii; EXEC SQL END DECLARE SECTION; EXEC SQL DECLARE cur1 CURSOR FOR SELECT ii[4] FROM t3; EXEC SQL OPEN cur1; EXEC SQL WHENEVER NOT FOUND DO BREAK; EXEC SQL FETCH cur1 INTO :ii;
要把多个数组元素映射到数组类型宿主变量的多个元素, 数组列的每个元素和宿主变量数组的每个元素 都必须单独管理,例如:
EXEC SQL BEGIN DECLARE SECTION; int ii_a[8]; EXEC SQL END DECLARE SECTION; EXEC SQL DECLARE cur1 CURSOR FOR SELECT ii[1], ii[2], ii[3], ii[4] FROM t3; EXEC SQL OPEN cur1; EXEC SQL WHENEVER NOT FOUND DO BREAK; EXEC SQL FETCH cur1 INTO :ii_a[0], :ii_a[1], :ii_a[2], :ii_a[3];
再次注意:
EXEC SQL BEGIN DECLARE SECTION;
int ii_a[8];
EXEC SQL END DECLARE SECTION;
EXEC SQL DECLARE cur1 CURSOR FOR SELECT ii FROM t3;
EXEC SQL OPEN cur1;
EXEC SQL WHENEVER NOT FOUND DO BREAK;
while (1)
{
/* WRONG */
EXEC SQL FETCH cur1 INTO :ii_a;
...
}
在这里由于隐式转换的关系, 这种形式在语法上是正确的,但不会产生预期的结果。
另一种解决办法是把数组以其外在字符串表示的形式
存储在类型为 char[] 或
VARCHAR[] 的宿主变量中。
关于这种表示形式的更多细节见
Section 8.15.2。
ECPG 并不直接支持复合类型, 但存在一种简单的解决办法。 可用的解决办法与上面针对数组描述的办法类似: 既可以单独访问每个属性, 也可以使用其外在字符串表示。
对于下面的示例,假设有如下类型和表:
CREATE TYPE comp_t AS (intval integer, textval varchar(32)); CREATE TABLE t4 (compval comp_t); INSERT INTO t4 VALUES ( (256, 'PostgreSQL') );
因此,访问该值最明显的方式是单独访问每个属性:
EXEC SQL BEGIN DECLARE SECTION;
int intval;
char textval[33];
EXEC SQL END DECLARE SECTION;
EXEC SQL DECLARE cur1 CURSOR FOR SELECT (compval).intval, (compval).textval FROM t4;
EXEC SQL OPEN cur1;
EXEC SQL WHENEVER NOT FOUND DO BREAK;
while (1)
{
EXEC SQL FETCH cur1 INTO :intval, :textval;
...
}
为了增强这个例子,
可以把 FETCH 命令中用于保存值的宿主变量
汇集到一个结构中。关于结构形式宿主变量的更多细节见
Section 34.4.4.3.2。
该结构的形式如下所示:
EXEC SQL BEGIN DECLARE SECTION;
typedef struct comp_t
{
int intval;
char textval[33];
} comp_t;
comp_t compval;
EXEC SQL END DECLARE SECTION;
EXEC SQL DECLARE cur1 CURSOR FOR SELECT compval FROM t4;
EXEC SQL OPEN cur1;
EXEC SQL WHENEVER NOT FOUND DO BREAK;
while (1)
{
/* 把复合值的所有列取出到结构中 */
EXEC SQL FETCH cur1 INTO :compval;
printf("intval=%d, textval=%s
", compval.intval, compval.textval);
}
最后,也可以把复合类型的值以其外在字符串表示的形式
存储在类型为 char[] 或
VARCHAR[] 的宿主变量中。
但这样一来,就不容易从宿主变量访问该值的字段。
ECPG 并不直接支持新的用户定义基类型。
你可以使用它们的外在字符串表示以及类型为
char[] 或 VARCHAR[] 的宿主变量,
这种解决方案确实合适,并且足以满足许多情况。
下面是一个使用 Section 36.13 示例中的
数据类型 complex 的例子。
该类型的外在字符串表示是 (%f,%f),
它定义在
complex_in() 和
complex_out() 函数中。
下面的例子把复合类型的值检索到类型为
char[] 的宿主变量中,并交给
complex_out() 函数处理:
EXEC SQL BEGIN DECLARE SECTION;
char val[64];
EXEC SQL END DECLARE SECTION;
EXEC SQL DECLARE cur1 CURSOR FOR SELECT b FROM t1;
EXEC SQL OPEN cur1;
/* 在找到结果集末尾时跳出 while 循环 */
EXEC SQL WHENEVER NOT FOUND DO BREAK;
while (1)
{
EXEC SQL FETCH cur1 INTO :val;
printf("value=%s
", val);
}
另一种解决办法是避免在 ECPG 中直接使用用户定义的类型,
而是创建一个函数或转换,
在用户定义类型与 ECPG 能处理的基本类型之间进行转换。
但要注意,类型转换应该被声明为
IMPLICIT,
这样 PostgreSQL 才会在比较时自动使用它们。
例如,
CREATE FUNCTION create_complex(r double, i double) RETURNS complex LANGUAGE SQL IMMUTABLE AS $$ SELECT $1 * complex '(1,0')' + $2 * complex '(0,1)' $$;
在这一定义之后,下面这段代码
EXEC SQL BEGIN DECLARE SECTION; double a, b, c, d; EXEC SQL END DECLARE SECTION; a = 1; b = 2; c = 3; d = 4; EXEC SQL INSERT INTO test_complex VALUES (create_complex(:a, :b), create_complex(:c, :d));
与下面的代码效果相同:
EXEC SQL INSERT INTO test_complex VALUES ('(1,2)', '(3,4)');
上面的例子没有处理空值。实际上, 如果检索示例从数据库抓取到一个空值,它们会引发一个错误。 要能够把空值传递给数据库,或从数据库检索空值, 需要给每个包含数据值的宿主变量附加第二个宿主变量。 这第二个宿主变量被称为指示符(indicator) 或指示符变量(indicator variable)。
指示符还有另一个功能:如果指示符的值为正, 表示该值不是空值, 但它在存入宿主变量时被截断了。
如果给预处理器 ecpg 传递
-r no_indicator 参数,
它将以“无指示符”模式工作。
在无指示符模式下,如果没有指定指示符变量,
空值会作为字符串(对输入和输出都是如此)用空字符串来表示。