使用OData进行事务性批处理

时间:2014-01-27 05:03:34

标签: entity-framework asp.net-web-api odata

使用Web API OData,我有$ batch处理工作,但是,数据库的持久性不是Transactional。如果我在请求中的Changeset中包含多个请求,并且其中一个项失败,则另一个仍然完成,因为对控制器的每个单独调用都有自己的DbContext。

例如,如果我提交一个包含两个更改集的批处理:

批次1   - ChangeSet 1   - - 补丁有效对象   - - 修补无效对象   - 结束变更集1   - ChangeSet 2   - - 插入有效对象   - 结束ChangeSet 2 结束批次

我希望第一个有效的补丁会被回滚,因为更改集无法完整地完成,但是,因为每个调用都有自己的DbContext,第一个Patch被提交,第二个不是,并且插入已提交。

有没有一种标准的方法来通过OData的$ batch请求来支持交易?

5 个答案:

答案 0 :(得分:13)

  1. 理论:让我们确保我们谈论同样的事情。
  2. 在实践中:只要我可以解决问题(没有确定答案)就可以。
  3. 在实践中,真的(更新):实现特定于后端的部分的规范方式。
  4. 等等,它能解决我的问题吗?:我们不要忘记实现(3)受规范(1)的约束。
  5. 另外:通常"你真的需要吗?" (没有确定的答案)。
  6. 理论

    为了记录,这是OData规范对此有何看法(强调我的):

      

    更改集中的所有操作都代表一个更改单元,因此a   服务必须成功处理并应用所有请求   更改集或者不应用任何。这取决于服务   实现定义回滚语义以撤消任何请求   在另一个请求之前可能已应用的更改集内   在同一个更改集失败,从而应用此全有或全无   要求即可。服务可以在变更集中执行请求   以任何顺序并且可以将响应返回给各个请求   以任何顺序。 (...)

    http://docs.oasis-open.org/odata/odata/v4.0/cos01/part1-protocol/odata-v4.0-cos01-part1-protocol.html#_Toc372793753

    这是V4,几乎没有更新有关批量请求的V3,所以同样的注意事项适用于V3服务AFAIK。

    要理解这一点,你需要一点点背景知识:

    • 批量请求是有序请求和更改集的集合。
    • Change Sets本身是由无序请求集组成的原子工作单元,尽管这些请求只能是Data Modification个请求(POST,PUT,PATCH,DELETE,但不是GET)或{{3请求。

    你可能会对变更集中的请求是无序的这一事实表示怀疑,坦率地说,我没有适当的理由提供。规范中的示例清楚地显示了相互引用的请求,这意味着必须推导出处理它们的顺序。实际上,我的猜测是变更集必须真正被认为是单个请求本身(因此是原子需求),它们被一起解析并且可能被折叠成单个后端操作(当然取决于后端)。在大多数SQL数据库中,我们可以合理地启动事务并按照它们的相互依赖性定义的特定顺序应用每个子请求,但是对于其他一些后端,可能需要在将任何更改发送到之前对这些子请求进行修改并一起理解。盘片。这可以解释为什么他们不需要按顺序应用(这个概念可能对某些后端没有意义)。

    这种解释的含义是,所有变更集必须在逻辑上保持一致;例如,您无法在同一个更改集上触摸相同属性的PUT和PATCH。这将是模棱两可的。因此,客户端有责任在将请求发送到服务器之前尽可能有效地合并操作。这应该是可能的。

    (我希望有人确认这一点。) 我现在相信这是正确的解释。

    虽然这似乎是一种明显的良好做法,但人们通常不会想到批量处理。我再次强调,所有这些都适用于变更集中的请求,而不是批处理请求中的请求和变更集(这些请求是按照您的预期排序和工作,减去非原子/非事务性质)。

    在实践中

    回到你的问题,这是特定于ASP.NET Web API的,似乎Action Invocation的OData批处理请求。 they claim full support。正如你所说的那样,每个子请求都会创建一个新的控制器实例(好吧,我接受你的话),这反过来又会带来一个新的上下文并打破原子性要求。那么,谁是对的?

    好吧,正如你正确地指出的那样,如果你的处理程序中有SaveChanges次调用,那么任何框架hackery都不会有多大帮助。看起来您应该自己处理这些子请求,并考虑我上面提到的注意事项(寻找不一致的更改集)。很明显,您需要(1)检测到您正在处理属于变更集的子请求(以便您可以有条件地提交)和(2)保持状态在调用之间。

    更新:请参阅下一节有关如何做(2)同时保持控制器不知道功能(不需要(1))。如果你想要下两段可能仍然有用关于HttpMessageHandler解决方案解决的问题的更多背景信息。

    我不知道您是否可以使用他们提供的当前API来检测您是否处于变更集中(1)。我不知道你是否可以强制ASP.NET使控制器保持活动状态(2)。你可以为后者做些什么(如果你不能保持活着)是在其他地方保持对上下文的引用(例如在某种会话状态 Request.Properties并有条件地重复使用(更新:或无条件地,如果你在更高级别管理交易,见下文)。我意识到这可能没有你想象的那么有用,但至少现在你应该有正确的问题指向你实现的开发人员/文档编写者。

    危险地漫步:您可以有条件地为每个变更集创建和终止SaveChanges,而不是有条件地调用TransactionScope。这并不能消除对(1)或(2)的需要,只是另一种做事方式。接下来,框架可以在技术上自动实现这一点(只要相同的控制器实例可以重复使用),但是如果不充分了解内部结构,我就不会重新审视我的框架没有足够的内容。现在就去做所有事情。毕竟,对于某些后端,TransactionScope的语义可能过于具体,不相关甚至不受欢迎。

    更新:这确实是正确的做事方式。下一节显示了使用Entity Framework显式事务API而不是TransactionScope的示例实现,但这具有相同的最终结果。虽然我觉得有很多方法可以实现通用的实体框架,但是目前ASP.NET并没有提供任何特定于EF的功能,因此您需要自己实现这一功能。如果您提取代码以使其可重用,请尽可能在ASP.NET项目之外共享它(或说服ASP.NET团队将它们包含在树中)。

    在实践中,真的(更新)

    请参阅snow_FFFFFF的有用答案,该答案引用了一个示例项目。

    将它放在本答案的上下文中,它展示了如何使用HttpMessageHandler来实现我在上面概述的要求#2(在单个请求中保持控制器的调用之间的状态)。这可以通过挂钩比控制器更高级别,并将请求拆分为多个"子请求",同时保持状态无视控制器(事务),甚至将状态暴露给控制器(实体)框架上下文,在这种情况下通过HttpRequestMessage.Properties)。控制器愉快地处理每个子请求,而不知道它们是正常请求,批处理请求的一部分,还是变更集的一部分。他们需要做的就是在请求的属性中使用Entity Framework上下文,而不是使用自己的。

    请注意,您实际上有很多内置支持来实现此目的。此实现构建于DefaultODataBatchHandler之上,ODataBatchHandler构建于HttpBatchHandler代码之上,该代码构建于HttpMessageHandler代码之上,即Routes.MapODataServiceRoute代码。使用{{1}}将相关请求显式路由到该处理程序。

    这个实现如何映射理论?很好,实际上。您可以看到每个子请求被发送以由相关控制器按原样处理,如果它是"操作" (正常请求),如果是变更集,则由更具体的代码处理。在此级别,它们按顺序处理,但不是原子

    但是,变更集处理代码确实将每个子请求包装在一个事务中(每个变更集一个事务)。虽然代码可以在此时尝试通过查看每个子请求的Content-ID头来构建依赖关系图来确定在事务中执行语句的顺序,但是这种实现采用了更直接的方法来要求客户端以正确的顺序对这些子请求进行排序,这是公平的。

    等等,它能解决我的问题吗?

    如果您可以将所有操作包装在一个变更集中,那么请求将是事务性的。如果不能,则必须修改此实现,以便将整个批处理包装在单个事务中。虽然规范并未排除这一点,但需要考虑明显的性能因素。您还可以添加一个非标准HTTP标头来标记您是否希望批处理请求是事务性的,并使您的实现相应地执行。

    在任何情况下,这都不是标准的,如果您想以可互操作的方式使用其他OData服务器,则无法依靠它。要解决这个问题,您需要争取向OASIS的OData委员会提出可选的原子批量请求。

    替代地

    如果您在处理变更集时无法找到分支代码的方法,或者您无法说服开发人员为您提供这样做的方法,或者您可以'以任何令人满意的方式保持特定于变更集的状态,然后看起来你必须 [你可能或者想要]公开一个全新的HTTP资源,其中包含特定于你需要执行的操作的语义。

    您可能知道这一点,这可能是您试图避免的,但这涉及使用DTO(数据传输对象)来填充请求中的数据。然后,您可以解释这些DTO,以便在单个处理程序控制器操作中操作您的实体,从而完全控制结果操作的原子性。

    请注意,有些人实际上更喜欢这种方法(更多面向流程,更少数据导向),尽管建模起来非常困难。没有正确的答案,它总是取决于域和用例,并且很容易陷入陷阱,使您的API不是非常RESTful。它是API设计的艺术。 无关:关于数据建模可以说同样的评论,有些人实际上发现更难。 YMMV。

    摘要

    有一些探索方法,从开发人员检索的一些信息一个规范的实现技术,一个创建通用实体框架实现的机会,以及一个非通用的替代方案

    如果您可以在其他地方收集答案(好吧,如果您有足够的动力)以及您最终决定要做的事情,那么您可以更新此帖子,因为它看似很多人会做的事情我很乐意为你提供某种明确的指导。

    祝你好运;)。

答案 1 :(得分:4)

以下链接显示了处理事务中变更集所需的Web API OData实现。您是正确的,默认批处理程序不会为您执行此操作:

http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/OData/v3/ODataEFBatchSample/

<强>更新 原始链接似乎消失了 - 以下链接包含用于事务处理的类似逻辑(和v4):

https://damienbod.com/2014/08/14/web-api-odata-v4-batching-part-10/

答案 2 :(得分:1)

我对使用OData和Web API有点陌生。我正走在学习自我的道路上,因此请以我的回应对你有帮助。

编辑-的确如此。我刚刚了解了TransactionScope类,并认为我发布的大部分内容都是错误的。因此,我正在更新以寻求更好的解决方案。

这个问题也很老了,从那时起ASP.Net Core出现了,因此根据您的目标,有必要进行一些更改。我只是对像我这样来这里的未来Google员工发布回复:-)

在继续之前,我想提出几点:

  • 最初提出的问题是每个控制器调用了 它自己的DbContext。这不是真的DBContext生存期的作用域是整个请求。审核Dependency lifetime in ASP.NET Core以了解 更多细节。 我怀疑原始发布者遇到了问题,因为批处理中的每个子请求都正在调用其分配的控制器方法, 并且每个方法都分别调用DbContext.SaveChanges()-导致该工作单元被落实。
  • 原始问题还询问是否存在“标准”。我不知道我要提出的建议是否像某人会认为是“标准”那样,但是对我有用。
  • 我正在对迫使我不得不回答的原始问题做出假设 否定某人的回应没有用。我对 问题来自执行数据库事务的基础, 即(预期的SQL伪代码):

    BEGIN TRAN
        DO SOMETHING
        DO MORE THINGS
        DO EVEN MORE THINGS
    IF FAILURES OCCURRED ROLLBACK EVERYTHING.  OTHERWISE, COMMIT EVERYTHING.
    

    这是一个合理的要求,我希望OData能够对POST执行一次[base URL]/odata/$batch操作。

批量执行订单问题

出于我们的目的,我们可能会或不一定会在意针对DbContext完成的订单工作。我们绝对关心,尽管正在执行的工作是作为批处理的一部分来完成的。我们希望它全部成功或全部还原到正在更新的数据库中。

如果您使用的是老式的Web API(换句话说,是在ASP.Net Core之前的版本),则批处理程序类可能是DefaultHttpBatchHandler类。根据此处Introducing batch support in Web API and Web API OData的Microsoft文档,默认情况下,在OData中使用DefaultHttpBatchHandler的批处理事务是顺序的。它具有一个ExecutionOrder属性,可以将其设置为更改此行为,以便同时执行操作。

如果您使用的是ASP.Net Core,则似乎有两个选择:

  • 如果您的批处理操作使用的是“老式”有效负载格式, 似乎默认情况下按顺序执行批处理操作 (假设我正确地解释了源代码)。
  • ASP.Net Core提供了一个新选项。一个新的 DefaultODataBatchHandler取代了旧的DefaultHttpBatchHandler 类。已放弃对ExecutionOrder的支持,而赞成采用 模型,有效负载中的元数据在其中传达是否批处理 操作应按顺序进行和/或可以同时执行。至 利用此功能,请求有效负载Content-Type更改为 application / json和有效负载本身为JSON格式(请参见下文)。流 通过添加依赖项和组在负载中建立控制 控制执行顺序的指令,以便可以拆分批处理请求 分成多组可以执行的单个请求 异步和并行(不存在任何依赖关系的地方)或按顺序 依赖确实存在。我们可以利用这一事实,简单地创建 有效负载中的“ Id”,“ atomicityGroup”和“ DependsOn”标签可确保 操作以适当的顺序执行。

交易控制

如前所述,您的代码可能使用DefaultHttpBatchHandler类或DefaultODataBatchHandler类。无论哪种情况,这些类都不是密封的,我们可以很容易地从它们派生出来,以将正在完成的工作包装在TransactionScope中。默认情况下,如果范围内未发生未处理的异常,则在处理事务时将提交事务。否则,它将回滚:

/// <summary>
/// An OData Batch Handler derived from <see cref="DefaultODataBatchHandler"/> that wraps the work being done 
/// in a <see cref="TransactionScope"/> so that if any errors occur, the entire unit of work is rolled back.
/// </summary>
public class TransactionedODataBatchHandler : DefaultODataBatchHandler
{
    public override async Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler)
    {
        using (TransactionScope scope = new TransactionScope( TransactionScopeAsyncFlowOption.Enabled))
        {
            await base.ProcessBatchAsync(context, nextHandler);
        }
    }
}

