【问题标题】:Recommended approach on handling SqlExceptions in db applications在数据库应用程序中处理 SqlExceptions 的推荐方法
【发布时间】:2010-11-27 18:08:36
【问题描述】:

我使用 C# 编写的数据库应用程序,使用 sql server 作为后端。为了数据完整性,我尝试在数据库级别尽可能多地执行 - 关系、检查约束、触发器。

由于它们,如果数据不一致,保存/更新/插入可能会失败,并且应用程序会抛出 SqlException。

我在 UI 中进行各种验证(如果输入的数据无效,则向用户提供有意义的信息),也在 BL 中进行各种验证,它将错误报告给 UI,然后将其呈现给用户。

但是,有些事情确实无法在应用程序中检查,并且由 db 处理:我的意思是当没有级联删除并且用户尝试从主表中删除实体等时删除错误。

例如员工表在许多关系中充当主人 - 员工经理,部门经理,收银员,团队负责人,团队成员等。如果我添加一个不涉及任何关系的新员工,我可以删除它,但用户尝试要删除此类关系中的主节点,由于在数据库级别强制执行 RI 规则,删除失败(应该如此),没关系。

我在尝试中编写删除代码...捕获并处理异常,告诉用户他不能删除该员工。但我想为用户提供更有意义的信息——无法删除记录的原因。也许这只是一个测试员工记录,它也被添加到了一个测试团队中。但是用户忘记了添加的位置,如果我可以告诉“无法删除员工,因为它是 T1 团队的一部分”,用户就会知道先去 T1 团队,删除用户然后再次尝试删除它。这是一个简单的例子,因为正如我所说的,一个员工可以参与很多关系——在我的应用程序中,我至少有 20 个。

解决办法是显示SqlException报告的Message,但这一点也不优雅。首先,这个消息是非常技术性的——它谈论的是 FK、PK、Triggers,这些对用户来说毫无意义,会吓到他们。其次,我的应用程序使用多语言 UI,所有菜单和消息都以用户选择的语言显示(在登录时或在用户配置文件中选择)。并且来自 SqlException 的消息是英语(如果我使用英语版本)或者最糟糕的,不太常见的语言,如德语或荷兰语,如果碰巧 sql server 是那种语言。

是否有任何常用或推荐的方法从 sql 异常中提取有意义的信息,以便能够向用户呈现有意义的消息(例如,什么关系或子表导致失败,或者什么触发器等)。但是我可以在程序中以独立于语言的方式进行测试,然后以用户友好的方式格式化我自己的错误消息?

你如何处理这种情况?

感谢所有回答

(PS:对不起,长篇大论)

