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


32

有两节课:

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

public class Child { ... }

分配给时ChildIdParent我应该首先检查数据库中是否存在它,或者等待数据库引发异常?

例如(使用实体框架核心):

注意,即使在官方的Microsoft文档中,这类检查也已遍及整个Internethttps//docs.microsoft.com/zh-cn/aspnet/mvc/overview/getting-started/getting-started-with-ef-using- MVC /处理与实体框架在ASP.NET MVC应用程序中的并发处理#修改部门控制器,但还有其他异常处理SaveChanges

另外,请注意,此检查的主要目的是向API用户返回友好消息和已知的HTTP状态,而不是完全忽略数据库异常。并且唯一引发异常的地方是在内部SaveChangesSaveChangesAsync调用中...因此,当您调用FindAsync或时不会有任何异常Any。因此,如果子项存在但在此之前被删除,SaveChangesAsync则将引发并发异常。

我这样做的原因是,foreign key violation异常将很难格式化以显示“找不到ID为{parent.ChildId}的孩子”。

public async Task<ActionResult<Parent>> 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<ActionResult<Parent>> 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;
}

Postgres中的第二个示例将引发23503 foreign_key_violation错误:https : //www.postgresql.org/docs/9.4/static/errcodes-appendix.html

在像EF这样的ORM中以这种方式处理异常的不利之处在于,它仅适用于特定的数据库后端。如果您想切换到SQL Server或其他服务器,则此命令将不再起作用,因为错误代码将更改。

如果没有为最终用户正确设置异常的格式,则可能会暴露一些您除了开发人员以外都不希望看到的东西。

有关:

https://stackoverflow.com/questions/6171588/preventing-race-condition-of-if-exists-update-else-insert-in-entity-framework

https://stackoverflow.com/questions/4189954/implementing-if-not-exists-insert-using-entity-framework-without-race-conditions

https://stackoverflow.com/questions/308905/should-there-be-a-transaction-for-read-queries


2
分享您的研究成果对每个人都有帮助。告诉我们您尝试过的内容以及为什么它不能满足您的需求。这表明您已花时间尝试自我帮助,这使我们免于重复显而易见的答案,并且最重要的是,它可以帮助您获得更具体和相关的答案。另请参阅如何提出
蚊蚋

5
正如其他人所提到的,有可能在检查NotFound的同时插入或删除记录。因此,先检查似乎是不可接受的解决方案。如果您担心编写无法移植到其他数据库后端的特定于Postgres的异常处理,请尝试构造异常处理程序,以使特定于数据库的类(SQL,Postgres等)可以扩展核心功能
Billrichards

3
浏览评论时,我需要这样说:不要再陈词滥调了。“快速失败”不是可以或应该盲目遵循的孤立的,脱离上下文的规则。这是一个经验法则。始终分析您实际要实现的目标,然后根据其是否有助于实现该目标来考虑任何技术。“快速失败”可帮助您防止意外的副作用。此外,“快速失败”实际上意味着“一旦发现问题就立即失败”。一旦发现问题,这两种技术都会失败,因此您必须考虑其他注意事项。
jpmc26

1
@Konrad异常与它有什么关系?停止将竞争条件视为代码中存在的东西:这是宇宙的财产。任何碰到它无法完全控制的资源(例如,直接内存访问,共享内存,数据库,REST API,文件系统等)的任何事情都超过一次,并期望它保持不变,这可能会导致竞争。哎呀,我们用C语言处理它,甚至没有例外。只要至少一个分支与该资源的状态混淆,就不要在您无法控制的资源的状态上分支。
贾里德·史密斯

1
@DanielPryden在我的问题中,我并不是说我不想处理数据库异常(我知道异常是不可避免的)。我认为很多人都误解了,我想为Web API提供友好的错误消息(供最终用户阅读),例如Child with id {parent.ChildId} could not be found.。在这种情况下,格式化“外键违规”的情况更糟。
康拉德(Konrad)'18

Answers:


3

而是一个令人困惑的问题,但是是的,您应该首先检查而不是仅处理数据库异常。

首先,在您的示例中,您位于数据层,直接在数据库上使用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;
}

关键是要问自己:

“您希望这种情况发生吗?”

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

如果您确实希望它发生,那么这不是例外,您应该检查孩子是否首先存在,如果不存在则以适当的友好消息进行响应。

编辑 -似乎对此有很多争议。下注之前,请考虑:

答:如果有两个FK约束怎么办。您是否主张解析异常消息以找出缺少的对象?

B.如果您错过了,则仅运行一个SQL语句。只有点击才能产生第二次查询的额外费用。

C.通常,Id将是代理密钥,很难想象您知道一个情况并且不确定是否在数据库中的情况。检查会很奇怪。但是,如果用户输入了自然键怎么办?那很有可能不会出现


评论不作进一步讨论;此对话已转移至聊天
maple_shaft

1
这是完全错误和误导的!正是这样的答案导致了我一直必须与之抗衡的专业人才。SELECT永远不会锁定表,因此在SELECT与INSERT,UPDATE或DELTE之间,记录可能会更改。因此,这是糟糕的糟糕软件设计和生产中等待发生的事故。
Daniel Lobo

1
@DanielLobo transactionscope修复了该问题
Ewan

1
如果您不相信我,请进行测试
Ewan

1
@yusha我的代码在这里
Ewan

111

检查唯一性然后设置是反模式;总是会在检查时间和写入时间之间同时插入ID。数据库具备通过约束和事务处理等机制来处理此问题的能力;大多数编程语言都不是。因此,如果您重视数据一致性,则将其交给专家(数据库)处理,即进行插入并在发生异常时捕获异常。


