我应该检查数据库中是否存在某些内容并快速失败或等待数据库异常

有两个班:

public class Parent 
{
    public int Id { get; set; }
    public int ChildId { get; set; }
}

public class Child { ... }

ChildId 分配给 Parent 时,我应先检查数据库中是否存在或等待数据库抛出异常?

例如(使用Entity Framework Core):

NOTE these kinds of checks are ALL OVER THE INTERNET even on official Microsoft's docs: https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application#modify-the-department-controller but there is additional exception handling for SaveChanges

另请注意,此检查的主要目的是将友好消息和已知HTTP状态返回给API用户,而不是完全忽略数据库异常。并且抛出的唯一地方异常是在 SaveChangesSaveChangesAsync 调用...所以当你调用 FindAsync 或者没有任何异常时<�代码>任何</代码>。因此,如果子存在但在 SaveChangesAsync 之前被删除,那么将抛出并发异常。

我这样做是因为外键违规异常将更难格式化以显示“无法找到ID为{parent.ChildId}的Child”。

public async Task> CreateParent(Parent parent)
{
   //is this code redundant?
  //NOTE: its probably better to use Any isntead of FindAsync because FindAsync selects *, and Any selects 1
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null)
       return NotFound($"Child with id {parent.ChildId} could not be found.");

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        

    return parent;
}

与:

public async Task> CreateParent(Parent parent)
{
    _db.Parents.Add(parent);
    await _db.SaveChangesAsync(); //handle exception somewhere globally when child with the specified id doesn't exist...  

    return parent;
}

The second example in Postgres will throw 23503 foreign_key_violation error: https://www.postgresql.org/docs/9.4/static/errcodes-appendix.html

在像EF这样的ORM中以这种方式处理异常的缺点是它只能用于特定的数据库后端。如果你想切换到SQL服务器或其他东西,这将不再起作用,因为错误代码将会改变。

不为最终用户正确格式化异常可能会暴露一些你不希望任何人但开发人员看到的东西。

有关:

https://stackoverflow.com/问题/ 6171588 /防止种族-条件的-IF-存在更新,否则,镶嵌在实体框架

https://stackoverflow.com/questions/ 4189954 /实施 - 如果 - 不存在 - 插入 - 使用实体框架,没有种族条件

https://stackoverflow.com/questions/308905/should-there-是-A-事务的读取查询

