PostgreSQL 控制結(jié)構(gòu)

2021-09-03 17:58 更新
42.6.1. 從一個(gè)函數(shù)返回
42.6.2. 從過(guò)程中返回
42.6.3. 調(diào)用存儲(chǔ)過(guò)程
42.6.4. 條件
42.6.5. 簡(jiǎn)單循環(huán)
42.6.6. 通過(guò)查詢結(jié)果循環(huán)
42.6.7. 通過(guò)數(shù)組循環(huán)
42.6.8. 俘獲錯(cuò)誤
42.6.9. 獲得執(zhí)行位置信息

控制結(jié)構(gòu)可能是PL/pgSQL中最有用的(以及最重要)的部分了。利用PL/pgSQL的控制結(jié)構(gòu),你可以以非常靈活而且強(qiáng)大的方法操縱PostgreSQL的數(shù)據(jù)。

42.6.1. 從一個(gè)函數(shù)返回

有兩個(gè)命令讓我們能夠從函數(shù)中返回?cái)?shù)據(jù):RETURNRETURN NEXT

42.6.1.1. RETURN

RETURN expression;

帶有一個(gè)表達(dá)式的RETURN用于終止函數(shù)并把expression的值返回給調(diào)用者。這種形式被用于不返回集合的PL/pgSQL函數(shù)。

如果一個(gè)函數(shù)返回一個(gè)標(biāo)量類型,表達(dá)式的結(jié)果將被自動(dòng)轉(zhuǎn)換成函數(shù)的返回類型。但是要返回一個(gè)復(fù)合(行)值,你必須寫一個(gè)正好產(chǎn)生所需列集合的表達(dá)式。這可能需要使用顯式造型。

如果你聲明帶輸出參數(shù)的函數(shù),那么就只需要寫不帶表達(dá)式的RETURN。輸出參數(shù)變量的當(dāng)前值將被返回。

如果你聲明函數(shù)返回void,一個(gè)RETURN語(yǔ)句可以被用來(lái)提前退出函數(shù);但是不要在RETURN后面寫一個(gè)表達(dá)式。

一個(gè)函數(shù)的返回值不能是未定義。如果控制到達(dá)了函數(shù)最頂層的塊而沒(méi)有碰到一個(gè)RETURN語(yǔ)句,那么會(huì)發(fā)生一個(gè)運(yùn)行時(shí)錯(cuò)誤。不過(guò),這個(gè)限制不適用于帶輸出參數(shù)的函數(shù)以及返回void的函數(shù)。在這些情況中,如果頂層的塊結(jié)束,將自動(dòng)執(zhí)行一個(gè)RETURN語(yǔ)句。

一些例子:

-- 返回一個(gè)標(biāo)量類型的函數(shù)
RETURN 1 + 2;
RETURN scalar_var;

-- 返回一個(gè)組合類型的函數(shù)
RETURN composite_type_var;
RETURN (1, 2, 'three'::text);  -- 必須把列造型成正確的類型

42.6.1.2. RETURN NEXT以及RETURN QUERY

RETURN NEXT expression;
RETURN QUERY query;
RETURN QUERY EXECUTE command-string [ USING expression [, ... ] ];

當(dāng)一個(gè)PL/pgSQL函數(shù)被聲明為返回SETOF sometype ,那么遵循的過(guò)程則略有不同。在這種情況下,要返回的個(gè)體項(xiàng)被用一個(gè)RETURN NEXT或者RETURN QUERY命令的序列指定,并且接著會(huì)用一個(gè)不帶參數(shù)的最終RETURN命令來(lái)指示這個(gè)函數(shù)已經(jīng)完成執(zhí)行。RETURN NEXT可以被用于標(biāo)量和復(fù)合數(shù)據(jù)類型;對(duì)于復(fù)合類型,將返回一個(gè)完整的結(jié)果 。RETURN QUERY將執(zhí)行一個(gè)查詢的結(jié)果追加到一個(gè)函數(shù)的結(jié)果集中。在一個(gè)單一的返回集合的函數(shù)中,RETURN NEXTRETURN QUERY可以被隨意地混合,這樣它們的結(jié)果將被串接起來(lái)。

RETURN NEXTRETURN QUERY實(shí)際上不會(huì)從函數(shù)中返回 — 它們簡(jiǎn)單地向函數(shù)的結(jié)果集中追加零或多行。然后會(huì)繼續(xù)執(zhí)行PL/pgSQL函數(shù)中的下一條語(yǔ)句。隨著后繼的RETURN NEXTRETURN QUERY命令的執(zhí)行,結(jié)果集就建立起來(lái)了。最后一個(gè) RETURN(應(yīng)該沒(méi)有參數(shù))會(huì)導(dǎo)致控制退出該函數(shù)(或者你可以讓控制到達(dá)函數(shù)的結(jié)尾)。

