当其他人对所有 DML 进行异常处理时我需要吗?

Do I need when others exception handling for all DML?

我对 Oracle 中处理未知异常的最佳实践感到很困惑。

我可以这样做:

BEGIN
    --do something
EXCEPTION
WHEN NO_DATA_FOUND THEN
    raise_application_error etc
WHEN OTHERS THEN
    raise;
END;

这似乎在相当多的博客和网站中被推荐,甚至在 Oracle documentation:

中进行了讨论

Avoid unhandled exceptions by including an OTHERS exception handler at the top level of every PL/SQL program.

Make the last statement in the OTHERS exception handler either RAISE or an invocation of the RAISE_APPLICATION_ERROR procedure. (If you do not follow this practice, and PL/SQL warnings are enabled, then you get PLW-06009.) For information about RAISE or an invocation of the RAISE_APPLICATION_ERROR, see "Raising Exceptions Explicitly".

但我也知道有几个地方提到这样做相当可怕,例如Ask Tom:

I truly wish we didn't even support WHEN OTHERS.

You should only catch the exceptions you are expecting and can do something about. Let the others propagate out so you can detect them (so you see them)

所以我的问题很简单:

我是否需要一个 when others 子句来记录并引发 每次 我有一些使用 数据操作语言(例如insert/update/delete)?如果不是,我什么时候想避免它?

与任何事情一样,视情况而定。

一般来说,我的偏见是只捕获那些你可以处理或者你可以添加额外信息/上下文的异常。例如,如果您知道您有一个 SELECT INTO 可能 return 0 行,那么如果您可以提供合理的默认值并继续 运行,那么处理 no_data_found 异常是有意义的].如果您可以为异常添加额外的上下文,通常是通过使错误消息的文本更有意义("Customer cannot be found" 而不是 "No data found")或通过包含诸如局部变量的值之类的东西来帮助调试。

设计您的代码可能有意义,这样您总是有一个 WHEN OTHERS 异常处理程序来捕获意外异常,将它们与适当的上下文一起记录到 table(或文件)(例如,局部变量的值),然后重新抛出它们。如果您始终如一地这样做,您最终会得到一些非常冗长的错误日志记录,它会为您提供有关抛出意外异常时程序状态的大量信息。不幸的是,在绝大多数情况下,实施和维护此类系统的团队在此过程中失去了纪律,WHEN OTHERS 的使用导致系统的可维护性大大降低。

如果您有一个不以 RAISE(或 RAISE_APPLICATION_ERROR)结尾的通用 WHEN OTHERS,您的代码将默默地吞下异常。调用者不会知道出了什么问题,并且会继续认为一切正常。但是,不可避免地,未来的某些步骤会失败,因为较早的静默失败使系统处于意外状态。如果你在一个大块的末尾有一个 WHEN OTHERS,这个大块有几十个 SQL 语句,只有一个通用的 RAISE,你将丢失关于实际错误发生在哪一行的信息.

在特定层捕获所有未处理的异常在这些情况下可能是合适的(可能不是完整列表):

  • 您想记录异常然后重新抛出它。
  • 您想用更具体的上下文错误消息重新抛出它。例如,您可能希望提供一条消息,以提供诸如向过程传递了哪些参数等信息。
  • 您想对来电者隐藏详细信息。可能出于安全考虑,并希望确保应用程序无法访问可能泄露敏感细节的真正异常。

如果从应用程序调用这些过程,最好让它们全部冒泡到应用程序,并让应用程序决定 where/when 到 handle/log/wrap 它们。

通常应用程序采用类似的技术。它通常有一个处理所有未处理的异常的处理程序,记录完整的 exception/stack,然后将它们包装在一个通用错误中以显示给用户,从而从原始错误中隐藏潜在的敏感信息,并为用户提供更具体的方向比如"If errors persist, contact support".

以下是应用程序程序员可能会头疼的地方: 您在 SP 层捕获异常,然后重新抛出一般错误。虽然防御性编码和避免异常总是最好的,但有时应用程序程序员别无选择,只能字面上 try,知道在某些情况下会发生异常,然后编写专门的代码来处理它。如果您将异常包装在通用异常中,那么程序员将无法解决特定的错误场景,因为您已将它们全部隐藏在同一个桶中。此外,应用程序级别的日志通常包含完整的堆栈跟踪,最深层次的是从数据库调用中冒出的错误,它将包含在您的一般错误中,从而隐藏了问题的真正原因曾是。在尝试解决难以重现的错误时,这可能是一个大问题,您确实需要详细的日志,让您看到真正的错误,以便您了解问题可能是什么。

当然不是所有的应用程序程序员都会这样想,因为他们并不都采用相同的技术。但是,任何体面的程序员都应该知道如何以通用方式包装来自数据库的错误,如果他们选择这样做的话。另一方面,展开异常通常更加困难或不可能,具体取决于原始包装时省略的内容。这就是为什么 IMO 最好在您处于与用户交互的层之前不包装异常。

虽然 Tom Kyte 是一位才华横溢的人,对 Oracle 有着丰富的知识,但他仍然只是一名 DBA。因此,虽然我永远不会梦想与他就调整队列或 table 空间的最佳布局或解决死锁的最佳方法等问题进行争论,但他关于编程的论述可能会有所保留。或者两个。