26
如果额外的查询是提供可读的错误消息,那么在我们知道有错误之后不应该发生这种情况,而不是之前?即 lock_db {try {add()} catch(db_error){if(id_not_present()){return NotFound(); } else {return OtherError(); } 。我不够专业,无法将其转化为实际代码,但这正是我所期待的。
额外 作者 Kenneth Kryger Sørensen,
@Konrad。我怀疑它最终完全取决于使用情况。如果该ID在99%的时间内是正确的,您可能会发现更快不检查(因为检查总是额外工作),并在错误处理代码中执行任何错误字符串格式化(在这种情况下涉及异常处理 - 如果不是您期望的特定错误,您可以重新抛出。如果ID在99%的时间内都是错误的,那么检查会更快。测量它,看看。
额外 作者 Kenneth Kryger Sørensen,
检查实际上做了两件事:1)输入验证,2)错误表示。如果您维护数据库状态(锁定/事务),则可以将这两者合并为一个,或者单独执行这两者。
额外 作者 Kenneth Kryger Sørensen,
我认为这证明需要额外的 SELECT 查询
额外 作者 Scott Young,
@ user673679数据库给出了一个类似“外键冲突”的错误,现在尝试将其格式化为“无法找到ID为{parent.ChildId}的Child”。因为您不希望最终用户了解您的底层数据库。只需返回 500内部服务器错误404 Not Found 就不会从用户的角度告诉你任何事情,所以你需要知道到底出了什么问题,这样你才能解决这个问题。重试。
额外 作者 Scott Young,
例外是针对开发人员,而不是针对最终用户,因此您必须格式化从数据库异常到用户友好消息的真正有意义的错误。
额外 作者 Scott Young,
另外,在我的第一个代码示例中,当我不需要从子代中读取任何内容时,我会使用 Any 而不是 Find ,因为ORM会将其转换为 SELECT 1 而不是 SELECT *
额外 作者 Scott Young,
@JaredSmith我知道竞争条件和锁定是什么。
额外 作者 Scott Young,
@JaredSmith但是FYI FindAsync 不会抛出任何异常
额外 作者 Scott Young,
如果在调用 FindAsync 之后某些内容同时发生变化且数据不一致,那么 SaveChangesAsync 将抛出异常
额外 作者 Scott Young,
我会更乐意通过代码示例的问题的实际示例得到答案,但它不起作用
额外 作者 Scott Young,
例如:如果您使用 Find 但有人添加了具有此特定ID的记录,则不会发生任何错误,它将返回404未找到。但是如果有人在此期间删除了它,那么它会影响其余的操作。
额外 作者 Scott Young,
@JaredSmith我认为你让我感到困惑,我知道竞争条件与异常无关,但你在这里对你在代码中处理的竞争条件与数据库竞争条件的概念感到困惑。
额外 作者 Scott Young,
@JaredSmith并不是因为数据库中的竞争条件以您选择的语言处理不同
额外 作者 Scott Young,
例如,在我使用EF Core的情况下,当数据库中存在竞争条件时,它将抛出异常
额外 作者 Scott Young,
@billrichards似乎是合理的:)但是当父属性依赖于子属性时呢?我需要查询然后代码然后将与我的第一个示例不同
额外 作者 Scott Young,
@DanielPryden使用ORM或不是每个选择都有一些你必须处理的缺点。 IMO在这种情况下你使用什么作为密钥并不重要,因为它是关于查询的,因此当你不使用ORM并使用像dapper或者某些微观orm这样的东西时你会遇到相同或类似的问题。
额外 作者 Scott Young,
您仍然可以直接执行查询,而不是依赖于自动生成。
额外 作者 Scott Young,
@DanielPryden在我的问题中,我没有说我不想处理数据库异常(我知道异常是不可避免的)。我想很多人都误解了,我想为我的Web API提供友好的错误信息(供最终用户阅读),例如找不到id为{parent.ChildId}的孩子。。并且格式化“外键违规”我认为在这种情况下更糟糕。
额外 作者 Scott Young,
当您首次检查是否已存在记录时,您有机会返回自定义消息。它还使业务规则更加冗长。当我看到 if(!Exists(item))时,我立刻知道必须永远不会有重复的记录。
额外 作者 DarrylGodden,
@Konrad:如果你有一组唯一标识记录的字段,那么你应该有一个强制执行它的UNIQUE约束。只要您的数据库强制执行数据的完整性(它应该是,这就是它的工作!),那么您将不得不处理来自应用程序的请求,即数据库因违反约束而拒绝。正如Mr.Mindor在下面指出的那样,这实际上是一个非常特殊的错误处理特例:无论你做什么,它总是可能导致插入失败,你需要处理无论如何。
额外 作者 DWGKNZ,
@Konrad:你在这个问题上的行为表明你并没有善意地提出这个问题。您想要一个特定的答案,在提出问题之前验证您已形成的意见。想要一个特定的答案本身并没有错,但是你与所有不同意你的人(这似乎是大多数人)的交往方式清楚地表明你对争论比对学习更感兴趣。对于社区或自己来说,这不是非常有建设性的行为。在你承认自己有需要学习的东西之前,你永远无法学到任何东西。
额外 作者 DWGKNZ,
作为一个有点不相关的旁边:您描述的许多问题都是您决定首先使用ORM的症状。如果您的主键是某种自然键或自动增量序列,那么您根本不会遇到此问题。如果您的工具使您难以为问题构建良好的解决方案,那么您可能需要仔细考虑是否需要不同的工具。
额外 作者 DWGKNZ,
分享您的研究可以帮助每个人。告诉我们您尝试了什么以及为什么它不能满足您的需求。这表明您花时间尝试帮助自己,这使我们无法重复明显的答案,最重要的是它可以帮助您获得更具体和相关的答案。另请参阅如何询问
额外 作者 gnat,
通过评论,我需要这样说:停止在陈词滥调中思考。 “快速失败”不是一个孤立的,脱离背景的规则,可以或应该盲目地遵循。这是一个经验法则。 始终分析您实际想要实现的目标,然后根据它是否有助于您实现该目标来考虑任何技术。 “快速失败”可以帮助您防止意外的副作用。此外,“快速失败”的确意味着“一旦发现问题就会失败”。 一旦检测到问题,两种技术都会失败,因此您必须查看其他注意事项。
额外 作者 jmibanez,
正如其他人所提到的那样,有可能在检查NotFound的同时插入或删除记录。出于这个原因,首先检查似乎是一个不可接受的解决方案。如果您担心编写不能移植到其他数据库后端的Postgres特定异常处理,请尝试构造异常处理程序,以便可以通过特定于数据库的类(SQL,Postgres等)扩展核心功能。
额外 作者 eddyce,
额外 作者 Sujan,
@Konrad没什么区别。没有。同样的问题,相同的解决方共享资源是共享资源,无论是RAM,文件系统,数据库等。
额外 作者 Sujan,
有关更长篇幅的论述,请参阅Mr.Mindor先生的回答:这真的很棒。
额外 作者 Sujan,
@Konrad什么例外与它有关?不要将竞争条件视为代码中存在的东西:它是宇宙的属性。什么,任何触及一个它不完全控制的资源(例如直接内存访问,共享内存,数据库,REST API,文件系统等等)不止一次,并期望它不变有潜在的竞争条件。哎呀,我们在C中处理这个问题,甚至没有例外。如果至少有一个分支与该资源的状态混淆,那么就不要分支您无法控制的资源的状态。
额外 作者 Sujan,
由于某种原因,没有人提到的第一种模式的短语是“竞争条件”。查看它的含义,然后永远使用第二种模式。
额外 作者 Sujan,

