44.7. 控制结构

44.7.1. 从函数中返回
44.7.2. 从过程返回
44.7.3. 调用存储过程
44.7.4. 条件语句
44.7.5. 简单循环
44.7.6. GOTO语句
44.7.7. 遍历查询结果
44.7.8. 捕获错误

控制结构可能是pl/sql中最有用(也是最重要)的部分。使用pl/sql的控制结构,可以以非常灵活和强大的方式操纵LightDB数据。

44.7.1. 从函数中返回

有一个命令可以让你从函数中返回数据:RETURN

44.7.1.1. RETURN

RETURN expression;

使用带有表达式的RETURN终止函数并将表达式的值返回给调用者。这种形式用于不返回集合的pl/sql函数。

如果使用输出参数声明了函数,则只需编写没有表达式的RETURN。输出参数变量的当前值将被返回。当前不支持混合使用Oracle输出参数和返回子句。

函数的返回值不能未定义。如果控制到达函数的顶层块的末尾而没有命中RETURN语句,则会发生运行时错误。在这些情况下,如果顶层块完成,则会自动执行RETURN语句。

一些例子:

-- 返回一个标量类型的函数
RETURN 1 + 2;
RETURN scalar_var;

44.7.2. 从过程返回

过程没有返回值。因此,过程可以在没有RETURN语句的情况下结束。如果您希望使用RETURN语句提前退出代码,则只需编写没有表达式的RETURN语句。

存储过程参数使用out进行修饰,您可以使用只有RETURN语句但没有返回值来结束存储过程。

如果存储过程具有输出参数,则输出参数变量的最终值将返回给调用方。

44.7.3. 调用存储过程

pl/sql函数、过程或匿名块可以调用存储过程。例如:

CREATE PROCEDURE triple(x in int, y out int)
AS
BEGIN
    y := x * 3;
END;
/

DECLARE
  myvar int := 5;
  myresult int := 10;
BEGIN
  triple(myvar, myresult);
  DBMS_OUTPUT.PUT_LINE('myresult = '||myresult);  -- prints 15
END;
/

44.7.4. 条件语句

IFCASE语句可让您根据某些条件执行替代命令。 pl/sql有三种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

44.7.4.1. IF-THEN

IF boolean-expression THEN
    statements
END IF;

IF-THEN语句是IF语句的最简单形式。如果条件为真,则执行THENEND IF之间的语句。否则,它们将被跳过。

示例:

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

44.7.4.2. IF-THEN-ELSE

IF boolean-expression THEN
    statements
ELSE
    statements
END IF;