RETURN QUERY有一種變體RETURN QUERY EXECUTE,它可以動(dòng)態(tài)指定要被執(zhí)行的查詢??梢酝ㄟ^(guò)USING向計(jì)算出的查詢字符串插入?yún)?shù)表達(dá)式,這和在EXECUTE命令中的方式相同。

如果你聲明函數(shù)帶有輸出參數(shù),只需要寫不帶表達(dá)式的RETURN NEXT。在每一次執(zhí)行時(shí),輸出參數(shù)變量的當(dāng)前值將被保存下來(lái)用于最終返回為結(jié)果的一行。注意為了創(chuàng)建一個(gè)帶有輸出參數(shù)的集合返回函數(shù),在有多個(gè)輸出參數(shù)時(shí),你必須聲明函數(shù)為返回SETOF record;或者如果只有一個(gè)類型為sometype的輸出參數(shù)時(shí),聲明函數(shù)為 SETOF sometype 。

下面是一個(gè)使用RETURN NEXT的函數(shù)例子:

CREATE TABLE foo (fooid INT, foosubid INT, fooname TEXT);
INSERT INTO foo VALUES (1, 2, 'three');
INSERT INTO foo VALUES (4, 5, 'six');

CREATE OR REPLACE FUNCTION get_all_foo() RETURNS SETOF foo AS
$BODY$
DECLARE
    r foo%rowtype;
BEGIN
    FOR r IN
        SELECT * FROM foo WHERE fooid > 0
    LOOP
        -- 這里可以做一些處理
        RETURN NEXT r; -- 返回 SELECT 的當(dāng)前行
    END LOOP;
    RETURN;
END;
$BODY$
LANGUAGE plpgsql;

SELECT * FROM get_all_foo();

這里是一個(gè)使用RETURN QUERY的函數(shù)的例子:

CREATE FUNCTION get_available_flightid(date) RETURNS SETOF integer AS
$BODY$
BEGIN
    RETURN QUERY SELECT flightid
                   FROM flight
                  WHERE flightdate >= $1
                    AND flightdate < ($1 + 1);

    -- 因?yàn)閳?zhí)行還未結(jié)束,我們可以檢查是否有行被返回
    -- 如果沒(méi)有就拋出異常。
    IF NOT FOUND THEN
        RAISE EXCEPTION 'No flight at %.', $1;
    END IF;

    RETURN;
 END;
$BODY$
LANGUAGE plpgsql;

-- 返回可用的航班或者在沒(méi)有可用航班時(shí)拋出異常。
SELECT * FROM get_available_flightid(CURRENT_DATE);

注意

如上所述,目前RETURN NEXTRETURN QUERY的實(shí)現(xiàn)在從函數(shù)返回之前會(huì)把整個(gè)結(jié)果集都保存起來(lái)。這意味著如果一個(gè)PL/pgSQL函數(shù)生成一個(gè)非常大的結(jié)果集,性能可能會(huì)很差:數(shù)據(jù)將被寫到磁盤上以避免內(nèi)存耗盡,但是函數(shù)本身在整個(gè)結(jié)果集都生成之前不會(huì)退出。將來(lái)的PL/pgSQL版本可能會(huì)允許用戶定義沒(méi)有這種限制的集合返回函數(shù)。目前,數(shù)據(jù)開(kāi)始被寫入到磁盤的時(shí)機(jī)由配置變量 work_mem控制。擁有足夠內(nèi)存來(lái)存儲(chǔ)大型結(jié)果集的管理員可以考慮增大這個(gè)參數(shù)。

42.6.2. 從過(guò)程中返回

過(guò)程沒(méi)有返回值。因此,過(guò)程的結(jié)束可以不用RETURN語(yǔ)句。 如果想用一個(gè)RETURN語(yǔ)句提前退出代碼,只需寫一個(gè)沒(méi)有表達(dá)式的RETURN。

如果過(guò)程有輸出參數(shù),那么輸出參數(shù)最終的值會(huì)被返回給調(diào)用者。

42.6.3. 調(diào)用存儲(chǔ)過(guò)程

PL/pgSQL函數(shù),存儲(chǔ)過(guò)程或DO塊可以使用 CALL調(diào)用存儲(chǔ)過(guò)程。 輸出參數(shù)的處理方式與純SQL中CALL的工作方式不同。 存儲(chǔ)過(guò)程的每個(gè)INOUT參數(shù)必須和CALL語(yǔ)句中的變量對(duì)應(yīng), 并且無(wú)論存儲(chǔ)過(guò)程返回什么,都會(huì)在返回后賦值給該變量。 例如:

CREATE PROCEDURE triple(INOUT x int)
LANGUAGE plpgsql
AS $$
BEGIN
    x := x * 3;
END;
$$;

DO $$
DECLARE myvar int := 5;
BEGIN
  CALL triple(myvar);
  RAISE NOTICE 'myvar = %', myvar;  -- prints 15
END;
$$;

42.6.4. 條件