【问题讨论】:

    标签: c# sql sql-server error-handling sqlexception


    【解决方案1】:

    很遗憾,这里没有一个简单的答案。

    所涉及的工作量取决于来自业务层的错误消息的一致性。您需要将“技术”错误消息转换为面向用户的消息。

    这应该是从错误消息到资源键的某种形式的查找问题,该资源键可用于提取特定于语言的错误消息。但是,如果您需要解析消息以获取更多信息(即:表名等),那么它会变得有点棘手。在这种情况下,您可能需要将错误消息映射到某种形式的正则表达式/处理器以及新的资源字符串。然后,您可以使用从原始错误中提取的信息格式化用户的字符串,并将其呈现给用户。

    【讨论】:

    • 我同意 - 您需要一个特定于应用程序的错误映射器,它将 sql 错误消息转换为业务错误消息。
    • 虽然我理解错误数量很少是足够的,但我也不认为解析错误消息是特别好的建议。这些不是 API 的一部分,它们是人类开发人员界面的一部分。除了尴尬之外,他们周围没有任何保证(“合同”)。在实践中,它们应该或多或少地稳定,但您绝对需要在升级任何东西之前至少运行一个测试套件。
    【解决方案2】:

    嗯,从数据库中,您只会得到这些技术信息,例如“违反外键关系 FK_something_to_another”等。

    通常,在 SqlException 中,您还会收到 SQL 错误代码或其他内容。

    最好的方法可能是在您的数据库中创建一个单独的表,该表基本上将那些可能发生在更有意义的、面向用户的消息上的技术 SQL 错误映射。例如。如果您的 SQL 错误显示类似于“fk 违规 blablabaal”,您可能在“UserErrorTable”中有一个条目,该条目将其映射到用户消息“无法删除用户(this.and.that),很可能是因为...... ..(他仍然是团队的成员)”或其他。

    然后您可以尝试在业务层中捕获这些 SqlException,将这些技术信息转换为您的用户的自定义异常,放入用户友好的消息,并将技术异常粘贴到您的自定义异常类型的 .InnerException 中:

    public class UserFriendlyException : Exception
    {
      public string UserErrorMessage { get; set; }
    
      public UserFriendlyException(string message, SqlException exc) : base(message, exc)
      {
         UserErrorMessage = MapTechnicalExecptionToUserMessage(exc);
      }
    }
    

    马克

    【讨论】:

      【解决方案3】:

      简短的回答是“不要”。让错误冒泡到全局错误处理/记录。验证和业务规则通常应该排除数据库异常,因此最好不要快速失败并且不提交脏数据。交易也有帮助。

      【讨论】:

      • 可能是适用于所有场景的一个正确答案。虽然 SQL Server 错误报告不是更有条理,但这有点令人遗憾。当我们需要比错误数更多的信息时,无法依赖它必然意味着域逻辑将被重复和冗余执行,从而增加事务的大小/重试次数,从而降低并发/事务吞吐量。
      【解决方案4】:

      错误消息不等同于异常。错误消息是用户应该发现的信息丰富且最重要的可操作User Experience Guidelines 中有一些关于错误消息的指南,我建议您阅读。 Apple 在Writing Good Alert Messages 中也有很好的通用指南。

      您会立即注意到大多数(如果不是全部)SQL 错误不是好的最终用户消息。 '约束 FKXF#455 违规' - 最终用户的错误错误。 '文件组已满' - 最终用户的错误错误。 '僵局' - 一样。好的应用程序所做的是将用户的角色分开。管理员需要看到这些错误,而不是最终用户。因此应用程序总是记录完整的 SQL 错误以及所有详细信息,最终通知管理员,然后向用户显示不同的错误,例如“发生系统错误,通知管理员”。

      如果最终用户可以处理 SQL 错误,那么您可以向他显示一条错误消息,指导他如何解决问题(例如,更改输入中的发票日期以满足约束条件)。但即使在这种情况下,大多数时候您也不应该直接向用户显示 SQL 错误(您已经看到了一个很好的理由:本地化)。我知道这会给您的开发团队带来更大的工作量。您必须从可能捕获的大量错误中了解在每种情况下哪些错误是用户可操作的。这是众所周知的,这就是为什么优秀的程序经理知道大约 80% 的代码正在处理错误情况,以及为什么应用程序“完成”通常意味着完成了 20%。这就是优秀应用与普通应用的区别:当出现问题时它们的行为方式错误

      我的建议是从渐进式披露的原则入手。显示一条通用错误消息,说明“操作失败”。如果用户在错误消息对话框上按下“显示详细信息...”按钮,则提供显示更多详细信息,并显示 SqlError 集合(顺便说一句,您应该始终记录并显示整个 SqlException.Errors 的 SqlError 集合,而不是 SqlException)。使用 SqlError.Number 属性在 catch 块中添加逻辑,以决定用户是否可以对错误执行任何操作(确定错误是否可操作)并添加适当的信息。

      不幸的是,没有精灵尘埃。您正在触及可能是您项目中最困难的部分。

      【讨论】:

      • 这是一个很好的答案。然而,OP 对它试图解释的几乎所有事情都表现出了理解。因此,这里的实际建议归结为:“使用报告的错误编号”。不幸的是,这个问题特别询问了需求,而不仅仅是错误编号(这是它的有趣之处),所以最终答案似乎没有太大帮助。可能唯一的方法是不幸地运行两次一致性检查(Wyatt Barnett 的)或无耻地解析消息(Reed Copsey 的)。
      【解决方案5】:

      这样做的方法是编写一个存储过程,并使用TRYCATCH。使用RAISERROR 提出您自己的自定义消息,并检查SqlException 中的错误代码。

      【讨论】:

      • 这意味着改变业务层,但是,bzamfir 似乎试图避免(这并不总是一种选择)。
      • 不幸的是,除非由 SP 提供,否则他正在寻找的信息将不存在。
      【解决方案6】:

      我们通常在我们的项目中编写某种翻译器。我们将 SQL 异常消息与一些预定义的模式进行比较,并将等效消息显示给用户

      【讨论】:

        【解决方案7】:

        关于通用技术> 用户友好的错误我只能支持已经给出的答案。

        但是对于您与雇主的具体示例,我必须鼓励您不要只依赖 SqlException。在尝试删除 Employee 之前,您应该检查一下他/她是否是任何团队的成员,是否是经理等。这将极大地提高您的应用程序的可用性。

        伪:

        Employee e;
        try {
        
           IEnumerable<Team> teams = Team.GetTeamsByEmployee(e);
           if (teams.Count() > 0) {
               throw new Exception("Employee is a part of team "+ String.Join(",", teams.Select(o => o.Name).ToArray());
           }
        
           IEnumerable<Employee> managingEmployees = Employee.GetEmployeesImManaging(e);
           if (managingEmployees.Count() > 0) {
               throw new Exception("Employee is manager for the following employees "+ String.Join(",", managingEmployees.Select(o => o.Name).ToArray());
           }
        
           Employee.Delete(e);
        } catch (Exception e) {
           // write out e
        }
        

        【讨论】:

          【解决方案8】:

          发生错误。当您遇到哪种类型的错误或如何处理它并不特别重要时,将您的代码放入 TRY...CATCH... 块并编写通用错误报告系统就可以了。当您想要(或被要求)写出比这更好的东西时,可能需要付出很大的努力,正如之前的一些帖子中所述(我也赞成)。

          我倾向于将错误分类为可预期或不可预期。如果您可以预料到一个错误并且您想用一个明确的消息来处理它,例如您的“删除员工”情况,您将必须相应地计划和编码。根据他们的定义,您不能对无法预料的错误执行此操作——这通常是 TRY/CATCH 的用武之地。

          对于您的情况,一种方法可能是在删除行之前,通过查询子表检查删除是否成功。如果不是,您将确切地知道原因(并且对于所有子表,而不仅仅是第一个阻止删除的表),并且可以向用户显示明确的消息。唉,您必须考虑数据是否会在检查和实际删除之间发生变化——这可能不是问题,但在其他情况下可能会发生。

          基于 TRY...CATCH... 的替代方法是检查 catch 块中的错误号。如果它是“由于外键而无法删除”,您可以查询子表并生成消息,如果是其他一些意外错误,您将不得不求助于通用消息。

          (警告:有时当它遇到错误时,SQL 会连续引发两条错误消息 [是其中之一违反 FK 约束吗?] 在这些情况下,各种 ERROR() 函数将只返回数据第二条信息总是不太有用。这非常令人恼火,但你无能为力。)

          【讨论】:

            【解决方案9】:

            总之,我会告诫不要依赖 SQL 异常和错误。对我来说,如果我们依赖这样的错误,我们就是在积蓄麻烦。此类错误消息通常不是用户可读的。此外,UI 开发人员可能会说“哦,好吧,它会被数据库人员捕获,我不需要验证”。完全错误!

            也许更好的方法是确保验证首先防止这些问题。例如如果因为对象 B 引用了对象 A 而不能删除它,那么我们应该在比数据库更高的级别上实现某种依赖服务。这取决于它是什么类型的应用程序。

            另一个例子是字符串长度验证。我们真的要依靠数据库验证来检查用户输入的字段的字符串长度吗?当然,这是极端情况!

            通常,如果数据库层抛出异常,则可能是其他地方出现错误的迹象。通过客户端/服务器设置,我们可以断言验证是两者的责任。如果它进入数据库,那么通常为时已晚。

            您可能希望使用 RAISEERROR,以便可以为调用代码提供来自存储过程的异常。从中您可以提供合理的错误消息。

            从来没有一刀切的方法。我的座右铭是预防胜于治疗!尽早验证而不是延迟验证。

            【讨论】:

              猜你喜欢
              • 2012-08-26
              • 1970-01-01
              • 1970-01-01
              • 2014-09-12
              • 1970-01-01
              • 2015-05-30
              • 2012-04-26
              • 2011-02-28
              • 1970-01-01
              相关资源
              最近更新 更多