IF-THEN-ELSE语句通过让您指定一组备选语句,以便在条件不为真时执行,增强了IF-THEN语句。(请注意,这包括条件评估为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;

44.7.4.3. IF-THEN-ELSIF

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

有时会有超过两个的备选方案。 IF-THEN-ELSIF提供了一种方便的方法,逐个检查几个备选方案。 IF条件逐个进行测试,直到找到第一个为真为止。 然后执行相关的语句,之后控制传递到 END IF之后的下一条语句。 (任何后续的 IF 条件都不会被测试。)如果没有任何 IF 条件为真,则执行 ELSE块(如果有的话)。

下面是一个例子:

IF number = 0 THEN
    result := 'zero';
ELSIF number > 0 THEN
    result := 'positive';
ELSIF number < 0 THEN
    result := 'negative';
ELSE
    -- 嗯,唯一的其他可能性是number为null
    result := 'NULL';
END IF;

关键字ELSIF也可以拼写为ELSEIF

完成相同任务的另一种方法是嵌套IF-THEN-ELSE语句,如下面的示例所示:

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

然而,这种方法需要为每个IF编写一个匹配的END IF,所以当有许多备选方案时,它比使用ELSIF更加麻烦。

44.7.4.4. 简单的 CASE

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

简单的CASE形式提供了基于操作数的相等性条件执行。 search-expression被评估(一次)并逐个与WHEN子句中的每个expression进行比较。 如果找到匹配项,则执行相应的statements,然后控制传递到 END CASE之后的下一条语句。(后续的WHEN表达式不会被评估。)如果找不到匹配项,则执行ELSEstatements;但是如果没有ELSE,则会引发CASE_NOT_FOUND异常。

下面是一个简单的例子:

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

44.7.4.5. 搜索型 CASE

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

CASE 的搜索形式根据布尔表达式的真值提供条件执行。 每个 WHEN 子句的 boolean-expression 会依次被评估, 直到找到一个产生 true 的表达式。然后执行相应的 statements, 然后控制权转移到 END CASE 后的下一条语句。 (后续的 WHEN 表达式不会被评估。) 如果找不到真实结果,则会执行 ELSE statements; 但是如果没有 ELSE,则会引发 CASE_NOT_FOUND 异常。

下面是一个例子:

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;

这种形式的 CASEIF-THEN-ELSIF 完全等价, 唯一的区别是遗漏了 ELSE 子句会导致错误,而不是什么都不做。

44.7.5. 简单循环

使用 LOOPEXITCONTINUEWHILEFORFOREACH 语句, 您可以安排 pl/sql 函数重复执行一系列命令。

44.7.5.1. LOOP

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

LOOP 定义了一个无条件循环,直到被 EXITRETURN 语句终止。 可选的 label 可以被嵌套循环内的 EXITCONTINUE 语句使用,以指定这些语句所引用的循环。

Tip

标签名称不能是保留字 label

44.7.5.2. EXIT

EXIT [ label ] [ WHEN boolean-expression ];

如果没有给出 label,则最内层的循环将被终止,然后执行 END LOOP 后的语句。 如果给出了 label,则它必须是当前循环或某个外层嵌套循环或块的标签。 然后,命名的循环或块将被终止,并继续执行循环/块对应的 END 语句之后的语句。

如果指定了 WHEN,则只有在 boolean-expression 为 true 时才会退出循环。否则,控制权传递到 EXIT 后的语句。

EXIT 可以与所有类型的循环一起使用,它不仅限于与无条件循环一起使用。

例如:

LOOP
    -- some computations
    IF count > 0 THEN
        EXIT;  -- 退出循环
    END IF;
END LOOP;

LOOP
    -- some computations
    EXIT WHEN count > 0;  -- 与前面的例子产生相同的结果
END LOOP;

44.7.5.3. CONTINUE

CONTINUE [ label ] [ WHEN boolean-expression ];

如果没有给出 标签,则开始内层循环的下一次迭代。也就是说,跳过循环体中剩余的所有语句,控制返回循环控制表达式(如果有的话),以确定是否需要进行另一次循环迭代。 如果有 标签,则指定将继续执行的循环的标签。

如果指定了 WHEN,那么仅当 布尔表达式 为真时才开始循环的下一次迭代。否则,控制传递到 CONTINUE 之后的语句。

CONTINUE 可以与所有类型的循环一起使用,它不仅仅局限于无条件循环的使用。

示例:

LOOP
    -- 一些计算
    EXIT WHEN count > 100;
    CONTINUE WHEN count < 50;
    -- 一些计算,用于计数在 [50 .. 100] 范围内的情况
END LOOP;

44.7.5.4. WHILE

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

WHILE 语句会重复执行一系列语句,只要 boolean-expression 评估为 true。该表达式在每次进入循环体之前进行检查。

例如:

WHILE amount_owed > 0 AND gift_certificate_balance > 0 LOOP
    -- 在这里进行一些计算
END LOOP;

WHILE NOT done LOOP
    -- 在这里进行一些计算
END LOOP;

44.7.5.5. FOR (整数变量)

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

这种形式的FOR创建一个循环,遍历整数值的范围。 变量name会自动定义为integer类型, 仅在循环内存在(循环内会忽略任何现有的该变量的定义)。 指定范围的两个表达式在进入循环时只会计算一次。 如果没有指定BY子句,则迭代步长为1, 否则为BY子句中指定的值,该值也会在循环进入时计算一次。 如果指定了REVERSE,则在每次迭代后,步长值会被减去而不是加上。

一些整数型FOR循环的例子:

FOR i IN 1..10 LOOP
    -- i在循环内将取值1,2,3,4,5,6,7,8,9,10
END LOOP;

FOR i IN REVERSE 10..1 LOOP
    -- i在循环内将取值10,9,8,7,6,5,4,3,2,1
END LOOP;

FOR i IN REVERSE 10..1 BY 2 LOOP
    -- i在循环内将取值10,8,6,4,2
END LOOP;

如果下限大于上限(或者在REVERSE情况下,小于上限),则根本不执行循环体。不会引发任何错误。

如果FOR循环附有一个label,则可以使用该label来限定名称,从而引用整数型循环变量。

为了兼容Oracle数据库,end loop 后面可以有一个label,并且不检查label是否匹配。这个功能只是语法糖。

DECLARE
    i NUMBER(38);
BEGIN
    FOR i IN 1..5 LOOP
        DBMS_OUTPUT.PUT_LINE('Value: ' || i);
    END LOOP i;
END;

DECLARE
    i NUMBER(38);
BEGIN
    FOR i IN 1..5 LOOP
        DBMS_OUTPUT.PUT_LINE('Value: ' || i);
    END LOOP xxxxxxxxxxxx;
END;

44.7.6. GOTO语句

GOTO语句无条件地使控制权转到一个标签处。该标签在其作用域内必须是唯一的,并且必须位于一条可执行语句或一个PL/oraSQL块之前。当程序运行时,GOTO语句将控制权转到一个带标签的语句或块处。

语法如下:

GOTO labelName;

44.7.6.1. GOTO语句的限制

  • GOTO语句无法将控制权转入IF语句、CASE语句、LOOP语句或子块中。

  • GOTO语句无法将控制权从一个IF语句子句传到另一个子句,或者从一个CASE语句的WHEN子句传到另一个子句。

  • GOTO语句无法将控制权传出子程序。

  • GOTO语句无法将控制权传入异常处理程序。

  • GOTO语句无法将控制权从异常处理程序传回当前块。

  • LightDB的关键词无法作为标签名。

44.7.6.2. 例子

该示例可以在ltsql下运行。进入ltsql,执行下列命令。

create database test_oracle lightdb_syntax_compatible_type oracle;
\c test_oracle
select dbms_output.serveroutput(true);
44.7.6.2.1. 示例1 GOTO到语句
DECLARE
  p  VARCHAR2(30);
  n  PLS_INTEGER := 37;
BEGIN
  FOR j in 2..ROUND(SQRT(n)) LOOP
    IF n % j = 0 THEN
      p := ' is not a prime number';
      GOTO print_now;
    END IF;
  END LOOP;

  p := ' is a prime number';

  <<print_now>>
  DBMS_OUTPUT.PUT_LINE(TO_CHAR(n) || p);
END;
/
结果:
    37 is a prime number
44.7.6.2.2. 示例2 GOTO到NULL
DECLARE
  done  BOOLEAN;
  j     int;
BEGIN
  done := true;
  FOR i IN 1..50 LOOP
    IF done THEN
      j := 100;
      GOTO end_loop;
    END IF;
  END LOOP;
  <<end_loop>>
  NULL;
  DBMS_OUTPUT.PUT_LINE(j);
END;
/
结果:
    100
44.7.6.2.3. 示例3 GOTO到块
BEGIN
    DBMS_OUTPUT.PUT_LINE('out block 1');
    goto testlabel;
    DBMS_OUTPUT.PUT_LINE('out block 2');
    <<testlabel>>
    BEGIN
        DBMS_OUTPUT.PUT_LINE('in block 1');
        goto testlabel2;
        DBMS_OUTPUT.PUT_LINE('in block 2');
    END;
    DBMS_OUTPUT.PUT_LINE('out block 3');
    <<testlabel2>>
    DBMS_OUTPUT.PUT_LINE('out block 4');
END;
/
结果:
    out block 1
    in block 1
    out block 4
44.7.6.2.4. 示例4 语句无法将控制权传到IF语句中

GOTO语句将控制权转入IF语句,导致出现错误。

DECLARE
  done BOOLEAN := TRUE;
BEGIN
  GOTO update_row;

  IF done THEN
  <<update_row>>
    NULL;
  END IF;
END;
/
结果:
    ERROR:  illegal GOTO statement; this GOTO cannot branch to label "update_row"
    CONTEXT:  pl/sql function inline_code_block

44.7.7. 遍历查询结果

使用不同类型的FOR循环,可以遍历查询结果并相应地操作该数据。语法如下:

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

target是一个记录变量、行变量或逗号分隔的标量变量列表。 target依次分配query生成的每一行,并为每一行执行循环体。 如果循环由EXIT语句终止,则最后分配的行值在循环后仍然可访问。

在这种类型的FOR语句中使用的query可以是任何返回行的SQL命令:SELECT是最常见的情况,但您也可以使用带有RETURNING子句的INSERTUPDATEDELETE。一些实用程序命令,如EXPLAIN也可以使用。

pl/sql变量会被替换为查询文本,并且查询计划会被缓存以便可能重复使用。

FOR-IN-EXECUTE-IMMEDIATE语句是迭代行的另一种方式:

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

这类似于前面的形式,但源查询指定为字符串表达式,在进入FOR循环的每个条目上进行评估和重新规划。这允许程序员选择预安排查询的速度或动态查询的灵活性,就像普通的EXECUTE IMMEDIATE语句一样。与EXECUTE IMMEDIATE一样,可以通过USING将参数值插入动态命令中。

另一种指定应遍历其结果的查询的方法是将其声明为游标。这在Section 44.8.3.2中有描述。

44.7.8. 捕获错误

默认情况下,在pl/sql函数中发生任何错误都会中止函数的执行以及周围的事务。可以通过使用带有EXCEPTION子句的BEGIN块来捕获错误并从中恢复。语法是BEGIN块的正常语法的扩展形式:

[ DECLARE
    declarations ]
BEGIN
    statements
EXCEPTION
    WHEN condition [ OR condition ... ] THEN
        handler_statements
    [ WHEN condition [ OR condition ... ] THEN
          handler_statements
      ... ]
END;
/

如果没有错误发生,此块形式只会执行所有statements,然后控制权转移到END后的下一条语句。但是,如果在statements中发生错误,则进一步处理statements被放弃,并且控制权转移到EXCEPTION列表。该列表会查找与发生的错误匹配的第一个condition。如果找到匹配项,则执行相应的handler_statements,然后控制权传递到END后的下一条语句。如果找不到匹配项,则错误会像根本没有EXCEPTION子句一样传播:错误可以由带有EXCEPTION 的封闭块捕获,如果没有,则会中止函数的处理。

特殊的条件名称OTHERS匹配每种错误类型。条件名称不区分大小写。

Tip

包含EXCEPTION子句的块进入和退出的成本要比没有该子句的块高得多。因此,在没有必要的情况下不要使用EXCEPTION