IFCASE語(yǔ)句讓你可以根據(jù)某種條件執(zhí)行二選其一的命令。PL/pgSQL有三種形式的IF

  • IF ... THEN ... END IF

  • IF ... THEN ... ELSE ... END IF

  • IF ... THEN ... ELSIF ... THEN ... ELSE ... END IF

以及兩種形式的CASE

  • CASE ... WHEN ... THEN ... ELSE ... END CASE

  • CASE WHEN ... THEN ... ELSE ... END CASE

42.6.4.1. IF-THEN

IF boolean-expression THEN
    statements
END IF;

IF-THEN語(yǔ)句是IF的最簡(jiǎn)單形式。 如果條件為真,在THENEND IF之間的語(yǔ)句將被執(zhí)行。否則,將忽略它們。

例子:

IF v_user_id <> 0 THEN
    UPDATE users SET email = v_email WHERE user_id = v_user_id;
END IF;

42.6.4.2. IF-THEN-ELSE

IF boolean-expression THEN
    statements
ELSE
    statements
END IF;

IF-THEN-ELSE語(yǔ)句對(duì)IF-THEN進(jìn)行了增加,它讓你能夠指定一組在條件不為真時(shí)應(yīng)該被執(zhí)行的語(yǔ)句(注意這也包括條件為 NULL 的情況)。

例子:

IF parentid IS NULL OR parentid = ''
THEN
    RETURN fullname;
ELSE
    RETURN hp_true_filename(parentid) || '/' || fullname;
END IF;

IF v_count > 0 THEN
    INSERT INTO users_count (count) VALUES (v_count);
    RETURN 't';
ELSE
    RETURN 'f';
END IF;

42.6.4.3. IF-THEN-ELSIF

IF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements
    ...
]
]
[ ELSE
    statements ]
END IF;

有時(shí)會(huì)有多于兩種選擇。IF-THEN-ELSIF則提供了一個(gè)簡(jiǎn)便的方法來(lái)檢查多個(gè)條件。IF條件會(huì)被一個(gè)接一個(gè)測(cè)試,直到找到第一個(gè)為真的。然后執(zhí)行相關(guān)語(yǔ)句,然后控制會(huì)被交給END IF之后的下一個(gè)語(yǔ)句(后續(xù)的任何IF條件不會(huì)被測(cè)試)。如果沒(méi)有一個(gè) IF條件為真,那么ELSE塊(如果有)將被執(zhí)行。

這里有一個(gè)例子:

IF number = 0 THEN
    result := 'zero';
ELSIF number > 0 THEN
    result := 'positive';
ELSIF number < 0 THEN
    result := 'negative';
ELSE
    -- 嗯,唯一的其他可能性是數(shù)字為空
    result := 'NULL';
END IF;

關(guān)鍵詞ELSIF也可以被拼寫成ELSEIF

另一個(gè)可以完成相同任務(wù)的方法是嵌套IF-THEN-ELSE語(yǔ)句,如下例:

IF demo_row.sex = 'm' THEN
    pretty_sex := 'man';
ELSE
    IF demo_row.sex = 'f' THEN
        pretty_sex := 'woman';
    END IF;
END IF;

不過(guò),這種方法需要為每個(gè)IF都寫一個(gè)匹配的END IF,因此當(dāng)有很多選擇時(shí),這種方法比使用ELSIF要麻煩得多。

42.6.4.4. 簡(jiǎn)單CASE