5 答案

检查唯一性然后设置是反模式;在检查时间和写入时间之间同时插入ID总是会发生。数据库可以通过约束和事务等机制来解决这个问题。大多数编程语言都没有。因此,如果您重视数据一致性,请将其保留给专家(数据库),即插入并捕获异常(如果发生)。

105
额外
检查和失败不仅仅是“尝试”并希望最好。前者意味着您的系统将执行和执行2个操作,而DB会执行2个操作,而最新的操作仅表示其中一个操作。检查委派给DB服务器。它还意味着少一个跳入网络,少一个任务要由DB参与。我们可能会认为对DB的另一个查询是可以承受的,但我们经常忘记大思考。以高并发性思考,一次又一次地触发查询。它可以复制到DB的整个流量。如果重要由您决定。
额外 作者 glacier,
额外 作者 Scott Young,
我更新了我的问题。
额外 作者 Scott Young,
但快速失败不是更好吗?
额外 作者 Scott Young,
我认为这取决于,因为如果某些Parent属性依赖于子属性,那么您必须首先获取子级。
额外 作者 Scott Young,
@mtraceur这就是为什么我目前不这样做并且有单独的检查查询。
额外 作者 Scott Young,
当这将成为一个问题,那么我将适当地重构和处理数据库异常
额外 作者 Scott Young,
@mtraceur也许你是对的,我实际上并不确定EF Core是否隐含地将其作为交易的一部分。此1查询的唯一问题是它本身会失败正确格式化它以将其发送回客户端(用户友好的消息而不暴露细节)
额外 作者 Scott Young,
就像我之前说过的那样,有些情况(不是这一个),你需要2个查询,其中1个操作依赖于来自不同表的列
额外 作者 Scott Young,
在Web API中,您需要将其映射到适当的状态代码,例如404找不到...
额外 作者 Scott Young,
@Mindor先生你怎么解决的?将检查和插入都放在1个事务范围内?
额外 作者 Scott Young,
正如您在表中看到的那样 SELECTINSERT UPDATE DELETE 同时运行
额外 作者 Scott Young,
“在检查时间和写入时间之间同时插入ID总是会发生” FindAsync 将始终返回 null 或实体。只有在检查和保存之间删除或更新并发冲突时才会发生并发冲突
额外 作者 Scott Young,
你检查了它,但同时有人将其删除,因此 SaveChanges 将引发异常
额外 作者 Scott Young,
但是如果你检查它并且它不在那里那么它将返回404但是没有找到它,即使有人在不久之后添加了具有此特定ID的行也没有任何问题...至少我是这么认为的,因为没有其他处理它的方法
额外 作者 Scott Young,
@Konrad如果你知道你正在运行的主要查询会在失败之前浪费大量时间/ cpu/memory/etc,但是检查查询可以更快地执行,然后运行检查查询然后有条件地在同一个事务 中运行主查询可能是值得的 - 您的数据库的编程语言库应该公开启动和结束交易。 然而,请记住,过早优化往往是浪费精力,所以只有当你真的知道它效率低下时才这样做。
额外 作者 user223697,
@Konrad我的立场是默认正确的选择是一个自身失败的查询,它是单独的查询预飞行方法,它有举证责任证明自己。至于“成为一个问题”:所以你 使用交易,否则确保你对ToCToU错误是安全的,对吧?从你发布的代码中我并不是很明显,但如果你不是,那么它已经成为一个问题,就像一个定时炸弹在实际爆炸之前很久就会成为一个问题。
额外 作者 user223697,
首先检查唯一性并不能消除处理可能的故障的需要。另一方面,如果某个操作需要执行多个操作,则在启动任何操作之前检查是否所有操作都可能成功通常比执行可能</>需要回滚的操作更好。进行初始检查可能无法避免所有需要回滚的情况,但它可以帮助减少此类情况的发生频率。
额外 作者 supercat,
“检查唯一性然后设置是反模式”我不会这样说。这在很大程度上取决于你是否可以假设没有其他修改发生,以及检查是否产生一些更有用的结果(即使只是一个错误消息,实际上对读者来说意味着什么),当它不存在时。使用数据库处理并发Web请求,不能,您不能保证不会发生其他修改,但有时候这是合理的假设。
额外 作者 jmibanez,
@Konrad不依赖于状态保持不变,这是非常重要的返工。有问题的代码设计得很差,并且完全出现了这个问题涉及的错误。(不承认它需要以并发方式运行)当它被编写并最初测试时,开发人员是唯一的用户和手动完成所有测试,所以一切正常。一旦在UAT中只有两个用户,一切都变得非常奇怪。使事务中的读锁定工作的尝试已经是保存它的最后努力。
额外 作者 Laurens Swart,
@Konrad EF Core不会隐式地将你的支票和插入放入一个事务中,你必须明确地请求它。没有事务,首先检查是没有意义的,因为无论如何数据库状态可以在检查和插入之间改变。即使进行了交易,您也可能无法防止数据库在您脚下发生变化。几年前我们使用EF和Oracle运行了一个问题,尽管db支持它,但实体并未触发事务中锁定读取记录,只有插入被视为事务性。
额外 作者 Laurens Swart,
@Konrad如果1个操作依赖于来自不同表的列,为什么不加入它们并在1个数据库查询中查询它们?
额外 作者 user55642,