只需将默认类替换为该类的一个实例,就可以了!

routeBuilder.MapODataServiceRoute("ODataRoutes", "odata", 
  modelBuilder.GetEdmModel(app.ApplicationServices),
  new TransactionedODataBatchHandler());

在ASP.Net Core POST中控制执行顺序以批量有效负载

ASP.Net Core批处理程序的有效负载使用“ Id”,“ atomicityGroup”和“ DependsOn”标签来控制子请求的执行顺序。我们还从中受益,因为像以前的版本一样,不需要Content-Type标头上的border参数:

    HEADER

    Content-Type: application/json

    BODY

    {
        "requests": [
            {
                "method": "POST",
                "id": "PIG1",
                "url": "http://localhost:50548/odata/DoSomeWork",
                "headers": {
                    "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
                    "odata-version": "4.0"
                },
                "body": { "message": "Went to market and had roast beef" }
            },
            {
                "method": "POST",
                "id": "PIG2",
                "dependsOn": [ "PIG1" ],
                "url": "http://localhost:50548/odata/DoSomeWork",
                "headers": {
                    "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
                    "odata-version": "4.0"
                },
                "body": { "message": "Stayed home, stared longingly at the roast beef, and remained famished" }
            },
            {
                "method": "POST",
                "id": "PIG3",
                "dependsOn": [ "PIG2" ],
                "url": "http://localhost:50548/odata/DoSomeWork",
                "headers": {
                    "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
                    "odata-version": "4.0"
                },
                "body": { "message": "Did not play nice with the others and did his own thing" }
            },
            {
                "method": "POST",
                "id": "TEnd",
                "dependsOn": [ "PIG1", "PIG2", "PIG3" ],
                "url": "http://localhost:50548/odata/HuffAndPuff",
                "headers": {
                    "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
                    "odata-version": "4.0"
                }
            }
        ]
    }

就足够了。将批处理操作包装在TransactionScope中后,如果发生任何故障,则全部回滚。

答案 3 :(得分:0)

OData批处理请求应该只有一个DbContext。 WCF数据服务和HTTP Web API都支持OData批处理方案并以事务方式处理它。您可以查看以下示例:http://blogs.msdn.com/b/webdev/archive/2013/11/01/introducing-batch-support-in-web-api-and-web-api-odata.aspx

答案 4 :(得分:0)

我在Odata Samples的V3中使用了相同的内容,我看到我的transaction.rollback被调用但数据没有回滚。缺乏一些东西,但我无法弄清楚是什么。这可能是每个Odata调用使用保存更改的问题,并且他们实际上看到事务在范围内。我们可能需要来自实体框架团队的大师来帮助解决这个问题。