CASE search-expression
    WHEN expression [, expression [ ... ]] THEN
      statements
  [ WHEN expression [, expression [ ... ]] THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

CASE的簡(jiǎn)單形式提供了基于操作數(shù)等值判斷的有條件執(zhí)行。search-expression會(huì)被計(jì)算(一次)并且一個(gè)接一個(gè)地與WHEN子句中的每個(gè)expression比較。如果找到一個(gè)匹配,那么相應(yīng)的statements會(huì)被執(zhí)行,并且接著控制會(huì)被交給 END CASE之后的下一個(gè)語(yǔ)句(后續(xù)的WHEN表達(dá)式不會(huì)被計(jì)算)。如果沒(méi)有找到匹配,ELSE 語(yǔ)句會(huì)被執(zhí)行。但是如果ELSE不存在,將會(huì)拋出一個(gè)CASE_NOT_FOUND異常。

這里是一個(gè)簡(jiǎn)單的例子:

CASE x
    WHEN 1, 2 THEN
        msg := 'one or two';
    ELSE
        msg := 'other value than one or two';
END CASE;

42.6.4.5. 搜索CASE

CASE
    WHEN boolean-expression THEN
      statements
  [ WHEN boolean-expression THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

CASE的搜索形式基于布爾表達(dá)式真假的有條件執(zhí)行。每一個(gè)WHEN子句的boolean-expression會(huì)被依次計(jì)算,直到找到一個(gè)得到的。然后相應(yīng)的statements會(huì)被執(zhí)行,并且接下來(lái)控制會(huì)被傳遞給 END CASE之后的下一個(gè)語(yǔ)句(后續(xù)的WHEN表達(dá)式不會(huì)被計(jì)算)。如果沒(méi)有找到為真的結(jié)果,ELSE statements會(huì)被執(zhí)行。但是如果ELSE不存在,那么將會(huì)拋出一個(gè)CASE_NOT_FOUND異常。

這里是一個(gè)例子:

CASE
    WHEN x BETWEEN 0 AND 10 THEN
        msg := 'value is between zero and ten';
    WHEN x BETWEEN 11 AND 20 THEN
        msg := 'value is between eleven and twenty';
END CASE;

這種形式的CASE整體上等價(jià)于IF-THEN-ELSIF,不同之處在于CASE到達(dá)一個(gè)被忽略的ELSE子句時(shí)會(huì)導(dǎo)致一個(gè)錯(cuò)誤而不是什么也不做。

42.6.5. 簡(jiǎn)單循環(huán)

使用LOOP、EXIT、CONTINUE、WHILE、FORFOREACH語(yǔ)句,你可以安排PL/pgSQL重復(fù)一系列命令。

42.6.5.1. LOOP

[ <<label>> ]
LOOP
    statements
END LOOP [ label ];

LOOP定義一個(gè)無(wú)條件的循環(huán),它會(huì)無(wú)限重復(fù)直到被EXITRETURN語(yǔ)句終止??蛇x的label可以被EXITCONTINUE語(yǔ)句用在嵌套循環(huán)中指定這些語(yǔ)句引用的是哪一層循環(huán)。

42.6.5.2. EXIT

EXIT [ label ] [ WHEN boolean-expression ];

如果沒(méi)有給出label,那么最內(nèi)層的循環(huán)會(huì)被終止,然后跟在END LOOP后面的語(yǔ)句會(huì)被執(zhí)行。如果給出了label,那么它必須是當(dāng)前或者更高層的嵌套循環(huán)或者語(yǔ)句塊的標(biāo)簽。然后該命名循環(huán)或塊就會(huì)被終止,并且控制會(huì)轉(zhuǎn)移到該循環(huán)/塊相應(yīng)的END之后的語(yǔ)句上。

如果指定了WHEN,只有boolean-expression為真時(shí)才會(huì)發(fā)生循環(huán)退出。否則,控制會(huì)轉(zhuǎn)移到EXIT之后的語(yǔ)句。

EXIT可以被用在所有類型的循環(huán)中,它并不限于在無(wú)條件循環(huán)中使用。

在和BEGIN塊一起使用時(shí),EXIT會(huì)把控制交給塊結(jié)束后的下一個(gè)語(yǔ)句。需要注意的是,一個(gè)標(biāo)簽必須被用于這個(gè)目的;一個(gè)沒(méi)有被標(biāo)記的EXIT永遠(yuǎn)無(wú)法被認(rèn)為與一個(gè)BEGIN塊匹配(這種狀況從PostgreSQL 8.4 之前的發(fā)布就已經(jīng)開(kāi)始改變。這可能允許一個(gè)未被標(biāo)記的EXIT匹配一個(gè)BEGIN塊)。

例子:

LOOP
    -- 一些計(jì)算
    IF count > 0 THEN
        EXIT;  -- 退出循環(huán)
    END IF;
END LOOP;

LOOP
    -- 一些計(jì)算
    EXIT WHEN count > 0;  -- 和前一個(gè)例子相同的結(jié)果
END LOOP;

<<ablock>>
BEGIN
    -- 一些計(jì)算
    IF stocks > 100000 THEN
        EXIT ablock;  -- 導(dǎo)致從 BEGIN 塊中退出
    END IF;
    -- 當(dāng)stocks > 100000時(shí),這里的計(jì)算將被跳過(guò)
END;

42.6.5.3. CONTINUE

CONTINUE [ label ] [ WHEN boolean-expression ];

如果沒(méi)有給出label,最內(nèi)層循環(huán)的下一次迭代會(huì)開(kāi)始。也就是,循環(huán)體中剩余的所有語(yǔ)句將被跳過(guò),并且控制會(huì)返回到循環(huán)控制表達(dá)式(如果有)來(lái)決定是否需要另一次循環(huán)迭代。如果label存在,它指定應(yīng)該繼續(xù)執(zhí)行的循環(huán)的標(biāo)簽。

如果指定了WHEN,該循環(huán)的下一次迭代只有在boolean-expression為真時(shí)才會(huì)開(kāi)始。否則,控制會(huì)傳遞給CONTINUE后面的語(yǔ)句。

CONTINUE可以被用在所有類型的循環(huán)中,它并不限于在無(wú)條件循環(huán)中使用。

例子:

LOOP
    -- 一些計(jì)算
    EXIT WHEN count > 100;
    CONTINUE WHEN count < 50;
    -- 一些用于 count IN [50 .. 100] 的計(jì)算
END LOOP;

42.6.5.4. WHILE

[ <<label>> ]
WHILE boolean-expression LOOP
    statements
END LOOP [ label ];

只要boolean-expression被計(jì)算為真,WHILE語(yǔ)句就會(huì)重復(fù)一個(gè)語(yǔ)句序列。在每次進(jìn)入到循環(huán)體之前都會(huì)檢查該表達(dá)式。

例如:

WHILE amount_owed > 0 AND gift_certificate_balance > 0 LOOP
    -- 這里是一些計(jì)算
END LOOP;

WHILE NOT done LOOP
    -- 這里是一些計(jì)算
END LOOP;

42.6.5.5. FOR(整型變體)

[ <<label>> ]
FOR name IN [ REVERSE ] expression .. expression [ BY expression ] LOOP
    statements
END LOOP [ label ];

這種形式的FOR會(huì)創(chuàng)建一個(gè)在一個(gè)整數(shù)范圍上迭代的循環(huán)。變量name會(huì)自動(dòng)定義為類型integer并且只在循環(huán)內(nèi)存在(任何該變量名的現(xiàn)有定義在此循環(huán)內(nèi)都將被忽略)。給出范圍上下界的兩個(gè)表達(dá)式在進(jìn)入循環(huán)的時(shí)候計(jì)算一次。如果沒(méi)有指定BY子句,迭代步長(zhǎng)為 1,否則步長(zhǎng)是BY中指定的值,該值也只在循環(huán)進(jìn)入時(shí)計(jì)算一次。如果指定了REVERSE,那么在每次迭代后步長(zhǎng)值會(huì)被減除而不是增加。

整數(shù)FOR循環(huán)的一些例子:

FOR i IN 1..10 LOOP
    -- 我在循環(huán)中將取值 1,2,3,4,5,6,7,8,9,10 
END LOOP;

FOR i IN REVERSE 10..1 LOOP
    -- 我在循環(huán)中將取值 10,9,8,7,6,5,4,3,2,1 
END LOOP;

FOR i IN REVERSE 10..1 BY 2 LOOP
    -- 我在循環(huán)中將取值 10,8,6,4,2 
END LOOP;

如果下界大于上界(或者在REVERSE情況下是小于),循環(huán)體根本不會(huì)被執(zhí)行。而且不會(huì)拋出任何錯(cuò)誤。

如果一個(gè)label被附加到FOR循環(huán),那么整數(shù)循環(huán)變量可以用一個(gè)使用那個(gè)label的限定名引用。

42.6.6. 通過(guò)查詢結(jié)果循環(huán)

使用一種不同類型的FOR循環(huán),你可以通過(guò)一個(gè)查詢的結(jié)果進(jìn)行迭代并且操縱相應(yīng)的數(shù)據(jù)。語(yǔ)法是:

[ <<label>> ]
FOR target IN query LOOP
    statements
END LOOP [ label ];

target是一個(gè)記錄變量、行變量或者逗號(hào)分隔的標(biāo)量變量列表。target被連續(xù)不斷被賦予來(lái)自query的每一行,并且循環(huán)體將為每一行執(zhí)行一次。下面是一個(gè)例子:

CREATE FUNCTION refresh_mviews() RETURNS integer AS $$
DECLARE
    mviews RECORD;
BEGIN
    RAISE NOTICE 'Refreshing all materialized views...';

    FOR mviews IN
       SELECT n.nspname AS mv_schema,
              c.relname AS mv_name,
              pg_catalog.pg_get_userbyid(c.relowner) AS owner
         FROM pg_catalog.pg_class c
    LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
        WHERE c.relkind = 'm'
     ORDER BY 1
    LOOP

        -- Now "mviews" has one record with information about the materialized view

        RAISE NOTICE 'Refreshing materialized view %.% (owner: %)...',
                     quote_ident(mviews.mv_schema),
                     quote_ident(mviews.mv_name),
                     quote_ident(mviews.owner);
        EXECUTE format('REFRESH MATERIALIZED VIEW %I.%I', mviews.mv_schema, mviews.mv_name);
    END LOOP;

    RAISE NOTICE 'Done refreshing materialized views.';
    RETURN 1;
END;
$$ LANGUAGE plpgsql;

如果循環(huán)被一個(gè)EXIT語(yǔ)句終止,那么在循環(huán)之后你仍然可以訪問(wèn)最后被賦予的行值。

在這類FOR語(yǔ)句中使用的query可以是任何返回行給調(diào)用者的 SQL 命令:最常見(jiàn)的是SELECT,但你也可以使用帶有RETURNING子句的INSERT、UPDATEDELETE。一些EXPLAIN之類的功能性命令也可以用在這里。

PL/pgSQL變量會(huì)被替換到查詢文本中,并且如第 42.11.1 節(jié)第 42.11.2 節(jié)中詳細(xì)討論的,查詢計(jì)劃會(huì)被緩存以用于可能的重用。

FOR-IN-EXECUTE語(yǔ)句是在行上迭代的另一種方式:

[ <<label>> ]
FOR target IN EXECUTE text_expression [ USING expression [, ... ] ] LOOP
    statements
END LOOP [ label ];

這個(gè)例子類似前面的形式,只不過(guò)源查詢被指定為一個(gè)字符串表達(dá)式,在每次進(jìn)入FOR循環(huán)時(shí)都會(huì)計(jì)算它并且重新規(guī)劃。這允許程序員在一個(gè)預(yù)先規(guī)劃好了的命令的速度和一個(gè)動(dòng)態(tài)命令的靈活性之間進(jìn)行選擇,就像一個(gè)純EXECUTE語(yǔ)句那樣。在使用EXECUTE時(shí),可以通過(guò)USING將參數(shù)值插入到動(dòng)態(tài)命令中。

另一種指定要對(duì)其結(jié)果迭代的查詢的方式是將它聲明為一個(gè)游標(biāo)。這會(huì)在第 42.7.4 節(jié)中描述。

42.6.7. 通過(guò)數(shù)組循環(huán)

FOREACH循環(huán)很像一個(gè)FOR循環(huán),但不是通過(guò)一個(gè) SQL 查詢返回的行進(jìn)行迭代,它通過(guò)一個(gè)數(shù)組值的元素來(lái)迭代(通常,FOREACH意味著通過(guò)一個(gè)組合值表達(dá)式的部件迭代;用于通過(guò)除數(shù)組之外組合類型進(jìn)行循環(huán)的變體可能會(huì)在未來(lái)被加入)。在一個(gè)數(shù)組上循環(huán)的FOREACH語(yǔ)句是:

[ <<label>> ] FOREACH target [ SLICE number ] IN ARRAY
        expression

            LOOP statements
END LOOP [ label ];

如果沒(méi)有SLICE,或者如果沒(méi)有指定SLICE 0,循環(huán)會(huì)通過(guò)計(jì)算expression得到的數(shù)組的個(gè)體元素進(jìn)行迭代。target變量被逐一賦予每一個(gè)元素值,并且循環(huán)體會(huì)為每一個(gè)元素執(zhí)行。這里是一個(gè)通過(guò)整數(shù)數(shù)組的元素循環(huán)的例子:

CREATE FUNCTION sum(int[]) RETURNS int8 AS $$ DECLARE s int8 := 0; x int; BEGIN FOREACH x IN ARRAY $1 LOOP s := s + x; END LOOP; RETURN s; END; $$ LANGUAGE plpgsql;

元素會(huì)被按照存儲(chǔ)順序訪問(wèn),而不管數(shù)組的維度數(shù)。盡管target通常只是一個(gè)單一變量,當(dāng)通過(guò)一個(gè)組合值(記錄)的數(shù)組循環(huán)時(shí),它可以是一個(gè)變量列表。在那種情況下,對(duì)每一個(gè)數(shù)組元素,變量會(huì)被從組合值的連續(xù)列賦值。

通過(guò)一個(gè)正SLICE值,FOREACH通過(guò)數(shù)組的切片而不是單一元素迭代。SLICE值必須是一個(gè)不大于數(shù)組維度數(shù)的整數(shù)常量。target變量必須是一個(gè)數(shù)組,并且它接收數(shù)組值的連續(xù)切片,其中每一個(gè)切片都有SLICE指定的維度數(shù)。這里是一個(gè)通過(guò)一維切片迭代的例子:

CREATE FUNCTION scan_rows(int[]) RETURNS void AS $$ DECLARE x int[]; BEGIN FOREACH x SLICE 1 IN ARRAY $1 LOOP RAISE NOTICE 'row = %', x; END LOOP; END; $$ LANGUAGE plpgsql; SELECT scan_rows(ARRAY[[1,2,3],[4,5,6],[7,8,9],[10,11,12]]); NOTICE: row = {1,2,3}
                    NOTICE: row = {4,5,6} NOTICE: row = {7,8,9} NOTICE: row = {10,11,12}

42.6.8. 俘獲錯(cuò)誤

默認(rèn)情況下,PL/pgSQL函數(shù)中發(fā)生的任何錯(cuò)誤都會(huì)中止函數(shù)和周圍事務(wù)的執(zhí)行。你可以使用一個(gè)帶有EXCEPTION子句的BEGIN塊俘獲錯(cuò)誤并且從中恢復(fù)。其語(yǔ)法是BEGIN塊通常的語(yǔ)法的一個(gè)擴(kuò)展:

[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements
EXCEPTION
    WHEN condition [ OR condition ... ] THEN
        handler_statements
    [ WHEN condition [ OR condition ... ] THEN
          handler_statements
      ... ]
END;

如果沒(méi)有發(fā)生錯(cuò)誤,這種形式的塊只是簡(jiǎn)單地執(zhí)行所有statements, 并且接著控制轉(zhuǎn)到END之后的下一個(gè)語(yǔ)句。但是如果在statements內(nèi)發(fā)生了一個(gè)錯(cuò)誤,則會(huì)放棄對(duì)statements的進(jìn)一步處理,然后控制會(huì)轉(zhuǎn)到 EXCEPTION列表。系統(tǒng)會(huì)在列表中尋找匹配所發(fā)生錯(cuò)誤的第一個(gè)condition。如果找到一個(gè)匹配,則執(zhí)行對(duì)應(yīng)的handler_statements,并且接著把控制轉(zhuǎn)到END之后的下一個(gè)語(yǔ)句。如果沒(méi)有找到匹配,該錯(cuò)誤就會(huì)傳播出去,就好像根本沒(méi)有EXCEPTION一樣:錯(cuò)誤可以被一個(gè)帶有 EXCEPTION的閉合塊捕捉,如果沒(méi)有EXCEPTION則中止該函數(shù)的處理。

condition的名字可以是附錄 A中顯示的任何名字。一個(gè)分類名匹配其中所有的錯(cuò)誤。特殊的條件名OTHERS匹配除了QUERY_CANCELEDASSERT_FAILURE之外的所有錯(cuò)誤類型(雖然通常并不明智,還是可以用名字捕獲這兩種錯(cuò)誤類型)。條件名是大小寫無(wú)關(guān)的。一個(gè)錯(cuò)誤條件也可以通過(guò)SQLSTATE代碼指定,例如以下是等價(jià)的:

WHEN division_by_zero THEN ...
WHEN SQLSTATE '22012' THEN ...

如果在選中的handler_statements內(nèi)發(fā)生了新的錯(cuò)誤,那么它不能被這個(gè)EXCEPTION子句捕獲,而是被傳播出去。一個(gè)外層的EXCEPTION子句可以捕獲它。

當(dāng)一個(gè)錯(cuò)誤被EXCEPTION捕獲時(shí),PL/pgSQL函數(shù)的局部變量會(huì)保持錯(cuò)誤發(fā)生時(shí)的值,但是該塊中所有對(duì)持久數(shù)據(jù)庫(kù)狀態(tài)的改變都會(huì)被回滾。例如,考慮這個(gè)片段:

INSERT INTO mytab(firstname, lastname) VALUES('Tom', 'Jones');
BEGIN
    UPDATE mytab SET firstname = 'Joe' WHERE lastname = 'Jones';
    x := x + 1;
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'caught division_by_zero';
        RETURN x;
END;

當(dāng)控制到達(dá)對(duì)y賦值的地方時(shí),它會(huì)帶著一個(gè)division_by_zero錯(cuò)誤失敗。這個(gè)錯(cuò)誤將被EXCEPTION子句捕獲。而在RETURN語(yǔ)句中返回的值將是x增加過(guò)后的值。但是UPDATE命令的效果將已經(jīng)被回滾。不過(guò),在該塊之前的 INSERT將不會(huì)被回滾,因此最終的結(jié)果是數(shù)據(jù)庫(kù)包含Tom Jones但不包含Joe Jones。

提示

進(jìn)入和退出一個(gè)包含EXCEPTION子句的塊要比不包含EXCEPTION的塊開(kāi)銷大的多。因此,只在必要的時(shí)候使用EXCEPTION。

例 42.2. UPDATE/INSERT的異常

這個(gè)例子使用異常處理來(lái)酌情執(zhí)行UPDATEINSERT。我們推薦應(yīng)用使用帶有 ON CONFLICT DO UPDATEINSERT 而不是真正使用這種模式。下面的例子主要是為了展示 PL/pgSQL如何控制流程:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT); CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS $$ BEGIN LOOP -- 首先嘗試更新見(jiàn) UPDATE db SET b = data WHERE a = key; IF found THEN RETURN; END IF; -- 不在這里,那么嘗試插入該鍵 -- 如果其他某人并發(fā)地插入同一個(gè)鍵, -- 我們可能得到一個(gè)唯一鍵失敗
                BEGIN INSERT INTO db(a,b) VALUES (key, data); RETURN; EXCEPTION WHEN unique_violation THEN -- 什么也不做,并且循環(huán)再次嘗試 UPDATE END; END LOOP; END; $$ LANGUAGE plpgsql; SELECT merge_db(1, 'david'); SELECT merge_db(1, 'dennis');

這段代碼假定unique_violation錯(cuò)誤是INSERT造成,并且不是由該表上一個(gè)觸發(fā)器函數(shù)中的INSERT導(dǎo)致。如果在該表上有多于一個(gè)唯一索引,也可能會(huì)發(fā)生不正確的行為,因?yàn)椴还苣膫€(gè)索引導(dǎo)致該錯(cuò)誤它都將重試該操作。通過(guò)接下來(lái)要討論的特性來(lái)檢查被捕獲的錯(cuò)誤是否為所預(yù)期的會(huì)更安全。


42.6.8.1. 得到有關(guān)一個(gè)錯(cuò)誤的信息

異常處理器經(jīng)常被用來(lái)標(biāo)識(shí)發(fā)生的特定錯(cuò)誤。有兩種方法來(lái)得到PL/pgSQL中當(dāng)前異常的信息:特殊變量和GET STACKED DIAGNOSTICS命令。

在一個(gè)異常處理器內(nèi),特殊變量SQLSTATE包含了對(duì)應(yīng)于被拋出異常的錯(cuò)誤代碼(可能的錯(cuò)誤代碼列表見(jiàn)表 A.1)。特殊變量SQLERRM包含與該異常相關(guān)的錯(cuò)誤消息。這些變量在異常處理器外是未定義的。

在一個(gè)異常處理器內(nèi),我們也可以用GET STACKED DIAGNOSTICS命令檢索有關(guān)當(dāng)前異常的信息,該命令的形式為:

GET STACKED DIAGNOSTICS variable { = | := } item [ , ... ];

每個(gè)item是一個(gè)關(guān)鍵詞,它標(biāo)識(shí)一個(gè)被賦予給指定變量(應(yīng)該具有接收該值的正確數(shù)據(jù)類型)的狀態(tài)值。表 42.2中顯示了當(dāng)前可用的狀態(tài)項(xiàng)。

表 42.2. 錯(cuò)誤診斷項(xiàng)

名稱 類型 描述
RETURNED_SQLSTATE text 該異常的 SQLSTATE 錯(cuò)誤代碼
COLUMN_NAME text 與異常相關(guān)的列名
CONSTRAINT_NAME text 與異常相關(guān)的約束名
PG_DATATYPE_NAME text 與異常相關(guān)的數(shù)據(jù)類型名
MESSAGE_TEXT text 該異常的主要消息的文本
TABLE_NAME text 與異常相關(guān)的表名
SCHEMA_NAME text 與異常相關(guān)的模式名
PG_EXCEPTION_DETAIL text 該異常的詳細(xì)消息文本(如果有)
PG_EXCEPTION_HINT text 該異常的提示消息文本(如果有)
PG_EXCEPTION_CONTEXT text 描述產(chǎn)生異常時(shí)調(diào)用棧的文本行(見(jiàn)本文中的第 42.6.9 節(jié))

如果異常沒(méi)有為一個(gè)項(xiàng)設(shè)置值,將返回一個(gè)空字符串。

這里是一個(gè)例子:

DECLARE
  text_var1 text;
  text_var2 text;
  text_var3 text;
BEGIN
  -- 某些可能導(dǎo)致異常的處理
  ...
EXCEPTION WHEN OTHERS THEN
  GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT,
                          text_var2 = PG_EXCEPTION_DETAIL,
                          text_var3 = PG_EXCEPTION_HINT;
END;

42.6.9. 獲得執(zhí)行位置信息

GET DIAGNOSTICS(之前在第 42.5.5 節(jié)中描述)命令檢索有關(guān)當(dāng)前執(zhí)行狀態(tài)的信息(反之上文討論的GET STACKED DIAGNOSTICS命令會(huì)把有關(guān)執(zhí)行狀態(tài)的信息報(bào)告成一個(gè)以前的錯(cuò)誤)。它的 PG_CONTEXT狀態(tài)項(xiàng)可用于標(biāo)識(shí)當(dāng)前執(zhí)行位置。狀態(tài)項(xiàng)PG_CONTEXT將返回一個(gè)文本字符串,其中有描述該調(diào)用棧的多行文本。第一行會(huì)指向當(dāng)前函數(shù)以及當(dāng)前正在執(zhí)行GET DIAGNOSTICS的命令。第二行及其后的行表示調(diào)用棧中更上層的調(diào)用函數(shù)。例如:

CREATE OR REPLACE FUNCTION outer_func() RETURNS integer AS $$
BEGIN
  RETURN inner_func();
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION inner_func() RETURNS integer AS $$
DECLARE
  stack text;
BEGIN
  GET DIAGNOSTICS stack = PG_CONTEXT;
  RAISE NOTICE E'--- Call Stack ---\n%', stack;
  RETURN 1;
END;
$$ LANGUAGE plpgsql;

SELECT outer_func();

NOTICE:  --- Call Stack ---
PL/pgSQL function inner_func() line 5 at GET DIAGNOSTICS
PL/pgSQL function outer_func() line 3 at RETURN
CONTEXT:  PL/pgSQL function outer_func() line 3 at RETURN
 outer_func
 ------------
           1
(1 row)

GET STACKED DIAGNOSTICS ... PG_EXCEPTION_CONTEXT返回同類的棧跟蹤,但是它描述檢測(cè)到錯(cuò)誤的位置而不是當(dāng)前位置。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)