我认为你所谓的“快速失败”,我称之为不一样。

告诉数据库进行更改并处理失败, 很快。你的方式复杂,缓慢而且不是特别可靠。

你的技术不是快速失败,而是“预检”。有时候有充分的理由,但是在使用数据库时却没有。

36
额外
您假设您的数据库存储不一致的数据。换句话说,看起来你不相信你的数据库和数据的一致性。如果是这种情况,那么你就会遇到一个非常大的问题,而你的解决方案就是一个小问题。一种姑息性的解决方案注定要迟早被推翻。在某些情况下,您可能会被迫从您的控制和管理中消耗数据库。从其他应用程序。在这些情况下,我会考虑这样的验证。在任何情况下,@gnasher是对的,你的不是快速失败,或者不是我们所理解的快速失败。
额外 作者 glacier,
有些情况下,当一个类依赖另一个类时需要第二个查询,因此在这种情况下你别无选择。
额外 作者 Scott Young,
我认为这也取决于应用程序,如果你只是为少数用户创建它,那么它应该没有区别,代码更易读,有2个查询。
额外 作者 Scott Young,
但不是在这里。而数据库查询可能非常聪明,所以我一般都怀疑“没有选择”。
额外 作者 gnasher729,

这开始是一个评论,但变得太大了。

不,正如其他答案所述,不应使用此模式。*

处理使用异步组件的系统时,始终是竞争条件,其中数据库(或文件系统或其他异步系统)可能在检查和更改之间发生变化。检查这种类型并不是一种可靠的方法,可以防止您不想处理的错误类型 更糟糕的是,一眼就看出它给人的印象是应该防止重复记录错误给出错误的安全感。

无论如何,您需要进行错误处理。

