【问题标题】:How to handle db constraint violations in the user interface?如何处理用户界面中的 db 约束违规?
【发布时间】:2009-02-22 00:37:50
【问题描述】:

我们使用存储过程在数据库中实现大部分业务规则。

我永远无法决定如何最好地将数据约束违规错误从数据库传递回用户界面。我所说的约束更多地与业务规则相关,而不是数据完整性。

例如,“无法插入重复的键行”之类的 db 错误与业务规则“您不能有多个同名的 Foo”相同。但是我们已经在最常见的位置“实现”了它:作为一个独特的约束,当违反规则时会抛出异常。

其他规则,例如“您每天只允许 100 个 Foos”不会导致错误,因为它们由自定义代码(例如 return empty dataset)优雅地处理,应用程序代码检查并传递回给ui层。

问题就在这里。我们的 ui 代码如下所示(这是 AJAX.NET webservices 代码,但任何 ajax 框架都可以):

WebService.AddFoo("foo", onComplete, onError); // ajax call to web service

function onComplete(newFooId) {
    if(!newFooId) {
        alert('You reached your max number of Foos for the day')
        return
    }
    // update ui as normal here
}

function onError(e) {
    if(e.get_message().indexOf('duplicate key')) {
        alert('A Foo with that name already exists');
        return;
    }
    // REAL error handling code here
}

(附带说明:我注意到当您提交 cmets 过快时,stackoverflow 会执行此操作:服务器生成 HTTP 500 响应并且 ui 会捕获它。)

所以你看,我们在这里在两个地方处理违反业务规则的行为,其中一个(即唯一约束错误)作为应该处理真实的代码的特殊情况处理> 错误(不是违反业务规则),因为 .NET 将异常一直传播到 onError() 处理程序。

这感觉不对。我认为我的选择是:

  1. 在应用服务器级别捕获“重复密钥违规”异常,并将其转换为用户界面期望的任何“违反业务规则”标志,
  2. 抢占错误(例如,使用"select name from Foo where name = @Name")并返回应用服务器期望的任何内容作为“违反业务规则”标志,
  3. 与 2) 相同:利用 db 层内置的唯一约束并盲目地insert into Foo,捕获任何异常并将其转换为应用服务器期望的任何内容,作为“违反业务规则”标志李>
  4. 盲目地insert into Foo(如 3)并让该异常传播到 ui,加上让应用服务器将业务规则违规作为真正的 Exceptions(而不是 1)提出。这样,所有错误都在 ui 层的 onError()(或类似)代码中处理。

我喜欢 2) 和 3) 的地方在于,违反业务规则的行为被“抛出”在哪里实施:在存储过程中。我不喜欢 1) 和 3) 是我认为它们涉及像 "if error.IndexOf('duplicate key')" 这样的愚蠢检查,就像当前 ui 层中的内容一样。

编辑:我喜欢 4),但大多数人说只在例外情况下使用Exceptions。

那么,你们如何优雅地处理将违反业务规则的行为传播到用户界面?