34
检查并失败并不比“尝试”并希望获得最佳结果要快。前者隐含着系统要执行和执行的2个操作,而DB隐含了2个操作,而最新的隐含着其中之一。检查委托给数据库服务器。这也意味着进入网络的跳数减少,并且数据库要承担的任务减少了。我们可能会认为可以再向数据库查询一次是可以承受的,但我们常常忘记进行大的思考。以高并发性思考一遍又一遍触发查询。它可能会消耗到数据库的全部流量。是否重要取决于您的决定。
Laiv

6
@Konrad我的立场是,默认正确的选择是一个将自行失败的查询,这是单独的查询预检方法,具有证明自己的理由。至于“成为问题”:那么您正在 使用事务以其他方式确保您可以防止ToCToU错误,对吗?从发布的代码对我来说,这不是很明显,但是如果您不是,则它已经成为一个问题,就像滴答炸弹在真正爆炸之前就成为问题一样。
mtraceur '18

4
@Konrad EF Core不会将支票和插入项都隐式地放入一个事务中,您将必须明确地请求它。没有事务,首先检查是没有意义的,因为数据库状态可以在检查和插入之间随时改变。即使进行事务处理,也可能无法阻止数据库更改。几年前,我们遇到了一个问题,将EF与Oracle一起使用,尽管db支持它,但Entity并未触发事务中读取记录的锁定,只有插入被视为事务性的。
Mindor先生

3
“不检查唯一性然后设置是一种反模式”,我不会这么说。这在很大程度上取决于您是否可以假设没有其他修改发生,以及在检查不存在时检查是否会产生一些更有用的结果(甚至只是一条错误消息,对阅读器实际上意味着一些影响)。使用数据库处理并发的Web请求,不能,您不能保证不会进行其他修改,但是在某些情况下,这是一个合理的假设。
jpmc26

5
首先检查唯一性并不能消除处理可能出现的故障的需要。另一方面,如果一个动作需要执行多个操作,则在启动任何一个动作之前检查所有是否都可能成功通常比执行可能需要回滚的动作更好。进行初步检查可能无法避免所有需要回滚的情况,但可以帮助减少此类情况的发生率。
超级猫

38

我认为您所说的“快速失败”与我所说的不一样。

告诉数据库进行更改并处理故障,非常快。您的方法很复杂,缓慢并且不是特别可靠。

您的技术并非很快就会失败,而是“预检”。有时候有很好的理由,但是当您使用数据库时却没有。


1
在某些情况下,当一个类依赖于另一个类时,您需要第二次查询,因此在这种情况下您别无选择。
康拉德

4
但是不在这里。而且数据库查询可能非常聪明,因此我通常会怀疑“别无选择”。
gnasher729

1
我认为它也取决于应用程序,如果仅为几个用户创建它,那么它就不会有所作为,并且代码在2个查询中更具可读性。
康拉德

21
您假设您的数据库存储的数据不一致。换句话说,您似乎不信任数据库和数据的一致性。如果是这种情况,那么您将面临一个非常大的问题,并且您的解决方案是一个解决方案。姑息性解决方案注定要迟早被否决。在某些情况下,您可能会因为无法控制和管理而被迫消耗数据库。来自其他应用程序。在那些情况下,我会考虑进行此类验证。无论如何,@ gnasher是正确的,您的失败不是很快的失败,或者不是我们所理解的快速失败。
Laiv

15

这开始只是评论,但太大了。

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

当处理使用异步组件的系统时,总是存在争用条件,数据库(或文件系统或其他异步系统)可能会在检查和更改之间更改。检查这种类型根本不是防止您不想处理的错误类型的可靠方法。
一目了然,它使人觉得应该防止重复记录错误,从而给人以错误的安全感,这是不足够的,而不是不够的。

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

在注释中,您询问是否需要多个来源的数据。
仍然没有。

基本的问题,如果你想检查什么变得更加复杂,不会消失。

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

即使此检查是防止您要防范的特定错误的可靠方法,其他错误仍然可能发生。如果您失去与数据库的连接,或者数据库空间不足,会发生什么?

无论如何,您很可能仍需要其他与数据库相关的错误处理。这个特殊错误的处理可能只是其中的一小部分。

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

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

我知道我正在重复,但我认为必须明确这一点。我之前已经清理了这个烂摊子。

最终它将失败。当它确实失败时,要弄清根本原因将是困难且耗时的。解决由竞赛条件引起的问题非常困难。它们不会始终如一地发生,因此孤立地复制将非常困难,甚至不可能。开始时您没有进行适当的错误处理,因此您可能没有太多事情要做:可能是最终用户的一些隐性文本报告(嘿,您试图首先避免看到它)。也许堆栈跟踪指向该函数,当您大胆地查看该函数时,该错误甚至应该是可能的。

*执行这些存在的检查可能有正当的业务原因,例如,防止应用程序重复进行繁重的工作,但对于适当的错误处理,它不是合适的替代方法。


2

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

我衷心推荐您:

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

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

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


简而言之-例外情况中充满了非常有用的技术信息。这些都不适合最终用户,您将失去这些信息的风险。


2
“针对发生的每个错误,向最终用户显示相同的通用错误消息。” 这是主要原因,为最终用户设置异常格式看起来很可怕
。.– Konrad

1
在任何合理的数据库系统中,您都应该能够以编程方式找出失败的原因。不必解析异常消息。更一般地说:谁说根本需要向用户显示错误消息?您可以使第一个插入操作失败并在循环中重试,直到成功为止(或达到重试次数或时间的限制)。实际上,无论如何,退回重试都是您最终想要实现的。
Daniel Pryden
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.