在评论中,您已经问过如果您需要来自多个来源的数据。
仍然没有。

如果要检查的内容变得更复杂,基本问题就不会消失。

无论如何,您仍然需要错误处理。

即使这种检查是一种可靠的方法来防止您试图防范的特定错误,仍然可能发生其他错误。如果丢失与数据库的连接,或者空间不足,或者?

您可能仍然需要其他数据库相关的错误处理。处理这个特殊错误应该只是它的一小部分。

如果您需要数据来确定要更改的内容,您显然需要从某个地方收集它。 (取决于您使用的工具,可能有更好的方法,而不是单独的查询来收集它)如果,在检查您收集的数据时,您确定您根本不需要进行更改,很好,不要使更改。该确定与错误处理问题完全分开。

无论如何,您仍然需要错误处理。

我知道我在重复,但我认为明确这一点很重要。 我以前清理过这个烂摊子。

它最终会失败。如果确实失败了,那么到达底部将是困难和耗时的。解决竞争条件引起的问题很难。它们不会始终如一地发生,因此单独复制将很困难甚至不可能。你没有开始正确的错误处理,所以你不太可能继续下去:也许最终用户的一些神秘文本的报告(嘿,你试图阻止首先看到它。)也许是一个指向该功能的堆栈跟踪,当你看到它时,公然否认错误应该是可能的。

*执行这些存在的检查可能存在有效的业务原因,例如为了防止应用程序重复昂贵的工作,但它不适合替代正确的错误处理。

14
额外

相反,这是一个混乱的问题,但是你应该首先检查,而不仅仅是处理数据库异常。

首先,在您的示例中,您位于数据层,直接在数据库上使用EF来运行SQL。你的代码等同于运行

select * from children where id = x
//if no results, perform logic
insert into parents (blah)

您建议的替代方案是:

insert into parents (blah)
//if exception, perform logic

使用异常来执行条件逻辑很慢并且普遍不受欢迎。

你确实有竞争条件,应该使用交易。但这可以在代码中完全完成。

using (var transaction = new TransactionScope())
{
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null) 
    {
       return NotFound($"Child with id {parent.ChildId} could not be found.");
    }

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        
    transaction.Complete();

    return parent;
}

关键是要问自己:

“你认为这种情况会发生吗?”

如果没有,那么确定,插入并抛出异常。但只需像处理可能发生的任何其他错误一样处理异常。

如果你确实希望它发生,那么它并不例外,你应该检查孩子是否先存在,如果不存在则回复相应的友好信息。

Edit - There's a lot of controversy over this it seems. Before you downvote consider:

A.如果有两个FK约束怎么办?您是否主张解析异常消息以确定缺少哪个对象?

B.如果您有未命中,则只运行一个SQL语句。这只是点击会导致第二次查询的额外费用。

C.通常Id会成为代理键,很难想象你知道一个你不确定它在数据库上的情况。检查会很奇怪。但是,如果它是用户键入的自然键呢?这可能很有可能不在场

3
额外
评论不适用于扩展讨论;这个对话已经转移到聊天
额外 作者 maple_shaft,

我认为这里需要注意的第二件事 - 您希望这样做的原因之一是您可以格式化错误消息供用户查看。

我衷心地建议你:

a)向最终用户显示发生的每个错误的相同通用错误消息。

b)记录只有开发人员可以访问的实际异常(如果在服务器上)或可以通过错误报告工具发送给您的某个地方(如果部署了客户端)

c)不要尝试格式化您记录的错误异常详细信息,除非您可以添加更多有用的信息。您不希望意外地“格式化”您可以用来跟踪问题的一条有用信息。


简而言之 - 例外情况充满了非常有用的技术信息。这些都不应该是针对最终用户的,并且您将面临失去这些信息的风险。

2
额外
“为每个发生的错误向最终用户显示相同的通用错误消息。”这是主要原因,为最终用户格式化异常看起来像是一件可怕的事情。
额外 作者 Scott Young,
在任何合理的数据库系统中,您都应该能够以编程方式找出出现故障的原因。不必解析异常消息。更一般地说:谁说需要向用户显示错误消息?您可以使第一次插入失败并在循环中重试,直到您成功(或达到一些重试或时间限制)。事实上,无论如何,退避和重试都是你最终想要实现的。
额外 作者 DWGKNZ,