【问题讨论】:

    标签: sql-server web-services error-handling


    【解决方案1】:

    我们不在数据库中执行我们的业务逻辑,但我们确实有我们所有的验证服务器端,低级数据库 CRUD 操作与高级业务逻辑和控制器代码分开。

    我们在内部尝试做的是传递一个带有Validation.addError(message,[fieldname]) 之类的函数的验证对象。各个应用层将它们的验证结果附加到这个对象上,然后我们调用Validation.toJson() 来生成如下所示的结果:

    {
        success:false,
        general_message:"You have reached your max number of Foos for the day",
        errors:{
            last_name:"This field is required",
            mrn:"Either SSN or MRN must be entered",
            zipcode:"996852 is not in Bernalillo county. Only Bernalillo residents are eligible"
        }
    }
    

    这可以很容易地在客户端进行处理,以显示与单个字段相关的消息以及一般消息。

    关于约束违规我们使用#2,即我们在插入/更新之前检查潜在的违规并将错误附加到验证对象。

    【讨论】:

    • 我注意到这是 SO 对投票等写入操作所做的事情。响应具有“成功”和“消息”字段。但是,如果您提交 cmets 太快,它们会抛出 500 服务器异常并且 ui 会捕获它(即我的代码示例所做的,我不喜欢)。
    • 我确实喜欢这种方法,即使业务规则检查全部在数据库中处理也不是不可能的。应用服务器只需将所有响应转换为 ui 可以处理的标准。无论如何,这不是一件坏事。
    【解决方案2】:

    问题实际上是系统架构的限制之一。通过将所有逻辑推入数据库,您需要在两个地方处理它(而不是构建一层将 UI 与数据库联系起来的业务逻辑。再说一次,一旦您拥有一层业务逻辑,您就会失去所有在存储过程中拥有逻辑的好处。不提倡其中一种。两者都一样糟糕。或者不糟糕。取决于你如何看待它。

    我在哪里? 对。

    我认为 2 和 3 的组合可能是要走的路。

    通过先发制人的错误,您可以创建一组可从面向 UI 的代码调用的过程,以便向用户提供特定于实现的详细反馈。您不一定需要在逐个字段的基础上使用 ajax 执行此操作,但您可以。

    数据库中的唯一约束和其他规则然后成为所有数据的最终完整性检查,并且可以在发送之前假设数据是好的,并且理所当然地抛出异常(前提是这些应始终使用有效数据调用程序,因此无效数据是一种例外情况。

    【讨论】:

    • 这实际上是 2 和 4 的组合,通过调整到 4,而不是在应用服务器上引发错误,而是在存储过程中引发错误。所以存储的过程会抛出所有违反业务规则的异常(一些它自己捕获,一些数据库捕获它)。我喜欢。
    【解决方案3】:

    为了捍卫#4,SQL Server 有一个非常有序的预定义错误严重级别层次结构。正如您指出的那样,处理逻辑所在的错误很好,我倾向于按照 SP 和 UI 抽象之间的约定来处理这个问题,而不是添加一堆额外的耦合。特别是因为您可以同时使用值和字符串引发错误。

    【讨论】:

      【解决方案4】:

      存储过程可以使用 RAISERROR 语句将错误信息返回给调用者。这可以用于允许用户界面决定错误将如何出现,同时允许存储过程提供错误的详细信息。

      RAISERROR 可以使用 msg_id、严重性和状态以及一组错误参数来调用。以这种方式使用时,必须使用 sp_addmessage 系统存储过程将具有给定 msg_id 的消息输入到数据库中。此 msg_id 可以作为 SqlException 中的 ErrorNumber 属性检索,该属性将在调用存储过程的 .NET 代码中引发。然后,用户界面可以决定要显示哪种消息或其他指示。

      错误参数被替换到生成的错误消息中,类似于 printf 语句在 C 中的工作方式。但是,如果您只想将参数传递回 UI 以便 UI 可以决定如何使用它们,只需使错误消息没有文本,只是参数的占位符。一条消息可能是 '"%s"|%d' 以传回一个字符串参数(用引号括起来)和一个数字参数。 .NET 代码可以将它们分开并在用户界面中随意使用它们。

      RAISERROR 也可以用在存储过程的 TRY CATCH 块中。这将允许您捕获重复键错误,并将其替换为您自己的错误号,这意味着“插入时重复键”到您的代码中,并且它可以包含实际的键值。您的 UI 可以使用它来显示“订单号已存在”,其中“x”是提供的键值。

      【讨论】:

      • 我希望投反对票的人和stackoverflow.com/questions/581994/… 能给出一个理由。在一分钟内,两个截然不同的答案,相同的结果。
      • 最终调用RAISERROR 的代码仍然需要执行像CHARINDEX('duplicate key', @errorMessage) > 0 这样的“愚蠢检查”,我发现了这个问题,因为我的示例场景涉及两个潜在的唯一性键约束违规,我很好奇是否有某种方法可以确定违反了哪个键,而无需搜索键名或相关列的名称。
      【解决方案5】:

      我见过很多基于 Ajax 的应用程序在用户离开编辑框后立即对诸如用户名之类的字段进行实时检查(以查看它是否已经存在)。在我看来,这似乎比留给数据库根据 db 约束引发异常更好的方法 - 它更主动,因为你有一个真实的过程:获取值,检查它是否有效,如果不是则显示错误,如果没有错误允许继续。所以看来选项2是一个不错的选择。

      【讨论】:

      • 这对 UX 来说很好,但是在提交数据时 UI 仍然必须处理一个很大的竞争条件。
      • 我们通过在提交时执行完整的服务器端验证来解决竞争条件。每个字段的验证只是为了方便用户
      • @Mark:当然。我只是想知道当违规确实发生时人们如何处理ui中的“完整服务器端验证”(假设ajax提交数据) - 你在其他答案中回答了,谢谢.
      【解决方案6】:

      这就是我做事的方式,虽然它可能不适合你:

      我通常选择先发制人的模型,尽管它在很大程度上取决于您的应用程序架构。

      对我(在我的环境中)来说,检查中间(业务对象)层中的大多数错误是有意义的。这是所有其他特定于业务的逻辑发生的地方,所以我也尽量保留我的其余逻辑。我认为数据库是保存我的对象的地方。

      当涉及到验证时,最简单的错误可以被困在 javascript 中(格式、字段长度等),当然您永远不会假设这些错误检查已经发生。这些错误也会在更安全、更可控的服务器端代码世界中得到检查。

      业务规则(例如“你每天只能有这么多 foo”)在业务对象层的服务器端代码中得到检查。

      仅在数据库中检查数据规则(参照完整性、唯一字段约束等)。我们也在中间层预先检查所有这些,以避免不必要地访问数据库。

      因此,我的数据库只保护自己免受其能够处理的简单、以数据为中心的规则的影响;更多变数、面向业务的规则存在于对象领域,而不是记录领域。

      【讨论】:

      • 不错。不过很好奇:对于中间层中的检查通过但在数据层失败(由于说,另一个线程击败它)的竞争条件,您会怎么做?
      • 是的,这完全是这种方法的弱点。在我的特定环境中,这种情况非常罕见,我们无需担心。这将导致一个相当丑陋的异常,没错,但数据库将保持不变。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-05
      • 1970-01-01
      • 2020-03-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多