您如何执行面向服务的父子交易?

时间:2014-08-21 16:22:52

标签: web-services entity-framework asp.net-web-api breeze graphdiff

  

实施例

     

SalesOrder由SalesOrderHeader和一个或多个SalesOrderItem组成。编辑现有SalesOrder时,可以修改SalesOrderHeader,并可以添加,修改和删除SalesOrderItems。所有更改必须保存在单个事务中。多个用户可以使用乐观并发同时编辑SalesOrder。

我认为在单个事务中完成保存的要求鼓励我们在单个服务调用中同时传达SaleOrderHeader和SalesOrderItems。将子数据与其父数据打包在一起的含义是,需要对子数据是否被添加,修改或删除有所了解。

可以在服务器上或客户端上进行子实体的更改跟踪。

更改服务器上的跟踪

此策略的想法是客户端可以将SalesOrder修改为其意愿,而无需跟踪添加,修改或删除哪些SalesOrderItem。调用保存服务时,将在服务器上确定SalesOrderItems的状态。

服务器应在服务调用之间保持无状态。这意味着服务器无法在检索和最终保存之间保留有关SalesOrder状态的任何信息。如果服务器通过将修改后的对象图与数据库对象图进行比较来确定其实体的状态,则唯一的选择就是。

使用nHibernate,有一个合并功能来完成此任务。使用实体框架,highest voted feature request将添加此功能。对于EF,还有一个名为GraphDiff的开源实现。

这在理论上听起来很棒,因为它使服务非常容易设计和使用。但是,我看到这个策略存在两个主要问题。首先是表现。必须在每次保存时发回整个对象图。无论SalesOrderItem是否被修改,都必须将其发回或服务器认为它已被删除。第二个问题更为关键,它与并发性有关。如果用户1将SalesOrderItem添加到SalesOrder并且用户2对同一SalesOrder进行更改,则当用户2保存服务器时,将假定应删除用户1添加的SalesOrderItem,因为它未包含在用户2的对象图中。我没有看到在服务器端更改跟踪的任何实现中都可以防止这种情况。

在客户端上更改跟踪

另一种方法是让客户端跟踪对其实体的更改,并在调用保存服务时传达该状态。一个好处是客户端不需要发送其未更改的子实体。这有助于提高性能。缺点是所有实体都需要一个名为“ObjectState”的附加属性来跟踪它是否被添加,修改或删除。这使得服务器上的实体模型非常混乱,并且充满了与业务领域无关的问题。这也使得服务的不同消费者有责任维持这种状态。另一个问题是处理已删除的实体变得困难。 SalesOrderHeader应该维护已删除的SalesOrderItems列表吗?或者SalesOrderItems是否应该被分配一个必须由客户端UI过滤掉的已删除状态?

我知道breeze javascript库有自己的客户端实体跟踪实现,但我担心的是它的实现需要客户端和服务器端组件。服务层是否应该隔离我们在哪一方使用哪种技术?如果非JavaScript客户想要使用我的服务怎么办?

问题

我认为这是一个应该由大多数服务实现解决的常见场景。我做了任何不正确的假设,或者我做了什么或平常的事情?你实施了什么策略?有没有合理的替代方案?

2 个答案:

答案 0 :(得分:2)

完全披露:我与Breeze合作,我认为客户端上的更改跟踪是可行的方法。客户端上的更改跟踪允许无状态服务器,减少客户端和服务器之间的流量,并允许脱机使用。

在Breeze中," ObjectState"你提到的是EntityAspect,每个实体都有一个,但它不是域模型的一部分。服务器端实体不需要EntityAspect,但服务器端服务必须知道如何处理来自客户端的实体状态信息。

基本上,服务需要根据来自客户端的信息创建,更新或删除实体。 Breeze现有服务器端后端已完成所有这些(.NET (EF and NHibernate)JavaPHPNodeRuby,但是你也可以自己写。您的服务器只需要知道如何与客户交谈。

我们已经更新了SalesOrder并添加了新的SalesOrderItem。 Breeze客户端发送一个类似于以下内容的保存包:

{
"entities": [
    {
        "Id": 123,
        "Title": "My Updated Title",
        "OrderDate": "2014-08-03T07:00:00.000Z",
        "entityAspect": {
            "entityTypeName": "SalesOrder:#My.DomainModel",
            "entityState": "Modified",
            "originalValuesMap": {
                "Title": "My Original Title"
            },
            "autoGeneratedKey": {
                "propertyName": "Id",
                "autoGeneratedKeyType": "Identity"
            }
        }
    },
    {
        "Id": -1,
        "SalesOrderId": 123,
        "ProductId": 456,
        "Quantity": 11,
        "entityAspect": {
            "entityTypeName": "SalesOrderItem:#My.DomainModel",
            "entityState": "Added",
            "originalValuesMap": {
            },
            "autoGeneratedKey": {
                "propertyName": "Id",
                "autoGeneratedKeyType": "Identity"
            }
        }
    }
]
}

此处,Id#123的SalesOrder已被修改(其标题已更改)。 entityAspect包含显示前一个标题的originalValuesMap 服务器需要使用新值更新现有的SalesOrder。服务器是否需要在应用更改之前从数据库中查询现有的SalesOrder,这取决于实现。

添加了新的SalesOrderItem。在客户端上为它创建了临时Id,-1。服务器需要创建并持久保存新的SalesOrderItem并为其生成真实的Id。

服务器的响应应包含已创建和更新的实体,以及KeyMapping信息,显示服务器生成的密钥映射到临时客户端密钥,以便客户端可以替换它们。

更改跟踪不是一个简单的问题,但Breeze会尝试为您完成难题。

答案 1 :(得分:1)

我想捎带史蒂夫的回答。

我们应该清楚:在关系数据模型中实现Order-graph(AKA“Order aggregate”)事务的责任落在开发人员身上。 BreezeJS(以及.NET服务器的Breeze帮助程序)可以提供便利,但您必须使其正常工作。

使这项工作的关键是在聚合中的任何实体的所有更改中包括聚合的根元素 - 订单。如果您添加,删除或修改OrderItem,请确保同时修改订单

如何?通过提高Order的并发属性(例如rowVersion)并确保Breeze知道这是你的并发属性。

  

如果要确保订单聚合一致性,则必须实现根实体乐观并发。

现在,您可以检测其他人是否对订单汇总的任何部分进行了更改。这可能是对Order或其中一个OrderItem的add / mod / delete的更改。

保存更改后的订单聚合时,不必在更改集中包含所有OrderItem。您只需要包含添加/修改/删除的OrderItem。

当然,其他一些用户可能会在保存您的订单聚合之前对其进行更改。当您尝试保存自己的时,保存将失败并出现乐观的并发错误。

在检测到订单的乐观并发错误时,请确保客户端从缓存中删除整个订单聚合 - 订单及其所有OrderItem - 然后重新获取聚合不要只需重新获取根Order实体并开始搞乱其项目。确保从缓存中删除整个聚合,然后重新获取它(订单及其项目)。

如果每个人都遵循此协议,您将在服务器上处于良好状态。