PostgreSQL 错误处理

PostgreSQLBeginner
立即练习

介绍

在本实验中,你将使用 PostgreSQL 的过程语言 PL/pgSQL 来探索错误处理技术。你将学习如何创建能够优雅地处理潜在错误而不是崩溃的函数。本实验涵盖了引发信息性消息、捕获特定异常(如“除零错误”)以及将错误详情记录到数据库表中以供后续审查。最终,你将理解如何构建更健壮、更可靠的数据库函数。

这是一个指导性实验 (Guided Lab),它提供分步说明来帮助你学习和练习。请仔细遵循说明来完成每个步骤并获得实践经验。历史数据显示,这是一个 初学者 级别的实验,完成率达到 90%。学习者对其的正面评价率达到了 100%

发出通知和警告

在本步骤中,你将学习在 PL/pgSQL 函数中使用 RAISE 语句。RAISE 语句可用于提供信息性消息、警告或调试输出,而不会中断函数的执行。

首先,以 postgres 用户连接到 PostgreSQL 交互式终端 psql。本实验中后续的所有 SQL 命令都将在该终端中运行。

sudo -u postgres psql

你现在会看到 psql 提示符,它看起来像 postgres=#

让我们创建一个名为 greet 的函数,它接受一个名字作为输入并引发一个 NOTICE。Notice 是低优先级的消息。

psql shell 中执行以下 SQL 命令:

CREATE OR REPLACE FUNCTION greet(name TEXT)
RETURNS TEXT AS $$
BEGIN
  RAISE NOTICE 'Greeting function called with name: %', name;
  RETURN 'Hello, ' || name;
END;
$$ LANGUAGE plpgsql;

此函数接受一个 name 参数。RAISE NOTICE '...', name; 这行会生成一条 notice 消息。% 是一个占位符,它会被 name 变量的值替换。

现在,调用该函数以查看其效果:

SELECT greet('LabEx');

输出同时显示了 notice 消息和函数的返回值:

NOTICE:  Greeting function called with name: LabEx
   greet
-----------
 Hello, LabEx
(1 row)

你也可以引发具有更高严重性级别的消息,例如 WARNING。让我们修改函数,在输入名称为空时引发警告。

CREATE OR REPLACE FUNCTION greet(name TEXT)
RETURNS TEXT AS $$
BEGIN
  IF name IS NULL OR name = '' THEN
    RAISE WARNING 'Input name is empty or NULL.';
    RETURN 'Hello, stranger';
  ELSE
    RAISE NOTICE 'Greeting function called with name: %', name;
    RETURN 'Hello, ' || name;
  END IF;
END;
$$ LANGUAGE plpgsql;

现在,使用空字符串调用该函数以触发警告:

SELECT greet('');

这次,你将看到一个 WARNING 消息而不是 NOTICE

WARNING:  Input name is empty or NULL.
      greet
------------------
 Hello, stranger
(1 row)

你已成功使用 RAISE 从函数内部生成了不同类型的消息。

触发和捕获异常

在本步骤中,你将学习如何处理运行时错误,即异常 (exceptions)。未处理的异常将立即终止你的函数。你将首先创建一个会失败的函数,然后修改它以优雅地捕获异常。

让我们创建一个执行整数除法的函数 simple_divide。此函数有一个潜在的缺陷:如果你尝试除以零,它将崩溃。

CREATE OR REPLACE FUNCTION simple_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
END;
$$ LANGUAGE plpgsql;

首先,使用有效的输入调用该函数以确认其正常工作:

SELECT simple_divide(10, 2);

输出将是除法的结果:

 simple_divide
---------------
             5
(1 row)

现在,使用零作为分母调用该函数以触发异常:

SELECT simple_divide(10, 0);

此命令将失败并返回错误消息。函数执行被中止。

ERROR:  division by zero
CONTEXT:  PL/pgSQL function simple_divide(integer,integer) line 3 at RETURN

这是一个未处理的异常。为了防止函数崩溃,你可以使用 EXCEPTION 块。让我们创建一个包含错误处理的新函数 safe_divide

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    RAISE NOTICE 'Error: Cannot divide by zero.';
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

以下是 EXCEPTION 块的工作原理:

  • BEGIN...END;: 这定义了一个代码块。
  • EXCEPTION: 这个关键字启动异常处理部分。
  • WHEN division_by_zero THEN: 这指定只有在发生 division_by_zero 错误时才运行后续代码。
  • 处理器内的代码会引发一个 notice 并返回 NULL,而不是崩溃。

现在,使用零作为分母调用新的 safe_divide 函数:

SELECT safe_divide(10, 0);

