使用 OData 进行事务性批处理
Posted
技术标签:
【中文标题】使用 OData 进行事务性批处理【英文标题】:Transactional batch processing with OData 【发布时间】:2014-02-17 20:22:34 【问题描述】:使用 Web API OData,我有 $batch 处理工作,但是,数据库的持久性不是事务性的。如果我在我的请求中的变更集中包含多个请求,并且其中一个项目失败,另一个仍然完成,因为对控制器的每个单独调用都有它自己的 DbContext。
例如,如果我提交一个包含两个变更集的批次:
第 1 批 - 变更集 1 - - 修补有效对象 - - 修补无效对象 - 结束变更集 1 - 变更集 2 - - 插入有效对象 - 结束变更集 2 结束批次
我希望第一个有效补丁会被回滚,因为更改集无法完整完成,但是,由于每个调用都有自己的 DbContext,第一个补丁被提交,第二个没有,并且插入已提交。
是否有标准的方法来支持通过 OData 的 $batch 请求进行事务处理?
【问题讨论】:
查看这个enter link description here 【参考方案1】:虽然非常详细,但 Mitselpliks 的回答对我来说并不是开箱即用的,因为在每次请求后事务都会回滚。要提交事务并将其应用到数据库,需要在释放/离开使用范围之前调用scope.Complete()
。
下一个问题是,虽然现在一切都在事务中运行,但单个请求的异常/失败并没有导致请求或事务失败,批处理响应的状态代码仍然是 200 和所有其他更改仍然被应用。
由于无法直接从 HttpContext 读取批量请求的单个请求的状态码,我还不得不重载 ExecuteRequestMessageAsync
方法并在那里检查结果。所以我的最终代码,它会在一切成功时应用事务,否则回滚一切,看起来像这样:
public class TransactionODataBatchHandler : DefaultODataBatchHandler
protected bool Failed get; set;
public override async Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler)
using (var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions IsolationLevel = IsolationLevel.ReadCommitted ,
TransactionScopeAsyncFlowOption.Enabled))
Failed = false;
await base.ProcessBatchAsync(context, nextHandler);
if (!Failed)
scope.Complete();
public override async Task<IList<ODataBatchResponseItem>> ExecuteRequestMessagesAsync(IEnumerable<ODataBatchRequestItem> requests, RequestDelegate handler)
var responses = await base.ExecuteRequestMessagesAsync(requests, handler);
Failed = responses.Cast<OperationResponseItem>().Any(r => !r.Context.Response.IsSuccessStatusCode());
return responses;
【讨论】:
【参考方案2】:我对使用 OData 和 Web API 有点陌生。我正在学习自己的道路上,所以请接受我的回应,看看它对你有什么价值。
编辑 - 如此真实。我刚刚了解了TransactionScope 课程,并确定我发布的大部分内容都是错误的。因此,我正在更新以支持更好的解决方案。
这个问题也很老了,从那时起 ASP.Net Core 就出现了,所以根据你的目标需要做一些改变。我只是为像我一样登陆这里的未来谷歌员工发布回复:-)
在继续之前我想说明几点:
原始问题假定每个控制器调用已接收 它自己的 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 能够通过对 [base URL]/odata/$batch
的单个 POST
操作来执行。
批量执行订单问题
出于我们的目的,我们可能关心也可能不一定关心针对 DbContext 执行的顺序工作。我们绝对关心正在执行的工作是作为批次的一部分完成的。我们希望它在正在更新的数据库中全部成功或全部回滚。
如果您使用的是老式 Web API(换句话说,在 ASP.Net Core 之前),那么您的批处理处理程序类可能是 DefaultHttpBatchHandler
类。根据此处Introducing batch support in Web API and Web API OData 的Microsoft 文档,在OData 中使用DefaultHttpBatchHandler
的批处理事务默认是顺序的。它有一个ExecutionOrder 属性,可以设置该属性以更改此行为,以便同时执行操作。
如果您使用的是 ASP.Net Core,我们似乎有两个选择:
如果您的批处理操作使用“old school”有效负载格式,它 看来批处理操作默认是按顺序执行的 (假设我正确解释了源代码)。 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 标头上的边界参数不像以前的版本那样是必需的:
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】:以下链接显示了处理事务中的变更集所需的 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/
【讨论】:
此链接似乎不再有效。 感谢您指出这一点。我更新了答案以包含更新的链接。【参考方案4】:我使用了 Odata Samples V3 中的相同内容,我看到我的 transaction.rollback 被调用但数据没有回滚。缺少一些东西,但我不知道是什么。这可能是每个 Odata 调用都使用保存更改的问题,并且他们实际上是否将事务视为在范围内。我们可能需要 Entity Framework 团队的专家来帮助解决这个问题。
【讨论】:
【参考方案5】:-
理论:让我们确保我们谈论的是同一件事。
在实践中:尽可能
理论
为了记录,以下是 OData 规范对此的说明(强调我的):
变更集中的所有操作都代表一个变更单元,因此 服务必须成功处理并应用所有请求 更改设置否则不应用任何设置。这取决于服务 定义回滚语义以撤消任何请求的实现 在可能在另一个请求之前应用的更改集中 在同一个更改集中失败,因此应用此 all-or-nothing 要求。服务可以在变更集中执行请求 以任何顺序并可以返回对单个请求的响应 以任何顺序。 (...)
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)或 Action Invocation 请求。您可能会对变更集中的请求无序这一事实感到惊讶,坦率地说,我没有提供适当的理由。规范中的示例清楚地显示了相互引用的请求,这意味着必须推导出处理它们的顺序。实际上,我的猜测是变更集必须真正被视为单个请求本身(因此是原子要求),它们被一起解析并可能折叠成单个后端操作(当然取决于后端)。在大多数 SQL 数据库中,我们可以合理地启动事务并按照由它们的相互依赖关系定义的特定顺序应用每个子请求,但对于其他一些后端,可能需要在将任何更改发送到拼盘。这可以解释为什么不需要按顺序应用它们(这个概念可能对某些后端没有意义)。
这种解释的一个含义是,您的所有变更集都必须在逻辑上保持一致;例如,您不能让 PUT 和 PATCH 在同一个更改集上触及相同的属性。这将是模棱两可的。因此,客户端有责任在将请求发送到服务器之前尽可能高效地合并操作。这应该总是可行的。
(我希望有人能证实这一点。)我现在相当有信心这是正确的解释。
虽然这似乎是一种明显的良好做法,但人们通常不会这么认为批处理。我再次强调,所有这些都适用于更改集中的请求,而不是批处理请求中的请求和更改集(这些请求是有序的,并且几乎可以按照您的预期工作,减去它们的非原子/非事务性质)。
在实践中
回到您的问题,这是特定于 ASP.NET Web API 的,似乎是 they claim full support 的 OData 批处理请求。 More information here。正如您所说,似乎确实为每个子请求创建了一个新的控制器实例(好吧,我相信您的话),这反过来又带来了一个新的上下文并打破了原子性要求。那么,谁是对的呢?
好吧,正如您也正确指出的那样,如果您要在处理程序中调用SaveChanges
,那么再多的框架hackery 也无济于事。看起来您应该按照我上面概述的注意事项自己处理这些子请求(注意不一致的更改集)。很明显,您需要 (1) 检测到您正在处理作为变更集一部分的子请求(以便您可以有条件地 提交)和 (2) 在调用之间保持状态。
更新:请参阅下一节,了解如何做到 (2),同时让控制器忽略功能(不需要 (1))。 如果您愿意,接下来的两段可能仍然很有趣有关HttpMessageHandler
解决方案解决的问题的更多背景信息。
我不知道您是否可以使用他们提供的当前 API 检测您是否在变更集中 (1)。我不知道您是否可以强制 ASP.NET 使控制器在 (2) 内保持活动状态。然而,你可以为后者做的(如果你不能让它活着)是保持对其他地方的上下文的引用(例如在 some kind of session state Request.Properties
)并重用它有条件地(更新:或无条件地,如果您在更高级别管理事务,请参见下文)。我意识到这可能没有您希望的那么有用,但至少现在您应该有正确的问题可以直接向您的实现的开发人员/文档编写者提出。
危险地漫无边际:您可以有条件地为每个变更集创建和终止TransactionScope
,而不是有条件地调用SaveChanges
。这并没有消除对 (1) 或 (2) 的需求,只是另一种做事方式。它有点遵循框架可以在技术上自动实现这一点(只要可以重用相同的控制器实例),但在不了解内部原理的情况下,我不会重新审视我的说法,即框架没有足够的能力去配合自己做所有的事情。毕竟,TransactionScope
的语义对于某些后端来说可能过于具体、不相关甚至不受欢迎。
更新:这确实是正确的做事方式。下一节展示了一个使用实体框架显式事务 API 而不是 TransactionScope
的示例实现,但这具有相同的最终结果。虽然我觉得有一些方法可以实现通用实体框架,但目前 ASP.NET 不提供任何特定于 EF 的功能,因此您需要自己实现。如果您曾经提取代码以使其可重用,请尽可能在 ASP.NET 项目之外共享它(或说服 ASP.NET 团队他们应该将其包含在他们的树中)。
在实践中,真的(更新)
查看 snow_FFFFFF 的有用答案,其中引用了一个示例项目。
为了把它放在这个答案的上下文中,它展示了如何使用HttpMessageHandler
来实现我上面概述的要求#2(在单个请求中保持控制器调用之间的状态)。这通过在比控制器更高的级别上挂钩来工作,并将请求拆分为多个“子请求”,同时保持状态对控制器(事务)不知情,甚至将状态暴露给控制器(实体框架上下文,在此案例通过HttpRequestMessage.Properties
)。控制器会愉快地处理每个子请求,而不知道它们是普通请求、批处理请求的一部分,还是变更集的一部分。他们需要做的就是在请求的属性中使用实体框架上下文,而不是使用他们自己的。
请注意,您实际上有很多内置支持来实现这一点。这个实现建立在DefaultODataBatchHandler
之上,它建立在ODataBatchHandler
代码之上,它建立在HttpBatchHandler
代码之上,这是一个HttpMessageHandler
。相关请求使用Routes.MapODataServiceRoute
显式路由到该处理程序。
这个实现如何映射到理论?很好,其实。您可以看到,如果每个子请求是“操作”(正常请求),则发送每个子请求以由相关控制器按原样处理,或者如果它是变更集,则由更具体的代码处理。在此级别,它们按顺序处理,但不是原子处理。
然而,变更集处理代码确实将它自己的每个子请求包装在一个事务中(每个变更集一个事务)。虽然此时代码可以通过查看每个子请求的 Content-ID 标头来构建依赖关系图来尝试找出在事务中执行语句的顺序,但此实现采用更直接的方法,即要求客户端以正确的顺序对这些子请求进行排序,这很公平。
等等,它解决了我的问题吗?
如果您可以将所有操作包装在一个变更集中,那么可以,请求将是事务性的。如果不能,则必须修改此实现,以便将整个批次包装在单个事务中。虽然规范应该不排除这一点,但有明显的性能考虑需要考虑。您还可以添加一个非标准的 HTTP 标头来标记您是否希望批处理请求是事务性的,并让您的实现采取相应的行动。
无论如何,这都不是标准的,如果您想以可互操作的方式使用其他 OData 服务器,则不能指望它。要解决此问题,您需要向 OASIS 的 OData 委员会提出可选的原子批处理请求。
或者
如果您在处理变更集时找不到分支代码的方法,或者您无法说服开发人员为您提供这样做的方法,或者您无法保持变更集特定的状态以任何令人满意的方式,那么看起来您必须 [您可能希望] 公开一个全新的 HTTP 资源,其语义特定于您需要执行的操作。
您可能知道这一点,这很可能是您想要避免的,但这涉及使用 DTO(数据传输对象)来填充请求中的数据。然后,您解释这些 DTO 以在单个 handler 控制器操作中操作您的实体,从而完全控制结果操作的原子性。
请注意,有些人实际上更喜欢这种方法(更多面向过程,更少面向数据),尽管它可能很难建模。没有正确的答案,它总是取决于领域和用例,而且很容易陷入使您的 API 不是非常 RESTful 的陷阱。这是 API 设计的艺术。 不相关:关于数据建模也可以这样说,有些人实际上觉得这更难。 YMMV。
总结
有几种方法可供探索,从开发人员那里检索一些信息使用的规范实现技术,创建通用实体框架实现的机会,以及非通用替代方案。 p>
如果你能在其他地方收集答案(好吧,如果你觉得有足够的动力)以及你最终决定做的事情时更新这个帖子会很好,因为这似乎是很多人喜欢做的事情有某种明确的指导。
祝你好运;)。
【讨论】:
【参考方案6】: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
【讨论】:
在您发布的示例中,情况并非如此。 defaultOdataBatchHandler 将为批处理中的每个调用实例化一个新的控制器实例,这意味着它自己的上下文副本。即使他们确实重用了相同的上下文,在每个函数调用(PUT,POST)结束时,都会执行一个保存更改,这会将上下文更改保存回每个片段的数据库,而不是作为单个事务,除非我错过了什么。 (我的测试代码基于该示例,并单步执行,sql profiler 显示了单独的提交)以上是关于使用 OData 进行事务性批处理的主要内容,如果未能解决你的问题,请参考以下文章
ASP .NET MVC 4 WebApi:手动处理 OData 查询