PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

34.4. 使用宿主变量 #

34.4.1. 概述
34.4.2. 声明区域
34.4.3. 获取查询结果
34.4.4. 数据类型映射
34.4.5. 处理非原始 SQL 数据类型
34.4.6. Indicators

在 Section 34.3 中你看到了如何从嵌入式 SQL 程序执行 SQL 语句。其中一些语句只使用了固定的值, 没有提供把用户提供的值插入语句、 或让程序处理查询返回值的方法。 这类语句在实际应用中并不真正有用。 本节详细解释如何使用一种称为 宿主变量的简单机制 在你的 C 程序与嵌入式 SQL 语句之间传递数据。 在嵌入式 SQL 程序中,我们把 SQL 语句视为 C 程序代码(即宿主语言)中的 客人。 因此 C 程序的变量被称为宿主变量。

在 PostgreSQL 后端与 ECPG 应用之间交换值的另一种方式 是使用 SQL 描述符,见 Section 34.7。

34.4.1. 概述 #

在嵌入式 SQL 中,在 C 程序与 SQL 语句之间传递数据 特别简单。你不必让程序把数据粘贴到语句中 (那会带来各种复杂性,例如正确地给值加引号), 只需在 SQL 语句中写上一个 C 变量的名字并在前面加一个冒号。 例如:

EXEC SQL INSERT INTO sometable VALUES (:v1, 'foo', :v2);

这条语句引用了名为 v1 和 v2 的两个 C 变量, 同时也使用了一个普通的 SQL 字符串字面值, 以说明你并不局限于只使用其中一种数据。

这种在 SQL 语句中插入 C 变量的风格 可以用在 SQL 语句中任何期望值表达式的地方。

34.4.2. 声明区域 #

要把数据从程序传递到数据库(例如作为查询中的参数), 或者把数据从数据库传回程序, 用于容纳这些数据的 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 区域内。否则预处理器无法处理这些类型, 因为它不知道其定义。

34.4.3. 获取查询结果 #

现在你应该能够把程序产生的数据传入 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 (...);

34.4.4. 数据类型映射 #

当 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 数据类型主变量类型
smallintshort
integerint
bigintlong long int
decimaldecimal[a]
numericnumeric[a]
realfloat
double precisiondouble
smallserialshort
serialint
bigseriallong long int
oidunsigned int
character(n), varchar(n), textchar[n+1], VARCHAR[n+1]
namechar[NAMEDATALEN]
timestamptimestamp[a]
intervalinterval[a]
datedate[a]
booleanbool[b]
byteachar *, bytea[n]

[a] 这种类型只能通过特殊的库函数访问;参见 Section 34.4.4.2。

[b] 如果不是原生类型,则在 ecpglib.h 中声明


34.4.4.1. 处理字符串 #

要处理 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 类型的值,这些值会以它们的字符串形式存储。

34.4.4.2. 访问特殊数据类型 #

ECPG 包含一些特殊类型, 帮助你轻松地与来自 PostgreSQL 服务器的某些特殊数据类型交互。 特别地,它实现了对 numeric、 decimal、date、 timestamp 和 interval 类型的支持。

以下小节描述这些特殊数据类型。关于 pgtypes 库函数的更多细节, 见 Section 34.6。

34.4.4.2.1. timestamp, date #

下面是在 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。

34.4.4.2.2. interval #

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;
}

34.4.4.2.3. numeric, decimal #

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;
}

34.4.4.2.4. bytea #

bytea 类型的处理方式与 VARCHAR 类似。类型 bytea 的数组定义会被转换为 每个变量一个命名的结构。类似这样的声明:

bytea var[180];

会被转换为:

struct bytea_var { int len; char arr[180]; } var;

Note

bytea 变量只能在 bytea_output 设置为 hex 时使用。

34.4.4.3. 使用非原始类型的主变量 #

作为主变量,你还可以使用数组、typedef、结构和指针。

34.4.4.3.1. Arrays #

数组作为宿主变量有两种使用场景。 第一种是在 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

34.4.4.3.2. Structures #

一种其成员名称与查询结果的列名相匹配的结构, 可以用来一次检索多个列。这种结构 能够在单个宿主变量中处理多个列的值。

下面的例子从 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;

34.4.4.3.3. Typedefs #

使用 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

Note

在 v16 之前的 PostgreSQL 版本中, 把 SQL 关键字用作 typedef 名称很可能导致与 typedef 本身的使用相关的语法错误, 而不是与把该名称用作 SQL 关键字相关的错误。 新的行为不太可能出现这种情况。

34.4.4.3.4. Pointers #

你可以为最常用的类型声明指针。 但要注意,在没有自动分配的情况下, 你不能把指针用作查询的目标变量。 关于自动分配的更多信息见 Section 34.7。

EXEC SQL BEGIN DECLARE SECTION;
    int   *intp;
    char **charp;
EXEC SQL END DECLARE SECTION;

34.4.5. 处理非原始 SQL 数据类型 #

本节介绍如何在 ECPG 应用中处理非标量和用户定义的 SQL 层面的数据类型。注意,这与上一节描述的 非原始类型宿主变量的处理是不同的。

34.4.5.1. Arrays #

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。

34.4.5.2. Composite Types #

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[] 的宿主变量中。 但这样一来,就不容易从宿主变量访问该值的字段。

34.4.5.3. 用户定义的基本类型 #

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)');

34.4.6. Indicators #

上面的例子没有处理空值。实际上, 如果检索示例从数据库抓取到一个空值,它们会引发一个错误。 要能够把空值传递给数据库,或从数据库检索空值, 需要给每个包含数据值的宿主变量附加第二个宿主变量。 这第二个宿主变量被称为指示符(indicator) 或指示符变量(indicator variable)。

指示符还有另一个功能:如果指示符的值为正, 表示该值不是空值, 但它在存入宿主变量时被截断了。

如果给预处理器 ecpg 传递 -r no_indicator 参数, 它将以“无指示符”模式工作。 在无指示符模式下,如果没有指定指示符变量, 空值会作为字符串(对输入和输出都是如此)用空字符串来表示。