这次,函数不会崩溃。它捕获了异常,显示了你自定义的 notice,并返回了 NULL

NOTICE:  Error: Cannot divide by zero.
 safe_divide
-------------

(1 row)

你已成功捕获特定异常并控制了函数的输出。

将错误记录到表中

捕获异常是好的,但为了审计和调试,通常最好将错误记录到持久位置,例如表。在本步骤中,你将修改 safe_divide 函数,将错误详情写入实验设置期间创建的 error_log 表。

首先,让我们在 psql 中使用 \d 命令检查 error_log 表的结构。

\d error_log

你将看到表的列,其中包括一个自动递增的 id、一个 timestamp、错误 messagefunction_name

                                              Table "public.error_log"
    Column     |            Type             | Collation | Nullable |                     Default
---------------+-----------------------------+-----------+----------+----------------------------------------------------
 id            | integer                     |           | not null | nextval('error_log_id_seq'::regclass)
 timestamp     | timestamp without time zone |           |          | (now() AT TIME ZONE 'utc'::text)
 message       | text                        |           |          |
 function_name | text                        |           |          |
Indexes:
    "error_log_pkey" PRIMARY KEY, btree (id)

现在,让我们修改 safe_divide 函数。在 EXCEPTION 块中,你将添加一个 INSERT 语句来记录错误,然后再返回。

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('Division by zero occurred!', 'safe_divide');

    RAISE NOTICE 'Error: Cannot divide by zero. Details logged.';
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

现在再次调用该函数以触发错误和新的日志记录逻辑:

SELECT safe_divide(10, 0);

函数将像以前一样返回 NULL,但它也执行了数据库写入操作。为了验证这一点,查询 error_log 表以查看新条目。

SELECT function_name, message FROM error_log;

输出应显示已记录的错误:

 function_name |          message
---------------+----------------------------
 safe_divide   | Division by zero occurred!
(1 row)

这证实了你的错误处理逻辑现在能够持久化错误信息以供后续分析。

使用 OTHERS 处理通用异常

到目前为止,你已经处理了一个非常具体的错误:division_by_zero。但对于其他意外错误呢?PL/pgSQL 提供了一个通用的 OTHERS 异常处理器,用于捕获任何未被更具体的 WHEN 子句捕获的错误。

在本步骤中,你将增强 safe_divide 函数以处理任何可能的异常。你还将使用两个特殊变量 SQLSTATE(SQL 错误代码)和 SQLERRM(SQL 错误消息),来记录更详细的信息。

让我们最后一次修改 safe_divide 函数。

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('Division by zero occurred!', 'safe_divide');

    RAISE NOTICE 'Error: Cannot divide by zero. Details logged.';
    RETURN NULL;

  WHEN OTHERS THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('An unexpected error occurred: ' || SQLSTATE || ' - ' || SQLERRM, 'safe_divide');

    RAISE NOTICE 'An unexpected error occurred. Details logged.';
    RETURN -1;
END;
$$ LANGUAGE plpgsql;

新的 WHEN OTHERS 块将捕获任何其他错误。它使用 SQLSTATESQLERRM 变量构建一个详细的消息,并将其插入日志。然后返回 -1 以表示发生了通用错误。

虽然我们当前的函数只能触发 division_by_zero 错误,但这种结构使其在面对未来的修改或意外输入时更加健壮。例如,如果输入类型更改为可能溢出的类型,OTHERS 块将捕获 numeric_value_out_of_range 错误。

让我们再次测试 division_by_zero 路径,以确保它仍然按预期工作。首先,清空日志表以进行干净的测试。

TRUNCATE error_log;

现在,调用函数:

SELECT safe_divide(10, 0);

最后,检查日志。它应该包含特定的“Division by zero”消息,确认第一个 WHEN 块被正确选择。

SELECT message FROM error_log;
          message
----------------------------
 Division by zero occurred!
(1 row)

你现在已经构建了一个健壮的函数,它可以处理特定、预期的错误以及通用、意外的错误。

总结

在本实验中,你学习了 PostgreSQL 的 PL/pgSQL 中错误处理的基础知识。你首先使用 RAISE 语句发出 NOTICEWARNING 消息,用于调试和信息目的。然后,你通过创建一个可能失败的函数,并实现 BEGIN...EXCEPTION...END 块来捕获特定的 division_by_zero 错误,从而开始处理运行时异常。在此基础上,你增强了函数功能,将错误详情记录到数据库表中,提供了持久的审计记录。最后,你通过添加一个通用的 WHEN OTHERS 处理器来捕获任何意外错误,使函数更加健壮,确保你的函数在各种条件下都能优雅地处理失败。