介绍
在本实验中,你将使用 PostgreSQL 的过程语言 PL/pgSQL 来探索错误处理技术。你将学习如何创建能够优雅地处理潜在错误而不是崩溃的函数。本实验涵盖了引发信息性消息、捕获特定异常(如“除零错误”)以及将错误详情记录到数据库表中以供后续审查。最终,你将理解如何构建更健壮、更可靠的数据库函数。
在本实验中,你将使用 PostgreSQL 的过程语言 PL/pgSQL 来探索错误处理技术。你将学习如何创建能够优雅地处理潜在错误而不是崩溃的函数。本实验涵盖了引发信息性消息、捕获特定异常(如“除零错误”)以及将错误详情记录到数据库表中以供后续审查。最终,你将理解如何构建更健壮、更可靠的数据库函数。
在本步骤中,你将学习在 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 错误时才运行后续代码。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、错误 message 和 function_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)
这证实了你的错误处理逻辑现在能够持久化错误信息以供后续分析。
到目前为止,你已经处理了一个非常具体的错误: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 块将捕获任何其他错误。它使用 SQLSTATE 和 SQLERRM 变量构建一个详细的消息,并将其插入日志。然后返回 -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 语句发出 NOTICE 和 WARNING 消息,用于调试和信息目的。然后,你通过创建一个可能失败的函数,并实现 BEGIN...EXCEPTION...END 块来捕获特定的 division_by_zero 错误,从而开始处理运行时异常。在此基础上,你增强了函数功能,将错误详情记录到数据库表中,提供了持久的审计记录。最后,你通过添加一个通用的 WHEN OTHERS 处理器来捕获任何意外错误,使函数更加健壮,确保你的函数在各种条件下都能优雅地处理失败。