为什么我们总是想和异常混在一起?在回答这个问题时,我们必须认识到异常分为两大类:

  1. 我们预期的例外情况。一般是我们调用的系统或用户例程引发的异常。
  2. 我们不希望出现异常。通常,当某些原始操作出错时系统会引发异常——比如除以零。

当我们设计任何类型的数据库存储过程(甚至 Java 或 C# 中的 class 方法)时,我们的代码可能需要某些最小输入和函数状态才能正常工作。如果调用者没有提供足够的输入,或者输入的类型错误,或者未能打开正确的通道到某些 I/O 流,则代码可能无法运行,并且这种失败不能忽略。发生这种情况时,我们想让来电者知道发生了什么,以便他们采取纠正措施。

现在我们可能不知道是哪个代码调用了我们的代码,或者在什么情况下,我们无法操作会对它自己的操作产生什么影响。因此,我们提出了一个有意义的异常,允许它以 its 开发人员认为合适的方式响应。

我们的例程如此,我们调用的任何例程也是如此。让我们举一个创建指定名称和结构的 table 过程的简单示例(在通用 SQL 语法中)。

procedure CreateTable( varchar tableName, varchar fieldDefs ) as begin
    DropTable( tableName );
    exec immediate 'create table ' || tableName || '(' || fieldDefs || ');';
end procedure;

什么地方会出错?首先,tableName 参数可能为空,或者它包含的字符串没有正确形成合法的 table 名称,或者可能已经存在该名称的 table,或者用户没有创建 table 的权限。其次,fieldDefs 参数也可能为 null,或者它包含的字符串不正确构成字段定义。

我们是否必须花费前几行代码来检查所有这些可能性。可能是。至少检查一个空的必需参数并引发一个有意义的异常会很好。

procedure CreateTable( varchar tableName, varchar fieldDefs ) as begin
    if tableName is null or fieldDefs is null then
        raise NULL_ARGUMENT;
    end if
    DropTable( tableName );
    exec immediate 'create table ' || tableName || '(' || fieldDefs || ');';
end procedure;

现在我们的例程调用另一个例程。我们知道我们传递给它的参数不是空的,但可能还有其他问题。或者 table 可能不存在。如果 table 不存在,我们想忽略该异常,但如果参数内容存在伪造,我们会发现当我们使用它来创建 table 和涉及的错误消息时从名为 "CreateTable" 的例程中创建 table 比涉及删除 table 更有意义。所以我们也选择忽略这些。这就是 others 派上用场的地方。

procedure CreateTable( varchar tableName, varchar fieldDefs ) as begin
    if tableName is null or fieldDefs is null then
        raise NULL_ARGUMENT;
    end if
    begin
        DropTable( tableName );
    exception
        when others then
           ; -- ignore
    end;
    exec immediate 'create table ' || tableName || '(' || fieldDefs || ');';
end procedure;

现在我们需要一个异常处理程序来检查上面提到的每个可能的故障源,这些故障源必须来自 create table 语句(或我们自己的 NULL_ARGUMENT)并且将我们自己有意义的异常返回给我们的调用者。

但这对我们的目的来说已经足够了。让我们看看如果没有可用的 when other 选项我们必须做什么。我们将不得不测试每一个可能的异常,只是为了忽略它们

when this then
    ; --ignore
when that then
    ; --ignore
when something then
    ; --ignore
when something_else then
    ; --ignore
when something_else_entirely then
    ; --ignore
when one_in_a_million_longshot then
    ; --ignore

至少可以说这很麻烦。即使我们想要捕获一些特定的异常并为了清楚起见而重新引发我们自己的异常但忽略其余异常,我们仍然必须将这个巨大列表的大部分附加到每个异常处理程序。我们有没有错过一个?我们希望不会,因为如果我们这样做,并且它曾经被引发,我们就会失去对整个异常处理操作的控制。

不,尽管您可能听说过,但指定我们要处理的能力 "all other heretofore unspecified exceptions" 减轻了太多无法忽略的编程问题。

会不会被滥用?当然。任何语言的任何特性和任何工具箱中的任何工具都可能被滥用。这是放弃功能或工具的站不住脚的借口。

如果使用得当,WHEN OTHERS 肯定是 pl/sql 的重要组成部分。一般来说,我不建议为每个 DML 使用 WHEN OTEHRS。相反,我们使用 'func_no' 跟踪我们在程序中的位置 - 一个简单的硬编码值,随着每个 DML 和代码中通常的其他关键点而变化。我们还会跟踪有用的信息,因为它在程序中发生了变化。这不仅仅是 - 'Creating Invoice.',而是“正在创建发票,客户='||rec.customer_number||',发票编号:'||to_char(invoice_number);

我的建议是使用 NO_DATA_FOUND、TOO_MANY_ROWS、DIVIDE_BY_ZERO 等显式处理预期的数据条件,并使用 SQL%ROWCOUNT 等在数据内进行测试。

每个程序单元我只使用 1 个 WHEN OTHERS。它在外区。它包含 func_no 和数据特定消息以及错误堆栈和回溯,通常执行回滚,并且始终执行 RAISE;