6

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

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

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

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

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

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

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

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

你如何处理这种情况?

感谢所有答案

(PS:抱歉发这么长)

4

9 回答 9

6

不幸的是,这里没有一个简单的答案。

所涉及的工作量将取决于来自业务层的错误消息的一致性。您将需要从“技术”错误消息到面向用户的消息进行某种形式的翻译。

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

于 2009-09-12T13:15:23.303 回答
5

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

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

最好的方法可能是在您的数据库中有一个单独的表,它基本上将那些可能发生在更有意义、面向用户的消息上的技术 SQL 错误映射。例如,如果您的 SQL 错误显示类似“fk violation 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);
  }
}

马克

于 2009-09-12T13:17:32.980 回答
3

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

于 2009-09-12T15:21:50.787 回答
2

这样做的方法是编写一个存储过程,并使用TRYand CATCH。用于RAISERROR引发您自己的自定义消息,并检查SqlException.

于 2009-09-12T13:15:36.953 回答
2

错误消息不等同于异常。错误消息是用户应该发现的信息丰富且最重要的可操作信息。我建议您阅读的用户体验指南中有一些关于错误消息的指南。Apple 在编写良好的警报消息方面也有很好的通用指南。

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

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

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

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

于 2009-09-13T20:43:28.977 回答
1

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

于 2009-09-12T13:18:19.970 回答
1

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

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

伪:

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
}
于 2009-09-12T14:07:31.293 回答
1

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

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

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

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

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

于 2009-09-12T14:42:54.517 回答
0

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

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

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

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

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

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

于 2016-03-28T13:36:39.423 回答